clone

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

docs: reframe the expiry design record as the reference spec for lofi#145

Dami committed ad9317e
The document records the vendor surface lofi would prefer if jazz offered
it; upstream wishes are labeled reference issues, and node-layer enforcement
is designed separately against the same contract. The wire-level sketch now
states the envelope fields and digest coverage precisely: no expiry field
exists in the submission path today, the batch digest covers only the member
manifest (so covering expires_at needs a v2 manifest), captured_frontier is
compatibility-only, and the rejection code is an open string.
commit
ad9317eb213b1e5473ea4381fe1400406d268f3c
parent
e8684e96eb776819d0b23ad7d52bdee5b6c880b5
author
Dami <dami.etoile@gmail.com>
1 file changed, +27 -10
Mdocs/upstream-write-expiry.md37++++++++++++++++++++++--------
M docs/upstream-write-expiry.md +27 -10
@@ -1,6 +1,13 @@
1-# Upstream ask: server-enforced write expiry (`expiresAt` on the cleartext envelope)1+# Design record: engine-enforced write expiry (`expiresAt` on the submission envelope)22 3-Target: jazz / jazz-napi (engine + sync protocol). Consumer: lofi-node and the lofi effect system.3+Reference spec for the vendor surface lofi would prefer if jazz offered it: an `expiresAt` field on4+the sync submission envelope with an engine-enforced `expired` verdict. Recorded as5+FelineStateMachine/lofi#145 (upstream wishes are labeled reference issues; nothing is filed with6+jazz). Enforcement at the node layer is designed separately against the same contract in7+[write-verdicts](site/write-verdicts.md#expiry).8+9+Target surface: jazz / jazz-napi (engine + sync protocol). Consumer: lofi-node and the lofi effect10+system.411 512 ## Motivation613
@@ -14,12 +21,19 @@ compensation safe, even for a device that never reconnects.
1421 1522 ## Requested surface1623 17-1. **Optional `expiresAt` on the transaction/batch cleartext envelope.** Private transaction18- payloads are encrypted, so the engine cannot inspect them; the deadline must live on the sealed19- batch submission's cleartext envelope (alongside the batch digest and captured frontier), as an20- optional client-asserted wall-clock timestamp (epoch milliseconds, UTC). Absent means "never21- expires" — every existing write keeps its meaning. The field should be covered by the batch22- digest so it cannot be stripped or altered in transit without invalidating the submission.24+1. **Optional `expiresAt` on the sealed batch submission envelope.** The deadline belongs on the25+ envelope, not in row data: enforcement must read only the declared deadline and an arrival clock,26+ never interpret application payloads (which may include client-sealed columns). At the pinned27+ alpha the envelope is28+ `SealedBatchSubmission { batch_id, mode, target_branch_name,29+ batch_digest, members, captured_frontier }`30+ — no expiry or deadline field exists anywhere in the submission path. The design adds31+ `expires_at` as an optional client-asserted wall-clock timestamp (epoch milliseconds, UTC).32+ Absent means "never expires" — every existing write keeps its meaning. The field must be covered33+ by the batch digest so it cannot be stripped or altered in transit without invalidating the34+ submission; today the digest is computed over the member manifest only (a domain-tagged hash of35+ member count, object ids, and row digests), so covering `expires_at` means a new manifest version36+ in the digest preimage.2337 2438 2. **Engine-side comparison at the sync node.** On first authoritative adjudication of a batch, the2539 engine compares its own arrival time against `expiresAt` and rejects when
@@ -87,5 +101,8 @@ db.onMutationError((event) => {
87101 });88102 ```89103 90-Wire-level sketch: `SealedBatchSubmission` gains an optional `expires_at` (epoch ms), and91-`BatchFate::Rejected` carries `code: "expired"`. No other message changes.104+Wire-level sketch: `SealedBatchSubmission` gains an optional `expires_at` (epoch ms) covered by a v2105+batch-digest manifest, and `BatchFate::Rejected` — whose `code` is an open string, not a closed106+enum, so no wire-format change is needed for the new code — carries `"expired"`. The107+`captured_frontier` field is compatibility-only at the pinned alpha (conflicts are validated from108+per-row parents) and plays no part in this design. No other message changes.