@cello-protocol/daemon 0.0.121 → 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.
- package/dist/document-delivery-transport.d.ts +78 -0
- package/dist/document-delivery-transport.d.ts.map +1 -0
- package/dist/document-delivery-transport.js +109 -0
- package/dist/document-delivery-transport.js.map +1 -0
- package/dist/document-delivery.d.ts +130 -0
- package/dist/document-delivery.d.ts.map +1 -0
- package/dist/document-delivery.js +246 -0
- package/dist/document-delivery.js.map +1 -0
- package/dist/document-engine.d.ts.map +1 -1
- package/dist/document-engine.js +10 -4
- package/dist/document-engine.js.map +1 -1
- package/dist/document-gate.d.ts +139 -0
- package/dist/document-gate.d.ts.map +1 -0
- package/dist/document-gate.js +465 -0
- package/dist/document-gate.js.map +1 -0
- package/dist/document-handshake.d.ts +88 -0
- package/dist/document-handshake.d.ts.map +1 -0
- package/dist/document-handshake.js +239 -0
- package/dist/document-handshake.js.map +1 -0
- package/dist/document-lifecycle.d.ts +104 -0
- package/dist/document-lifecycle.d.ts.map +1 -0
- package/dist/document-lifecycle.js +363 -0
- package/dist/document-lifecycle.js.map +1 -0
- package/dist/document-notify.d.ts +130 -0
- package/dist/document-notify.d.ts.map +1 -0
- package/dist/document-notify.js +313 -0
- package/dist/document-notify.js.map +1 -0
- package/dist/document-reachability.d.ts +42 -0
- package/dist/document-reachability.d.ts.map +1 -0
- package/dist/document-reachability.js +72 -0
- package/dist/document-reachability.js.map +1 -0
- package/dist/document-rejection.d.ts +224 -0
- package/dist/document-rejection.d.ts.map +1 -0
- package/dist/document-rejection.js +374 -0
- package/dist/document-rejection.js.map +1 -0
- package/dist/document-store.d.ts +88 -0
- package/dist/document-store.d.ts.map +1 -1
- package/dist/document-store.js +344 -1
- package/dist/document-store.js.map +1 -1
- package/dist/document-write-path.d.ts +84 -0
- package/dist/document-write-path.d.ts.map +1 -0
- package/dist/document-write-path.js +412 -0
- package/dist/document-write-path.js.map +1 -0
- package/dist/line-lcs.d.ts +51 -0
- package/dist/line-lcs.d.ts.map +1 -0
- package/dist/line-lcs.js +71 -0
- package/dist/line-lcs.js.map +1 -0
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document-engine.js","sourceRoot":"","sources":["../src/document-engine.ts"],"names":[],"mappings":"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
|
|
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"}
|
|
@@ -0,0 +1,465 @@
|
|
|
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
|
+
/** The one accepted update encoding (§16.7-8 pins it in the protocol types). */
|
|
45
|
+
export const UPDATE_ENCODING_V1 = "yjs-v1";
|
|
46
|
+
/** Published so a peer can discover them — a receiver-local limit nobody can learn is not a protocol. */
|
|
47
|
+
export const DEFAULT_GATE_LIMITS = {
|
|
48
|
+
// Deliberately the same number the engine enforces, taken from the engine at construction so
|
|
49
|
+
// the two cannot drift into two sources of truth.
|
|
50
|
+
maxUpdateBytes: 1024 * 1024,
|
|
51
|
+
maxDocumentBytes: 8 * 1024 * 1024,
|
|
52
|
+
maxNestingDepth: 64,
|
|
53
|
+
maxUpdatesPerMinute: 120,
|
|
54
|
+
};
|
|
55
|
+
export class DocumentGate {
|
|
56
|
+
#engine;
|
|
57
|
+
#limits;
|
|
58
|
+
#logger;
|
|
59
|
+
#rules = new Map();
|
|
60
|
+
/** senderAgentId → recent admission timestamps, for the rate limit. */
|
|
61
|
+
#recent = new Map();
|
|
62
|
+
constructor(engine, limits, logger) {
|
|
63
|
+
this.#engine = engine;
|
|
64
|
+
// The engine already enforces a pre-parse cap; take it rather than repeat the constant, so a
|
|
65
|
+
// change in one place cannot leave the gate admitting what the engine will refuse.
|
|
66
|
+
this.#limits = { ...DEFAULT_GATE_LIMITS, maxUpdateBytes: engine.maxUpdateBytes, ...limits };
|
|
67
|
+
this.#logger = logger;
|
|
68
|
+
}
|
|
69
|
+
limits() {
|
|
70
|
+
return { ...this.#limits };
|
|
71
|
+
}
|
|
72
|
+
/** Register a pluggable rule. The screening rule (DOD-DOC-SCREEN-1) plugs in here. */
|
|
73
|
+
addRule(name, rule) {
|
|
74
|
+
this.#rules.set(name, rule);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Validate an incoming update against the accepted state.
|
|
78
|
+
*
|
|
79
|
+
* Never mutates `accepted`. Never throws — every failure, including an unexpected one inside a
|
|
80
|
+
* rule, becomes a quarantine verdict.
|
|
81
|
+
*/
|
|
82
|
+
validate(accepted, update, context, now = Date.now()) {
|
|
83
|
+
try {
|
|
84
|
+
return this.#validate(accepted, update, context, now);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
// The catch-all is the no-silent-drop invariant made structural: a rule with a bug in it
|
|
88
|
+
// must not become an admission.
|
|
89
|
+
return this.#quarantine(context, update, {
|
|
90
|
+
admit: false,
|
|
91
|
+
reason: "document_gate_rule_failed",
|
|
92
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
#validate(accepted, update, context, now) {
|
|
97
|
+
// ── Pre-parse, before Yjs sees the bytes ────────────────────────────────
|
|
98
|
+
if (update.length > this.#limits.maxUpdateBytes) {
|
|
99
|
+
return this.#quarantine(context, update, {
|
|
100
|
+
admit: false,
|
|
101
|
+
reason: "document_update_too_large",
|
|
102
|
+
limit: { name: "maxUpdateBytes", limit: this.#limits.maxUpdateBytes, actual: update.length },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (update.length < 2) {
|
|
106
|
+
// (e) The floor. Below it Yjs throws "Unexpected end of array" — a decoder string naming
|
|
107
|
+
// lib0 internals rather than a protocol fault the peer could act on.
|
|
108
|
+
return this.#quarantine(context, update, {
|
|
109
|
+
admit: false,
|
|
110
|
+
reason: "document_update_too_small",
|
|
111
|
+
detail: `${update.length} bytes is below the 2-byte minimum for a Yjs update`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// (c) V2 bytes are ACCEPTED by the v1 decoder and silently drop all content, so the encoding
|
|
115
|
+
// is pinned. Declared, not sniffed — see GateContext.
|
|
116
|
+
if (context.declaredEncoding !== UPDATE_ENCODING_V1) {
|
|
117
|
+
return this.#quarantine(context, update, {
|
|
118
|
+
admit: false,
|
|
119
|
+
reason: "document_update_encoding_unsupported",
|
|
120
|
+
detail: `envelope declares encoding ${JSON.stringify(context.declaredEncoding ?? null)}; ` +
|
|
121
|
+
`only ${UPDATE_ENCODING_V1} is accepted, and v2 bytes decode to an empty document without error`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// (b) An update carries NO document identity, so the binding is the envelope's and the check
|
|
125
|
+
// is the receiver's. ABSENT IS NOT FINE: an unbound update is refused, not trusted.
|
|
126
|
+
if (context.declaredDocumentId !== context.documentId) {
|
|
127
|
+
return this.#quarantine(context, update, {
|
|
128
|
+
admit: false,
|
|
129
|
+
reason: "document_update_foreign_origin",
|
|
130
|
+
detail: `envelope declares document ${String(context.declaredDocumentId ?? "(none)").slice(0, 16)}… ` +
|
|
131
|
+
`but this is ${context.documentId.slice(0, 16)}… — updates carry no document identity, so ` +
|
|
132
|
+
`a well-formed update built on another document would otherwise merge silently`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// ── Rate, per sender ────────────────────────────────────────────────────
|
|
136
|
+
// Counted on ARRIVAL, after the cheap pre-parse checks and BEFORE the shadow rebuild.
|
|
137
|
+
// Counting admissions instead left a peer who sends only invalid updates un-rate-limited,
|
|
138
|
+
// and everything past this point costs a full encode+apply of the whole document per attempt.
|
|
139
|
+
const window = (this.#recent.get(context.senderAgentId) ?? []).filter((t) => now - t < 60_000);
|
|
140
|
+
if (window.length >= this.#limits.maxUpdatesPerMinute) {
|
|
141
|
+
this.#recent.set(context.senderAgentId, window);
|
|
142
|
+
return this.#quarantine(context, update, {
|
|
143
|
+
admit: false,
|
|
144
|
+
reason: "document_update_rate_exceeded",
|
|
145
|
+
limit: {
|
|
146
|
+
name: "maxUpdatesPerMinute",
|
|
147
|
+
limit: this.#limits.maxUpdatesPerMinute,
|
|
148
|
+
actual: window.length + 1,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
window.push(now);
|
|
153
|
+
this.#recent.set(context.senderAgentId, window);
|
|
154
|
+
// Prune senders whose window has emptied, so the map does not accumulate one entry per peer
|
|
155
|
+
// for the life of the process.
|
|
156
|
+
for (const [sender, times] of this.#recent) {
|
|
157
|
+
if (times.length > 0 && now - times[times.length - 1] > 60_000)
|
|
158
|
+
this.#recent.delete(sender);
|
|
159
|
+
}
|
|
160
|
+
// ── Shadow apply, on state rebuilt from ACCEPTED — never long-lived ─────
|
|
161
|
+
const shadow = new Y.Doc();
|
|
162
|
+
try {
|
|
163
|
+
Y.applyUpdate(shadow, Y.encodeStateAsUpdate(accepted));
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
return this.#quarantine(context, update, {
|
|
167
|
+
admit: false,
|
|
168
|
+
reason: "document_accepted_state_unreadable",
|
|
169
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const beforeText = shadow.getText("content").toString();
|
|
173
|
+
const beforeKeys = new Map(Object.entries(shadow.getMap("data").toJSON()));
|
|
174
|
+
try {
|
|
175
|
+
Y.applyUpdate(shadow, update);
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
// (g) ONE typed reason per throw. The decoder string is useful and travels as detail —
|
|
179
|
+
// it is simply not a reason an operator or a policy log can key on.
|
|
180
|
+
return this.#quarantine(context, update, {
|
|
181
|
+
admit: false,
|
|
182
|
+
reason: "document_update_malformed",
|
|
183
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// (a) Yjs returned success — that is not evidence the update integrated. BOTH pending sets
|
|
187
|
+
// count: an update carrying a DELETE SET for structs the receiver has never seen leaves
|
|
188
|
+
// pendingStructs null and pendingDs populated, and that retains forever exactly as the
|
|
189
|
+
// struct case does. Reading one field was coding the mechanism; the rule is about whether
|
|
190
|
+
// the update actually integrated.
|
|
191
|
+
const pendingStructs = shadow.store.pendingStructs;
|
|
192
|
+
const pendingDs = shadow.store.pendingDs;
|
|
193
|
+
if (pendingStructs !== null || pendingDs !== null) {
|
|
194
|
+
return this.#quarantine(context, update, {
|
|
195
|
+
admit: false,
|
|
196
|
+
reason: "document_update_unresolved_dependencies",
|
|
197
|
+
detail: pendingStructs
|
|
198
|
+
? `depends on ${pendingStructs.missing.size} client(s) whose earlier operations are absent`
|
|
199
|
+
: `carries a delete set referring to operations this document has never seen`,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
// (d) Re-encode THE UPDATE ITSELF and compare. Comparing against the canonical DELTA looked
|
|
203
|
+
// equivalent and is not: a peer that batches several transactions (the normal shape for an
|
|
204
|
+
// append-only log) or sends full state (what y-protocols sync step 2 does with no state
|
|
205
|
+
// vector) produces an update legitimately LARGER than the delta, and both were measured
|
|
206
|
+
// being refused as "trailing bytes" — an attack label on a benign encoding, which sends an
|
|
207
|
+
// operator hunting a malicious peer while the document stops converging.
|
|
208
|
+
//
|
|
209
|
+
// `Y.diffUpdate` against an empty state vector re-encodes the update's own content and drops
|
|
210
|
+
// slack. `Y.mergeUpdates([padded])` does NOT — it preserves the padding — so it is not a
|
|
211
|
+
// usable detector here.
|
|
212
|
+
let canonicalUpdate;
|
|
213
|
+
try {
|
|
214
|
+
canonicalUpdate = Y.diffUpdate(update, Y.encodeStateVector(new Y.Doc()));
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
// A throw here is a malformed update, not an internal rule failure — labelling it
|
|
218
|
+
// document_gate_rule_failed would name this gate's exit point rather than the peer's fault.
|
|
219
|
+
return this.#quarantine(context, update, {
|
|
220
|
+
admit: false,
|
|
221
|
+
reason: "document_update_malformed",
|
|
222
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const reEncoded = Y.encodeStateAsUpdate(shadow);
|
|
226
|
+
if (update.length > canonicalUpdate.length) {
|
|
227
|
+
return this.#quarantine(context, update, {
|
|
228
|
+
admit: false,
|
|
229
|
+
reason: "document_update_trailing_bytes",
|
|
230
|
+
detail: `${update.length - canonicalUpdate.length} bytes past the decoder's cursor — the encoding ` +
|
|
231
|
+
`would otherwise be malleable, and this update's hash becomes a 0x04 leaf`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
// (b) and (h): who wrote this, and on what.
|
|
235
|
+
const authorship = this.#checkAuthorship(accepted, shadow, context);
|
|
236
|
+
if (authorship)
|
|
237
|
+
return this.#quarantine(context, update, authorship);
|
|
238
|
+
// ── The PROJECTED DIFF — what the update would do ───────────────────────
|
|
239
|
+
const afterText = shadow.getText("content").toString();
|
|
240
|
+
const afterKeys = new Map(Object.entries(shadow.getMap("data").toJSON()));
|
|
241
|
+
const diff = {
|
|
242
|
+
inserted: insertedText(beforeText, afterText),
|
|
243
|
+
deletedChars: deletedCount(beforeText, afterText),
|
|
244
|
+
changedKeys: [...new Set([...beforeKeys.keys(), ...afterKeys.keys()])].filter((k) => JSON.stringify(beforeKeys.get(k)) !== JSON.stringify(afterKeys.get(k))),
|
|
245
|
+
resultingBytes: reEncoded.length,
|
|
246
|
+
maxDepth: deepestRoot(shadow, this.#limits.maxNestingDepth),
|
|
247
|
+
};
|
|
248
|
+
// ── Structural limits, on the projected result ──────────────────────────
|
|
249
|
+
if (diff.resultingBytes > this.#limits.maxDocumentBytes) {
|
|
250
|
+
return this.#quarantine(context, update, {
|
|
251
|
+
admit: false,
|
|
252
|
+
reason: "document_too_large",
|
|
253
|
+
limit: { name: "maxDocumentBytes", limit: this.#limits.maxDocumentBytes, actual: diff.resultingBytes },
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (diff.maxDepth > this.#limits.maxNestingDepth) {
|
|
257
|
+
return this.#quarantine(context, update, {
|
|
258
|
+
admit: false,
|
|
259
|
+
reason: "document_nesting_too_deep",
|
|
260
|
+
limit: { name: "maxNestingDepth", limit: this.#limits.maxNestingDepth, actual: diff.maxDepth },
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
// (i) append_only, judged on the PROJECTED DIFF rather than the update's size — a ten-byte
|
|
264
|
+
// update deletes everything and every structural limit passes it.
|
|
265
|
+
//
|
|
266
|
+
// Measured in BOTH coordinate spaces the write path projects from. Diffing only the text root
|
|
267
|
+
// made append_only a COMPLETE NO-OP for json documents, whose content lives in the map: an
|
|
268
|
+
// 8-byte update emptied `data` with deletedChars = 0 and was admitted.
|
|
269
|
+
if (context.appendOnly === true) {
|
|
270
|
+
// The UPDATE'S OWN DELETE SET, not a projection diff. Measured, it is exactly the right
|
|
271
|
+
// question and the projection was not:
|
|
272
|
+
// - it is root- and type-agnostic, so an append-only document is protected whatever roots
|
|
273
|
+
// a peer chooses — the previous version protected `content` and `data` and nothing else,
|
|
274
|
+
// which is no protection at all when the peer names the roots;
|
|
275
|
+
// - a map key REWRITE deletes the old item, so §16.7-1's "deletes OR EDITS" falls out;
|
|
276
|
+
// - and growing a NESTED entry deletes nothing, so it is admitted. The projection diff
|
|
277
|
+
// compared top-level keys with JSON.stringify and called that a removal, refusing the
|
|
278
|
+
// very append the feature is named for and telling the operator content was removed
|
|
279
|
+
// when nothing was.
|
|
280
|
+
const deletions = countDeletions(update);
|
|
281
|
+
if (deletions > 0) {
|
|
282
|
+
return this.#quarantine(context, update, {
|
|
283
|
+
admit: false,
|
|
284
|
+
reason: "document_append_only_violation",
|
|
285
|
+
detail: `the update deletes or rewrites ${deletions} existing range(s); this document ` +
|
|
286
|
+
`accepts appends only`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// ── Pluggable rules (DOD-DOC-SCREEN-1 registers here) ───────────────────
|
|
291
|
+
for (const [name, rule] of this.#rules) {
|
|
292
|
+
const refusal = rule(diff, context);
|
|
293
|
+
if (refusal) {
|
|
294
|
+
return this.#quarantine(context, update, { admit: false, ...refusal, rule: name });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
this.#logger.info("document.update.admitted", {
|
|
298
|
+
documentId: context.documentId,
|
|
299
|
+
senderAgentId: context.senderAgentId,
|
|
300
|
+
insertedChars: diff.inserted.length,
|
|
301
|
+
deletedChars: diff.deletedChars,
|
|
302
|
+
});
|
|
303
|
+
return { admit: true, projectedDiff: diff };
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* (h) Every client whose CLOCK ADVANCED must be one this peer is bound to.
|
|
307
|
+
*
|
|
308
|
+
* The first version asked whether a clientID was NEW — exempting any already present in the
|
|
309
|
+
* accepted state, including the document owner's own. That is exactly the attack AC (h) was
|
|
310
|
+
* written from, and it was measured working: integrate the accepted state, THEN set clientID to
|
|
311
|
+
* the owner's, and the forged update is admitted and attributed to the owner ("FORGED honest
|
|
312
|
+
* content"), with an empty pending set so rule (a) cannot see it either. Setting the clientID
|
|
313
|
+
* after integration sidesteps Yjs's own "Changed the client-id" guard; a hand-rolled encoder
|
|
314
|
+
* needs no trick at all.
|
|
315
|
+
*
|
|
316
|
+
* Advancement is the right question for INSERTIONS: a peer never legitimately advances another
|
|
317
|
+
* client's clock. Reusing a clock position below the owner's is deduped by Yjs, and one above
|
|
318
|
+
* leaves a gap that rule (a) catches.
|
|
319
|
+
*
|
|
320
|
+
* IT DOES NOT COVER DELETIONS. A Yjs delete set carries no clientID and advances no clock, so a
|
|
321
|
+
* BOUND peer can delete the owner's content and this rule sees nothing — which is legitimate
|
|
322
|
+
* CRDT behaviour for an authorized writer, not a forgery, but it means `append_only` is the only
|
|
323
|
+
* thing standing between a bound peer and erasure, and it defaults off. Recorded rather than
|
|
324
|
+
* left for a reader to infer, and carried as an AC on DOD-DOC-REJECT-1.
|
|
325
|
+
*/
|
|
326
|
+
#checkAuthorship(accepted, shadow, context) {
|
|
327
|
+
const before = Y.decodeStateVector(Y.encodeStateVector(accepted));
|
|
328
|
+
const after = Y.decodeStateVector(Y.encodeStateVector(shadow));
|
|
329
|
+
const bound = new Set(context.senderClientIds);
|
|
330
|
+
const advanced = [];
|
|
331
|
+
for (const [client, clock] of after) {
|
|
332
|
+
if (clock > (before.get(client) ?? 0))
|
|
333
|
+
advanced.push(client);
|
|
334
|
+
}
|
|
335
|
+
const unbound = advanced.filter((c) => !bound.has(c));
|
|
336
|
+
if (unbound.length > 0) {
|
|
337
|
+
return {
|
|
338
|
+
admit: false,
|
|
339
|
+
reason: "document_update_unbound_client",
|
|
340
|
+
detail: `clientID(s) ${unbound.join(", ")} advanced but are not bound to sender ` +
|
|
341
|
+
`${context.senderAgentId.slice(0, 16)}… — either an update authored under another ` +
|
|
342
|
+
`party's identity, or relayed third-party operations, which V1 does not support ` +
|
|
343
|
+
`(hub-and-spoke is deferred, §11.1)`,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
#quarantine(context, update, verdict) {
|
|
349
|
+
// A quarantined update is HELD — never admitted, never discarded — so the record carries what
|
|
350
|
+
// is needed to act on it: which document, which peer, and why (§3.2).
|
|
351
|
+
this.#logger.warn("document.update.quarantined", {
|
|
352
|
+
documentId: context.documentId,
|
|
353
|
+
senderAgentId: context.senderAgentId,
|
|
354
|
+
reason: verdict.reason,
|
|
355
|
+
detail: verdict.detail,
|
|
356
|
+
limit: verdict.limit,
|
|
357
|
+
rule: verdict.rule,
|
|
358
|
+
});
|
|
359
|
+
// COPY. Storing the caller's reference would let a pooled network read buffer be reused
|
|
360
|
+
// underneath us, so DOD-DOC-REJECT-1 would hash bytes that are no longer the ones refused —
|
|
361
|
+
// into a 0x05 leaf, silently.
|
|
362
|
+
return { ...verdict, quarantined: new Uint8Array(update) };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function insertedText(before, after) {
|
|
366
|
+
if (after.length <= before.length)
|
|
367
|
+
return "";
|
|
368
|
+
let from = 0;
|
|
369
|
+
while (from < before.length && before[from] === after[from])
|
|
370
|
+
from++;
|
|
371
|
+
const tail = before.length - from;
|
|
372
|
+
return after.slice(from, after.length - tail);
|
|
373
|
+
}
|
|
374
|
+
function deletedCount(before, after) {
|
|
375
|
+
let from = 0;
|
|
376
|
+
const max = Math.min(before.length, after.length);
|
|
377
|
+
while (from < max && before[from] === after[from])
|
|
378
|
+
from++;
|
|
379
|
+
let suffix = 0;
|
|
380
|
+
while (suffix < before.length - from &&
|
|
381
|
+
suffix < after.length - from &&
|
|
382
|
+
before[before.length - 1 - suffix] === after[after.length - 1 - suffix]) {
|
|
383
|
+
suffix++;
|
|
384
|
+
}
|
|
385
|
+
return Math.max(0, before.length - from - suffix);
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* The deepest nesting across EVERY root, whatever shape that root is.
|
|
389
|
+
*
|
|
390
|
+
* Two things this must not do, both measured going wrong:
|
|
391
|
+
*
|
|
392
|
+
* 1. **It must not recurse.** `YMap.toJSON()` and a recursive depth walk are mutually recursive,
|
|
393
|
+
* and past roughly a thousand levels they throw `RangeError: Maximum call stack size exceeded`.
|
|
394
|
+
* A `catch` around that returned 0, which INVERTED the rule — 30 levels were caught and 5,000
|
|
395
|
+
* sailed through, so the deeper the attack the more certain it passed. Worse, it converted the
|
|
396
|
+
* gate's own fail-safe catch-all into an admission. This walk is iterative and bails the moment
|
|
397
|
+
* it passes the limit it is enforcing, so it never goes deeper than it has to.
|
|
398
|
+
* 2. **It must not instantiate roots by a guessed type.** `doc.getMap(name)` MIGRATES an
|
|
399
|
+
* uninstantiated root to a map in place, so an Array- or Text-shaped root became an empty map
|
|
400
|
+
* and reported depth 0 — the same bypass, reached with a different type. Walking Yjs's own item
|
|
401
|
+
* graph is shape-agnostic: a nested type is a nested type whether it hangs off `_map` or the
|
|
402
|
+
* `_start` list.
|
|
403
|
+
*/
|
|
404
|
+
function deepestRoot(doc, bailAt) {
|
|
405
|
+
let deepest = 0;
|
|
406
|
+
const stack = [];
|
|
407
|
+
for (const root of doc.share.values()) {
|
|
408
|
+
stack.push({ type: root, depth: 0 });
|
|
409
|
+
}
|
|
410
|
+
while (stack.length > 0) {
|
|
411
|
+
const { type, depth } = stack.pop();
|
|
412
|
+
if (depth > deepest)
|
|
413
|
+
deepest = depth;
|
|
414
|
+
// Never walk deeper than the limit under enforcement — the answer cannot change.
|
|
415
|
+
if (deepest > bailAt)
|
|
416
|
+
return deepest;
|
|
417
|
+
const internals = type;
|
|
418
|
+
if (internals._map) {
|
|
419
|
+
for (const item of internals._map.values()) {
|
|
420
|
+
const nested = nestedType(item);
|
|
421
|
+
if (nested)
|
|
422
|
+
stack.push({ type: nested, depth: depth + 1 });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
let item = internals._start;
|
|
426
|
+
while (item) {
|
|
427
|
+
const nested = nestedType(item);
|
|
428
|
+
if (nested)
|
|
429
|
+
stack.push({ type: nested, depth: depth + 1 });
|
|
430
|
+
item = item.right;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return deepest;
|
|
434
|
+
}
|
|
435
|
+
/** The type nested inside an item, or null when the item holds a plain value. */
|
|
436
|
+
function nestedType(item) {
|
|
437
|
+
const content = item?.content;
|
|
438
|
+
if (content instanceof Y.ContentType) {
|
|
439
|
+
return content.type;
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* How many ranges this update deletes — the basis for `append_only`.
|
|
445
|
+
*
|
|
446
|
+
* The update's own delete set, rather than a diff of the projected document. Measured, that is
|
|
447
|
+
* the right question and a projection diff was not: this is root- and type-agnostic (so a peer
|
|
448
|
+
* cannot escape by naming its own roots), a map-key rewrite deletes the old item so §16.7-1's
|
|
449
|
+
* "deletes OR EDITS" falls out, and growing a nested entry deletes nothing so it is admitted.
|
|
450
|
+
*/
|
|
451
|
+
function countDeletions(update) {
|
|
452
|
+
try {
|
|
453
|
+
const { ds } = Y.decodeUpdate(update);
|
|
454
|
+
let total = 0;
|
|
455
|
+
for (const ranges of ds.clients.values())
|
|
456
|
+
total += ranges.length;
|
|
457
|
+
return total;
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
// Undecodable here means the malformed check upstream already refused it. Inventing an
|
|
461
|
+
// append_only violation from a decode failure would name the wrong cause.
|
|
462
|
+
return 0;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
//# sourceMappingURL=document-gate.js.map
|