Files
Paul HittandClaude Opus 5.5 4095ca60ec Wave 0b: introductions, Unlisted, contact links, anonymous reads
Additive and reversible. Three new migrations appended to the boot list
(CreateIntroTokens, CreateContactLinks, AddUserDiscoverable); nothing
existing is altered or dropped, and key_log is not touched.

C1, introductions. Anonymous POST /e2e/intro {handle} or {link} returns
three single-use tokens good for ten minutes, the target's bundles (one-time
prekeys popped, as GET /e2e/keys does) and a handle certificate.
intro_tokens holds (token_hash, user_id, expires_at) and nothing about who
asked; the sweep deletes expired rows. Metered per address, per network
(sign-up's /24 and /48), per account and per (network, account), and capped
at 90 unspent tokens per account. spendDeliveryToken tries delivery_tokens
then intro_tokens, each a single DELETE ... RETURNING. Handle certificates
sign "fnhandle-v1|audience|user_id|handle|exp" with the issuer signing key
whose public half both apps pin (RelayIdentityPin); GET /e2e/handle-cert
returns the caller's own.

The sealed send path: the recipient-wide bucket now counts only refused
attempts (so a working delivery key cannot be used to lock everyone else
out), plus a per-(recipient, address) bucket (FN_SEND_RATE_RECIPIENT_IP,
default a tenth of FN_SEND_RATE). Meters about an account are keyed by a
keyed hash (LimiterKeys), never the account id.

B1/B2, Unlisted. users.discoverable, default true, set by PUT /messenger/me
{"discoverable": false}. Read and written with raw SQL, not through the User
model, so migrations that load User early in the list still build a fresh
database. lookup ?slug=, POST /messenger/buddies, GET
/e2e/account/identity/:handle, GET /messenger/users/:id and POST /e2e/intro
{handle} answer an unlisted name as a missing one: same status and body,
same meter charge, and every "not there" answer held to a 5 ms floor.
Contact links: POST /e2e/contact-links {add, revoke} by hash, at most five,
refused (409) past that, table holds only (user_id, link_hash).

D1, anonymous reads. POST /e2e/anon/presence, /e2e/anon/devices and
/e2e/anon/bundles, targets in the body, no credential read. Directory and
bundles are metered by the subject's presence token when the caller holds
one, otherwise per address. The public directory (other accounts, federated
peers, the anonymous route) no longer carries created_at or last_seen; GET
/e2e/devices/{own id} keeps them because both apps' Devices screens read
created_at there, and GET /e2e/devices/me is the new home for them.
presenceByToken is split from caller().

Also: account deletion now removes the account's delivery_tokens (left
behind before), intro_tokens and contact_links.

Tests: 845 swift-testing tests in 93 suites and 61 XCTest pass, Postgres
18 suite included (fresh and populated upgrade for wave 0b, and a
concurrent intro-spend race). 23 falsifications, each red on its own
sabotage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:37:23 -04:00
..