Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions docs/specs/mobile-terminal-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Touch mode is global, so **each mounted pane's mouse override is a pure function
of that mode and the pane's *own* mouse-reporting state** (`selection` +
reporting ≠ `none` → `permanent`, else `off`), recomputed for **every** pane,
not just the active one — a pane switched away from must not keep a stale
override. The consumer owns this wiring.
override. `lib` exports the function; the consumer owns the loop.

Select mode **must route touch and pen drags through the shared terminal
mouse-selection router**, never a mobile-only one, so every selection and copy
Expand Down Expand Up @@ -99,8 +99,9 @@ the pointer, and **must never reach xterm or the pane** for focus, selection,
or pane interaction. **Non-primary mouse buttons
are ignored**, so their browser or host behavior continues.

Source of truth: `TOUCH_MODES` in `lib/src/components/MobileTerminalUi.tsx`;
per-pane override wiring in `lib/src/remote/pocket-app/PocketWall.tsx` and
Source of truth: `TOUCH_MODES` and `paneMouseOverride` in
`lib/src/components/MobileTerminalUi.tsx`; per-pane wiring in
`lib/src/remote/pocket-app/PocketWall.tsx` and
`website/src/components/PocketTerminalExperience.tsx`.

## Gesture mode
Expand Down Expand Up @@ -234,7 +235,7 @@ same rule as the touch selector.
| Draft | Draft reserve copy, filling the reserve. |

Default input mode is **Type**. Recent and Draft are placeholder-only today and
say so in the reserve — the real features are staged (see [Future](#future)).
say so in the reserve ([Future](#future)).

**Must focus the hidden input synchronously inside the Type selector's tap/click
handler** (rationale). A follow-up effect retries via rAF and staggered timers
Expand Down
119 changes: 59 additions & 60 deletions docs/specs/pocket-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ navigates, and hands the text to `parsePairingInvitationUrl`
included — is not a setup code for this Relay. `pairingInvitationExpired`
re-runs that same parser at the epoch, so the grammar has no second copy.
* **The camera tracks stop on every way out** — accepted, cancelled, errored,
unmounted, and on a start that finished after the screen was gone. Only a
refused permission gets its own line; it leaves paste working.
unmounted, and on a start that finished after the screen was gone. **A refused
permission and an unusable camera each get their own line**, and no other
failure does; both leave paste working.
* **The invitation lives in memory-only ceremony state**, cleared on every
terminal outcome.

Expand Down Expand Up @@ -99,7 +100,8 @@ v1 grants no phone-side kill or layout authority (rationale).

**An inline image crosses the relay as the ordered `terminal.data` messages its
PTY chunks produce**, reassembled by Pocket's own xterm — panes are built through
the same `createXtermHost`, so ImageAddon is loaded. **Nothing coalesces them and
the same `createXtermHost`, so ImageAddon is loaded wherever the terminal's
`inlineImages` setting is on, as it is by default. **Nothing coalesces them and
no size gates them** (rationale); each is bounded only by what the Burrow feeds
its parser ([remote-api.md](./remote-api.md) → Terminal surfaces).

Expand Down Expand Up @@ -216,7 +218,7 @@ with **no `type: 'module'`**, pinned by `service-worker.test.ts`. **A worker tha
is not one classic self-contained script fails the build**:
`lib/scripts/assert-pocket-worker.mjs` runs last in `build:pocket` and owns the
exact refusals (rationale). **Root `pnpm build` runs `build:pocket`**, so CI
checks a real bundler output (rationale; `assert-pocket-worker.test.ts`).
checks a real bundler output (rationale).
`dev:pocket` bundles the same config in memory per `/sw.js` request (rationale).

- **The worker caches nothing and registers no `fetch` handler** (rationale). It
Expand Down Expand Up @@ -254,9 +256,11 @@ returns to sign-in** (`PasskeyUnavailableError`); the verified response restores
it on any profile.

**Pocket states the order on the screen that leads with the scan**, above the
action rather than after it: install to the Home Screen **first**, then scan,
approve the pairing on the machine, and enable push from within it — everything
partition-bound is minted from there, the passkey and each per-Burrow static.
action rather than after it: install to the Home Screen **first**, then scan and
approve the pairing on the machine — everything partition-bound is minted from
there, the passkey and each per-Burrow static. **Push is not named there**,
since whether it works at all depends on the Relay's push config; the Burrows
view's notice and push rows gate on that.

**One phone can hold two Client identities, so Pocket names the mode in the label
it suggests at pairing** — `Dormouse Pocket (Home Screen)` versus
Expand All @@ -279,7 +283,7 @@ order, and every unavailable result is named in the UI:**

| Result | Condition | UI consequence |
|---|---|---|
| `needs-install` | `navigator.standalone` exists but the app is not installed; checked before capability probes, since iOS tabs omit those APIs | Explain Home Screen install above the scan action and again on the Burrows view; with no prompt API it stays advice — a tab must still scan |
| `needs-install` | `navigator.standalone` exists but the app is not installed; checked before capability probes, since iOS tabs omit those APIs | Explain Home Screen install above the scan action, and again on the Burrows view unless push is `disabled` there; with no prompt API it stays advice — a tab must still scan |
| `unsupported` | Service workers, `Notification`, or `PushManager` unavailable after the install gate | Explain that this browser cannot receive push |
| `no-worker` | The tracked registration failed or resolved empty, commonly on an insecure origin | Explain the worker failure |
| `denied` | Notification permission is denied | Direct the user to browser settings |
Expand All @@ -303,9 +307,9 @@ fetch offers Retry, which only caches the key, and the next tap reveals Enable.
**Permission is requested on that separate, fresh tap** — a network round trip
can consume an iOS gesture's transient activation. (rationale)

Browser availability and Burrow registration are separate states: a
`PushSubscription` belongs to the service-worker scope; the Relay stores one row
per `(burrowId, deliveryId)`.
Browser availability and Burrow registration are separate states: the
`PushSubscription` belongs to the service-worker scope, the Relay's rows to
`(burrowId, deliveryId)` ([relay.md](./relay.md) → State files).

**Push is asked for once per device, on one card, on the Burrows view** — the
prompt and the subscription it mints are scope-wide; the per-Burrow rows are
Expand Down Expand Up @@ -337,8 +341,8 @@ settles at empty on its own behalf**: the card re-offers its idempotent Enable.
**A Relay row is necessary but not sufficient for Push notifications on.**
Pocket also checks that permission is still granted, that the scope holds a
`PushSubscription` minted for the Relay's current VAPID key, and that it points
at the registered address; any of the four failing re-exposes Enable. **The
Relay likewise omits rows under an old VAPID key** (rationale).
at the registered address; any of the four failing re-exposes Enable, and the
Relay omits such rows too ([relay.md](./relay.md) → Web Push).

**Pocket records a SHA-256 digest of the address each time the Relay accepts a
registration** (`dormouse-pocket:push-endpoint`, beside the `:passkey:` cache)
Expand Down Expand Up @@ -390,29 +394,26 @@ Source of truth: `isInstalledWebApp` / `requiresInstallForPush` /
## What Pocket stores

**Must have a successful current-page storage probe before a scan starts
registration, sign-in, token retirement, or pairing.** Share in-flight work and
cache successful selection only in memory for that page; never retain failures.
Invalidate selection after production store or key-generation failure.
Probe native storage first, then encrypted
storage only if native fails, using fresh disposable keys in Pocket's record
shape. Reopen and verify identical key agreement; reject missing or extractable
runtime keys. Both formats failing shows a storage
compatibility error without resetting pairing data. Attempt probe database
deletion on exit. (rationale)
registration, sign-in, token retirement, or pairing** — one probe per page,
shared in flight, cached only on success and only in memory, and invalidated by
any later production store or key-generation failure. **Must probe native
storage first and encrypted storage only if native fails**, on fresh disposable
keys in Pocket's own record shape, selecting a format only once it survives
reopen and identical key agreement. **Both formats failing shows a storage
compatibility error without resetting pairing data**, pointing at
`/diagnostics/index.html`. (rationale)
**Must identify the failed probe stage and an allowlisted exception name;
never display browser exception messages or key material.**
**Must keep the separate-key experiment out of pairing preflight.** Direct
compatibility failures to `/diagnostics/index.html`.
**Must keep the separate-key experiment out of pairing preflight.**

**Must use metadata-only summaries for listing, push registration/queries,
removal, and re-pair identity checks.** `getSummary` and `listSummaries` omit key
material without decryption/import. Corrupt keys cannot block these operations;
re-pairing requires fresh approval and preserves the Burrow pin. Connection
and push decryption use full records.
material without decryption or import, so a corrupt key blocks none of them;
connection and push decryption use full records.

**Must report connection-record read failures with fixed retry/scan recovery
text, never browser exception details or authorization changes.** A fresh scan
retains the pin and requires approval; a read failure grants nothing.
keeps the Burrow pin and requires fresh approval; a read failure grants nothing.

**Must use the selected format for new keys and decode both formats in the
shared page/worker store.** The encrypted format stores AES-GCM ciphertext,
Expand Down Expand Up @@ -452,35 +453,6 @@ in `lib/src/remote/client/pocket-client.ts`.

## Serving the built bundle

**Must serve the opt-in capability harness at `/diagnostics/index.html`, built
from `lib/pocket/diagnostics/` as a second Pocket HTML entry.** Test fresh keys and isolated temporary
storage, report stage failures and cleanup failures, and never read pairing
data, request passkeys or media permissions, or upload results. API presence
is observational; crypto storage success requires reopening and using the key.
**Must use the production key codec for encrypted round-trip and restart tests,
including authenticated context.** Diagnostics never open production databases.
Keep primitive checks independently generated and database-isolated.
**Must keep diagnostics platform-neutral and state which browser/app context
was tested.** API presence alone never certifies Android, iOS, or desktop support.

**Must retain a restart checkpoint only on explicit preparation, in a
diagnostic-only database, until explicit cleanup.** Verification requires a new
page instance and derives the saved expected result using the recovered key;
never claim page reload proves process termination. The diagnostic manifest has
its own identity and start URL. Reports omit key material. Pinned by
`lib/src/remote/client/capability-harness.test.ts`.
**Must identify harness v3 production-format reports and reject legacy restart
checkpoints with explicit cleanup/reprepare instructions**, never silently
reclassify experimental evidence. Preserve the v1 production envelope/context.

Source of truth: `runCapabilities` in
`lib/pocket/diagnostics/capabilities.js`; UI:
`lib/pocket/diagnostics/page.js`; restart: `verifyRestart` in
`lib/pocket/diagnostics/restart.js`; codec: `generatePocketKeyPair` /
`loadPocketPrivateKey` in `lib/src/remote/client/pocket-private-key.ts`.
Both built HTML shells are checked by `assertPocketShell` in
`lib/scripts/assert-pocket-worker.mjs`.

Content types need no special-casing: `serveStatic` already answers
`application/manifest+json` for `.webmanifest` and `text/javascript` for `sw.js`.

Expand All @@ -498,7 +470,36 @@ previous build's hashed assets (rationale). Two rules make it hold:
response's cache policy describes the response, and the shell is never a useful
answer to a subresource miss. (rationale)

Source of truth: `registerPocketServing` in `relay/src/app.ts`.
Source of truth: `registerPocketServing` in `relay/src/app.ts`. Both built HTML
shells are checked by `assertPocketShell` in
`lib/scripts/assert-pocket-worker.mjs`.

### The capability harness

**Must serve the opt-in capability harness at `/diagnostics/index.html`**, built
from `lib/pocket/diagnostics/` as a second Pocket HTML entry with its own
manifest identity and start URL. Four rules make its evidence worth anything:

- **Never read pairing data, open a production database, request passkeys or
media permissions, or upload results**; reports omit key material.
- **Must use the production key codec, authenticated context included**, for the
encrypted round-trip and restart tests. API presence is observational and
certifies no platform, so a report states which browser/app context it
measured; a crypto-storage pass requires reopening and using the key.
- **Must retain a restart checkpoint only on explicit preparation**, in a
diagnostic-only database, until explicit cleanup; verification needs a new page
instance and derives the saved expected result with the recovered key.
- **Never claim a page reload proves process termination.**

**Must identify harness v3 production-format reports and reject legacy restart
checkpoints with explicit cleanup/reprepare instructions**, never silently
reclassifying experimental evidence; the v1 production envelope and context are
preserved. Pinned by `lib/src/remote/client/capability-harness.test.ts`.

Source of truth: `runCapabilities` in `lib/pocket/diagnostics/capabilities.js`;
UI: `lib/pocket/diagnostics/page.js`; restart: `verifyRestart` in
`lib/pocket/diagnostics/restart.js`; codec: `generatePocketKeyPair` /
`loadPocketPrivateKey` in `lib/src/remote/client/pocket-private-key.ts`.

## A backgrounded phone loses its Burrow session

Expand Down Expand Up @@ -643,5 +644,3 @@ Source of truth: `pocketContentSecurityPolicy` in `relay/src/app.ts`,
3. **Theme picker in Pocket** — the app restores the persisted theme but exposes
no picker; add the shared `ThemePicker` (and its theme-debugger entry) once
its dropdown is phone-friendly.
4. **Onboarding friction** — Pocket carries the phone-side items of the
**selfhost-onboarding** scope ([relay.md](./relay.md) `## Future`).
2 changes: 0 additions & 2 deletions docs/specs/pocket-app.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ mode. [WebKit's iOS Web Push guidance](https://webkit.org/blog/13878/web-push-fo

**Why a failed or in-flight read never settles at empty.** An empty result and "not yet known" would render the same card. Re-offering the idempotent Enable costs a redundant registration at worst, while a stale **Push notifications on.** claim hides the repair entirely.

**Why the Relay omits rows under an old VAPID key.** After a key rotation those rows can never be delivered to. Serving them would let a device that repaired one Burrow see another Burrow's dead endpoint as current, and stop offering the Enable that would fix it.

**Why the push endpoint is fingerprinted.** A push service may rotate an address on its own with the VAPID key unchanged: the subscription stays valid and correctly keyed while every stored Relay row points somewhere unreachable — a state no other check can see. One scope holds one subscription, so a move invalidates every Burrow row for that device at once, and one recorded digest covers them all.

**Why a matching subscription is reused rather than replaced.** Calling `subscribe()` again with a matching `applicationServerKey` mints a new endpoint and invalidates the one already stored for every other Burrow — turning a single Burrow's registration into a silent outage for all of them.
Expand Down
Loading
Loading