Push tokens outlive the device and the account (relay#19, relay#18) #138

Merged
paulhitt merged 1 commits from docs/metadata-pass-2026-09-07 into main 2026-09-07 14:16:06 +00:00
paulhitt commented 2026-09-07 12:47:50 +00:00 (Migrated from gitlab.com)

Relates to #19 and #18. Neither is closed — both are standing tickets.

The finding

Measured on production, 2026-09-07: 8 rows in push_tokens, 4 live devices.

user device row exists last written
(no user) no 2026-08-29
(no user) no 2026-08-31
fnreviewalpha no 2026-09-01
fnreviewalpha no 2026-09-02
(no user) no 2026-09-05
(no user) no 2026-09-05
fnreviewalpha yes 2026-09-04
fnreviewalpha yes 2026-09-05

Six of eight point at a device that is gone; four of those at an account that is gone. Oldest is nine days old and nothing would ever have collected it.

An APNs token is a stable per-install identifier Apple holds as well as we do. A row nobody can use is an identifier kept for no reason.

Why

All three paths that remove a device delete prekeys and envelopes and forget this table — revokeDevice, deleteAccount, and the retention expiry sweep. Each now takes the token with it. Account deletion filters by user rather than by its device list, so a token whose device row had already gone is collected too — the state four production rows were in.

The sweep collects what is already there. Compared against the device table, not swept by age: a push token is supposed to outlive its last use — a phone in a drawer for a month should still ring — so age says nothing about whether it is wanted. Having no device does. Counted as push_orphans beside the other sweep numbers.

There is a second cause this MR does not fix, because it is client-side: iOS signOut clears store.appPassword as step 1 (deliberately — it is what makes the keys left behind unable to fetch anything) and then calls forgetPushToken(), a try? call on an authenticated route. With a live cached relay token it works; once that has expired there is nothing left to mint another, it 401s, and the try? swallows it. Same shape as the attachment 401. Written up in the doc; needs its own ticket.

Falsified

  • sweep made unconditional → a token whose device is live survives goes red
  • sweep made to delete nothing → both go red

The second test is the one that matters more: a sweep that takes a working phone's token stops it ringing, and nothing on the phone would say why.

Two investigation passes, written up rather than acted on

Both tickets say investigate and document, do not change, so these are documents.

docs/METADATA-PASSES.md (#18) — new finding: length(payload) is visible and is not on /metadata. Fourteen envelopes, fourteen distinct lengths, clustering by kind (~6.3K ×2, ~9.4K ×6, ~13.9K ×4, ~16.7K ×2). Sealed sender removes attribution from the row; it does not remove size, timing or order, and a long enough window is made of those. Size-bucketing is proposed for its own ticket with numbers, since the trade has to be measured rather than asserted.

Two of the ticket's own bullets are stale, and the doc says so rather than repeating them: retention does sweep (in-process supervised loop, logged every cycle), and the unsealed fallback is measured — sends_sealed/sends_unsealed in the sweep line, and 14/14 sealed on live traffic. Fourteen envelopes from two accounts is not a sample to generalise from.

docs/IDENTIFYING-DATA.md (#19) — the edge-log retention timer runs hourly, exits 0, and has pruned 0 MB on every run since it was installed. Not because it is broken: edge-traefik-1 was recreated 13 hours ago for the obfs4 deploy and only .log and .log.1 exist, and the script starts at .2. "Exit 0" and "0 MB" together are not evidence, so the glob was checked against a fixture instead of assumed — it removes .2 upward including two-digit rotations and keeps the live file and one rotation.

Docker's own cap on that container is max-file 10 × 100m = about 3½ days of client IPs. Lowering max-file to 2 would put the bound at ~18 hours in Docker itself. Proposed, not done — that container fronts every site on the box.

Also stale in #19: users no longer has dead tuples (autovacuum has run); only envelopes does.

Relates to #19 and #18. Neither is closed — both are standing tickets. ## The finding Measured on production, 2026-09-07: **8 rows in `push_tokens`, 4 live devices.** | user | device row exists | last written | |---|---|---| | *(no user)* | no | 2026-08-29 | | *(no user)* | no | 2026-08-31 | | fnreviewalpha | no | 2026-09-01 | | fnreviewalpha | no | 2026-09-02 | | *(no user)* | no | 2026-09-05 | | *(no user)* | no | 2026-09-05 | | fnreviewalpha | **yes** | 2026-09-04 | | fnreviewalpha | **yes** | 2026-09-05 | Six of eight point at a device that is gone; four of those at an account that is gone. Oldest is nine days old and nothing would ever have collected it. An APNs token is a stable per-install identifier **Apple holds as well as we do**. A row nobody can use is an identifier kept for no reason. ## Why All three paths that remove a device delete prekeys and envelopes and forget this table — `revokeDevice`, `deleteAccount`, and the retention expiry sweep. Each now takes the token with it. Account deletion filters by *user* rather than by its device list, so a token whose device row had already gone is collected too — the state four production rows were in. The sweep collects what is already there. **Compared against the device table, not swept by age**: a push token is *supposed* to outlive its last use — a phone in a drawer for a month should still ring — so age says nothing about whether it is wanted. Having no device does. Counted as `push_orphans` beside the other sweep numbers. There is a second cause this MR does **not** fix, because it is client-side: iOS `signOut` clears `store.appPassword` as step 1 (deliberately — it is what makes the keys left behind unable to fetch anything) and then calls `forgetPushToken()`, a `try?` call on an authenticated route. With a live cached relay token it works; once that has expired there is nothing left to mint another, it 401s, and the `try?` swallows it. Same shape as the attachment `401`. Written up in the doc; needs its own ticket. ## Falsified - sweep made unconditional → *a token whose device is live survives* goes red - sweep made to delete nothing → **both** go red The second test is the one that matters more: a sweep that takes a working phone's token stops it ringing, and nothing on the phone would say why. ## Two investigation passes, written up rather than acted on Both tickets say investigate and document, do not change, so these are documents. **`docs/METADATA-PASSES.md` (#18)** — new finding: **`length(payload)` is visible and is not on `/metadata`.** Fourteen envelopes, fourteen distinct lengths, clustering by kind (~6.3K ×2, ~9.4K ×6, ~13.9K ×4, ~16.7K ×2). Sealed sender removes attribution from the row; it does not remove size, timing or order, and a long enough window is made of those. Size-bucketing is proposed for its own ticket with numbers, since the trade has to be measured rather than asserted. Two of the ticket's own bullets are **stale**, and the doc says so rather than repeating them: retention *does* sweep (in-process supervised loop, logged every cycle), and the unsealed fallback *is* measured — `sends_sealed`/`sends_unsealed` in the sweep line, and **14/14 sealed** on live traffic. Fourteen envelopes from two accounts is not a sample to generalise from. **`docs/IDENTIFYING-DATA.md` (#19)** — the edge-log retention timer runs hourly, exits 0, and has **pruned 0 MB on every run since it was installed.** Not because it is broken: `edge-traefik-1` was recreated 13 hours ago for the obfs4 deploy and only `.log` and `.log.1` exist, and the script starts at `.2`. "Exit 0" and "0 MB" together are not evidence, so the glob was checked against a fixture instead of assumed — it removes `.2` upward including two-digit rotations and keeps the live file and one rotation. Docker's own cap on that container is `max-file 10 × 100m` = **about 3½ days of client IPs**. Lowering `max-file` to `2` would put the bound at ~18 hours in Docker itself. Proposed, **not done** — that container fronts every site on the box. Also stale in #19: `users` no longer has dead tuples (autovacuum has run); only `envelopes` does.
paulhitt commented 2026-09-07 13:32:41 +00:00 (Migrated from gitlab.com)

mentioned in issue #18

mentioned in issue #18
paulhitt commented 2026-09-07 13:32:41 +00:00 (Migrated from gitlab.com)

mentioned in issue #19

mentioned in issue #19
paulhitt commented 2026-09-07 13:33:02 +00:00 (Migrated from gitlab.com)

mentioned in issue flamenet-messenger-ios#71

mentioned in issue flamenet-messenger-ios#71
paulhitt (Migrated from gitlab.com) merged commit bd46682723 into main 2026-09-07 14:16:06 +00:00
paulhitt commented 2026-09-07 14:16:07 +00:00 (Migrated from gitlab.com)

mentioned in commit bd46682723

mentioned in commit bd466827239e019c7a2ec1949703cb5303a71112
paulhitt commented 2026-09-09 18:58:48 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
Sign in to join this conversation.