@cello-protocol/daemon 0.0.120 → 0.0.122

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/content-park.d.ts +1 -1
  2. package/dist/content-park.d.ts.map +1 -1
  3. package/dist/content-park.js +64 -12
  4. package/dist/content-park.js.map +1 -1
  5. package/dist/daemon.d.ts.map +1 -1
  6. package/dist/daemon.js +46 -32
  7. package/dist/daemon.js.map +1 -1
  8. package/dist/document-delivery-transport.d.ts +78 -0
  9. package/dist/document-delivery-transport.d.ts.map +1 -0
  10. package/dist/document-delivery-transport.js +109 -0
  11. package/dist/document-delivery-transport.js.map +1 -0
  12. package/dist/document-delivery.d.ts +130 -0
  13. package/dist/document-delivery.d.ts.map +1 -0
  14. package/dist/document-delivery.js +246 -0
  15. package/dist/document-delivery.js.map +1 -0
  16. package/dist/document-engine.d.ts +134 -0
  17. package/dist/document-engine.d.ts.map +1 -0
  18. package/dist/document-engine.js +281 -0
  19. package/dist/document-engine.js.map +1 -0
  20. package/dist/document-gate.d.ts +139 -0
  21. package/dist/document-gate.d.ts.map +1 -0
  22. package/dist/document-gate.js +465 -0
  23. package/dist/document-gate.js.map +1 -0
  24. package/dist/document-handshake.d.ts +88 -0
  25. package/dist/document-handshake.d.ts.map +1 -0
  26. package/dist/document-handshake.js +239 -0
  27. package/dist/document-handshake.js.map +1 -0
  28. package/dist/document-lifecycle.d.ts +104 -0
  29. package/dist/document-lifecycle.d.ts.map +1 -0
  30. package/dist/document-lifecycle.js +363 -0
  31. package/dist/document-lifecycle.js.map +1 -0
  32. package/dist/document-notify.d.ts +130 -0
  33. package/dist/document-notify.d.ts.map +1 -0
  34. package/dist/document-notify.js +313 -0
  35. package/dist/document-notify.js.map +1 -0
  36. package/dist/document-reachability.d.ts +42 -0
  37. package/dist/document-reachability.d.ts.map +1 -0
  38. package/dist/document-reachability.js +72 -0
  39. package/dist/document-reachability.js.map +1 -0
  40. package/dist/document-rejection.d.ts +224 -0
  41. package/dist/document-rejection.d.ts.map +1 -0
  42. package/dist/document-rejection.js +374 -0
  43. package/dist/document-rejection.js.map +1 -0
  44. package/dist/document-store.d.ts +269 -0
  45. package/dist/document-store.d.ts.map +1 -0
  46. package/dist/document-store.js +752 -0
  47. package/dist/document-store.js.map +1 -0
  48. package/dist/document-write-path.d.ts +84 -0
  49. package/dist/document-write-path.d.ts.map +1 -0
  50. package/dist/document-write-path.js +412 -0
  51. package/dist/document-write-path.js.map +1 -0
  52. package/dist/line-lcs.d.ts +51 -0
  53. package/dist/line-lcs.d.ts.map +1 -0
  54. package/dist/line-lcs.js +71 -0
  55. package/dist/line-lcs.js.map +1 -0
  56. package/dist/reconnect-drain.d.ts +15 -0
  57. package/dist/reconnect-drain.d.ts.map +1 -0
  58. package/dist/reconnect-drain.js +51 -0
  59. package/dist/reconnect-drain.js.map +1 -0
  60. package/dist/session-node-manager.d.ts +39 -0
  61. package/dist/session-node-manager.d.ts.map +1 -1
  62. package/dist/session-node-manager.js +152 -12
  63. package/dist/session-node-manager.js.map +1 -1
  64. package/dist/session-relay-client.d.ts +2 -0
  65. package/dist/session-relay-client.d.ts.map +1 -1
  66. package/dist/session-relay-client.js +16 -1
  67. package/dist/session-relay-client.js.map +1 -1
  68. package/package.json +6 -5
