@cello-protocol/protocol-types 0.0.44 → 0.0.45
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/cbor.d.ts +16 -0
- package/dist/cbor.d.ts.map +1 -1
- package/dist/cbor.js +56 -0
- package/dist/cbor.js.map +1 -1
- package/dist/document-ack.d.ts +111 -0
- package/dist/document-ack.d.ts.map +1 -0
- package/dist/document-ack.js +228 -0
- package/dist/document-ack.js.map +1 -0
- package/dist/document-control.d.ts +74 -0
- package/dist/document-control.d.ts.map +1 -0
- package/dist/document-control.js +174 -0
- package/dist/document-control.js.map +1 -0
- package/dist/document-envelope.d.ts +132 -0
- package/dist/document-envelope.d.ts.map +1 -0
- package/dist/document-envelope.js +243 -0
- package/dist/document-envelope.js.map +1 -0
- package/dist/document-proposal-ack.d.ts +98 -0
- package/dist/document-proposal-ack.d.ts.map +1 -0
- package/dist/document-proposal-ack.js +203 -0
- package/dist/document-proposal-ack.js.map +1 -0
- package/dist/document-proposal.d.ts +127 -0
- package/dist/document-proposal.d.ts.map +1 -0
- package/dist/document-proposal.js +244 -0
- package/dist/document-proposal.js.map +1 -0
- package/dist/document-rejection-envelope.d.ts +87 -0
- package/dist/document-rejection-envelope.d.ts.map +1 -0
- package/dist/document-rejection-envelope.js +189 -0
- package/dist/document-rejection-envelope.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-DOC-TOOLS-1 — the document CONTROL frame: close and kill (§16.5).
|
|
3
|
+
*
|
|
4
|
+
* A unilateral end, told to the other party.
|
|
5
|
+
*
|
|
6
|
+
* ── WHY THE PEER MUST BE TOLD, AND WHY IT IS BEST-EFFORT ──────────────────────────────────────
|
|
7
|
+
*
|
|
8
|
+
* `DocumentLifecycle` already ends a document locally without asking anyone — that is deliberate: a
|
|
9
|
+
* kill is a safety verb, and a safety verb that needs the counterparty's cooperation is not one. But
|
|
10
|
+
* a peer who is never told keeps publishing into a document that will never answer. Their updates
|
|
11
|
+
* are refused at the far end forever, and nothing on their screen explains why. So the notification
|
|
12
|
+
* is REQUIRED to be attempted and ALLOWED to fail, and the operator is told which happened.
|
|
13
|
+
*
|
|
14
|
+
* ── CLOSE AND KILL ARE DIFFERENT FRAMES OF THE SAME SHAPE ─────────────────────────────────────
|
|
15
|
+
*
|
|
16
|
+
* `close` is "I am done with this, and I expect you are too" — the document settles when both sides
|
|
17
|
+
* have said it. `kill` is "this is over now", one-sided and immediate. They travel as one frame with
|
|
18
|
+
* a verb rather than two types because the receiving side's routing, verification and settle-once
|
|
19
|
+
* rules are identical, and two decoders for one shape is how the rules drift apart.
|
|
20
|
+
*
|
|
21
|
+
* The verb is REFUSED BY VALUE on decode. A third verb from a future build must not be admitted as
|
|
22
|
+
* one of these two — a `kill` silently read as a `close` would leave a killed document waiting for
|
|
23
|
+
* a reciprocal close that is never coming.
|
|
24
|
+
*
|
|
25
|
+
* ── WHY IT IS SIGNED ──────────────────────────────────────────────────────────────────────────
|
|
26
|
+
*
|
|
27
|
+
* A kill frame ends a collaboration. Unsigned, anyone reaching the channel could end any document
|
|
28
|
+
* between any two parties, and each operator would believe the other walked away. The signature
|
|
29
|
+
* covers the document id — which commits to both parties, the properties and the nonce — and the
|
|
30
|
+
* verb, because the whole statement is the claim.
|
|
31
|
+
*/
|
|
32
|
+
import { createHash } from "node:crypto";
|
|
33
|
+
import { encodeCbor, decodeCbor } from "./cbor.js";
|
|
34
|
+
/** Domain tag in slot 0. Distinct from every other document domain. */
|
|
35
|
+
export const DOCUMENT_CONTROL_DOMAIN = "CELLO-DOCUMENT-CONTROL-v1";
|
|
36
|
+
/** The frame version, ON THE WIRE, so skew is DETECTED rather than misread as a bad signature. */
|
|
37
|
+
export const DOCUMENT_CONTROL_VERSION = 1;
|
|
38
|
+
/** Peer-controlled display text bound for an operator's screen. Capped for that reason. */
|
|
39
|
+
export const MAX_CONTROL_REASON_LENGTH = 200;
|
|
40
|
+
/** The two ways a document ends. Closed set — see the header on refusing a third by value. */
|
|
41
|
+
export const DOCUMENT_CONTROL_VERBS = ["close", "kill"];
|
|
42
|
+
const HEX32 = /^[0-9a-f]{64}$/;
|
|
43
|
+
function normalizeReason(reason) {
|
|
44
|
+
return reason === undefined || reason === "" ? null : reason;
|
|
45
|
+
}
|
|
46
|
+
export function assertDocumentControlConsistent(control) {
|
|
47
|
+
if (!DOCUMENT_CONTROL_VERBS.includes(control.verb)) {
|
|
48
|
+
throw new Error(`document_control_verb: expected one of ${DOCUMENT_CONTROL_VERBS.join(", ")}, got "${control.verb}"`);
|
|
49
|
+
}
|
|
50
|
+
if (!HEX32.test(control.document_id)) {
|
|
51
|
+
throw new Error("document_control_document_id: expected 64 lowercase hex characters");
|
|
52
|
+
}
|
|
53
|
+
const reason = normalizeReason(control.reason);
|
|
54
|
+
if (reason !== null && reason.length > MAX_CONTROL_REASON_LENGTH) {
|
|
55
|
+
throw new Error(`document_control_reason: a reason may be at most ${MAX_CONTROL_REASON_LENGTH} characters, ` +
|
|
56
|
+
`and this one is ${reason.length}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The canonical to-be-signed preimage: a fixed-order CBOR ARRAY with the domain in slot 0.
|
|
61
|
+
*
|
|
62
|
+
* An array rather than a map for the reason `cbor.ts` gives — this encoder is deliberately not
|
|
63
|
+
* deterministic for maps, so a map preimage would make the signature depend on the order the sender
|
|
64
|
+
* happened to build the object in, and two honest implementations would disagree.
|
|
65
|
+
*/
|
|
66
|
+
export function buildDocumentControlTbs(control, opts = {}) {
|
|
67
|
+
assertDocumentControlConsistent(control);
|
|
68
|
+
const preimage = encodeCbor([
|
|
69
|
+
DOCUMENT_CONTROL_DOMAIN,
|
|
70
|
+
control.control_version,
|
|
71
|
+
control.document_id,
|
|
72
|
+
control.sender_agent_id,
|
|
73
|
+
// THE VERB IS SIGNED. Unsigned, a captured `close` could be replayed as a `kill` — the peer
|
|
74
|
+
// would end a collaboration the sender only meant to wind down, with a valid signature on it.
|
|
75
|
+
control.verb,
|
|
76
|
+
normalizeReason(control.reason),
|
|
77
|
+
// BIGINT past 0xffffffff — cbor-x encodes a large JS number as IEEE float64 (`fb`) rather than
|
|
78
|
+
// uint64 (`1b`), so a canonically-encoding implementation would compute different TBS bytes and
|
|
79
|
+
// reject a GENUINE frame, surfacing as a signature failure that reads as forgery.
|
|
80
|
+
typeof control.sent_at_ms === "number" && control.sent_at_ms > 0xffffffff
|
|
81
|
+
? BigInt(control.sent_at_ms)
|
|
82
|
+
: control.sent_at_ms,
|
|
83
|
+
]);
|
|
84
|
+
if (opts.preHash === false)
|
|
85
|
+
return preimage;
|
|
86
|
+
return new Uint8Array(createHash("sha256").update(preimage).digest());
|
|
87
|
+
}
|
|
88
|
+
export function encodeDocumentControl(control) {
|
|
89
|
+
assertDocumentControlConsistent(control);
|
|
90
|
+
return encodeCbor({
|
|
91
|
+
type: control.type,
|
|
92
|
+
control_version: control.control_version,
|
|
93
|
+
document_id: control.document_id,
|
|
94
|
+
sender_agent_id: control.sender_agent_id,
|
|
95
|
+
verb: control.verb,
|
|
96
|
+
reason: normalizeReason(control.reason),
|
|
97
|
+
sent_at_ms: control.sent_at_ms,
|
|
98
|
+
signature: control.signature,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function present(map, field) {
|
|
102
|
+
if (!(field in map)) {
|
|
103
|
+
throw new Error(`document_control_missing_field: ${field} is mandatory and was not present`);
|
|
104
|
+
}
|
|
105
|
+
return map[field];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Decode and validate. Refuses rather than defaulting on every field.
|
|
109
|
+
*
|
|
110
|
+
* The one that matters most is `verb`. Defaulted or coerced, a `kill` read as a `close` leaves a
|
|
111
|
+
* killed document waiting for a reciprocal close that is never coming — the operator sees a
|
|
112
|
+
* collaboration that will not settle and no reason anywhere for why.
|
|
113
|
+
*/
|
|
114
|
+
export function decodeDocumentControl(bytes) {
|
|
115
|
+
const decoded = decodeCbor(bytes);
|
|
116
|
+
if (typeof decoded !== "object" || decoded === null || Array.isArray(decoded)) {
|
|
117
|
+
throw new Error("document_control_malformed: not a CBOR map");
|
|
118
|
+
}
|
|
119
|
+
const map = decoded;
|
|
120
|
+
if (present(map, "type") !== "document_control") {
|
|
121
|
+
throw new Error("document_control_type: expected document_control");
|
|
122
|
+
}
|
|
123
|
+
const version = present(map, "control_version");
|
|
124
|
+
if (typeof version !== "number" || !Number.isInteger(version)) {
|
|
125
|
+
throw new Error("document_control_version: must be an integer");
|
|
126
|
+
}
|
|
127
|
+
if (version !== DOCUMENT_CONTROL_VERSION) {
|
|
128
|
+
throw new Error(`document_control_version_unsupported: this build speaks version ${DOCUMENT_CONTROL_VERSION} ` +
|
|
129
|
+
`and the frame declares ${version} — the peer is running a newer CELLO and one of you needs ` +
|
|
130
|
+
`to upgrade`);
|
|
131
|
+
}
|
|
132
|
+
const documentId = present(map, "document_id");
|
|
133
|
+
if (typeof documentId !== "string" || !HEX32.test(documentId)) {
|
|
134
|
+
throw new Error("document_control_document_id: expected 64 lowercase hex characters");
|
|
135
|
+
}
|
|
136
|
+
const senderAgentId = present(map, "sender_agent_id");
|
|
137
|
+
if (typeof senderAgentId !== "string" || senderAgentId.length === 0) {
|
|
138
|
+
throw new Error("document_control_sender: must be a non-empty string");
|
|
139
|
+
}
|
|
140
|
+
const verb = present(map, "verb");
|
|
141
|
+
if (typeof verb !== "string" || !DOCUMENT_CONTROL_VERBS.includes(verb)) {
|
|
142
|
+
// REFUSED BY VALUE. A third verb from a future build must not be admitted as one of these two.
|
|
143
|
+
throw new Error(`document_control_verb_unsupported: this build understands ` +
|
|
144
|
+
`${DOCUMENT_CONTROL_VERBS.join(" and ")}, and the frame says "${String(verb)}" — the peer is ` +
|
|
145
|
+
`running a newer CELLO and one of you needs to upgrade`);
|
|
146
|
+
}
|
|
147
|
+
const reason = present(map, "reason");
|
|
148
|
+
if (reason !== null && typeof reason !== "string") {
|
|
149
|
+
throw new Error("document_control_reason: must be a string or null");
|
|
150
|
+
}
|
|
151
|
+
const sentAt = present(map, "sent_at_ms");
|
|
152
|
+
if (typeof sentAt !== "number" || !Number.isFinite(sentAt)) {
|
|
153
|
+
throw new Error("document_control_sent_at: must be a finite number");
|
|
154
|
+
}
|
|
155
|
+
const signature = present(map, "signature");
|
|
156
|
+
if (!(signature instanceof Uint8Array) || signature.length !== 64) {
|
|
157
|
+
throw new Error("document_control_signature: expected 64 bytes");
|
|
158
|
+
}
|
|
159
|
+
const control = {
|
|
160
|
+
type: "document_control",
|
|
161
|
+
control_version: version,
|
|
162
|
+
document_id: documentId,
|
|
163
|
+
sender_agent_id: senderAgentId,
|
|
164
|
+
verb: verb,
|
|
165
|
+
...(reason !== null && reason !== "" ? { reason } : {}),
|
|
166
|
+
sent_at_ms: sentAt,
|
|
167
|
+
// COPIED out of the decode buffer — cbor-x returns byte strings as VIEWS into the input, so
|
|
168
|
+
// retaining one pins the whole frame and lets a later reuse mutate a verified signature.
|
|
169
|
+
signature: Uint8Array.from(signature),
|
|
170
|
+
};
|
|
171
|
+
assertDocumentControlConsistent(control);
|
|
172
|
+
return control;
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=document-control.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-control.js","sourceRoot":"","sources":["../src/document-control.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-DOC-ENVELOPE-1 — the document UPDATE envelope (§14).
|
|
3
|
+
*
|
|
4
|
+
* A distinct wire type from the handshake PROPOSAL envelope (whose hash mints the `document_id`).
|
|
5
|
+
* The two share the word, not the shape.
|
|
6
|
+
*
|
|
7
|
+
* ── WHY THREE OF THESE FIELDS ARE SECURITY FIELDS, NOT METADATA ───────────────────────────────
|
|
8
|
+
*
|
|
9
|
+
* DOD-DOC-GATE-1 enforces three bindings that no property of a Yjs update's BYTES can establish.
|
|
10
|
+
* The gate is only as trustworthy as the signature covering them, which is why all three sit
|
|
11
|
+
* inside the TBS rather than beside it:
|
|
12
|
+
*
|
|
13
|
+
* `document_id` A Yjs update carries no document identity. A well-formed update authored
|
|
14
|
+
* against a DIFFERENT document merges into this one silently and converges —
|
|
15
|
+
* there is no error, and the result is a splice of two documents.
|
|
16
|
+
*
|
|
17
|
+
* `update_encoding` v2-encoded bytes are ACCEPTED by the v1 decoder and silently drop all
|
|
18
|
+
* content. It cannot be sniffed either: a v2 update begins [0,0,…] and a
|
|
19
|
+
* legitimate pure-delete v1 delta begins [0,1,…], so a first-byte heuristic
|
|
20
|
+
* would refuse real deletions. The sender must SAY, under signature.
|
|
21
|
+
*
|
|
22
|
+
* `sender_client_id` Authorship in Yjs IS the clientID, and the gate refuses any client whose
|
|
23
|
+
* clock advances without being bound to the sender. §14 forbids persisting or
|
|
24
|
+
* deriving a clientID (a shared one splices two authors together with an
|
|
25
|
+
* empty pending set — measured in DOD-DOC-FUZZ-1), so an honest peer mints a
|
|
26
|
+
* fresh one on every restart. The gate's rule is therefore correct ONLY if
|
|
27
|
+
* the binding is LEARNABLE, and this envelope is where it is learned. Outside
|
|
28
|
+
* the signature, "learnable" would mean "assertable by anyone on the wire".
|
|
29
|
+
*
|
|
30
|
+
* `epoch_id` and `doc_prev_hash` are not optional and are not defaulted (§14). An update that does
|
|
31
|
+
* not state its epoch cannot be verified unambiguously after a compaction, and the chain link is
|
|
32
|
+
* what lets the document log be extracted and verified across sealed sessions.
|
|
33
|
+
*/
|
|
34
|
+
/** Domain tag in slot 0 of the to-be-signed array. */
|
|
35
|
+
export declare const DOCUMENT_UPDATE_DOMAIN = "CELLO-DOCUMENT-UPDATE-v1";
|
|
36
|
+
/**
|
|
37
|
+
* The pinned Yjs update encoding (§16.7-8). Pinned in the protocol types precisely so that two
|
|
38
|
+
* supporting clients can never disagree silently — the failure mode of disagreement is an empty
|
|
39
|
+
* document, not an error.
|
|
40
|
+
*/
|
|
41
|
+
export declare const DOCUMENT_UPDATE_ENCODING_V1 = "yjs-v1";
|
|
42
|
+
/** V1 has exactly one epoch. Compaction (which mints new epochs) is V2. */
|
|
43
|
+
export declare const DOCUMENT_EPOCH_V1 = 0;
|
|
44
|
+
export interface DocumentUpdateEnvelope {
|
|
45
|
+
type: "document_update";
|
|
46
|
+
/** Hash of the handshake proposal envelope that minted this document. */
|
|
47
|
+
document_id: string;
|
|
48
|
+
/** Constant `DOCUMENT_EPOCH_V1` in V1 — carried explicitly, never omitted. */
|
|
49
|
+
epoch_id: number;
|
|
50
|
+
/**
|
|
51
|
+
* The per-sender chain link: the `documentEnvelopeHash` of this sender's previous envelope for
|
|
52
|
+
* this document, or `null` for this sender's FIRST envelope. `null` and absent are different
|
|
53
|
+
* facts and are treated differently — see `decodeDocumentUpdateEnvelope`.
|
|
54
|
+
*/
|
|
55
|
+
doc_prev_hash: string | null;
|
|
56
|
+
sender_agent_id: string;
|
|
57
|
+
/** The Yjs clientID this update was authored under. See the header. */
|
|
58
|
+
sender_client_id: number;
|
|
59
|
+
/** Always `DOCUMENT_UPDATE_ENCODING_V1` in V1. See the header. */
|
|
60
|
+
update_encoding: string;
|
|
61
|
+
/** The sender's Yjs state vector at publish (§7) — what it had seen when it authored. */
|
|
62
|
+
state_vector: Uint8Array;
|
|
63
|
+
/** The Yjs update payload. */
|
|
64
|
+
update: Uint8Array;
|
|
65
|
+
/** Ed25519 (RFC 8032) over `buildDocumentUpdateTbs`. */
|
|
66
|
+
signature: Uint8Array;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The canonical to-be-signed preimage: a fixed-order CBOR ARRAY with the domain in slot 0.
|
|
70
|
+
*
|
|
71
|
+
* An array, not a map, because `encodeCbor` is deliberately NOT deterministic for maps — keys
|
|
72
|
+
* follow insertion order and the header is not minimal-length (see `cbor.ts`). Two honest senders
|
|
73
|
+
* that built the same envelope in a different field order would produce different signatures over
|
|
74
|
+
* it. Arrays are order-fixed and self-delimiting, which also removes the concatenation ambiguity
|
|
75
|
+
* that length-framing exists to prevent.
|
|
76
|
+
*
|
|
77
|
+
* The `signature` is excluded, as it must be — a statement cannot cover its own signature.
|
|
78
|
+
*
|
|
79
|
+
* `preHash: false` returns the preimage itself rather than its digest; it exists for the hash
|
|
80
|
+
* construction below and for tests that need to inspect the structure.
|
|
81
|
+
*/
|
|
82
|
+
export declare function buildDocumentUpdateTbs(env: DocumentUpdateEnvelope, opts?: {
|
|
83
|
+
preHash?: boolean;
|
|
84
|
+
}): Uint8Array;
|
|
85
|
+
/**
|
|
86
|
+
* The envelope's identity: SHA-256 over the same preimage that is signed.
|
|
87
|
+
*
|
|
88
|
+
* Deliberately the SAME preimage, so a chain link commits to exactly the bytes whose signature was
|
|
89
|
+
* verified — a link can never point at an envelope other than the one that was authenticated.
|
|
90
|
+
*
|
|
91
|
+
* It therefore excludes the signature, which is the property the chain needs: a re-signed but
|
|
92
|
+
* otherwise identical envelope keeps its hash, so anything already pointing at it (the next
|
|
93
|
+
* envelope in the chain, a `0x05` rejection leaf) is not orphaned.
|
|
94
|
+
*/
|
|
95
|
+
export declare function documentEnvelopeHash(env: DocumentUpdateEnvelope): string;
|
|
96
|
+
export declare function encodeDocumentUpdateEnvelope(env: DocumentUpdateEnvelope): Uint8Array;
|
|
97
|
+
/**
|
|
98
|
+
* Decode and VALIDATE. Every refusal names the field, because the caller's next move is to tell an
|
|
99
|
+
* operator which peer sent what — an unnamed refusal is a label on the exit point, not a diagnosis.
|
|
100
|
+
*/
|
|
101
|
+
export declare function decodeDocumentUpdateEnvelope(bytes: Uint8Array): DocumentUpdateEnvelope;
|
|
102
|
+
/**
|
|
103
|
+
* The receive-time chain check: does this envelope link to what we already hold from this sender?
|
|
104
|
+
*
|
|
105
|
+
* `known` is every envelope hash we hold from this sender for this document, and `head` is their
|
|
106
|
+
* most recent. Both, not just the head: replay is SET-based per §16.7-5, and delivery derives
|
|
107
|
+
* pending from the log and retries across restarts — so an older envelope arriving again, because
|
|
108
|
+
* its ack was lost while a later one landed, is designed behaviour. Comparing against the head
|
|
109
|
+
* alone recognised only the newest redelivery and reported every other one as a broken chain.
|
|
110
|
+
*
|
|
111
|
+
* The set is also what separates the two failures. A predecessor we have never seen is a GAP; a
|
|
112
|
+
* predecessor we hold that is not the head is a BRANCH. One reason string for both would hide a
|
|
113
|
+
* fork inside a delivery-shaped error.
|
|
114
|
+
*
|
|
115
|
+
* The refusal reasons are deliberately the SAME STRINGS the daemon's whole-log verifier uses
|
|
116
|
+
* (`DocumentStore.verifyChainLinkage`): `document_chain_forked` and `document_chain_broken`. Two
|
|
117
|
+
* vocabularies for one fact means a policy-log query or an operator keyed on one silently misses
|
|
118
|
+
* the other. The division of labour is real — this checks a single link on arrival, that one walks
|
|
119
|
+
* the whole log for reachability — but they are the same two failures.
|
|
120
|
+
*
|
|
121
|
+
* **LINKAGE ONLY — this is not authenticity.** `documentEnvelopeHash` excludes the signature, so
|
|
122
|
+
* `duplicate: true` means the TBS is byte-identical to one we hold, NOT that whoever sent it holds
|
|
123
|
+
* the key. A caller must verify the signature before consulting the chain; `duplicate` is the
|
|
124
|
+
* value most likely to be read as "already known, ack it and move on".
|
|
125
|
+
*/
|
|
126
|
+
export declare function verifyDocumentChainLink(env: DocumentUpdateEnvelope, sender: {
|
|
127
|
+
head: string | null;
|
|
128
|
+
known: ReadonlySet<string>;
|
|
129
|
+
}): {
|
|
130
|
+
duplicate: boolean;
|
|
131
|
+
};
|
|
132
|
+
//# sourceMappingURL=document-envelope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-envelope.d.ts","sourceRoot":"","sources":["../src/document-envelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAKH,sDAAsD;AACtD,eAAO,MAAM,sBAAsB,6BAA6B,CAAC;AAEjE;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,WAAW,CAAC;AAEpD,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAKnC,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,iBAAiB,CAAC;IACxB,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,gBAAgB,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,eAAe,EAAE,MAAM,CAAC;IACxB,yFAAyF;IACzF,YAAY,EAAE,UAAU,CAAC;IACzB,8BAA8B;IAC9B,MAAM,EAAE,UAAU,CAAC;IACnB,wDAAwD;IACxD,SAAS,EAAE,UAAU,CAAC;CACvB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,sBAAsB,EAC3B,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAC/B,UAAU,CAcZ;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,sBAAsB,GAAG,MAAM,CAIxE;AAED,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,sBAAsB,GAAG,UAAU,CAapF;AAmCD;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,UAAU,GAAG,sBAAsB,CAsEtF;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,sBAAsB,EAC3B,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAAE,GAC1D;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,CAiCxB"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-DOC-ENVELOPE-1 — the document UPDATE envelope (§14).
|
|
3
|
+
*
|
|
4
|
+
* A distinct wire type from the handshake PROPOSAL envelope (whose hash mints the `document_id`).
|
|
5
|
+
* The two share the word, not the shape.
|
|
6
|
+
*
|
|
7
|
+
* ── WHY THREE OF THESE FIELDS ARE SECURITY FIELDS, NOT METADATA ───────────────────────────────
|
|
8
|
+
*
|
|
9
|
+
* DOD-DOC-GATE-1 enforces three bindings that no property of a Yjs update's BYTES can establish.
|
|
10
|
+
* The gate is only as trustworthy as the signature covering them, which is why all three sit
|
|
11
|
+
* inside the TBS rather than beside it:
|
|
12
|
+
*
|
|
13
|
+
* `document_id` A Yjs update carries no document identity. A well-formed update authored
|
|
14
|
+
* against a DIFFERENT document merges into this one silently and converges —
|
|
15
|
+
* there is no error, and the result is a splice of two documents.
|
|
16
|
+
*
|
|
17
|
+
* `update_encoding` v2-encoded bytes are ACCEPTED by the v1 decoder and silently drop all
|
|
18
|
+
* content. It cannot be sniffed either: a v2 update begins [0,0,…] and a
|
|
19
|
+
* legitimate pure-delete v1 delta begins [0,1,…], so a first-byte heuristic
|
|
20
|
+
* would refuse real deletions. The sender must SAY, under signature.
|
|
21
|
+
*
|
|
22
|
+
* `sender_client_id` Authorship in Yjs IS the clientID, and the gate refuses any client whose
|
|
23
|
+
* clock advances without being bound to the sender. §14 forbids persisting or
|
|
24
|
+
* deriving a clientID (a shared one splices two authors together with an
|
|
25
|
+
* empty pending set — measured in DOD-DOC-FUZZ-1), so an honest peer mints a
|
|
26
|
+
* fresh one on every restart. The gate's rule is therefore correct ONLY if
|
|
27
|
+
* the binding is LEARNABLE, and this envelope is where it is learned. Outside
|
|
28
|
+
* the signature, "learnable" would mean "assertable by anyone on the wire".
|
|
29
|
+
*
|
|
30
|
+
* `epoch_id` and `doc_prev_hash` are not optional and are not defaulted (§14). An update that does
|
|
31
|
+
* not state its epoch cannot be verified unambiguously after a compaction, and the chain link is
|
|
32
|
+
* what lets the document log be extracted and verified across sealed sessions.
|
|
33
|
+
*/
|
|
34
|
+
import { createHash } from "node:crypto";
|
|
35
|
+
import { encodeCbor, decodeCbor } from "./cbor.js";
|
|
36
|
+
/** Domain tag in slot 0 of the to-be-signed array. */
|
|
37
|
+
export const DOCUMENT_UPDATE_DOMAIN = "CELLO-DOCUMENT-UPDATE-v1";
|
|
38
|
+
/**
|
|
39
|
+
* The pinned Yjs update encoding (§16.7-8). Pinned in the protocol types precisely so that two
|
|
40
|
+
* supporting clients can never disagree silently — the failure mode of disagreement is an empty
|
|
41
|
+
* document, not an error.
|
|
42
|
+
*/
|
|
43
|
+
export const DOCUMENT_UPDATE_ENCODING_V1 = "yjs-v1";
|
|
44
|
+
/** V1 has exactly one epoch. Compaction (which mints new epochs) is V2. */
|
|
45
|
+
export const DOCUMENT_EPOCH_V1 = 0;
|
|
46
|
+
/** A 32-byte hex digest — the shape of `document_id` and of every chain link. */
|
|
47
|
+
const HEX32 = /^[0-9a-f]{64}$/;
|
|
48
|
+
/**
|
|
49
|
+
* The canonical to-be-signed preimage: a fixed-order CBOR ARRAY with the domain in slot 0.
|
|
50
|
+
*
|
|
51
|
+
* An array, not a map, because `encodeCbor` is deliberately NOT deterministic for maps — keys
|
|
52
|
+
* follow insertion order and the header is not minimal-length (see `cbor.ts`). Two honest senders
|
|
53
|
+
* that built the same envelope in a different field order would produce different signatures over
|
|
54
|
+
* it. Arrays are order-fixed and self-delimiting, which also removes the concatenation ambiguity
|
|
55
|
+
* that length-framing exists to prevent.
|
|
56
|
+
*
|
|
57
|
+
* The `signature` is excluded, as it must be — a statement cannot cover its own signature.
|
|
58
|
+
*
|
|
59
|
+
* `preHash: false` returns the preimage itself rather than its digest; it exists for the hash
|
|
60
|
+
* construction below and for tests that need to inspect the structure.
|
|
61
|
+
*/
|
|
62
|
+
export function buildDocumentUpdateTbs(env, opts = {}) {
|
|
63
|
+
const preimage = encodeCbor([
|
|
64
|
+
DOCUMENT_UPDATE_DOMAIN,
|
|
65
|
+
env.document_id,
|
|
66
|
+
env.epoch_id,
|
|
67
|
+
env.doc_prev_hash, // null encodes distinctly from any string — genesis is unambiguous
|
|
68
|
+
env.sender_agent_id,
|
|
69
|
+
env.sender_client_id,
|
|
70
|
+
env.update_encoding,
|
|
71
|
+
env.state_vector,
|
|
72
|
+
env.update,
|
|
73
|
+
]);
|
|
74
|
+
if (opts.preHash === false)
|
|
75
|
+
return preimage;
|
|
76
|
+
return new Uint8Array(createHash("sha256").update(preimage).digest());
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The envelope's identity: SHA-256 over the same preimage that is signed.
|
|
80
|
+
*
|
|
81
|
+
* Deliberately the SAME preimage, so a chain link commits to exactly the bytes whose signature was
|
|
82
|
+
* verified — a link can never point at an envelope other than the one that was authenticated.
|
|
83
|
+
*
|
|
84
|
+
* It therefore excludes the signature, which is the property the chain needs: a re-signed but
|
|
85
|
+
* otherwise identical envelope keeps its hash, so anything already pointing at it (the next
|
|
86
|
+
* envelope in the chain, a `0x05` rejection leaf) is not orphaned.
|
|
87
|
+
*/
|
|
88
|
+
export function documentEnvelopeHash(env) {
|
|
89
|
+
return createHash("sha256")
|
|
90
|
+
.update(buildDocumentUpdateTbs(env, { preHash: false }))
|
|
91
|
+
.digest("hex");
|
|
92
|
+
}
|
|
93
|
+
export function encodeDocumentUpdateEnvelope(env) {
|
|
94
|
+
return encodeCbor({
|
|
95
|
+
type: env.type,
|
|
96
|
+
document_id: env.document_id,
|
|
97
|
+
epoch_id: env.epoch_id,
|
|
98
|
+
doc_prev_hash: env.doc_prev_hash,
|
|
99
|
+
sender_agent_id: env.sender_agent_id,
|
|
100
|
+
sender_client_id: env.sender_client_id,
|
|
101
|
+
update_encoding: env.update_encoding,
|
|
102
|
+
state_vector: env.state_vector,
|
|
103
|
+
update: env.update,
|
|
104
|
+
signature: env.signature,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function requirePresent(map, field) {
|
|
108
|
+
// `in`, not a truthiness or nullish check. ABSENT and `null` are different facts: absent
|
|
109
|
+
// doc_prev_hash read as null would turn every gap in a chain into a fresh valid-looking genesis,
|
|
110
|
+
// and absent epoch_id read as 0 would make a post-compaction ambiguity silent. A decoder that
|
|
111
|
+
// supplies a default for a field the spec calls mandatory is manufacturing the sender's claim.
|
|
112
|
+
if (!(field in map)) {
|
|
113
|
+
throw new Error(`document_envelope_missing_field: ${field} is mandatory and was not present`);
|
|
114
|
+
}
|
|
115
|
+
return map[field];
|
|
116
|
+
}
|
|
117
|
+
function requireBytes(map, field) {
|
|
118
|
+
const value = requirePresent(map, field);
|
|
119
|
+
if (!(value instanceof Uint8Array)) {
|
|
120
|
+
throw new Error(`document_envelope_field_type: ${field} must be a CBOR byte string`);
|
|
121
|
+
}
|
|
122
|
+
// COPIED, not returned as-is. Measured: cbor-x returns byte strings as VIEWS into the buffer it
|
|
123
|
+
// was handed (`decoded.buffer === input.buffer`), so an aliased update mutates when the caller
|
|
124
|
+
// reuses or zeroes its read buffer — and a pooled network read buffer is exactly that. Every
|
|
125
|
+
// check in this decoder, and every check the gate makes afterwards, would then have been made
|
|
126
|
+
// against bytes that are no longer the bytes applied. The copy is also what makes the return
|
|
127
|
+
// type honest: a Buffer view and a Uint8Array are not interchangeable to a structural comparison.
|
|
128
|
+
return new Uint8Array(value);
|
|
129
|
+
}
|
|
130
|
+
function requireString(map, field) {
|
|
131
|
+
const value = requirePresent(map, field);
|
|
132
|
+
if (typeof value !== "string") {
|
|
133
|
+
throw new Error(`document_envelope_field_type: ${field} must be a text string`);
|
|
134
|
+
}
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Decode and VALIDATE. Every refusal names the field, because the caller's next move is to tell an
|
|
139
|
+
* operator which peer sent what — an unnamed refusal is a label on the exit point, not a diagnosis.
|
|
140
|
+
*/
|
|
141
|
+
export function decodeDocumentUpdateEnvelope(bytes) {
|
|
142
|
+
const decoded = decodeCbor(bytes);
|
|
143
|
+
if (typeof decoded !== "object" || decoded === null || Array.isArray(decoded)) {
|
|
144
|
+
throw new Error("document_envelope_malformed: not a CBOR map");
|
|
145
|
+
}
|
|
146
|
+
const map = decoded;
|
|
147
|
+
const type = requireString(map, "type");
|
|
148
|
+
if (type !== "document_update") {
|
|
149
|
+
throw new Error(`document_envelope_type: expected document_update, got ${type}`);
|
|
150
|
+
}
|
|
151
|
+
const documentId = requireString(map, "document_id");
|
|
152
|
+
if (!HEX32.test(documentId)) {
|
|
153
|
+
throw new Error(`document_envelope_document_id: must be a 32-byte lowercase hex digest, got "${documentId}"`);
|
|
154
|
+
}
|
|
155
|
+
const epochId = requirePresent(map, "epoch_id");
|
|
156
|
+
if (typeof epochId !== "number" || !Number.isInteger(epochId)) {
|
|
157
|
+
throw new Error(`document_envelope_epoch: must be an integer, got ${String(epochId)}`);
|
|
158
|
+
}
|
|
159
|
+
if (epochId !== DOCUMENT_EPOCH_V1) {
|
|
160
|
+
// V2 introduces compaction and non-zero epochs. Accepting one now would mean applying an
|
|
161
|
+
// update whose base state this build cannot reconstruct.
|
|
162
|
+
throw new Error(`document_envelope_epoch: this build speaks epoch ${DOCUMENT_EPOCH_V1} only, got ${epochId}`);
|
|
163
|
+
}
|
|
164
|
+
const prev = requirePresent(map, "doc_prev_hash");
|
|
165
|
+
if (prev !== null && (typeof prev !== "string" || !HEX32.test(prev))) {
|
|
166
|
+
throw new Error(`document_envelope_prev_hash: must be a 32-byte hex digest or explicit null, got "${String(prev)}"`);
|
|
167
|
+
}
|
|
168
|
+
const senderAgentId = requireString(map, "sender_agent_id");
|
|
169
|
+
if (senderAgentId.length === 0) {
|
|
170
|
+
throw new Error("document_envelope_sender: sender_agent_id must not be empty");
|
|
171
|
+
}
|
|
172
|
+
const clientId = requirePresent(map, "sender_client_id");
|
|
173
|
+
if (typeof clientId !== "number" || !Number.isInteger(clientId) || clientId < 0) {
|
|
174
|
+
throw new Error(`document_envelope_client_id: must be a non-negative integer, got ${String(clientId)}`);
|
|
175
|
+
}
|
|
176
|
+
const encoding = requireString(map, "update_encoding");
|
|
177
|
+
if (encoding !== DOCUMENT_UPDATE_ENCODING_V1) {
|
|
178
|
+
// The one refusal that cannot be replaced by a heuristic. See the header.
|
|
179
|
+
throw new Error(`document_envelope_encoding: this build speaks ${DOCUMENT_UPDATE_ENCODING_V1} only, got "${encoding}"`);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
type: "document_update",
|
|
183
|
+
document_id: documentId,
|
|
184
|
+
epoch_id: epochId,
|
|
185
|
+
doc_prev_hash: prev,
|
|
186
|
+
sender_agent_id: senderAgentId,
|
|
187
|
+
sender_client_id: clientId,
|
|
188
|
+
update_encoding: encoding,
|
|
189
|
+
state_vector: requireBytes(map, "state_vector"),
|
|
190
|
+
update: requireBytes(map, "update"),
|
|
191
|
+
signature: requireBytes(map, "signature"),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The receive-time chain check: does this envelope link to what we already hold from this sender?
|
|
196
|
+
*
|
|
197
|
+
* `known` is every envelope hash we hold from this sender for this document, and `head` is their
|
|
198
|
+
* most recent. Both, not just the head: replay is SET-based per §16.7-5, and delivery derives
|
|
199
|
+
* pending from the log and retries across restarts — so an older envelope arriving again, because
|
|
200
|
+
* its ack was lost while a later one landed, is designed behaviour. Comparing against the head
|
|
201
|
+
* alone recognised only the newest redelivery and reported every other one as a broken chain.
|
|
202
|
+
*
|
|
203
|
+
* The set is also what separates the two failures. A predecessor we have never seen is a GAP; a
|
|
204
|
+
* predecessor we hold that is not the head is a BRANCH. One reason string for both would hide a
|
|
205
|
+
* fork inside a delivery-shaped error.
|
|
206
|
+
*
|
|
207
|
+
* The refusal reasons are deliberately the SAME STRINGS the daemon's whole-log verifier uses
|
|
208
|
+
* (`DocumentStore.verifyChainLinkage`): `document_chain_forked` and `document_chain_broken`. Two
|
|
209
|
+
* vocabularies for one fact means a policy-log query or an operator keyed on one silently misses
|
|
210
|
+
* the other. The division of labour is real — this checks a single link on arrival, that one walks
|
|
211
|
+
* the whole log for reachability — but they are the same two failures.
|
|
212
|
+
*
|
|
213
|
+
* **LINKAGE ONLY — this is not authenticity.** `documentEnvelopeHash` excludes the signature, so
|
|
214
|
+
* `duplicate: true` means the TBS is byte-identical to one we hold, NOT that whoever sent it holds
|
|
215
|
+
* the key. A caller must verify the signature before consulting the chain; `duplicate` is the
|
|
216
|
+
* value most likely to be read as "already known, ack it and move on".
|
|
217
|
+
*/
|
|
218
|
+
export function verifyDocumentChainLink(env, sender) {
|
|
219
|
+
if (sender.known.has(documentEnvelopeHash(env)))
|
|
220
|
+
return { duplicate: true };
|
|
221
|
+
if (env.doc_prev_hash === null) {
|
|
222
|
+
if (sender.head !== null) {
|
|
223
|
+
// Two roots for one sender. DOD-DOC-REJECT-1 measured what this costs: the chain no longer
|
|
224
|
+
// verifies, so the document rebuilds until the next daemon restart and is permanently
|
|
225
|
+
// unopenable after it. The log is append-only; there is no repair.
|
|
226
|
+
throw new Error(`document_chain_forked: ${env.sender_agent_id} sent a genesis envelope for document ` +
|
|
227
|
+
`${env.document_id}, but we already hold their chain at ${sender.head}`);
|
|
228
|
+
}
|
|
229
|
+
return { duplicate: false };
|
|
230
|
+
}
|
|
231
|
+
if (!sender.known.has(env.doc_prev_hash)) {
|
|
232
|
+
throw new Error(`document_chain_broken: ${env.sender_agent_id} chained to ${env.doc_prev_hash} for document ` +
|
|
233
|
+
`${env.document_id}, which we have never seen — their last envelope we hold is ` +
|
|
234
|
+
`${sender.head ?? "none (no envelope from this sender yet)"}`);
|
|
235
|
+
}
|
|
236
|
+
if (env.doc_prev_hash !== sender.head) {
|
|
237
|
+
throw new Error(`document_chain_forked: ${env.sender_agent_id} chained to ${env.doc_prev_hash} for document ` +
|
|
238
|
+
`${env.document_id}, which we hold but which is not their head ${sender.head} — a chain ` +
|
|
239
|
+
`branches nowhere`);
|
|
240
|
+
}
|
|
241
|
+
return { duplicate: false };
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=document-envelope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-envelope.js","sourceRoot":"","sources":["../src/document-envelope.ts"],"names":[],"mappings":"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"}
|