Skip to content

Inbox mobile: the wire contract (C1) - #1775

Merged
backnotprop merged 2 commits into
mainfrom
feat/mobile-contract
Oct 8, 2026
Merged

backnotprop merged 2 commits into
mainfrom
feat/mobile-contract

Conversation

@backnotprop

@backnotprop backnotprop commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

The story

The iPhone app, the Inbox's device door, the LAN listener, the relay Worker and the surface bridge need one frozen wire before two lanes build them in parallel. This PR adds that wire as adr/implementation/inbox-mobile.md, in the style of guide-share-hosting.md, matched to the Inbox's handlers as they are on main. It is step C1 of the mobile plan; no product code changes.

What the file holds

  1. The pairing offer and QR link. POST /api/inbox/pairing (window route, window guards), plannotator://pair?v=1&name&tailnet&lan&fp&secret&code, and the tailnet publication on 8443.
  2. The device token door. POST /api/inbox/device/pair (secret or six digits, answering a tok_ bearer), inbox/devices.jsonl, list and revoke, and /api/inbox/device/* as an explicit allowlist of 23 routes mapped onto the window's handlers, each with the request and answer it has today. It also names what the door never answers (/mcp, the bridge, control, settings, restart, project delete, decision retire and replace), the door's guards in order, and its error codes. A non-loopback Host (the tailnet) reaches the door and nothing else.
  3. The LAN listener. A self-signed P-256 certificate in inbox/tls/, its SHA-256 pinned by the phone and carried in the QR and in Bonjour _plannotator-inbox._tcp TXT v, fp.
  4. The relay. Ten routes, the Inbox's socket frames, the envelope (packages/core/crypto.ts's exact layout under K = HKDF-SHA256(S, device id, "plannotator-inbox relay key v1")), the relay secret derivation, the push plaintext and the APNs body, and what the relay stores and deletes.
  5. The surface bridge. packages/core/inbox-surface-bridge.ts v1: seven messages to the surface and eight back, the asset scheme and the <base href> rewrite.
  6. Idempotency. idempotency_key in the body of every phone command, the door's replay log inbox/device-commands.jsonl, and the relay command id set to the same key.
  7. 37 curl exchanges for P1, P2, R1 and R2 to replay, with low-entropy fixtures.

Plus one line in AGENTS.md's Inbox section pointing at the file.

Limits named once

An offer lives 10 minutes. Its six-digit code closes after 5 wrong tries. The relay's mailbox creation reuses guides.show's existing brake, with no new number. Apple's ceilings (a 4096-byte push body, four actions) are named where they apply. There are no other limits.

Review fixes (REVIEW-PLANNOTATOR-MOBILE-C1-2026-10-08, head c4a2f0e)

  1. The relay switch. PUT /v1/mailboxes/:mbx/devices/:dev/carriage { on, cursor? } (device bearer, exchange 7.35). The state lives in the device record (carriage) and in hello. A reconnect registers only non-revoked devices with carriage on that the relay does not list, and a device that turned carriage off stays listed, so it is never registered again. DELETE .../devices/:dev is the Inbox's alone, on revoke.
  2. Screen 1.3, "On your tailnet". I took the simpler choice: the phone does not discover tailnet computers. Bonjour does not cross a tailnet and an iOS app cannot ask Tailscale for its peers. The section lists the computers the phone already knows from a QR, plus a row to type the host:port the Inbox prints. This matches the render's own caption for 1.3 ("A tailnet cannot be listed from another app, so the tailnet row takes the address the Inbox prints"), so it is a reading of the record, not a change to it.
  3. The raw-bytes route is gone. Share writes view's text to a file named attachment.name. Exchanges 7.16 to 7.35 moved down one number.

Also from the review's items, because each is one sentence: exchanges for the window's device list and revoke (7.36, 7.37), Edit as a re-save with the same annotation id, Idempotent-Replayed as the replay signal, and the shell fetching view again after an asset 404.

Additions beyond the brief's lists, each needed to work (three)

  • GET health on the door, as the phone's reachability probe for 9.2.
  • POST revoke on the door, for "Remove this source".
  • message_id, key, revision and context in the push plaintext (the lock-screen Send needs the first three, and render 7.1 draws the context). The surface's annotation message and focus field (4.3, 3.6) come with the bridge.

Rulings written in (section 9 of the file; no open questions remain)

  1. Pairing through the relay is not in v1. "Pair a phone" offers the Wi-Fi switch.
  2. mailbox is out of the QR. The redemption's answer carries the relay.
  3. The carriage route, as above.
  4. Pushes go out for questions and guided reviews only.
  5. No sweep. A phone that reads directly acknowledges by store cursor (ack { cursor }).
  6. Mailbox creation reuses guides.show's existing per-IP rule, named once, with no new number.
  7. openssl and dns-sd (avahi-publish on Linux) run as child processes, with no dependency.
  8. The tailnet HTTPS port is 8443, kept in inbox.json. When another mapping holds it, the window says so and never overwrites it.

What it leaves out

Workspaces (W0 to W2), FCM and Android, the iPad, mailbox deletion, retention sweeps, relay rates, attestation, request signing, re-authentication, and every computer-chore route.

Gates

  • bun run check:release-version: green (v0.28.8).
  • bun run typecheck: green.
  • gitleaks git . --config .gitleaks.toml --log-opts="origin/main..HEAD": no leaks.

The pairing offer and QR link, the device token door and its allowlist,
the LAN listener, the relay's routes, envelope and push payload, the
surface bridge, the idempotency rule, and a curl exchange per route.
No product code.
The relay switch is PUT .../carriage, kept in the device record, and a
reconnect registers only devices with carriage on. Screen 1.3's tailnet
list is the known computers plus a typed address (no discovery). The raw
attachment route is gone: Share uses view. Section 9 now records the
coordinator's rulings in place of the open questions.
@backnotprop
backnotprop merged commit 47486cd into main Oct 8, 2026
28 checks passed
@backnotprop
backnotprop deleted the feat/mobile-contract branch October 8, 2026 03:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant