sync-and-recovery gains the split bullet: a provision ticket becomes a derived sync sink plus in-memory admin capability, sealed behind a passkey only through an explicit ceremony, with the password-manager copy as the durable one on no-PRF devices. auth.md documents the attempt-don't-detect posture and the combined assert+derive ceremony. store-provisioning points browser flows at the capability custody for the provision URL. tickets-explained tells the user-facing version of the split. Decision record 0001's follow-up reflects phase 2 landing. Part of #123 (A3 phase 2).
- commit
- 1d6e5293761b1633d21722e7f14cb2677d2c32ff
- parent
- 5e158e58f6809b4831e6096ad3cb829f1eca13ce
- author
- Dami <dami.etoile@gmail.com>
| M | docs/ | 7 | ++++++- |
| M | docs/ | 6 | +++--- |
| M | docs/ | 7 | +++++-- |
| M | docs/ | 4 | +++- |
| M | docs/ | 9 | +++++++++ |
@@ -34,7 +34,12 @@ The runtime lives at `package/runtime/auth.ts` and is exposed through `@nzip/lof
3434 secret bound to the credential, from which `deriveAtRestKey` produces an AES-GCM key via3535 HKDF-SHA-256. PRF support is feature-detected (`getAuthCapability`); if the client or3636 authenticator does not return a PRF result, the derive path throws `prf-unavailable` rather than37- inventing a key.37+ inventing a key. The capability report is a UX hint only — flows that seal data attempt the38+ evaluation and record what actually succeeded, never gate on the report.39+- **Assertion and PRF evaluation compose into one prompt.** `authenticateAndDerivePrfSecret` rides40+ the PRF evaluation on a user-verifying assertion, so a flow that needs both the asserted41+ credential and a credential-bound key (sealing or unlocking an envelope) costs one ceremony, with42+ the same `credential-mismatch` pinning as plain authentication.3843 - **No recovery claim for this primitive.** `enrollDeviceCredential` and the PRF-derived key do not3944 contain the Jazz account secret, so they cannot restore an account. Account recovery uses the4045 separate `createRecoverablePasskeyBackup` / `restoreFromPasskey` flow documented in@@ -89,6 +89,6 @@ The stored declaration is no longer cleartext; declaration APIs (`declareDataSin
8989 9090 ## Follow-up9191 92-PRF slot wiring, the enrollment exchange flow, and password-manager custody UX (tracked as A3 phase93-2 on #123); field encryption scoped in #126 with the account-private/group-shared split; claim94-alignment in #122 §2 and §4.92+PRF slot wiring, the enrollment exchange flow, and password-manager custody UX landed as A3 phase 293+(`provision.ts`, `authenticateAndDerivePrfSecret`, `TicketEnrollForm`); field encryption is scoped94+in #126 with the account-private/group-shared split; claim alignment continues in #122 §2 and §4.@@ -61,5 +61,8 @@ You paste; the app does the rest. The ticket is validated (against the same rule
6161 enforces; the two repos share machine-readable conformance fixtures), declared as the device's sync6262 location, and sync is elected in one step. The app keeps the ticket URL in a sealed device-local6363 record (encrypted at rest under a device-bound key) and never surfaces it through its session64-snapshot — only the host and your label show. For app developers, the framework call behind that65-paste is `enrollSyncTicket`, documented in [Sync and recovery](/docs/sync-and-recovery).64+snapshot — only the host and your label show. A provision ticket gets split first: the app asks the65+node for a derived sync ticket to store, and the provision original is either sealed behind your66+passkey or kept only in memory with your password manager holding the durable copy. For app67+developers, the framework call behind that paste is `enrollSyncTicket`, documented in68+[Sync and recovery](/docs/sync-and-recovery).@@ -25,7 +25,9 @@ so the authoring surface stays free of the provisioning client.
2525 ## Classify before you connect2626 2727 With store administration (admin secret, or a provision-scoped ticket URL as `serverUrl` with28-`adminSecret` omitted):28+`adminSecret` omitted — in a browser app the provision URL comes from the runtime's capability29+custody: held in memory after enrollment, or unlocked through its passkey ceremony with30+`unlockProvisionCapability`; see [Sync and recovery](sync-and-recovery.md)):2931 3032 ```ts3133 import { readStoreStatus } from "@nzip/lofi/schema/store";@@ -84,6 +84,15 @@ Semantics to rely on:
8484 `scope: "provision"` ticket additionally administers the store through the node's gate — the basis8585 for opt-in store provisioning. A ticket with an unrecognized scope is rejected outright rather8686 than silently granted less than it claims.87+- **A provision ticket is split before anything persists.** Enrollment asks the node's scope-down88+ exchange for a derived sync ticket; that becomes the declared sink, and the provision capability89+ is only _held_ in memory. Sealing it at rest is a separate, explicit passkey ceremony90+ (`sealProvisionCapability` / `unlockProvisionCapability`), attempted rather than91+ capability-detected; on a device whose authenticator cannot evaluate the WebAuthn PRF extension,92+ nothing persists and the durable copy is the ticket in the user's password manager. The packaged93+ `TicketEnrollForm` (`@nzip/lofi/preact`) is shaped for exactly that custody — the ticket is a94+ `current-password` field a manager saves on first paste and autofills later. Against a node95+ without the exchange, the ticket enrolls as pasted, exactly as before.8796 - If the node revokes the ticket, requests fail with 401 and live sockets close; treat the stored8897 sink as dead and surface re-enrollment rather than retrying silently.8998