@@ -0,0 +1,281 @@
1
+ /**
2
+ * DOD-DOC-ENGINE-1 — the daemon's Y.Doc lifecycle.
3
+ *
4
+ * The engine owns HOW to apply; the store (DOD-DOC-STORE-1) owns what to replay and in what
5
+ * order. `replay` takes whole envelope rows rather than payload bytes so it can tell a
6
+ * payload-free AUDIT record (a withdrawal or rejection, which is expected and skipped) from a
7
+ * payload-free PURGED update (an operation whose bytes are gone, which must refuse). That is the
8
+ * only thing `kind` is read for — `referencesEnvelopeHash` is pure audit at replay time, never
9
+ * read, because a withdrawal excludes nothing (§16.4; see `replay`).
10
+ *
11
+ * EVERY GUARD BELOW IS A RESPONSE TO A MEASUREMENT, not to a guess. DOD-DOC-FUZZ-1 fuzzed
12
+ * `Y.applyUpdate` and found:
13
+ *
14
+ * - Malformed input THROWS — so a wrapped apply genuinely contains it, and V1 needs no sandbox.
15
+ * - But the dangerous class is what Yjs ACCEPTS. An update whose dependencies the receiver
16
+ * lacks returns success, contributes nothing, and is RETAINED forever in
17
+ * `doc.store.pendingStructs` — a peer streams those until the daemon dies, and a try/catch
18
+ * sees only success. Hence the pending-set check after every apply.
19
+ * - An empty or one-byte update throws a lib0 DECODER error ("Unexpected end of array"), which
20
+ * names Yjs internals rather than a protocol fault. Hence a floor as well as a cap.
21
+ * - Yjs does not bound nesting depth at all, and the size cap bounds it poorly (~16 bytes per
22
+ * level, so roughly 65,000 levels fit in 1 MiB). Structural limits are DOD-DOC-GATE-1's job;
23
+ * the engine's contract is only that a bad update is a typed error, never a crash.
24
+ *
25
+ * ONE TYPED REASON PER FAILURE CLASS. A lib0 string like "Integer out of Range" describes where
26
+ * the decoder gave up, not what the peer did wrong, so it travels as `detail` and never as the
27
+ * reason an operator or a policy log sees.
28
+ */
29
+ import * as Y from "yjs";
30
+ /** Thrown by the *OrThrow variants and by `replay`, where returning a verdict would let a caller
31
+ * persist a state that was never fully applied. */
32
+ export class DocumentUpdateError extends Error {
33
+ reason;
34
+ detail;
35
+ constructor(reason, detail) {
36
+ super(detail ? `${reason}: ${detail}` : reason);
37
+ this.name = "DocumentUpdateError";
38
+ this.reason = reason;
39
+ this.detail = detail;
40
+ }
41
+ }
42
+ /** Pre-parse size cap — bytes are refused on LENGTH before Yjs is invoked at all. */
43
+ const MAX_UPDATE_BYTES = 1024 * 1024;
44
+ /**
45
+ * The minimum valid update. An empty encoded state is two bytes (`[0,0]`), measured — anything
46
+ * shorter throws a decoder error that says nothing about the protocol.
47
+ */
48
+ const MIN_UPDATE_BYTES = 2;
49
+ /** The single root text field a V1 text document uses. */
50
+ const TEXT_ROOT = "content";
51
+ export class DocumentEngine {
52
+ #logger;
53
+ constructor(logger) {
54
+ this.#logger = logger;
55
+ }
56
+ /** The pre-parse cap, exposed so callers and tests agree on one number. */
57
+ get maxUpdateBytes() {
58
+ return MAX_UPDATE_BYTES;
59
+ }
60
+ /**
61
+ * A fresh live document.
62
+ *
63
+ * Yjs mints its own random clientID and NOTHING here touches it (§14). Deriving it from agent
64
+ * identity, or persisting and restoring one, means two live docs can share it — and
65
+ * DOD-DOC-FUZZ-1 measured that outcome: the colliding writer silently wins, the honest client's
66
+ * update is accepted-and-dropped, and the document becomes a splice of two authors with an
67
+ * empty pending set and no error on any path.
68
+ */
69
+ createDocument(startingContent = "") {
70
+ const doc = new Y.Doc();
71
+ if (startingContent.length > 0)
72
+ doc.getText(TEXT_ROOT).insert(0, startingContent);
73
+ return doc;
74
+ }
75
+ /**
76
+ * Read the single text root a text-typed document uses.
77
+ *
78
+ * Named for the root it reads, not for "the document's content". `DocumentRow.documentType`
79
+ * admits markdown, json and xml; on the latter two the data does not live in this root, and an
80
+ * unnamed `readText` would return "" — indistinguishable from an empty document. Structured
81
+ * types get their own accessors when a unit needs them.
82
+ */
83
+ readTextRoot(doc) {
84
+ return doc.getText(TEXT_ROOT).toString();
85
+ }
86
+ insertIntoTextRoot(doc, index, text) {
87
+ doc.getText(TEXT_ROOT).insert(index, text);
88
+ }
89
+ /** The full state, or just what a peer holding `sinceStateVector` is missing (§7). */
90
+ encodeState(doc, sinceStateVector) {
91
+ return Y.encodeStateAsUpdate(doc, sinceStateVector);
92
+ }
93
+ encodeStateVector(doc) {
94
+ return Y.encodeStateVector(doc);
95
+ }
96
+ snapshot(doc) {
97
+ return { binary: Y.encodeStateAsUpdate(doc), stateVector: Y.encodeStateVector(doc) };
98
+ }
99
+ /**
100
+ * Materialize a document from a snapshot binary.
101
+ *
102
+ * The restored document mints a FRESH clientID — the binary carries the operations, never an
103
+ * identity to resume under. See `createDocument` for what sharing one costs.
104
+ */
105
+ restore(binary) {
106
+ const doc = new Y.Doc();
107
+ try {
108
+ Y.applyUpdate(doc, binary);
109
+ }
110
+ catch (err) {
111
+ // This is the one method that reads bytes off disk, and it used to be a bare applyUpdate —
112
+ // so a corrupt `document_snapshots` row surfaced as "Unexpected end of array", the exact
113
+ // lib0 string this module says must never be a reason. It named Yjs internals and pointed
114
+ // an operator at the wrong subsystem.
115
+ const detail = err instanceof Error ? err.message : String(err);
116
+ this.#logger.error("document.snapshot.malformed", { detail });
117
+ throw new DocumentUpdateError("document_snapshot_malformed", detail);
118
+ }
119
+ if (doc.store.pendingStructs !== null) {
120
+ // A snapshot that decodes but is incomplete would otherwise restore SHORT and silent.
121
+ this.#logger.error("document.snapshot.incomplete", {
122
+ missing: doc.store.pendingStructs.missing.size,
123
+ });
124
+ // Its OWN class. An incomplete snapshot is not "an incoming update depends on absent
125
+ // structs" — same symptom, different fault, and one reason per class is this module's rule.
126
+ throw new DocumentUpdateError("document_snapshot_incomplete", `snapshot is missing operations from ${doc.store.pendingStructs.missing.size} client(s)`);
127
+ }
128
+ return doc;
129
+ }
130
+ /**
131
+ * Apply one update, with every guard the fuzz pass motivated.
132
+ *
133
+ * Never throws and never leaves the document half-integrated: an update that cannot be fully
134
+ * resolved is applied to a THROWAWAY doc first, so the caller's document is untouched when the
135
+ * answer is no.
136
+ */
137
+ /**
138
+ * Apply one update to a caller's live document, leaving it untouched if the answer is no.
139
+ *
140
+ * The trial runs on a FRESH scratch document every time. Reusing one across calls is unsound:
141
+ * `Y.applyUpdate` MERGES, it does not reset, so a scratch doc seeded from an empty document
142
+ * still holds the previous trial's operations — and with that residue present, an update whose
143
+ * dependencies the target lacks reports an EMPTY pending set. The one guard that catches the
144
+ * accept class would go green on exactly the input it exists to refuse. (Measured, not
145
+ * reasoned: a shadow holding "AAA" re-seeded from an empty doc still reads "AAA".)
146
+ */
147
+ applyUpdate(doc, update) {
148
+ return this.#applyChecked(doc, new Y.Doc(), update);
149
+ }
150
+ #applyChecked(doc, shadow, update) {
151
+ if (update.length > MAX_UPDATE_BYTES) {
152
+ return this.#refuse("document_update_too_large", `${update.length} bytes exceeds the ${MAX_UPDATE_BYTES}-byte cap`, update);
153
+ }
154
+ if (update.length < MIN_UPDATE_BYTES) {
155
+ return this.#refuse("document_update_too_small", `${update.length} bytes is below the ${MIN_UPDATE_BYTES}-byte minimum for a Yjs update`, update);
156
+ }
157
+ // Trial it on scratch state first. Yjs has no atomic-apply mode, and an update that resolves
158
+ // only partially would otherwise leave the caller's document in a state nobody chose.
159
+ try {
160
+ Y.applyUpdate(shadow, Y.encodeStateAsUpdate(doc));
161
+ Y.applyUpdate(shadow, update);
162
+ }
163
+ catch (err) {
164
+ return this.#refuse("document_update_malformed", err instanceof Error ? err.message : String(err), update);
165
+ }
166
+ // THE ACCEPT CLASS. Yjs returned success — that is not evidence the update integrated. A
167
+ // non-empty pending set means it is waiting on structs that never arrived; admitting it would
168
+ // retain them indefinitely while contributing nothing (measured, DOD-DOC-FUZZ-1).
169
+ //
170
+ // PRECONDITION THIS IMPLIES: a sender's envelopes must be delivered in chain order. Yjs would
171
+ // otherwise BUFFER an out-of-order update and resolve it on the next state-vector exchange;
172
+ // this refuses instead. That is right for replay, where a gap means a corrupt log — a live
173
+ // receive path wanting buffer-and-re-request semantics needs its own entry point, not this one.
174
+ // BOTH pending sets: an update carrying a delete set for structs never seen leaves
175
+ // pendingStructs null and pendingDs populated, and retains just the same.
176
+ if (shadow.store.pendingStructs !== null || shadow.store.pendingDs !== null) {
177
+ return this.#refuse("document_update_unresolved_dependencies", shadow.store.pendingStructs
178
+ ? `depends on ${shadow.store.pendingStructs.missing.size} client(s) whose earlier operations are absent`
179
+ : "carries a delete set referring to operations this document has never seen", update);
180
+ }
181
+ Y.applyUpdate(doc, update);
182
+ return { ok: true };
183
+ }
184
+ /**
185
+ * Apply straight to `doc`, checking the pending set afterwards.
186
+ *
187
+ * No trial copy, because atomicity buys nothing here: `replay` throws on the first failure and
188
+ * the half-built document is discarded whole. Trialling each envelope would cost a full
189
+ * encode/decode of a growing document per envelope — the fold is O(n·|doc|) either way, but
190
+ * this halves the constant and removes the aliasing hazard entirely.
191
+ */
192
+ #applyDirect(doc, update) {
193
+ if (update.length > MAX_UPDATE_BYTES) {
194
+ return this.#refuse("document_update_too_large", `${update.length} bytes exceeds the ${MAX_UPDATE_BYTES}-byte cap`, update);
195
+ }
196
+ if (update.length < MIN_UPDATE_BYTES) {
197
+ return this.#refuse("document_update_too_small", `${update.length} bytes is below the ${MIN_UPDATE_BYTES}-byte minimum for a Yjs update`, update);
198
+ }
199
+ try {
200
+ Y.applyUpdate(doc, update);
201
+ }
202
+ catch (err) {
203
+ return this.#refuse("document_update_malformed", err instanceof Error ? err.message : String(err), update);
204
+ }
205
+ if (doc.store.pendingStructs !== null || doc.store.pendingDs !== null) {
206
+ return this.#refuse("document_update_unresolved_dependencies", doc.store.pendingStructs
207
+ ? `depends on ${doc.store.pendingStructs.missing.size} client(s) whose earlier operations are absent`
208
+ : "carries a delete set referring to operations this document has never seen", update);
209
+ }
210
+ return { ok: true };
211
+ }
212
+ /** Every refusal is logged, not merely returned — a return value nobody reads is not observable. */
213
+ #refuse(reason, detail, update) {
214
+ this.#logger.warn("document.update.refused", { reason, detail, bytes: update.length });
215
+ return { ok: false, reason, detail };
216
+ }
217
+ /** `applyUpdate` for callers that want the failure to be unmissable. */
218
+ applyUpdateOrThrow(doc, update) {
219
+ const res = this.applyUpdate(doc, update);
220
+ if (!res.ok)
221
+ throw new DocumentUpdateError(res.reason, res.detail);
222
+ }
223
+ /**
224
+ * Fold an ordered envelope log into a materialized state — the `ReplayFn` the store injects.
225
+ *
226
+ * **A WITHDRAWAL EXCLUDES NOTHING.** §16.4 is explicit: withdrawing rolls the change back with
227
+ * a *Yjs undo* and writes a record "beside the original envelope — marked withdrawn, never
228
+ * deleted, so the log stays intact". Rejection resolves the same way, by supersession —
229
+ * "inverses, not erasure" (§3.2). So the undo is itself an ordinary update in the log, and
230
+ * replay simply applies every payload in order.
231
+ *
232
+ * An earlier version of this method excluded the referenced envelope instead, which is
233
+ * unsound in a CRDT log and was measured to be so: Yjs operations are causally chained, so
234
+ * dropping any but the LAST envelope leaves every later one depending on structs that never
235
+ * arrive — the document rebuilds until the next daemon restart and is permanently unopenable
236
+ * after it. It also let ANY sender suppress ANY other sender's content by appending a
237
+ * payload-free row, since nothing upstream checks authorship of a reference. Both problems
238
+ * dissolve when the log is simply replayed, which is what the spec said to do.
239
+ *
240
+ * REFUSES rather than skipping. A payload that will not apply means the log is corrupt, and
241
+ * folding the rest would produce a document that reads as complete while missing operations —
242
+ * the silent divergence the whole two-layer design exists to prevent.
243
+ */
244
+ replay(envelopes) {
245
+ const doc = new Y.Doc();
246
+ let applied = 0;
247
+ for (const e of envelopes) {
248
+ if (e.payload === null) {
249
+ // A withdrawal or rejection RECORD legitimately carries no payload — it is audit, not
250
+ // content. An `update` row with no payload is different: it is a PURGED operation whose
251
+ // bytes are gone (§16.7-12), and folding around it would produce a document that reads
252
+ // as complete while missing an operation. Purge is V2, so this cannot happen yet — which
253
+ // is exactly why it must refuse now rather than become a silent skip later.
254
+ if (e.kind === "update") {
255
+ this.#logger.error("document.replay.failed", {
256
+ documentId: e.documentId,
257
+ envelopeHash: e.envelopeHash,
258
+ reason: "document_envelope_purged",
259
+ });
260
+ throw new DocumentUpdateError("document_envelope_purged", `envelope ${e.envelopeHash.slice(0, 16)}… is an update whose payload has been purged — ` +
261
+ `the document cannot be rebuilt without it`);
262
+ }
263
+ continue;
264
+ }
265
+ const res = this.#applyDirect(doc, e.payload);
266
+ if (!res.ok) {
267
+ this.#logger.error("document.replay.failed", {
268
+ documentId: e.documentId,
269
+ envelopeHash: e.envelopeHash,
270
+ reason: res.reason,
271
+ detail: res.detail,
272
+ });
273
+ throw new DocumentUpdateError(res.reason, `envelope ${e.envelopeHash.slice(0, 16)}…: ${res.detail ?? ""}`);
274
+ }
275
+ applied++;
276
+ }
277
+ this.#logger.info("document.replay.completed", { envelopes: envelopes.length, applied });
278
+ return { binary: Y.encodeStateAsUpdate(doc), stateVector: Y.encodeStateVector(doc) };
279
+ }
280
+ }
281
+ //# sourceMappingURL=document-engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-engine.js","sourceRoot":"","sources":["../src/document-engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * DOD-DOC-GATE-1 — the validation gate (§3.2).
3
+ *
4
+ * arrive → shadow-apply → validate the PROJECTED DIFF → admit or quarantine
5
+ *
6
+ * The shadow document is rebuilt from ACCEPTED state and discarded. Validation asks a question;
7
+ * admitting is a separate, explicit act, so a refused update never touches the live document.
8
+ *
9
+ * ── WHY THE RULES ARE WHAT THEY ARE ───────────────────────────────────────────────────────────
10
+ *
11
+ * Every rule here answers something DOD-DOC-FUZZ-1 MEASURED against real Yjs, and six of the nine
12
+ * are the ACCEPT class — input `Y.applyUpdate` returns SUCCESS for. That matters because the V1
13
+ * posture is "cap, catch, contain" (§16.7-7), and none of its three legs catches an accept: the
14
+ * size cap sees a small update, the try/catch sees success, and "structural limits on the shadow"
15
+ * has nothing to measure because the shadow looks fine. Measured, in order:
16
+ *
17
+ * (a) an update whose dependencies never arrive is ACCEPTED and RETAINED forever — a peer
18
+ * streams those until the daemon dies, and a try/catch sees only success;
19
+ * (b) an update carries NO document identity, so one built on a different document merges
20
+ * silently — binding is out-of-band work the gate must do;
21
+ * (c) V2-format bytes are accepted by the v1 decoder and silently drop all content;
22
+ * (d) trailing bytes past the decoder's cursor are ignored, so unlimited byte strings decode to
23
+ * identical state — which makes an update's hash a poor identifier for what it says, and
24
+ * that hash becomes a `0x04` leaf;
25
+ * (h) authorship IS the clientID, so a colliding one silently wins and the honest client's
26
+ * update is then accepted-and-dropped, leaving a splice of two authors with an EMPTY
27
+ * pending set — the (a) rule cannot see this, which is why (h) is separate;
28
+ * (i) a ten-byte well-formed update deletes a document's entire content. Structural limits are
29
+ * UPPER bounds, so a shrinking update passes every one of them.
30
+ *
31
+ * The remaining three are the throw class: a size floor (e) because an empty update throws a lib0
32
+ * decoder string rather than a protocol fault, one typed reason per throw (g), and a nesting-depth
33
+ * limit (f) because Yjs bounds depth not at all and the size cap bounds it poorly — ~16 bytes per
34
+ * level, so roughly 65,000 levels fit inside 1 MiB.
35
+ *
36
+ * ── QUARANTINE, NEVER DISCARD ─────────────────────────────────────────────────────────────────
37
+ *
38
+ * A refused update is HELD. Every path out of this gate — including an unexpected failure inside
39
+ * a rule — produces a verdict and an event. A gate that admitted on internal error would be
40
+ * strictly worse than one with no rules at all, and one that dropped silently would diverge the
41
+ * two copies permanently and invisibly (§3.2).
42
+ */
43
+ import * as Y from "yjs";
44
+ import type { DocumentEngine } from "./document-engine.js";
45
+ import type { Logger } from "./types.js";
46
+ /** The one accepted update encoding (§16.7-8 pins it in the protocol types). */
47
+ export declare const UPDATE_ENCODING_V1 = "yjs-v1";
48
+ export interface GateLimits {
49
+ /** Pre-parse cap: bytes are refused on LENGTH before Yjs is invoked. */
50
+ maxUpdateBytes: number;
51
+ /** The document's size AFTER the update would apply. */
52
+ maxDocumentBytes: number;
53
+ /** Yjs bounds nesting not at all, and the size cap bounds it poorly. */
54
+ maxNestingDepth: number;
55
+ /** Per sender, per rolling minute. */
56
+ maxUpdatesPerMinute: number;
57
+ }
58
+ /** Published so a peer can discover them — a receiver-local limit nobody can learn is not a protocol. */
59
+ export declare const DEFAULT_GATE_LIMITS: GateLimits;
60
+ /** What the update WOULD do to the document — the only form in which a policy can judge it. */
61
+ export interface ProjectedDiff {
62
+ inserted: string;
63
+ deletedChars: number;
64
+ /** Keys a JSON-shaped update would touch. */
65
+ changedKeys: string[];
66
+ /** The document's size if this were admitted. */
67
+ resultingBytes: number;
68
+ maxDepth: number;
69
+ }
70
+ export interface GateContext {
71
+ documentId: string;
72
+ senderAgentId: string;
73
+ /** clientIDs this peer is KNOWN to write under — the out-of-band binding (h) requires. */
74
+ senderClientIds: number[];
75
+ /**
76
+ * The document the ENVELOPE says this update belongs to (§14). Declared, never inferred: an
77
+ * update carries no document identity, so a well-formed one built on an unrelated document
78
+ * merges silently and no property of the bytes can reveal it. Absent is a refusal.
79
+ *
80
+ * REQUIRED rather than optional so a caller cannot forget it — and the runtime refusal stays
81
+ * for the untyped boundary. **These fields carry no security on their own: they are only as
82
+ * trustworthy as the envelope signature that covers them.** DOD-DOC-ENVELOPE-1 owes a blocking
83
+ * AC that `documentId`, the encoding, and the sender's clientID are inside the signed TBS.
84
+ */
85
+ declaredDocumentId: string | undefined;
86
+ /**
87
+ * The update encoding the ENVELOPE declares (§16.7-8 pins it). Declared rather than sniffed
88
+ * because the byte-level signatures overlap: a v2 update begins `[0, 0, …]` and a legitimate
89
+ * pure-delete v1 delta begins `[0, 1, …]`, so a first-byte heuristic refuses real deletions.
90
+ * Absent is a refusal.
91
+ */
92
+ declaredEncoding: string | undefined;
93
+ appendOnly?: boolean;
94
+ }
95
+ /** A pluggable rule. Returns null to allow. DOD-DOC-SCREEN-1 registers here. */
96
+ export type GateRule = (diff: ProjectedDiff, context: GateContext) => {
97
+ reason: string;
98
+ detail?: string;
99
+ } | null;
100
+ /** Machine-readable, so a peer's daemon can act without parsing prose (§16.7-6). */
101
+ export interface GateLimitBreach {
102
+ name: string;
103
+ limit: number;
104
+ actual: number;
105
+ }
106
+ export interface GateQuarantine {
107
+ admit: false;
108
+ reason: string;
109
+ detail?: string;
110
+ limit?: GateLimitBreach;
111
+ /** The pluggable rule that refused, when one did. */
112
+ rule?: string;
113
+ /**
114
+ * THE UPDATE ITSELF. §3.2: a quarantined update is HELD — never admitted, never discarded — so
115
+ * the caller can persist it, reference it from a `0x05` leaf, and resolve it by supersession
116
+ * (DOD-DOC-REJECT-1). Returning only a reason would have made "never discarded" a comment
117
+ * rather than a property: the bytes would die with this stack frame.
118
+ */
119
+ quarantined: Uint8Array;
120
+ }
121
+ export type GateVerdict = {
122
+ admit: true;
123
+ projectedDiff: ProjectedDiff;
124
+ } | GateQuarantine;
125
+ export declare class DocumentGate {
126
+ #private;
127
+ constructor(engine: DocumentEngine, limits: Partial<GateLimits>, logger: Logger);
128
+ limits(): GateLimits;
129
+ /** Register a pluggable rule. The screening rule (DOD-DOC-SCREEN-1) plugs in here. */
130
+ addRule(name: string, rule: GateRule): void;
131
+ /**
132
+ * Validate an incoming update against the accepted state.
133
+ *
134
+ * Never mutates `accepted`. Never throws — every failure, including an unexpected one inside a
135
+ * rule, becomes a quarantine verdict.
136
+ */
137
+ validate(accepted: Y.Doc, update: Uint8Array, context: GateContext, now?: number): GateVerdict;
138
+ }
139
+ //# sourceMappingURL=document-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-gate.d.ts","sourceRoot":"","sources":["../src/document-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,gFAAgF;AAChF,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAE3C,MAAM,WAAW,UAAU;IACzB,wEAAwE;IACxE,cAAc,EAAE,MAAM,CAAC;IACvB,wDAAwD;IACxD,gBAAgB,EAAE,MAAM,CAAC;IACzB,wEAAwE;IACxE,eAAe,EAAE,MAAM,CAAC;IACxB,sCAAsC;IACtC,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED,yGAAyG;AACzG,eAAO,MAAM,mBAAmB,EAAE,UAOjC,CAAC;AAEF,+FAA+F;AAC/F,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,iDAAiD;IACjD,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,0FAA0F;IAC1F,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;;;;;;OASG;IACH,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,gFAAgF;AAChF,MAAM,MAAM,QAAQ,GAAG,CACrB,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,WAAW,KACjB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC;AAEhD,oFAAoF;AACpF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,WAAW,EAAE,UAAU,CAAC;CACzB;AAED,MAAM,MAAM,WAAW,GAAG;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,aAAa,EAAE,aAAa,CAAA;CAAE,GAAG,cAAc,CAAC;AAEzF,qBAAa,YAAY;;gBAQX,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,MAAM;IAQ/E,MAAM,IAAI,UAAU;IAIpB,sFAAsF;IACtF,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI;IAI3C;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,SAAa,GAAG,WAAW;CA4SnG"}