clone

a self-hosted sync node for lofi apps: jazz sync over iroh, ticket-gated, with no static ip or cloud needed

docs(site): the expiry contract on the write-verdicts page

Dami committed 994f088
Cleartext client-asserted deadline, arrival-time enforcement with a
two-minute skew tolerance, the honest-client threat model, and the
soundness rule that makes local compensation safe — stated as the
contract for when enforcement exists, with today's behavior (deadlines
ignored) called out plainly.
commit
994f08824b59da3542edfe9a1aaf5021bc9e66f2
parent
15339c579900e81fe280667ef5dd89b99f2088a0
author
Dami <dami.etoile@gmail.com>
1 file changed, +64 -4
Mdocs/site/write-verdicts.md68++++++++++++++++++++++++++++--
M docs/site/write-verdicts.md +64 -4
@@ -66,10 +66,70 @@ the package exports (`classifyMutationError`, `MUTATION_ERROR_CLASSES`):
6666 | `transient` | No adjudication happened (unreachable, timeout) | Write stays pending; keep waiting |6767 6868 The pinned alpha emits exactly one rejection code over the sync protocol: `permission_denied`,69-classified permanent. Codes not in the registry classify as transient by design — an unrecognized70-code must never trigger irreversible compensation, because waiting is recoverable and a wrong71-`rejected` verdict is not. As the upstream code space grows (validation, schema enforcement), codes72-enter the registry here rather than in every consumer.69+classified permanent. The registry also carries `expired`, classified permanent; its semantics are70+the [expiry contract below](#expiry). Codes not in the registry classify as transient by design — an71+unrecognized code must never trigger irreversible compensation, because waiting is recoverable and a72+wrong `rejected` verdict is not. As the upstream code space grows (validation, schema enforcement),73+codes enter the registry here rather than in every consumer.74+75+## Expiry76+77+A write may declare a deadline: a wall-clock instant after which the app no longer wants it78+accepted. This section states the enforcement contract. **The pinned Jazz alpha does not enforce79+it** — no deadline travels on the wire today, no store rejects a late write, and the `expired` code80+is never emitted. The taxonomy entry and everything below define the behavior a store commits to81+once enforcement exists, so apps written against this page need no changes when it arrives.82+83+### The deadline is cleartext, and client-asserted84+85+Private transaction payloads are encrypted end to end; neither the sync node's access gate nor the86+store's engine can read them. A deadline the store is supposed to enforce therefore cannot live in87+the payload — it travels as an optional field on the transaction's cleartext envelope, asserted by88+the writing client as a wall-clock time. The store never inspects what the write does, only when it89+arrived.90+91+### The enforcement rule and the skew tolerance92+93+The store compares its own arrival time against the declared deadline and refuses the write when94+95+```96+arrival > expiresAt + tolerance97+```98+99+The refusal surfaces as a rejected batch fate with `code: "expired"`, through the same settlement100+path as `permission_denied`, with the same three properties: correlated to the originating batch id,101+re-derived on reconnect, and deterministic. `expired` classifies as **permanent**: late arrival is102+final, because waiting or re-sending only arrives later still.103+104+The tolerance is **two minutes**. Deadlines are client-asserted wall-clock times, so enforcement is105+only as exact as the two clocks involved: devices with OS time sync sit within seconds of true time,106+and two minutes is a wide margin above realistic drift while keeping the moment a client may safely107+give up close to the intent's own deadline. A device whose clock is wrong by more than the tolerance108+forfeits the guarantee for its own writes, which is consistent with the threat model below.109+110+### What expiry is for — and what it is not111+112+The threat model is the **honest client**. A deadline protects an app from its own stale intents: a113+write composed before a crash, a queue drained days later, an offer that should not surface after114+the window it was meant for. It is not an integrity control. The deadline is asserted by the writer,115+so a malicious client simply omits it, or writes a fresh timestamp; nothing about expiry constrains116+what a writer can do. Guarantees against hostile writers come from117+[permissions](provision-a-store.md), never from expiry.118+119+### The soundness property for clients120+121+Client-side expiry alone is unsound: a client can never prove that a pending write is not already in122+flight, or was not delivered before a crash, so compensating locally risks watching the write sync123+anyway. Server-side enforcement inverts this. The store accepts a write until `expiresAt` plus the124+tolerance on its own clock, so once a client — granting its own clock the same two-minute tolerance125+— observes its clock pass `expiresAt` plus twice the tolerance, future acceptance is impossible by126+construction: one tolerance for the store's acceptance window, one for the client's own possible127+offset. From that instant, retiring the write locally and running compensation is safe even for a128+device that never reconnects.129+130+Until enforcement exists, none of this holds: a declared deadline is ignored, an expired-looking131+pending write may still be accepted whenever the device reconnects, and clients must keep treating132+such writes as pending.73133 74134 ## Pending is not offline75135