@cello-protocol/protocol-types 0.0.54 → 0.0.55
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-amendment.d.ts +27 -22
- package/dist/document-amendment.d.ts.map +1 -1
- package/dist/document-amendment.js +63 -181
- package/dist/document-amendment.js.map +1 -1
- package/dist/document-derive.d.ts +96 -0
- package/dist/document-derive.d.ts.map +1 -0
- package/dist/document-derive.js +605 -0
- package/dist/document-derive.js.map +1 -0
- package/dist/document-envelope.d.ts +18 -8
- package/dist/document-envelope.d.ts.map +1 -1
- package/dist/document-envelope.js +35 -19
- package/dist/document-envelope.js.map +1 -1
- package/dist/document-governance.d.ts +1 -1
- package/dist/document-governance.d.ts.map +1 -1
- package/dist/document-governance.js +41 -6
- package/dist/document-governance.js.map +1 -1
- package/dist/document-proposal.d.ts +6 -0
- package/dist/document-proposal.d.ts.map +1 -1
- package/dist/document-proposal.js +21 -0
- package/dist/document-proposal.js.map +1 -1
- package/dist/document-reconcile.d.ts +100 -0
- package/dist/document-reconcile.d.ts.map +1 -0
- package/dist/document-reconcile.js +197 -0
- package/dist/document-reconcile.js.map +1 -0
- package/dist/index.d.ts +9 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/document-ack.d.ts +0 -111
- package/dist/document-ack.d.ts.map +0 -1
- package/dist/document-ack.js +0 -228
- package/dist/document-ack.js.map +0 -1
- package/dist/document-control.d.ts +0 -74
- package/dist/document-control.d.ts.map +0 -1
- package/dist/document-control.js +0 -174
- package/dist/document-control.js.map +0 -1
- package/dist/document-join-answer.d.ts +0 -34
- package/dist/document-join-answer.d.ts.map +0 -1
- package/dist/document-join-answer.js +0 -100
- package/dist/document-join-answer.js.map +0 -1
- package/dist/document-join.d.ts +0 -83
- package/dist/document-join.d.ts.map +0 -1
- package/dist/document-join.js +0 -225
- package/dist/document-join.js.map +0 -1
package/dist/document-ack.js
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DOD-DOC-INBOUND-2 — the document ACK (§16.4).
|
|
3
|
-
*
|
|
4
|
-
* The frame that closes DELIVERY-2's loop. Until it exists, a sent envelope's outcome is
|
|
5
|
-
* `admitted: null` forever: the worker knows the content left and nothing more, so it re-sends on
|
|
6
|
-
* the ack timeout and eventually stalls the document at the unacked ceiling.
|
|
7
|
-
*
|
|
8
|
-
* ── WHY A REJECTION IS AN ACK ─────────────────────────────────────────────────────────────────
|
|
9
|
-
*
|
|
10
|
-
* `admitted: false` is not a failure to acknowledge — it is an acknowledgement that says no. The
|
|
11
|
-
* peer has DECIDED, so the sender must stop retrying and supersede instead (§3.2). Modelling
|
|
12
|
-
* rejection as "no ack" would leave the sender redelivering an envelope the peer has already ruled
|
|
13
|
-
* on, re-triggering their gate and their retry counter until the document stalls for reasons the
|
|
14
|
-
* operator cannot see.
|
|
15
|
-
*
|
|
16
|
-
* ── WHY IT IS SIGNED ──────────────────────────────────────────────────────────────────────────
|
|
17
|
-
*
|
|
18
|
-
* The ack settles an envelope permanently: an acked envelope stops being redelivered and, if the
|
|
19
|
-
* ack says rejected, the sender rolls back local work. Both are consequences an unauthenticated
|
|
20
|
-
* party must not be able to cause. An unsigned ack lets anyone who can reach the channel silence a
|
|
21
|
-
* delivery — the content is dropped from the pending set and neither operator ever learns it was
|
|
22
|
-
* never applied, which is exactly the silent divergence the two-layer design exists to prevent.
|
|
23
|
-
*
|
|
24
|
-
* The signature covers the ENVELOPE HASH, so an ack cannot be moved to a different envelope, and
|
|
25
|
-
* the DOCUMENT ID, so it cannot be moved to a different document. It does not cover `admitted`
|
|
26
|
-
* alone for the same reason a signature never covers one field: the whole statement is the claim.
|
|
27
|
-
*
|
|
28
|
-
* ── CONTRACT FOR THE CONSUMER: SETTLE ONCE ────────────────────────────────────────────────────
|
|
29
|
-
*
|
|
30
|
-
* Nothing here binds an ack to the acker's chain, and nothing at the type level stops one acker
|
|
31
|
-
* producing a valid ADMISSION and a valid REJECTION for the same envelope. Envelopes have
|
|
32
|
-
* `verifyDocumentChainLink` because per-sender ordering matters; acks have no equivalent. So the
|
|
33
|
-
* consumer must settle an envelope ONCE and treat a second, contradicting ack as an ERROR with both
|
|
34
|
-
* signatures retained — never as an update. Applying the later one would let a peer that admitted
|
|
35
|
-
* an envelope later claim it refused it, and the sender would roll back work the peer already has.
|
|
36
|
-
*/
|
|
37
|
-
import { createHash } from "node:crypto";
|
|
38
|
-
import { encodeCbor, decodeCbor } from "./cbor.js";
|
|
39
|
-
/** Domain tag in slot 0. Distinct from the update and proposal domains. */
|
|
40
|
-
export const DOCUMENT_ACK_DOMAIN = "CELLO-DOCUMENT-ACK-v1";
|
|
41
|
-
/**
|
|
42
|
-
* The ack frame version, ON THE WIRE.
|
|
43
|
-
*
|
|
44
|
-
* The `-v1` in the domain string never travels — it lives inside the preimage — so a V2 acker's
|
|
45
|
-
* frame would decode cleanly as V1 and fail SIGNATURE VERIFICATION, sending an operator to key
|
|
46
|
-
* management and peer identity for a version-skew bug. The update envelope refuses `epoch_id` and
|
|
47
|
-
* `update_encoding` by value for exactly this reason: skew that is not SAID becomes silent loss or
|
|
48
|
-
* a misattributed error.
|
|
49
|
-
*/
|
|
50
|
-
export const DOCUMENT_ACK_VERSION = 1;
|
|
51
|
-
/**
|
|
52
|
-
* A rejection reason is peer-controlled text bound for an operator's screen and the policy log.
|
|
53
|
-
* Capped because unbounded peer-controlled display text is not something to hand onward untouched.
|
|
54
|
-
*/
|
|
55
|
-
export const MAX_REJECTION_REASON_LENGTH = 200;
|
|
56
|
-
const HEX32 = /^[0-9a-f]{64}$/;
|
|
57
|
-
/**
|
|
58
|
-
* The canonical to-be-signed preimage: a fixed-order CBOR ARRAY with the domain in slot 0.
|
|
59
|
-
*
|
|
60
|
-
* An array rather than a map for the reason `cbor.ts` gives — this encoder is deliberately not
|
|
61
|
-
* deterministic for maps, so a map preimage would make the signature depend on the order the acker
|
|
62
|
-
* happened to build the object in, and two honest implementations would disagree.
|
|
63
|
-
*
|
|
64
|
-
* `rejection_reason` is encoded as `null` when absent rather than omitted, so the slot is always
|
|
65
|
-
* occupied and no field's meaning depends on whether the one before it was present.
|
|
66
|
-
*
|
|
67
|
-
* An earlier version of this comment claimed omission would be "silently absorbed by the next
|
|
68
|
-
* field". That is FALSE for this encoder and worth correcting rather than deleting, because a wrong
|
|
69
|
-
* fact about the wire is what the next structure's justification gets built on: a 6-element array
|
|
70
|
-
* begins `0x86` and a 7-element one `0x87`, so a missing slot is loud, in byte 0. Measured.
|
|
71
|
-
* `cbor.ts` says the same thing — arrays are minimal and order-fixed. The explicit null is still
|
|
72
|
-
* right; the reason is stable slot indices, not collision avoidance.
|
|
73
|
-
*/
|
|
74
|
-
export function buildDocumentAckTbs(ack, opts = {}) {
|
|
75
|
-
assertDocumentAckConsistent(ack);
|
|
76
|
-
const preimage = encodeCbor([
|
|
77
|
-
DOCUMENT_ACK_DOMAIN,
|
|
78
|
-
ack.ack_version,
|
|
79
|
-
ack.document_id,
|
|
80
|
-
ack.envelope_hash,
|
|
81
|
-
ack.acker_agent_id,
|
|
82
|
-
ack.admitted,
|
|
83
|
-
normalizeReason(ack.rejection_reason),
|
|
84
|
-
// BIGINT past 0xffffffff. cbor-x encodes a JS number that large as an IEEE float64 (`fb`)
|
|
85
|
-
// rather than a uint64 (`1b`) — measured: 1700000000000 gives fb4278bcfe56800000, not
|
|
86
|
-
// 1b0000018bcfe56800. A millisecond timestamp is always that large, so any implementation
|
|
87
|
-
// encoding RFC 8949-canonically would compute different TBS bytes and reject a GENUINE ack —
|
|
88
|
-
// surfacing as a signature failure, which reads as forgery. Three sibling builders carry this
|
|
89
|
-
// same coercion and primary-transfer.ts names it as a defect it shipped without.
|
|
90
|
-
typeof ack.acked_at_ms === "number" && ack.acked_at_ms > 0xffffffff
|
|
91
|
-
? BigInt(ack.acked_at_ms)
|
|
92
|
-
: ack.acked_at_ms,
|
|
93
|
-
]);
|
|
94
|
-
if (opts.preHash === false)
|
|
95
|
-
return preimage;
|
|
96
|
-
return new Uint8Array(createHash("sha256").update(preimage).digest());
|
|
97
|
-
}
|
|
98
|
-
/** Empty string means ABSENT. See `assertDocumentAckConsistent`. */
|
|
99
|
-
function normalizeReason(reason) {
|
|
100
|
-
return reason === undefined || reason === "" ? null : reason;
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* The cross-field rules, checked on ENCODE as well as decode.
|
|
104
|
-
*
|
|
105
|
-
* On encode too, because a locally-built contradictory ack would otherwise be signed and shipped
|
|
106
|
-
* and fail only on the remote decode: the sender sees a silent stall, the peer sees the error, and
|
|
107
|
-
* the two never meet. The rule belongs where the object is built, not only where it is read.
|
|
108
|
-
*/
|
|
109
|
-
export function assertDocumentAckConsistent(ack) {
|
|
110
|
-
const reason = normalizeReason(ack.rejection_reason);
|
|
111
|
-
if (!ack.admitted && reason === null) {
|
|
112
|
-
// The sender is being told to stop and supersede. Without the reason it cannot know what to
|
|
113
|
-
// change — the failure the whole rejection protocol exists to prevent.
|
|
114
|
-
throw new Error("document_ack_reason: a rejection must carry its reason");
|
|
115
|
-
}
|
|
116
|
-
if (ack.admitted && reason !== null) {
|
|
117
|
-
// A contradiction: whichever field a reader trusts, the other is lying to them.
|
|
118
|
-
throw new Error(`document_ack_reason: an admission must not carry a rejection reason, and this one carries "${reason}"`);
|
|
119
|
-
}
|
|
120
|
-
if (reason !== null && reason.length > MAX_REJECTION_REASON_LENGTH) {
|
|
121
|
-
throw new Error(`document_ack_reason: a rejection reason may be at most ${MAX_REJECTION_REASON_LENGTH} ` +
|
|
122
|
-
`characters, and this one is ${reason.length}`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
export function encodeDocumentAck(ack) {
|
|
126
|
-
assertDocumentAckConsistent(ack);
|
|
127
|
-
return encodeCbor({
|
|
128
|
-
type: ack.type,
|
|
129
|
-
ack_version: ack.ack_version,
|
|
130
|
-
document_id: ack.document_id,
|
|
131
|
-
envelope_hash: ack.envelope_hash,
|
|
132
|
-
acker_agent_id: ack.acker_agent_id,
|
|
133
|
-
admitted: ack.admitted,
|
|
134
|
-
rejection_reason: normalizeReason(ack.rejection_reason),
|
|
135
|
-
acked_at_ms: ack.acked_at_ms,
|
|
136
|
-
signature: ack.signature,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
function present(map, field) {
|
|
140
|
-
// `in`, not a nullish check — the same discipline as the update envelope. A defaulted field here
|
|
141
|
-
// is a claim the acker never made, and this frame's whole job is to carry their claim.
|
|
142
|
-
if (!(field in map)) {
|
|
143
|
-
throw new Error(`document_ack_missing_field: ${field} is mandatory and was not present`);
|
|
144
|
-
}
|
|
145
|
-
return map[field];
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Decode and validate. Refuses rather than defaulting on every field.
|
|
149
|
-
*
|
|
150
|
-
* The one that matters most: `admitted` must be a real boolean. Coerced, a truthy string like
|
|
151
|
-
* `"false"` would settle a REJECTED envelope as admitted — the sender would stop retrying, never
|
|
152
|
-
* roll back, and both parties would believe content was applied that the receiver refused.
|
|
153
|
-
*/
|
|
154
|
-
export function decodeDocumentAck(bytes) {
|
|
155
|
-
const decoded = decodeCbor(bytes);
|
|
156
|
-
if (typeof decoded !== "object" || decoded === null || Array.isArray(decoded)) {
|
|
157
|
-
throw new Error("document_ack_malformed: not a CBOR map");
|
|
158
|
-
}
|
|
159
|
-
const map = decoded;
|
|
160
|
-
const type = present(map, "type");
|
|
161
|
-
if (type !== "document_ack") {
|
|
162
|
-
throw new Error(`document_ack_type: expected document_ack, got ${String(type)}`);
|
|
163
|
-
}
|
|
164
|
-
const version = present(map, "ack_version");
|
|
165
|
-
if (typeof version !== "number" || !Number.isInteger(version)) {
|
|
166
|
-
throw new Error("document_ack_version: must be an integer");
|
|
167
|
-
}
|
|
168
|
-
if (version !== DOCUMENT_ACK_VERSION) {
|
|
169
|
-
// Named as SKEW. Without a wire version this frame decoded cleanly and failed signature
|
|
170
|
-
// verification instead, sending an operator to key management for a version problem.
|
|
171
|
-
throw new Error(`document_ack_version: this build speaks ack version ${DOCUMENT_ACK_VERSION} and the frame ` +
|
|
172
|
-
`declares ${version} — one of the two clients needs upgrading`);
|
|
173
|
-
}
|
|
174
|
-
const documentId = present(map, "document_id");
|
|
175
|
-
if (typeof documentId !== "string" || !HEX32.test(documentId)) {
|
|
176
|
-
throw new Error(`document_ack_document_id: must be a 32-byte hex digest`);
|
|
177
|
-
}
|
|
178
|
-
const envelopeHash = present(map, "envelope_hash");
|
|
179
|
-
if (typeof envelopeHash !== "string" || !HEX32.test(envelopeHash)) {
|
|
180
|
-
throw new Error("document_ack_envelope_hash: must be a 32-byte hex digest");
|
|
181
|
-
}
|
|
182
|
-
const ackerAgentId = present(map, "acker_agent_id");
|
|
183
|
-
if (typeof ackerAgentId !== "string" || ackerAgentId.length === 0) {
|
|
184
|
-
throw new Error("document_ack_acker: acker_agent_id must be a non-empty text string");
|
|
185
|
-
}
|
|
186
|
-
const admitted = present(map, "admitted");
|
|
187
|
-
if (typeof admitted !== "boolean") {
|
|
188
|
-
throw new Error(`document_ack_admitted: must be a boolean, got ${typeof admitted}`);
|
|
189
|
-
}
|
|
190
|
-
const rawReason = present(map, "rejection_reason");
|
|
191
|
-
if (rawReason !== null && typeof rawReason !== "string") {
|
|
192
|
-
throw new Error("document_ack_reason: rejection_reason must be a text string or explicit null");
|
|
193
|
-
}
|
|
194
|
-
// NORMALIZED ONCE, before the cross-field rules. The empty string meant "absent" on the rejection
|
|
195
|
-
// branch and "present" on the admission branch, so an honest peer written in a language where a
|
|
196
|
-
// non-nullable string field defaults to "" — Go, Rust — had its ADMISSION refused as a
|
|
197
|
-
// contradiction it never expressed. The sender then never settles, retries to the unacked
|
|
198
|
-
// ceiling, and the document stalls: precisely the failure this frame exists to end.
|
|
199
|
-
const reason = rawReason === "" ? null : rawReason;
|
|
200
|
-
const ackedAt = present(map, "acked_at_ms");
|
|
201
|
-
// BOUNDED. `Number.isInteger(1e300)` is true, and this is the only field available to order two
|
|
202
|
-
// conflicting acks — leaving it unbounded hands that tiebreak to an attacker-chosen value.
|
|
203
|
-
if (typeof ackedAt !== "number" || !Number.isSafeInteger(ackedAt) || ackedAt <= 0) {
|
|
204
|
-
throw new Error(`document_ack_time: acked_at_ms must be a positive safe integer, got ${String(ackedAt)}`);
|
|
205
|
-
}
|
|
206
|
-
const signature = present(map, "signature");
|
|
207
|
-
if (!(signature instanceof Uint8Array)) {
|
|
208
|
-
throw new Error("document_ack_signature: must be a CBOR byte string");
|
|
209
|
-
}
|
|
210
|
-
const ack = {
|
|
211
|
-
type: "document_ack",
|
|
212
|
-
ack_version: version,
|
|
213
|
-
document_id: documentId,
|
|
214
|
-
envelope_hash: envelopeHash,
|
|
215
|
-
acker_agent_id: ackerAgentId,
|
|
216
|
-
admitted,
|
|
217
|
-
...(reason === null ? {} : { rejection_reason: reason }),
|
|
218
|
-
acked_at_ms: ackedAt,
|
|
219
|
-
// COPIED — cbor-x returns byte strings as views into the buffer it decoded, so a caller reusing
|
|
220
|
-
// a pooled read buffer would have the signature change after it was verified.
|
|
221
|
-
signature: new Uint8Array(signature),
|
|
222
|
-
};
|
|
223
|
-
// The same cross-field rules the encoder applies. One implementation, so the two surfaces cannot
|
|
224
|
-
// drift into disagreeing about what a valid ack is.
|
|
225
|
-
assertDocumentAckConsistent(ack);
|
|
226
|
-
return ack;
|
|
227
|
-
}
|
|
228
|
-
//# sourceMappingURL=document-ack.js.map
|
package/dist/document-ack.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"document-ack.js","sourceRoot":"","sources":["../src/document-ack.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,74 +0,0 @@
|
|
|
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
|
-
/** Domain tag in slot 0. Distinct from every other document domain. */
|
|
33
|
-
export declare const DOCUMENT_CONTROL_DOMAIN = "CELLO-DOCUMENT-CONTROL-v1";
|
|
34
|
-
/** The frame version, ON THE WIRE, so skew is DETECTED rather than misread as a bad signature. */
|
|
35
|
-
export declare const DOCUMENT_CONTROL_VERSION = 1;
|
|
36
|
-
/** Peer-controlled display text bound for an operator's screen. Capped for that reason. */
|
|
37
|
-
export declare const MAX_CONTROL_REASON_LENGTH = 200;
|
|
38
|
-
/** The two ways a document ends. Closed set — see the header on refusing a third by value. */
|
|
39
|
-
export declare const DOCUMENT_CONTROL_VERBS: readonly ["close", "kill"];
|
|
40
|
-
export type DocumentControlVerb = (typeof DOCUMENT_CONTROL_VERBS)[number];
|
|
41
|
-
export interface DocumentControl {
|
|
42
|
-
type: "document_control";
|
|
43
|
-
control_version: number;
|
|
44
|
-
document_id: string;
|
|
45
|
-
/** Who is ending it. */
|
|
46
|
-
sender_agent_id: string;
|
|
47
|
-
verb: DocumentControlVerb;
|
|
48
|
-
/** Optional, and optional for both verbs — an end is a decision, not something one must justify. */
|
|
49
|
-
reason?: string;
|
|
50
|
-
sent_at_ms: number;
|
|
51
|
-
/** Ed25519 (RFC 8032) over `buildDocumentControlTbs`. */
|
|
52
|
-
signature: Uint8Array;
|
|
53
|
-
}
|
|
54
|
-
export declare function assertDocumentControlConsistent(control: DocumentControl): void;
|
|
55
|
-
/**
|
|
56
|
-
* The canonical to-be-signed preimage: a fixed-order CBOR ARRAY with the domain in slot 0.
|
|
57
|
-
*
|
|
58
|
-
* An array rather than a map for the reason `cbor.ts` gives — this encoder is deliberately not
|
|
59
|
-
* deterministic for maps, so a map preimage would make the signature depend on the order the sender
|
|
60
|
-
* happened to build the object in, and two honest implementations would disagree.
|
|
61
|
-
*/
|
|
62
|
-
export declare function buildDocumentControlTbs(control: DocumentControl, opts?: {
|
|
63
|
-
preHash?: boolean;
|
|
64
|
-
}): Uint8Array;
|
|
65
|
-
export declare function encodeDocumentControl(control: DocumentControl): Uint8Array;
|
|
66
|
-
/**
|
|
67
|
-
* Decode and validate. Refuses rather than defaulting on every field.
|
|
68
|
-
*
|
|
69
|
-
* The one that matters most is `verb`. Defaulted or coerced, a `kill` read as a `close` leaves a
|
|
70
|
-
* killed document waiting for a reciprocal close that is never coming — the operator sees a
|
|
71
|
-
* collaboration that will not settle and no reason anywhere for why.
|
|
72
|
-
*/
|
|
73
|
-
export declare function decodeDocumentControl(bytes: Uint8Array): DocumentControl;
|
|
74
|
-
//# sourceMappingURL=document-control.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"document-control.d.ts","sourceRoot":"","sources":["../src/document-control.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAKH,uEAAuE;AACvE,eAAO,MAAM,uBAAuB,8BAA8B,CAAC;AAEnE,kGAAkG;AAClG,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C,2FAA2F;AAC3F,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAE7C,8FAA8F;AAC9F,eAAO,MAAM,sBAAsB,4BAA6B,CAAC;AACjE,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAI1E,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,kBAAkB,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,wBAAwB;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,EAAE,mBAAmB,CAAC;IAC1B,oGAAoG;IACpG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,SAAS,EAAE,UAAU,CAAC;CACvB;AAMD,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAgB9E;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,eAAe,EACxB,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAC/B,UAAU,CAoBZ;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,eAAe,GAAG,UAAU,CAY1E;AASD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,UAAU,GAAG,eAAe,CAiExE"}
|
package/dist/document-control.js
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"document-control.js","sourceRoot":"","sources":["../src/document-control.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DOD-MP-JOIN-1 — the invitee's ANSWER to a join offer: a signed, settle-once fact.
|
|
3
|
-
*
|
|
4
|
-
* Keyed on the ADMITTING AMENDMENT'S HASH — the amendment is what the invitee answers, and a
|
|
5
|
-
* re-invitation after a refusal is a new amendment with a new hash, so two answers can never be
|
|
6
|
-
* confused. Its own frame for the proposal-ack reason: a join offer is not in the envelope log,
|
|
7
|
-
* so `document_ack` would settle a hash of nothing.
|
|
8
|
-
*
|
|
9
|
-
* Consent is local and final the moment the operator makes it; sending the answer is
|
|
10
|
-
* best-effort, and an unreachable inviter gets no veto over the invitee's choice — the same
|
|
11
|
-
* doctrine as the proposal ack.
|
|
12
|
-
*/
|
|
13
|
-
/** Domain tag in slot 0 of the to-be-signed array. */
|
|
14
|
-
export declare const DOCUMENT_JOIN_ANSWER_DOMAIN = "CELLO-DOCUMENT-JOIN-ANSWER-v1";
|
|
15
|
-
export declare const MAX_JOIN_REFUSAL_REASON_LENGTH = 512;
|
|
16
|
-
export interface DocumentJoinAnswer {
|
|
17
|
-
type: "document_join_answer";
|
|
18
|
-
document_id: string;
|
|
19
|
-
/** Hex hash of the admitting `add_holder` amendment — the settle key. */
|
|
20
|
-
amendment_hash: string;
|
|
21
|
-
invitee_agent_id: string;
|
|
22
|
-
accepted: boolean;
|
|
23
|
-
/** Operator prose on a refusal; explicit null on an accept — never absent. */
|
|
24
|
-
refusal_reason: string | null;
|
|
25
|
-
answered_at_ms: number;
|
|
26
|
-
/** Ed25519 (RFC 8032) by the invitee over `buildDocumentJoinAnswerTbs`. */
|
|
27
|
-
signature: Uint8Array;
|
|
28
|
-
}
|
|
29
|
-
export declare function buildDocumentJoinAnswerTbs(answer: DocumentJoinAnswer, opts?: {
|
|
30
|
-
preHash?: boolean;
|
|
31
|
-
}): Uint8Array;
|
|
32
|
-
export declare function encodeDocumentJoinAnswer(answer: DocumentJoinAnswer): Uint8Array;
|
|
33
|
-
export declare function decodeDocumentJoinAnswer(input: Uint8Array): DocumentJoinAnswer;
|
|
34
|
-
//# sourceMappingURL=document-join-answer.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"document-join-answer.d.ts","sourceRoot":"","sources":["../src/document-join-answer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,sDAAsD;AACtD,eAAO,MAAM,2BAA2B,kCAAkC,CAAC;AAE3E,eAAO,MAAM,8BAA8B,MAAM,CAAC;AAElD,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;IAClB,8EAA8E;IAC9E,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,SAAS,EAAE,UAAU,CAAC;CACvB;AAED,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,kBAAkB,EAC1B,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAC/B,UAAU,CAcZ;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,GAAG,UAAU,CAW/E;AAiBD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,UAAU,GAAG,kBAAkB,CA8C9E"}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DOD-MP-JOIN-1 — the invitee's ANSWER to a join offer: a signed, settle-once fact.
|
|
3
|
-
*
|
|
4
|
-
* Keyed on the ADMITTING AMENDMENT'S HASH — the amendment is what the invitee answers, and a
|
|
5
|
-
* re-invitation after a refusal is a new amendment with a new hash, so two answers can never be
|
|
6
|
-
* confused. Its own frame for the proposal-ack reason: a join offer is not in the envelope log,
|
|
7
|
-
* so `document_ack` would settle a hash of nothing.
|
|
8
|
-
*
|
|
9
|
-
* Consent is local and final the moment the operator makes it; sending the answer is
|
|
10
|
-
* best-effort, and an unreachable inviter gets no veto over the invitee's choice — the same
|
|
11
|
-
* doctrine as the proposal ack.
|
|
12
|
-
*/
|
|
13
|
-
import { createHash } from "node:crypto";
|
|
14
|
-
import { encodeCbor, decodeCbor } from "./cbor.js";
|
|
15
|
-
/** Domain tag in slot 0 of the to-be-signed array. */
|
|
16
|
-
export const DOCUMENT_JOIN_ANSWER_DOMAIN = "CELLO-DOCUMENT-JOIN-ANSWER-v1";
|
|
17
|
-
export const MAX_JOIN_REFUSAL_REASON_LENGTH = 512;
|
|
18
|
-
export function buildDocumentJoinAnswerTbs(answer, opts = {}) {
|
|
19
|
-
const preimage = encodeCbor([
|
|
20
|
-
DOCUMENT_JOIN_ANSWER_DOMAIN,
|
|
21
|
-
answer.document_id,
|
|
22
|
-
answer.amendment_hash,
|
|
23
|
-
answer.invitee_agent_id,
|
|
24
|
-
answer.accepted,
|
|
25
|
-
answer.refusal_reason,
|
|
26
|
-
typeof answer.answered_at_ms === "number" && answer.answered_at_ms > 0xffffffff
|
|
27
|
-
? BigInt(answer.answered_at_ms)
|
|
28
|
-
: answer.answered_at_ms,
|
|
29
|
-
]);
|
|
30
|
-
if (opts.preHash === false)
|
|
31
|
-
return preimage;
|
|
32
|
-
return new Uint8Array(createHash("sha256").update(preimage).digest());
|
|
33
|
-
}
|
|
34
|
-
export function encodeDocumentJoinAnswer(answer) {
|
|
35
|
-
return encodeCbor({
|
|
36
|
-
type: answer.type,
|
|
37
|
-
document_id: answer.document_id,
|
|
38
|
-
amendment_hash: answer.amendment_hash,
|
|
39
|
-
invitee_agent_id: answer.invitee_agent_id,
|
|
40
|
-
accepted: answer.accepted,
|
|
41
|
-
refusal_reason: answer.refusal_reason,
|
|
42
|
-
answered_at_ms: answer.answered_at_ms,
|
|
43
|
-
signature: answer.signature,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
function present(map, field) {
|
|
47
|
-
if (!(field in map)) {
|
|
48
|
-
throw new Error(`document_join_answer_missing_field: ${field} is mandatory and was not present`);
|
|
49
|
-
}
|
|
50
|
-
return map[field];
|
|
51
|
-
}
|
|
52
|
-
function str(map, field) {
|
|
53
|
-
const v = present(map, field);
|
|
54
|
-
if (typeof v !== "string" || v.length === 0) {
|
|
55
|
-
throw new Error(`document_join_answer_field_type: ${field} must be a non-empty text string`);
|
|
56
|
-
}
|
|
57
|
-
return v;
|
|
58
|
-
}
|
|
59
|
-
export function decodeDocumentJoinAnswer(input) {
|
|
60
|
-
const decoded = decodeCbor(input);
|
|
61
|
-
if (typeof decoded !== "object" || decoded === null || Array.isArray(decoded)) {
|
|
62
|
-
throw new Error("document_join_answer_malformed: not a CBOR map");
|
|
63
|
-
}
|
|
64
|
-
const map = decoded;
|
|
65
|
-
const type = str(map, "type");
|
|
66
|
-
if (type !== "document_join_answer") {
|
|
67
|
-
throw new Error(`document_join_answer_type: expected document_join_answer, got ${type}`);
|
|
68
|
-
}
|
|
69
|
-
const accepted = present(map, "accepted");
|
|
70
|
-
if (typeof accepted !== "boolean") {
|
|
71
|
-
throw new Error("document_join_answer_field_type: accepted must be a boolean — consent is never inferred " +
|
|
72
|
-
"from a truthy value");
|
|
73
|
-
}
|
|
74
|
-
const reason = present(map, "refusal_reason");
|
|
75
|
-
if (reason !== null && typeof reason !== "string") {
|
|
76
|
-
throw new Error("document_join_answer_field_type: refusal_reason must be a text string or explicit null");
|
|
77
|
-
}
|
|
78
|
-
if (typeof reason === "string" && reason.length > MAX_JOIN_REFUSAL_REASON_LENGTH) {
|
|
79
|
-
throw new Error(`document_join_answer_field_type: refusal_reason exceeds ${MAX_JOIN_REFUSAL_REASON_LENGTH} chars`);
|
|
80
|
-
}
|
|
81
|
-
const answeredAt = present(map, "answered_at_ms");
|
|
82
|
-
if (typeof answeredAt !== "number" || !Number.isInteger(answeredAt)) {
|
|
83
|
-
throw new Error("document_join_answer_field_type: answered_at_ms must be an integer");
|
|
84
|
-
}
|
|
85
|
-
const signature = present(map, "signature");
|
|
86
|
-
if (!(signature instanceof Uint8Array)) {
|
|
87
|
-
throw new Error("document_join_answer_field_type: signature must be a CBOR byte string");
|
|
88
|
-
}
|
|
89
|
-
return {
|
|
90
|
-
type: "document_join_answer",
|
|
91
|
-
document_id: str(map, "document_id"),
|
|
92
|
-
amendment_hash: str(map, "amendment_hash"),
|
|
93
|
-
invitee_agent_id: str(map, "invitee_agent_id"),
|
|
94
|
-
accepted,
|
|
95
|
-
refusal_reason: reason,
|
|
96
|
-
answered_at_ms: answeredAt,
|
|
97
|
-
signature: new Uint8Array(signature),
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
//# sourceMappingURL=document-join-answer.js.map
|