@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.
@@ -0,0 +1,98 @@
1
+ /**
2
+ * DOD-DOC-TOOLS-1 — the PROPOSAL ack (§16.3).
3
+ *
4
+ * The consent decision, told to the party who asked for it.
5
+ *
6
+ * ── WHY THIS EXISTS ───────────────────────────────────────────────────────────────────────────
7
+ *
8
+ * Without it the proposer is never told the answer. `cello_doc_list` had to INFER acceptance from
9
+ * "the peer has published into it", which conflates three different situations — refused, never
10
+ * received, and accepted-but-untouched — into one absent flag. Two of those want the operator to
11
+ * act and one does not, and the surface could not tell them apart. A protocol that asks for consent
12
+ * and never reports it is not asking; it is announcing.
13
+ *
14
+ * ── WHY IT IS SEPARATE FROM `document_ack` ────────────────────────────────────────────────────
15
+ *
16
+ * `document_ack` settles an ENVELOPE, identified by its hash in the log. A proposal is not in the
17
+ * envelope log — there is no document yet, which is the whole point — so an ack shaped around an
18
+ * envelope hash would carry a hash of nothing, and its consumer looks the envelope up. Overloading
19
+ * the frame would mean one decoder branching on whether the thing it references exists, which is
20
+ * how a settle-once rule gets applied to the wrong table.
21
+ *
22
+ * ── WHY IT IS SIGNED, AND WHAT THE SIGNATURE COVERS ───────────────────────────────────────────
23
+ *
24
+ * A refusal ack tells the proposer to stop: no retry, no document. Unsigned, anyone reaching the
25
+ * channel could make a proposal appear refused by a party who never saw it, and the two operators
26
+ * would each believe the other had walked away. The signature covers the DOCUMENT ID — which
27
+ * commits to the proposer, the peer, the properties and the nonce — so an ack cannot be moved to
28
+ * another proposal, and the decision itself, because the whole statement is the claim.
29
+ *
30
+ * ── SETTLE ONCE ───────────────────────────────────────────────────────────────────────────────
31
+ *
32
+ * Nothing here orders two acks from one acker, and the same rule `document_ack` states applies: a
33
+ * second, CONTRADICTING ack is an error with both signatures retained, never an update. A peer that
34
+ * accepted must not be able to later claim it refused — the proposer would tear down a document the
35
+ * peer is still editing.
36
+ */
37
+ /** Domain tag in slot 0. Distinct from the update, ack, proposal and rejection domains. */
38
+ export declare const DOCUMENT_PROPOSAL_ACK_DOMAIN = "CELLO-DOCUMENT-PROPOSAL-ACK-v1";
39
+ /**
40
+ * The frame version, ON THE WIRE.
41
+ *
42
+ * The `-v1` in the domain never travels, so a V2 acker's frame would decode cleanly as V1 and fail
43
+ * SIGNATURE verification — sending an operator to key management for a version-skew bug. Skew that
44
+ * is not SAID becomes silent loss or a misattributed error.
45
+ */
46
+ export declare const DOCUMENT_PROPOSAL_ACK_VERSION = 1;
47
+ /** A refusal reason is peer-controlled text bound for an operator's screen. Capped for that reason. */
48
+ export declare const MAX_PROPOSAL_REFUSAL_REASON_LENGTH = 200;
49
+ export interface DocumentProposalAck {
50
+ type: "document_proposal_ack";
51
+ ack_version: number;
52
+ /** The proposal being answered. Hash of the proposal's own preimage — see `documentIdFromProposal`. */
53
+ document_id: string;
54
+ /** Who decided — the party the proposal was addressed to. */
55
+ acker_agent_id: string;
56
+ /**
57
+ * `true` accepted, `false` refused. Both are ANSWERS; there is no third value, because an ack
58
+ * that does not say which is not a decision.
59
+ */
60
+ accepted: boolean;
61
+ /** Present iff `accepted` is false. The operator's words, or the machine reason that auto-refused. */
62
+ refusal_reason?: string;
63
+ decided_at_ms: number;
64
+ /** Ed25519 (RFC 8032) over `buildDocumentProposalAckTbs`. */
65
+ signature: Uint8Array;
66
+ }
67
+ /**
68
+ * The cross-field rules, checked on ENCODE as well as decode.
69
+ *
70
+ * On encode too, because a locally-built contradictory ack would otherwise be signed and shipped and
71
+ * fail only at the remote decode: the acker sees success, the proposer sees an error, and the two
72
+ * never meet. The rule belongs where the object is built, not only where it is read.
73
+ */
74
+ export declare function assertDocumentProposalAckConsistent(ack: DocumentProposalAck): void;
75
+ /**
76
+ * The canonical to-be-signed preimage: a fixed-order CBOR ARRAY with the domain in slot 0.
77
+ *
78
+ * An array rather than a map for the reason `cbor.ts` gives — this encoder is deliberately not
79
+ * deterministic for maps, so a map preimage would make the signature depend on the order the acker
80
+ * happened to build the object in, and two honest implementations would disagree.
81
+ *
82
+ * `refusal_reason` occupies its slot as `null` when absent, so no field's meaning depends on
83
+ * whether the one before it was present.
84
+ */
85
+ export declare function buildDocumentProposalAckTbs(ack: DocumentProposalAck, opts?: {
86
+ preHash?: boolean;
87
+ }): Uint8Array;
88
+ export declare function encodeDocumentProposalAck(ack: DocumentProposalAck): Uint8Array;
89
+ /**
90
+ * Decode and validate. Refuses rather than defaulting on every field.
91
+ *
92
+ * The one that matters most is `accepted`. Coerced, a truthy string like `"false"` would record a
93
+ * REFUSED proposal as accepted, and the proposer would keep a document, keep publishing into it,
94
+ * and keep delivering to a peer who declined — every update refused at the far end for a reason
95
+ * neither operator can see.
96
+ */
97
+ export declare function decodeDocumentProposalAck(bytes: Uint8Array): DocumentProposalAck;
98
+ //# sourceMappingURL=document-proposal-ack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-proposal-ack.d.ts","sourceRoot":"","sources":["../src/document-proposal-ack.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAKH,2FAA2F;AAC3F,eAAO,MAAM,4BAA4B,mCAAmC,CAAC;AAE7E;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,IAAI,CAAC;AAE/C,uGAAuG;AACvG,eAAO,MAAM,kCAAkC,MAAM,CAAC;AAItD,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,uBAAuB,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,uGAAuG;IACvG,WAAW,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,cAAc,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB,sGAAsG;IACtG,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,SAAS,EAAE,UAAU,CAAC;CACvB;AAOD;;;;;;GAMG;AACH,wBAAgB,mCAAmC,CAAC,GAAG,EAAE,mBAAmB,GAAG,IAAI,CAsBlF;AAED;;;;;;;;;GASG;AACH,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,mBAAmB,EACxB,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAC/B,UAAU,CAoBZ;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,mBAAmB,GAAG,UAAU,CAY9E;AAWD;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,UAAU,GAAG,mBAAmB,CAgEhF"}
@@ -0,0 +1,203 @@
1
+ /**
2
+ * DOD-DOC-TOOLS-1 — the PROPOSAL ack (§16.3).
3
+ *
4
+ * The consent decision, told to the party who asked for it.
5
+ *
6
+ * ── WHY THIS EXISTS ───────────────────────────────────────────────────────────────────────────
7
+ *
8
+ * Without it the proposer is never told the answer. `cello_doc_list` had to INFER acceptance from
9
+ * "the peer has published into it", which conflates three different situations — refused, never
10
+ * received, and accepted-but-untouched — into one absent flag. Two of those want the operator to
11
+ * act and one does not, and the surface could not tell them apart. A protocol that asks for consent
12
+ * and never reports it is not asking; it is announcing.
13
+ *
14
+ * ── WHY IT IS SEPARATE FROM `document_ack` ────────────────────────────────────────────────────
15
+ *
16
+ * `document_ack` settles an ENVELOPE, identified by its hash in the log. A proposal is not in the
17
+ * envelope log — there is no document yet, which is the whole point — so an ack shaped around an
18
+ * envelope hash would carry a hash of nothing, and its consumer looks the envelope up. Overloading
19
+ * the frame would mean one decoder branching on whether the thing it references exists, which is
20
+ * how a settle-once rule gets applied to the wrong table.
21
+ *
22
+ * ── WHY IT IS SIGNED, AND WHAT THE SIGNATURE COVERS ───────────────────────────────────────────
23
+ *
24
+ * A refusal ack tells the proposer to stop: no retry, no document. Unsigned, anyone reaching the
25
+ * channel could make a proposal appear refused by a party who never saw it, and the two operators
26
+ * would each believe the other had walked away. The signature covers the DOCUMENT ID — which
27
+ * commits to the proposer, the peer, the properties and the nonce — so an ack cannot be moved to
28
+ * another proposal, and the decision itself, because the whole statement is the claim.
29
+ *
30
+ * ── SETTLE ONCE ───────────────────────────────────────────────────────────────────────────────
31
+ *
32
+ * Nothing here orders two acks from one acker, and the same rule `document_ack` states applies: a
33
+ * second, CONTRADICTING ack is an error with both signatures retained, never an update. A peer that
34
+ * accepted must not be able to later claim it refused — the proposer would tear down a document the
35
+ * peer is still editing.
36
+ */
37
+ import { createHash } from "node:crypto";
38
+ import { encodeCbor, decodeCbor } from "./cbor.js";
39
+ /** Domain tag in slot 0. Distinct from the update, ack, proposal and rejection domains. */
40
+ export const DOCUMENT_PROPOSAL_ACK_DOMAIN = "CELLO-DOCUMENT-PROPOSAL-ACK-v1";
41
+ /**
42
+ * The frame version, ON THE WIRE.
43
+ *
44
+ * The `-v1` in the domain never travels, so a V2 acker's frame would decode cleanly as V1 and fail
45
+ * SIGNATURE verification — sending an operator to key management for a version-skew bug. Skew that
46
+ * is not SAID becomes silent loss or a misattributed error.
47
+ */
48
+ export const DOCUMENT_PROPOSAL_ACK_VERSION = 1;
49
+ /** A refusal reason is peer-controlled text bound for an operator's screen. Capped for that reason. */
50
+ export const MAX_PROPOSAL_REFUSAL_REASON_LENGTH = 200;
51
+ const HEX32 = /^[0-9a-f]{64}$/;
52
+ /** Empty string means ABSENT — see `assertDocumentProposalAckConsistent`. */
53
+ function normalizeReason(reason) {
54
+ return reason === undefined || reason === "" ? null : reason;
55
+ }
56
+ /**
57
+ * The cross-field rules, checked on ENCODE as well as decode.
58
+ *
59
+ * On encode too, because a locally-built contradictory ack would otherwise be signed and shipped and
60
+ * fail only at the remote decode: the acker sees success, the proposer sees an error, and the two
61
+ * never meet. The rule belongs where the object is built, not only where it is read.
62
+ */
63
+ export function assertDocumentProposalAckConsistent(ack) {
64
+ const reason = normalizeReason(ack.refusal_reason);
65
+ if (!ack.accepted && reason === null) {
66
+ // A refusal with no reason leaves the proposer unable to propose anything better. That is the
67
+ // failure that makes people give up on a protocol rather than adjust to it.
68
+ throw new Error("document_proposal_ack_reason: a refusal must carry its reason");
69
+ }
70
+ if (ack.accepted && reason !== null) {
71
+ // A contradiction: whichever field a reader trusts, the other is lying to them.
72
+ throw new Error(`document_proposal_ack_reason: an acceptance must not carry a refusal reason, and this one carries "${reason}"`);
73
+ }
74
+ if (reason !== null && reason.length > MAX_PROPOSAL_REFUSAL_REASON_LENGTH) {
75
+ throw new Error(`document_proposal_ack_reason: a refusal reason may be at most ` +
76
+ `${MAX_PROPOSAL_REFUSAL_REASON_LENGTH} characters, and this one is ${reason.length}`);
77
+ }
78
+ if (!HEX32.test(ack.document_id)) {
79
+ throw new Error(`document_proposal_ack_document_id: expected 64 lowercase hex characters`);
80
+ }
81
+ }
82
+ /**
83
+ * The canonical to-be-signed preimage: a fixed-order CBOR ARRAY with the domain in slot 0.
84
+ *
85
+ * An array rather than a map for the reason `cbor.ts` gives — this encoder is deliberately not
86
+ * deterministic for maps, so a map preimage would make the signature depend on the order the acker
87
+ * happened to build the object in, and two honest implementations would disagree.
88
+ *
89
+ * `refusal_reason` occupies its slot as `null` when absent, so no field's meaning depends on
90
+ * whether the one before it was present.
91
+ */
92
+ export function buildDocumentProposalAckTbs(ack, opts = {}) {
93
+ assertDocumentProposalAckConsistent(ack);
94
+ const preimage = encodeCbor([
95
+ DOCUMENT_PROPOSAL_ACK_DOMAIN,
96
+ ack.ack_version,
97
+ ack.document_id,
98
+ ack.acker_agent_id,
99
+ ack.accepted,
100
+ normalizeReason(ack.refusal_reason),
101
+ // BIGINT past 0xffffffff. cbor-x encodes a JS number that large as an IEEE float64 (`fb`)
102
+ // rather than a uint64 (`1b`), so an implementation encoding RFC 8949-canonically would compute
103
+ // different TBS bytes and reject a GENUINE ack — surfacing as a signature failure, which reads
104
+ // as forgery. A millisecond timestamp is always that large. Four sibling builders carry this
105
+ // same coercion.
106
+ typeof ack.decided_at_ms === "number" && ack.decided_at_ms > 0xffffffff
107
+ ? BigInt(ack.decided_at_ms)
108
+ : ack.decided_at_ms,
109
+ ]);
110
+ if (opts.preHash === false)
111
+ return preimage;
112
+ return new Uint8Array(createHash("sha256").update(preimage).digest());
113
+ }
114
+ export function encodeDocumentProposalAck(ack) {
115
+ assertDocumentProposalAckConsistent(ack);
116
+ return encodeCbor({
117
+ type: ack.type,
118
+ ack_version: ack.ack_version,
119
+ document_id: ack.document_id,
120
+ acker_agent_id: ack.acker_agent_id,
121
+ accepted: ack.accepted,
122
+ refusal_reason: normalizeReason(ack.refusal_reason),
123
+ decided_at_ms: ack.decided_at_ms,
124
+ signature: ack.signature,
125
+ });
126
+ }
127
+ function present(map, field) {
128
+ // `in`, not a nullish check. A defaulted field is a claim the acker never made, and carrying
129
+ // their claim is this frame's entire job.
130
+ if (!(field in map)) {
131
+ throw new Error(`document_proposal_ack_missing_field: ${field} is mandatory and was not present`);
132
+ }
133
+ return map[field];
134
+ }
135
+ /**
136
+ * Decode and validate. Refuses rather than defaulting on every field.
137
+ *
138
+ * The one that matters most is `accepted`. Coerced, a truthy string like `"false"` would record a
139
+ * REFUSED proposal as accepted, and the proposer would keep a document, keep publishing into it,
140
+ * and keep delivering to a peer who declined — every update refused at the far end for a reason
141
+ * neither operator can see.
142
+ */
143
+ export function decodeDocumentProposalAck(bytes) {
144
+ const decoded = decodeCbor(bytes);
145
+ if (typeof decoded !== "object" || decoded === null || Array.isArray(decoded)) {
146
+ throw new Error("document_proposal_ack_malformed: not a CBOR map");
147
+ }
148
+ const map = decoded;
149
+ if (present(map, "type") !== "document_proposal_ack") {
150
+ throw new Error(`document_proposal_ack_type: expected document_proposal_ack`);
151
+ }
152
+ const ackVersion = present(map, "ack_version");
153
+ if (typeof ackVersion !== "number" || !Number.isInteger(ackVersion)) {
154
+ throw new Error("document_proposal_ack_version: must be an integer");
155
+ }
156
+ if (ackVersion !== DOCUMENT_PROPOSAL_ACK_VERSION) {
157
+ // REFUSED BY VALUE, and said out loud. A frame from a future build would otherwise decode
158
+ // cleanly and fail signature verification, sending whoever reads the error to key management.
159
+ throw new Error(`document_proposal_ack_version_unsupported: this build speaks version ` +
160
+ `${DOCUMENT_PROPOSAL_ACK_VERSION} and the ack declares ${ackVersion} — the peer is running a ` +
161
+ `newer CELLO and one of you needs to upgrade`);
162
+ }
163
+ const documentId = present(map, "document_id");
164
+ if (typeof documentId !== "string" || !HEX32.test(documentId)) {
165
+ throw new Error("document_proposal_ack_document_id: expected 64 lowercase hex characters");
166
+ }
167
+ const ackerAgentId = present(map, "acker_agent_id");
168
+ if (typeof ackerAgentId !== "string" || ackerAgentId.length === 0) {
169
+ throw new Error("document_proposal_ack_acker: must be a non-empty string");
170
+ }
171
+ const accepted = present(map, "accepted");
172
+ if (typeof accepted !== "boolean") {
173
+ throw new Error("document_proposal_ack_accepted: must be a boolean, never a coerced value");
174
+ }
175
+ const refusalReason = present(map, "refusal_reason");
176
+ if (refusalReason !== null && typeof refusalReason !== "string") {
177
+ throw new Error("document_proposal_ack_reason: must be a string or null");
178
+ }
179
+ const decidedAt = present(map, "decided_at_ms");
180
+ if (typeof decidedAt !== "number" || !Number.isFinite(decidedAt)) {
181
+ throw new Error("document_proposal_ack_decided_at: must be a finite number");
182
+ }
183
+ const signature = present(map, "signature");
184
+ if (!(signature instanceof Uint8Array) || signature.length !== 64) {
185
+ throw new Error("document_proposal_ack_signature: expected 64 bytes");
186
+ }
187
+ const ack = {
188
+ type: "document_proposal_ack",
189
+ ack_version: ackVersion,
190
+ document_id: documentId,
191
+ acker_agent_id: ackerAgentId,
192
+ accepted,
193
+ ...(refusalReason !== null && refusalReason !== "" ? { refusal_reason: refusalReason } : {}),
194
+ decided_at_ms: decidedAt,
195
+ // COPIED out of the decode buffer. cbor-x returns byte strings as VIEWS into the input, so
196
+ // retaining one pins the whole frame and lets a later reuse of that buffer mutate a signature
197
+ // already verified.
198
+ signature: Uint8Array.from(signature),
199
+ };
200
+ assertDocumentProposalAckConsistent(ack);
201
+ return ack;
202
+ }
203
+ //# sourceMappingURL=document-proposal-ack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-proposal-ack.js","sourceRoot":"","sources":["../src/document-proposal-ack.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEnD,2FAA2F;AAC3F,MAAM,CAAC,MAAM,4BAA4B,GAAG,gCAAgC,CAAC;AAE7E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC;AAE/C,uGAAuG;AACvG,MAAM,CAAC,MAAM,kCAAkC,GAAG,GAAG,CAAC;AAEtD,MAAM,KAAK,GAAG,gBAAgB,CAAC;AAqB/B,6EAA6E;AAC7E,SAAS,eAAe,CAAC,MAA0B;IACjD,OAAO,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;AAC/D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mCAAmC,CAAC,GAAwB;IAC1E,MAAM,MAAM,GAAG,eAAe,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACnD,IAAI,CAAC,GAAG,CAAC,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACrC,8FAA8F;QAC9F,4EAA4E;QAC5E,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;IACnF,CAAC;IACD,IAAI,GAAG,CAAC,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpC,gFAAgF;QAChF,MAAM,IAAI,KAAK,CACb,sGAAsG,MAAM,GAAG,CAChH,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,MAAM,GAAG,kCAAkC,EAAE,CAAC;QAC1E,MAAM,IAAI,KAAK,CACb,gEAAgE;YAC9D,GAAG,kCAAkC,gCAAgC,MAAM,CAAC,MAAM,EAAE,CACvF,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,yEAAyE,CAAC,CAAC;IAC7F,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,2BAA2B,CACzC,GAAwB,EACxB,OAA8B,EAAE;IAEhC,mCAAmC,CAAC,GAAG,CAAC,CAAC;IACzC,MAAM,QAAQ,GAAG,UAAU,CAAC;QAC1B,4BAA4B;QAC5B,GAAG,CAAC,WAAW;QACf,GAAG,CAAC,WAAW;QACf,GAAG,CAAC,cAAc;QAClB,GAAG,CAAC,QAAQ;QACZ,eAAe,CAAC,GAAG,CAAC,cAAc,CAAC;QACnC,0FAA0F;QAC1F,gGAAgG;QAChG,+FAA+F;QAC/F,6FAA6F;QAC7F,iBAAiB;QACjB,OAAO,GAAG,CAAC,aAAa,KAAK,QAAQ,IAAI,GAAG,CAAC,aAAa,GAAG,UAAU;YACrE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC;YAC3B,CAAC,CAAC,GAAG,CAAC,aAAa;KACtB,CAAC,CAAC;IACH,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK;QAAE,OAAO,QAAQ,CAAC;IAC5C,OAAO,IAAI,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,GAAwB;IAChE,mCAAmC,CAAC,GAAG,CAAC,CAAC;IACzC,OAAO,UAAU,CAAC;QAChB,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,WAAW,EAAE,GAAG,CAAC,WAAW;QAC5B,WAAW,EAAE,GAAG,CAAC,WAAW;QAC5B,cAAc,EAAE,GAAG,CAAC,cAAc;QAClC,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,cAAc,EAAE,eAAe,CAAC,GAAG,CAAC,cAAc,CAAC;QACnD,aAAa,EAAE,GAAG,CAAC,aAAa;QAChC,SAAS,EAAE,GAAG,CAAC,SAAS;KACzB,CAAC,CAAC;AACL,CAAC;AAED,SAAS,OAAO,CAAC,GAA4B,EAAE,KAAa;IAC1D,6FAA6F;IAC7F,0CAA0C;IAC1C,IAAI,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,wCAAwC,KAAK,mCAAmC,CAAC,CAAC;IACpG,CAAC;IACD,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC;AACpB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAAiB;IACzD,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;IAClC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9E,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACrE,CAAC;IACD,MAAM,GAAG,GAAG,OAAkC,CAAC;IAE/C,IAAI,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,uBAAuB,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;IAChF,CAAC;IACD,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;IAC/C,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE,CAAC;QACpE,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;IACvE,CAAC;IACD,IAAI,UAAU,KAAK,6BAA6B,EAAE,CAAC;QACjD,0FAA0F;QAC1F,8FAA8F;QAC9F,MAAM,IAAI,KAAK,CACb,uEAAuE;YACrE,GAAG,6BAA6B,yBAAyB,UAAU,2BAA2B;YAC9F,6CAA6C,CAChD,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;IAC/C,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9D,MAAM,IAAI,KAAK,CAAC,yEAAyE,CAAC,CAAC;IAC7F,CAAC;IACD,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC;IACpD,IAAI,OAAO,YAAY,KAAK,QAAQ,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;IAC7E,CAAC;IACD,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;IAC1C,IAAI,OAAO,QAAQ,KAAK,SAAS,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,0EAA0E,CAAC,CAAC;IAC9F,CAAC;IACD,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC;IACrD,IAAI,aAAa,KAAK,IAAI,IAAI,OAAO,aAAa,KAAK,QAAQ,EAAE,CAAC;QAChE,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;IAC5E,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;IAChD,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC;IAC/E,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;IAC5C,IAAI,CAAC,CAAC,SAAS,YAAY,UAAU,CAAC,IAAI,SAAS,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QAClE,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,GAAG,GAAwB;QAC/B,IAAI,EAAE,uBAAuB;QAC7B,WAAW,EAAE,UAAU;QACvB,WAAW,EAAE,UAAU;QACvB,cAAc,EAAE,YAAY;QAC5B,QAAQ;QACR,GAAG,CAAC,aAAa,KAAK,IAAI,IAAI,aAAa,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5F,aAAa,EAAE,SAAS;QACxB,2FAA2F;QAC3F,8FAA8F;QAC9F,oBAAoB;QACpB,SAAS,EAAE,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC;KACtC,CAAC;IACF,mCAAmC,CAAC,GAAG,CAAC,CAAC;IACzC,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * DOD-DOC-HANDSHAKE-1 — the document PROPOSAL envelope (§16.3).
3
+ *
4
+ * A distinct wire type from `document-envelope.ts`'s UPDATE envelope. The two share the word
5
+ * "envelope" and nothing else: this one is proposed once, consented to once, and its hash becomes
6
+ * the `document_id` that every subsequent update names.
7
+ *
8
+ * **`document_id` is the hash of this envelope** — globally unique with no minting authority and
9
+ * no coordination round. That is the property that makes the whole scheme federated, and it is why
10
+ * the proposal must be canonically encodable: two parties that hash it differently are on two
11
+ * different documents while believing they are on one.
12
+ *
13
+ * ── THE SEAM (§3.4, §11.1) ────────────────────────────────────────────────────────────────────
14
+ *
15
+ * Three properties are DECLARED in V1 but support exactly one value each, and the pattern is
16
+ * deliberate — §3.3 chose it for `schema_enforcement` and §16.1 generalized it. Declaring a field
17
+ * that only accepts one value looks redundant until you need the second value: the field is
18
+ * already on the wire and already signed, so V2 is a validation change rather than a wire break
19
+ * that strands every document created before it.
20
+ *
21
+ * Refused at BOTH ends — at proposal and again at accept. Not belt-and-braces: the proposer and
22
+ * the accepter run different builds. A V2 peer proposing `topology: "mesh"` to a V1 peer must be
23
+ * refused by the V1 accepter, and a V1 proposer must not be able to emit a value it cannot honour.
24
+ * One-sided validation means whichever side is newer silently decides for both.
25
+ *
26
+ * ── THE FEATURE VERSION (§16.7-8) ─────────────────────────────────────────────────────────────
27
+ *
28
+ * Alpha owes no backward compatibility, so this is not a negotiation — it is the difference
29
+ * between a human answer and a timeout. A peer whose client predates documents does not understand
30
+ * the proposal at all, and without a version the symptom is silence: the proposal is never
31
+ * answered and the operator sees a hang. `documentFeatureIncompatibility` turns that into "your
32
+ * peer's client doesn't support shared documents yet — ask them to upgrade".
33
+ */
34
+ /** Domain tag in slot 0 of the to-be-signed array. Distinct from the UPDATE envelope's domain. */
35
+ export declare const DOCUMENT_PROPOSAL_DOMAIN = "CELLO-DOCUMENT-PROPOSAL-v1";
36
+ /**
37
+ * The document feature version this build speaks. Bumped when the document wire changes in a way
38
+ * a peer must understand; see `documentFeatureIncompatibility` for what a mismatch produces.
39
+ */
40
+ export declare const DOCUMENT_FEATURE_VERSION = 1;
41
+ /** §6 — Tier 1. Tier 2 (attested) is V2; the field exists so V2 is a validation change. */
42
+ export declare const ASSURANCE_TIER_V1 = "authenticated";
43
+ /** §11 — the pairwise two-document form. Mesh is deferred (§11.1). */
44
+ export declare const TOPOLOGY_V1 = "hub-and-spoke";
45
+ export type DocumentConsentState = "pending" | "accepted" | "refused";
46
+ /**
47
+ * The properties agreed at handshake (§3.4). **Immutable after accept** — a property change is an
48
+ * epoch event and therefore V2 (§16.3), so no mutate call exists in V1. That is not an omission to
49
+ * fill in later: mutating a property after acceptance would silently change the rules the other
50
+ * party consented to, which is the one thing the handshake exists to prevent.
51
+ */
52
+ export interface DocumentProperties {
53
+ /** Only `authenticated` in V1. */
54
+ assurance_tier: string;
55
+ /** Only `false` in V1 — the schema-as-first-update path (§3.3) is V2. */
56
+ schema_enforcement: boolean;
57
+ /** Only `hub-and-spoke` in V1. */
58
+ topology: string;
59
+ /** Receiver-local append-only enforcement (§16.7-1). Freely settable — not a seam field. */
60
+ append_only: boolean;
61
+ }
62
+ export interface DocumentProposalEnvelope {
63
+ type: "document_proposal";
64
+ /** Which build proposed this. A peer that cannot read it answers with a human-readable refusal. */
65
+ feature_version: number;
66
+ proposer_agent_id: string;
67
+ peer_agent_id: string;
68
+ /** Free-form label chosen by the proposer, e.g. "markdown". Not a seam field. */
69
+ document_type: string;
70
+ properties: DocumentProperties;
71
+ /**
72
+ * The epoch-zero template (§16.3 step 1), or null. A Yjs update in the pinned encoding, so both
73
+ * sides mint byte-identical starting state rather than each building their own from a template
74
+ * string — two "identical" documents built independently do not converge.
75
+ */
76
+ starting_content: Uint8Array | null;
77
+ /** Distinguishes two otherwise identical proposals, so each gets its own document_id. */
78
+ nonce: Uint8Array;
79
+ proposed_at_ms: number;
80
+ /** Ed25519 (RFC 8032) over `buildDocumentProposalTbs`. */
81
+ signature: Uint8Array;
82
+ }
83
+ /**
84
+ * The canonical to-be-signed preimage: a fixed-order CBOR ARRAY with the domain in slot 0.
85
+ *
86
+ * The properties are flattened into their own slots rather than nested as a map. `encodeCbor` is
87
+ * not deterministic for maps — keys follow insertion order — so a nested `properties` object would
88
+ * make the signature depend on the order the proposer happened to build it in, and two honest
89
+ * builds of the same proposal would produce different `document_id`s. For this envelope that is
90
+ * worse than a bad signature: the id IS the hash, so the two parties would be on two documents.
91
+ */
92
+ export declare function buildDocumentProposalTbs(env: DocumentProposalEnvelope, opts?: {
93
+ preHash?: boolean;
94
+ }): Uint8Array;
95
+ /**
96
+ * `document_id` = the hash of the proposal envelope (§16.3).
97
+ *
98
+ * Over the same preimage that is signed, and therefore NOT over the signature — so the id a party
99
+ * computes is the id of the thing whose signature it verified, and re-signing cannot change the
100
+ * identity of an already-accepted document.
101
+ */
102
+ export declare function documentIdFromProposal(env: DocumentProposalEnvelope): string;
103
+ /**
104
+ * Refuse any seam value this build cannot honour, at proposal AND at accept.
105
+ *
106
+ * Returns the reason, or null when the properties are acceptable. A REASON rather than a boolean
107
+ * because the operator on the refusing side has to be able to tell "your peer asked for mesh
108
+ * topology, which this version does not support" from a generic refusal — a handshake that fails
109
+ * without saying which property failed is indistinguishable from the peer being unreachable.
110
+ */
111
+ export declare function seamViolation(props: DocumentProperties): string | null;
112
+ /**
113
+ * A human answer for a version mismatch, or null when the peer is compatible.
114
+ *
115
+ * Deliberately phrased for the operator rather than for a log: the alternative to this string is a
116
+ * proposal that is never answered, which surfaces as a hang and gets diagnosed as a network fault.
117
+ */
118
+ export declare function documentFeatureIncompatibility(peerFeatureVersion: number | null): string | null;
119
+ export declare function encodeDocumentProposal(env: DocumentProposalEnvelope): Uint8Array;
120
+ /**
121
+ * Decode and validate. Does NOT check the seam — `seamViolation` is called separately at both the
122
+ * proposal and the accept step, because a decoder that refused seam values would make a
123
+ * V2 proposal undecodable and therefore unanswerable, which is exactly the silent hang the feature
124
+ * version exists to replace.
125
+ */
126
+ export declare function decodeDocumentProposal(input: Uint8Array): DocumentProposalEnvelope;
127
+ //# sourceMappingURL=document-proposal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-proposal.d.ts","sourceRoot":"","sources":["../src/document-proposal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAKH,kGAAkG;AAClG,eAAO,MAAM,wBAAwB,+BAA+B,CAAC;AAErE;;;GAGG;AACH,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C,2FAA2F;AAC3F,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AACjD,sEAAsE;AACtE,eAAO,MAAM,WAAW,kBAAkB,CAAC;AAE3C,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AAEtE;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,kCAAkC;IAClC,cAAc,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,kBAAkB,EAAE,OAAO,CAAC;IAC5B,kCAAkC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,4FAA4F;IAC5F,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,mBAAmB,CAAC;IAC1B,mGAAmG;IACnG,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,kBAAkB,CAAC;IAC/B;;;;OAIG;IACH,gBAAgB,EAAE,UAAU,GAAG,IAAI,CAAC;IACpC,yFAAyF;IACzF,KAAK,EAAE,UAAU,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,0DAA0D;IAC1D,SAAS,EAAE,UAAU,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,wBAAwB,EAC7B,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAC/B,UAAU,CA4BZ;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,wBAAwB,GAAG,MAAM,CAI5E;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,GAAG,IAAI,CAoBtE;AAED;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAAC,kBAAkB,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAoB/F;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,wBAAwB,GAAG,UAAU,CAkBhF;AA6BD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,UAAU,GAAG,wBAAwB,CA8DlF"}