@cello-protocol/daemon 0.0.132 → 0.0.133
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/content-park.d.ts.map +1 -1
- package/dist/content-park.js +29 -11
- package/dist/content-park.js.map +1 -1
- package/dist/daemon.d.ts +11 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +394 -17
- package/dist/daemon.js.map +1 -1
- package/dist/document-ack-inbound.d.ts +57 -0
- package/dist/document-ack-inbound.d.ts.map +1 -0
- package/dist/document-ack-inbound.js +174 -0
- package/dist/document-ack-inbound.js.map +1 -0
- package/dist/document-control-notifier.d.ts +61 -0
- package/dist/document-control-notifier.d.ts.map +1 -0
- package/dist/document-control-notifier.js +70 -0
- package/dist/document-control-notifier.js.map +1 -0
- package/dist/document-delivery-transport.d.ts +94 -0
- package/dist/document-delivery-transport.d.ts.map +1 -0
- package/dist/document-delivery-transport.js +179 -0
- package/dist/document-delivery-transport.js.map +1 -0
- package/dist/document-delivery.d.ts +181 -0
- package/dist/document-delivery.d.ts.map +1 -0
- package/dist/document-delivery.js +289 -0
- package/dist/document-delivery.js.map +1 -0
- package/dist/document-frame-router.d.ts +210 -0
- package/dist/document-frame-router.d.ts.map +1 -0
- package/dist/document-frame-router.js +396 -0
- package/dist/document-frame-router.js.map +1 -0
- package/dist/document-handlers.d.ts +47 -0
- package/dist/document-handlers.d.ts.map +1 -0
- package/dist/document-handlers.js +657 -0
- package/dist/document-handlers.js.map +1 -0
- package/dist/document-handshake.d.ts +156 -0
- package/dist/document-handshake.d.ts.map +1 -0
- package/dist/document-handshake.js +398 -0
- package/dist/document-handshake.js.map +1 -0
- package/dist/document-inbound.d.ts +91 -0
- package/dist/document-inbound.d.ts.map +1 -0
- package/dist/document-inbound.js +290 -0
- package/dist/document-inbound.js.map +1 -0
- package/dist/document-layer.d.ts +137 -0
- package/dist/document-layer.d.ts.map +1 -0
- package/dist/document-layer.js +255 -0
- package/dist/document-layer.js.map +1 -0
- package/dist/document-lifecycle.d.ts +125 -0
- package/dist/document-lifecycle.d.ts.map +1 -0
- package/dist/document-lifecycle.js +433 -0
- package/dist/document-lifecycle.js.map +1 -0
- package/dist/document-live-docs.d.ts +58 -0
- package/dist/document-live-docs.d.ts.map +1 -0
- package/dist/document-live-docs.js +126 -0
- package/dist/document-live-docs.js.map +1 -0
- package/dist/document-notify.d.ts +173 -0
- package/dist/document-notify.d.ts.map +1 -0
- package/dist/document-notify.js +438 -0
- package/dist/document-notify.js.map +1 -0
- package/dist/document-publish.d.ts +67 -0
- package/dist/document-publish.d.ts.map +1 -0
- package/dist/document-publish.js +149 -0
- package/dist/document-publish.js.map +1 -0
- package/dist/document-reachability.d.ts +42 -0
- package/dist/document-reachability.d.ts.map +1 -0
- package/dist/document-reachability.js +80 -0
- package/dist/document-reachability.js.map +1 -0
- package/dist/document-rejection.d.ts +240 -0
- package/dist/document-rejection.d.ts.map +1 -0
- package/dist/document-rejection.js +407 -0
- package/dist/document-rejection.js.map +1 -0
- package/dist/document-store.d.ts +154 -8
- package/dist/document-store.d.ts.map +1 -1
- package/dist/document-store.js +462 -4
- package/dist/document-store.js.map +1 -1
- package/dist/document-write-path.d.ts.map +1 -1
- package/dist/document-write-path.js +10 -43
- package/dist/document-write-path.js.map +1 -1
- package/dist/inbound-sessions.d.ts.map +1 -1
- package/dist/inbound-sessions.js +4 -0
- package/dist/inbound-sessions.js.map +1 -1
- package/dist/initiate-session-handler.d.ts +24 -1
- package/dist/initiate-session-handler.d.ts.map +1 -1
- package/dist/initiate-session-handler.js +35 -9
- package/dist/initiate-session-handler.js.map +1 -1
- package/dist/ipc-server.d.ts +11 -1
- package/dist/ipc-server.d.ts.map +1 -1
- package/dist/ipc-server.js +7 -1
- package/dist/ipc-server.js.map +1 -1
- package/dist/line-lcs.d.ts +51 -0
- package/dist/line-lcs.d.ts.map +1 -0
- package/dist/line-lcs.js +71 -0
- package/dist/line-lcs.js.map +1 -0
- package/dist/outbound-sessions.d.ts +2 -0
- package/dist/outbound-sessions.d.ts.map +1 -1
- package/dist/outbound-sessions.js +11 -1
- package/dist/outbound-sessions.js.map +1 -1
- package/dist/session-content-handlers.d.ts.map +1 -1
- package/dist/session-content-handlers.js +3 -2
- package/dist/session-content-handlers.js.map +1 -1
- package/dist/session-node-manager.d.ts +15 -0
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +188 -9
- package/dist/session-node-manager.js.map +1 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +16 -0
- package/dist/vocabulary.js.map +1 -1
- package/dist/wire-content-hash.d.ts +27 -0
- package/dist/wire-content-hash.d.ts.map +1 -0
- package/dist/wire-content-hash.js +37 -0
- package/dist/wire-content-hash.js.map +1 -0
- package/package.json +5 -5
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-DOC-INBOUND-1 — the receiving half (§3.2, §14, §16.4).
|
|
3
|
+
*
|
|
4
|
+
* Every P2 unit built a piece of this. ENVELOPE-1 decodes and checks the chain link, GATE-1
|
|
5
|
+
* decides, REJECT-1 records a refusal and quarantines the bytes, ENGINE-1 applies. Nothing owned
|
|
6
|
+
* assembling them, and assembling them is not clerical work — **the order is the security
|
|
7
|
+
* property**. A check performed after admission is a check that did not protect anything.
|
|
8
|
+
*
|
|
9
|
+
* The order, and what each step is protecting:
|
|
10
|
+
*
|
|
11
|
+
* 1. DECODE — refuse malformed input before any of it is interpreted.
|
|
12
|
+
* 2. KNOW the document — an envelope for a document we do not hold has no context to judge it in.
|
|
13
|
+
* 3. SENDER is the peer — a document is a pairwise agreement; a third party's envelope does not
|
|
14
|
+
* belong here at all, and is not a rejection case (there is no
|
|
15
|
+
* collaboration to supersede).
|
|
16
|
+
* 4. VERIFY the signature — BEFORE the gate. The gate runs pluggable rules, and eventually
|
|
17
|
+
* screening, over peer-controlled bytes; running them first means those
|
|
18
|
+
* rules process input from a party we have not authenticated. An
|
|
19
|
+
* unverified envelope is also not REJECTED — a rejection is a protocol act
|
|
20
|
+
* that presumes an authenticated counterparty, and writing a 0x05 leaf
|
|
21
|
+
* naming an unauthenticated sender puts their claim in our permanent log.
|
|
22
|
+
* 5. CHAIN link — a redelivery is benign and must still ACK (delivery retries by design, and
|
|
23
|
+
* a redelivery that goes unacked never lets the sender settle it); a gap
|
|
24
|
+
* or a fork refuses.
|
|
25
|
+
* 6. GATE — the §3.2 validation, against a TRIAL copy so a refusal cannot have landed.
|
|
26
|
+
* 7. ADMIT or REJECT — and either way, ANSWER. A rejection is an ack for delivery purposes: the
|
|
27
|
+
* peer has decided, so the sender must stop retrying and supersede.
|
|
28
|
+
*/
|
|
29
|
+
import * as Y from "yjs";
|
|
30
|
+
import type { DocumentStore } from "./document-store.js";
|
|
31
|
+
import type { DocumentEngine } from "./document-engine.js";
|
|
32
|
+
import type { DocumentGate } from "./document-gate.js";
|
|
33
|
+
import type { DocumentRejections } from "./document-rejection.js";
|
|
34
|
+
import type { Logger } from "./types.js";
|
|
35
|
+
export type InboundResult = {
|
|
36
|
+
ok: true;
|
|
37
|
+
admitted: true;
|
|
38
|
+
envelopeHash: string;
|
|
39
|
+
duplicate: boolean;
|
|
40
|
+
} | {
|
|
41
|
+
ok: true;
|
|
42
|
+
admitted: false;
|
|
43
|
+
envelopeHash: string;
|
|
44
|
+
rejectionReason: string;
|
|
45
|
+
duplicate: false;
|
|
46
|
+
/** The signed `document_rejection` to put on the wire. Absent when this refusal is a repeat. */
|
|
47
|
+
rejectionWire?: Uint8Array;
|
|
48
|
+
} | {
|
|
49
|
+
ok: false;
|
|
50
|
+
reason: string;
|
|
51
|
+
detail: string;
|
|
52
|
+
};
|
|
53
|
+
export interface DocumentInboundDeps {
|
|
54
|
+
store: DocumentStore;
|
|
55
|
+
engine: DocumentEngine;
|
|
56
|
+
gate: DocumentGate;
|
|
57
|
+
rejections: DocumentRejections;
|
|
58
|
+
logger: Logger;
|
|
59
|
+
/**
|
|
60
|
+
* Verify the envelope signature against `sender_agent_id`. INJECTED and REQUIRED — this module
|
|
61
|
+
* cannot resolve an agent id to a public key, and a default would be a default answer to "is this
|
|
62
|
+
* authentic", which is the one question that must never have one.
|
|
63
|
+
*/
|
|
64
|
+
verifySignature(senderAgentId: string, tbs: Uint8Array, signature: Uint8Array): boolean;
|
|
65
|
+
/** The live document to apply admitted updates to. */
|
|
66
|
+
liveDocFor(ownerAgentId: string, documentId: string): Y.Doc;
|
|
67
|
+
/**
|
|
68
|
+
* Sign as the OWNING agent, over the rejection's canonical preimage (DOD-DOC-REJECT-2).
|
|
69
|
+
*
|
|
70
|
+
* Takes the agent so it cannot sign with the wrong key. ASYNC, and that is what makes `receive`
|
|
71
|
+
* async: signing goes through a key provider, and REJECT-1 requires a real signature rather than
|
|
72
|
+
* a placeholder.
|
|
73
|
+
*/
|
|
74
|
+
sign(ownerAgentId: string, tbs: Uint8Array): Promise<Uint8Array>;
|
|
75
|
+
}
|
|
76
|
+
export declare class DocumentInbound {
|
|
77
|
+
#private;
|
|
78
|
+
constructor(deps: DocumentInboundDeps);
|
|
79
|
+
/**
|
|
80
|
+
* ASYNC because a gate refusal must SIGN a `0x05` leaf, and signing goes through an async key
|
|
81
|
+
* provider. REJECT-1 refuses to fabricate a signature — an all-zero placeholder in an immutable
|
|
82
|
+
* log is indistinguishable from a real one that fails to verify — so the async is real, not
|
|
83
|
+
* incidental.
|
|
84
|
+
*
|
|
85
|
+
* The session content path still decides the LEAF KIND synchronously; `DocumentFrameRouter`
|
|
86
|
+
* splits the two, and serializes the handling per owner so an envelope's predecessor is always
|
|
87
|
+
* stored before its successor is checked.
|
|
88
|
+
*/
|
|
89
|
+
receive(ownerAgentId: string, wire: Uint8Array, nowMs: number, correlationId?: string): Promise<InboundResult>;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=document-inbound.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-inbound.d.ts","sourceRoot":"","sources":["../src/document-inbound.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AAQzB,OAAO,KAAK,EAAE,aAAa,EAAsB,MAAM,qBAAqB,CAAC;AAC7E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,MAAM,aAAa,GACrB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,IAAI,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GACtE;IACE,EAAE,EAAE,IAAI,CAAC;IACT,QAAQ,EAAE,KAAK,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,KAAK,CAAC;IACjB,gGAAgG;IAChG,aAAa,CAAC,EAAE,UAAU,CAAC;CAC5B,GACD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,aAAa,CAAC;IACrB,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,YAAY,CAAC;IACnB,UAAU,EAAE,kBAAkB,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,eAAe,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,GAAG,OAAO,CAAC;IACxF,sDAAsD;IACtD,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC;IAC5D;;;;;;OAMG;IACH,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CAClE;AAOD,qBAAa,eAAe;;gBAGd,IAAI,EAAE,mBAAmB;IAkBrC;;;;;;;;;OASG;IACG,OAAO,CACX,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,MAAM,EACb,aAAa,SAAY,GACxB,OAAO,CAAC,aAAa,CAAC;CA0P1B"}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-DOC-INBOUND-1 — the receiving half (§3.2, §14, §16.4).
|
|
3
|
+
*
|
|
4
|
+
* Every P2 unit built a piece of this. ENVELOPE-1 decodes and checks the chain link, GATE-1
|
|
5
|
+
* decides, REJECT-1 records a refusal and quarantines the bytes, ENGINE-1 applies. Nothing owned
|
|
6
|
+
* assembling them, and assembling them is not clerical work — **the order is the security
|
|
7
|
+
* property**. A check performed after admission is a check that did not protect anything.
|
|
8
|
+
*
|
|
9
|
+
* The order, and what each step is protecting:
|
|
10
|
+
*
|
|
11
|
+
* 1. DECODE — refuse malformed input before any of it is interpreted.
|
|
12
|
+
* 2. KNOW the document — an envelope for a document we do not hold has no context to judge it in.
|
|
13
|
+
* 3. SENDER is the peer — a document is a pairwise agreement; a third party's envelope does not
|
|
14
|
+
* belong here at all, and is not a rejection case (there is no
|
|
15
|
+
* collaboration to supersede).
|
|
16
|
+
* 4. VERIFY the signature — BEFORE the gate. The gate runs pluggable rules, and eventually
|
|
17
|
+
* screening, over peer-controlled bytes; running them first means those
|
|
18
|
+
* rules process input from a party we have not authenticated. An
|
|
19
|
+
* unverified envelope is also not REJECTED — a rejection is a protocol act
|
|
20
|
+
* that presumes an authenticated counterparty, and writing a 0x05 leaf
|
|
21
|
+
* naming an unauthenticated sender puts their claim in our permanent log.
|
|
22
|
+
* 5. CHAIN link — a redelivery is benign and must still ACK (delivery retries by design, and
|
|
23
|
+
* a redelivery that goes unacked never lets the sender settle it); a gap
|
|
24
|
+
* or a fork refuses.
|
|
25
|
+
* 6. GATE — the §3.2 validation, against a TRIAL copy so a refusal cannot have landed.
|
|
26
|
+
* 7. ADMIT or REJECT — and either way, ANSWER. A rejection is an ack for delivery purposes: the
|
|
27
|
+
* peer has decided, so the sender must stop retrying and supersede.
|
|
28
|
+
*/
|
|
29
|
+
import { decodeDocumentUpdateEnvelope, documentEnvelopeHash, buildDocumentUpdateTbs, verifyDocumentChainLink, } from "@cello-protocol/protocol-types";
|
|
30
|
+
/** The agreed property, read from the row rather than from whoever called us. */
|
|
31
|
+
function appendOnly(doc) {
|
|
32
|
+
return doc.properties.append_only === true;
|
|
33
|
+
}
|
|
34
|
+
export class DocumentInbound {
|
|
35
|
+
#d;
|
|
36
|
+
constructor(deps) {
|
|
37
|
+
this.#d = deps;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The clientIDs this sender has SIGNED for on this document: the one this envelope declares,
|
|
41
|
+
* plus every one previously admitted from them. Both are authenticated — each arrived inside a
|
|
42
|
+
* verified TBS — so the set can only grow through signatures the peer actually made.
|
|
43
|
+
*/
|
|
44
|
+
#boundClientIds(ownerAgentId, env) {
|
|
45
|
+
return [
|
|
46
|
+
...new Set([
|
|
47
|
+
env.sender_client_id,
|
|
48
|
+
...this.#d.store.senderClientIdsFor(ownerAgentId, env.document_id, env.sender_agent_id),
|
|
49
|
+
]),
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* ASYNC because a gate refusal must SIGN a `0x05` leaf, and signing goes through an async key
|
|
54
|
+
* provider. REJECT-1 refuses to fabricate a signature — an all-zero placeholder in an immutable
|
|
55
|
+
* log is indistinguishable from a real one that fails to verify — so the async is real, not
|
|
56
|
+
* incidental.
|
|
57
|
+
*
|
|
58
|
+
* The session content path still decides the LEAF KIND synchronously; `DocumentFrameRouter`
|
|
59
|
+
* splits the two, and serializes the handling per owner so an envelope's predecessor is always
|
|
60
|
+
* stored before its successor is checked.
|
|
61
|
+
*/
|
|
62
|
+
async receive(ownerAgentId, wire, nowMs, correlationId = "inbound") {
|
|
63
|
+
// 1. DECODE.
|
|
64
|
+
let env;
|
|
65
|
+
try {
|
|
66
|
+
env = decodeDocumentUpdateEnvelope(wire);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
70
|
+
this.#d.logger.warn("document.inbound.malformed", { detail, correlationId });
|
|
71
|
+
// ONE reason code, with the upstream message in the detail. Two kinds of failure arrive here:
|
|
72
|
+
// our own decoder's named refusals ("document_envelope_missing_field: …"), and CBOR/lib0
|
|
73
|
+
// errors from bytes that are not a CBOR map at all ("Data read, but end of buffer not
|
|
74
|
+
// reached"). Splitting the message on ":" turned the second kind into a reason code made of
|
|
75
|
+
// English prose, which nothing can match on and which reads as a crash rather than a refusal.
|
|
76
|
+
return { ok: false, reason: "document_inbound_malformed", detail };
|
|
77
|
+
}
|
|
78
|
+
const envelopeHash = documentEnvelopeHash(env);
|
|
79
|
+
// 2. KNOW the document.
|
|
80
|
+
const doc = this.#d.store.getDocument(ownerAgentId, env.document_id);
|
|
81
|
+
if (!doc) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
reason: "document_unknown",
|
|
85
|
+
detail: `no document ${env.document_id.slice(0, 16)}… for this agent`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// 2b. The document must still ACCEPT. `acceptsUpdates` exists for exactly this and carries a
|
|
89
|
+
// comment describing the bug it was written to fix — "after a restart the document row said
|
|
90
|
+
// `stalled` while the daemon happily accepted updates on it". The only inbound path in the
|
|
91
|
+
// codebase was reintroducing it, and a killed document is defined by LIFECYCLE-1 as one that
|
|
92
|
+
// stops accepting.
|
|
93
|
+
if (doc.status === "killed" || doc.status === "closed") {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
reason: doc.status === "killed" ? "document_killed" : "document_closed",
|
|
97
|
+
detail: doc.status === "killed"
|
|
98
|
+
? "this document was ended locally and no longer accepts updates"
|
|
99
|
+
: "this document was closed by agreement and no longer accepts updates",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const accepts = this.#d.rejections.acceptsUpdates(ownerAgentId, env.document_id);
|
|
103
|
+
if (!accepts.ok) {
|
|
104
|
+
// The stall reason, composed by the unit that owns the stall. Reporting this envelope's own
|
|
105
|
+
// gate verdict instead would send an operator to the append-only rule for a document that has
|
|
106
|
+
// been frozen for days.
|
|
107
|
+
return { ok: false, reason: accepts.reason, detail: accepts.detail };
|
|
108
|
+
}
|
|
109
|
+
// 3. The sender must be THIS document's peer.
|
|
110
|
+
if (env.sender_agent_id !== doc.peerAgentId) {
|
|
111
|
+
this.#d.logger.warn("document.inbound.not_peer", {
|
|
112
|
+
documentId: env.document_id,
|
|
113
|
+
senderAgentId: env.sender_agent_id,
|
|
114
|
+
peerAgentId: doc.peerAgentId,
|
|
115
|
+
correlationId,
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
reason: "document_sender_not_peer",
|
|
120
|
+
// The peer's identity is NOT in the reply. This sender has not authenticated, and anyone
|
|
121
|
+
// reaching the channel with a guessed or leaked document_id would otherwise learn who the
|
|
122
|
+
// owner collaborates with. The full detail is in the warn above, where it belongs.
|
|
123
|
+
detail: "you are not a party to this document",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// 4. VERIFY, before the gate. See the header.
|
|
127
|
+
if (!this.#d.verifySignature(env.sender_agent_id, buildDocumentUpdateTbs(env), env.signature)) {
|
|
128
|
+
this.#d.logger.error("document.inbound.signature_invalid", {
|
|
129
|
+
documentId: env.document_id,
|
|
130
|
+
senderAgentId: env.sender_agent_id,
|
|
131
|
+
envelopeHash,
|
|
132
|
+
correlationId,
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
reason: "document_signature_invalid",
|
|
137
|
+
detail: `the envelope claims to come from ${env.sender_agent_id} but its signature does not ` +
|
|
138
|
+
`verify against that agent — nothing was recorded`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// 5. CHAIN. A redelivery is expected traffic and must still be answered.
|
|
142
|
+
// Hash-only. Reading the whole log meant materializing every payload BLOB on every inbound
|
|
143
|
+
// update, just to build a set for one membership test.
|
|
144
|
+
const known = this.#d.store.knownEnvelopeHashesBySender(ownerAgentId, env.document_id, env.sender_agent_id);
|
|
145
|
+
const head = this.#d.store.lastEnvelopeHashBySender(ownerAgentId, env.document_id, env.sender_agent_id);
|
|
146
|
+
let duplicate;
|
|
147
|
+
try {
|
|
148
|
+
duplicate = verifyDocumentChainLink(env, { head, known }).duplicate;
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
152
|
+
this.#d.logger.warn("document.inbound.chain_refused", {
|
|
153
|
+
documentId: env.document_id,
|
|
154
|
+
correlationId,
|
|
155
|
+
detail,
|
|
156
|
+
});
|
|
157
|
+
// MATCHED against the known set, not split on ":". This file explains sixty lines earlier why
|
|
158
|
+
// splitting an upstream message produces reason codes made of English prose — and the old
|
|
159
|
+
// `?? "document_chain_broken"` fallback mislabelled a FORK as a gap, which is precisely the
|
|
160
|
+
// distinction the envelope module went out of its way to create.
|
|
161
|
+
const reason = ["document_chain_forked", "document_chain_broken"].find((r) => detail.startsWith(r));
|
|
162
|
+
return { ok: false, reason: reason ?? "document_chain_refused", detail };
|
|
163
|
+
}
|
|
164
|
+
if (duplicate) {
|
|
165
|
+
// ACK, and change nothing. Not acking would leave the sender retrying an envelope we already
|
|
166
|
+
// hold, forever; applying it again is unnecessary (Yjs is idempotent) and appending it again
|
|
167
|
+
// is refused by the store anyway.
|
|
168
|
+
this.#d.logger.info("document.inbound.duplicate", {
|
|
169
|
+
documentId: env.document_id,
|
|
170
|
+
senderAgentId: env.sender_agent_id,
|
|
171
|
+
envelopeHash,
|
|
172
|
+
correlationId,
|
|
173
|
+
});
|
|
174
|
+
return { ok: true, admitted: true, envelopeHash, duplicate: true };
|
|
175
|
+
}
|
|
176
|
+
// 5b. ALREADY REJECTED? A rejected envelope's hash is deliberately never written to the log, so
|
|
177
|
+
// it is not in `known` and a redelivery is NOT a duplicate. Without this it re-ran the gate,
|
|
178
|
+
// was rejected again, and `reject()` minted a fresh nonce — advancing the retry round. Since
|
|
179
|
+
// MAX_REJECTED_ROUNDS is 3, a peer whose ACKS ARE BEING LOST — which is delivery's ordinary
|
|
180
|
+
// retry behaviour — permanently stalled the shared document in three attempts, with no
|
|
181
|
+
// hostility required. Hostile, it was a one-line denial of service.
|
|
182
|
+
//
|
|
183
|
+
// Re-answered with the RECORDED reason: the peer needs the same answer it was given before, so
|
|
184
|
+
// it supersedes rather than retries.
|
|
185
|
+
const alreadyRejected = this.#d.store
|
|
186
|
+
.listQuarantined(ownerAgentId, env.document_id)
|
|
187
|
+
.find((q) => q.rejectedEnvelopeHash === envelopeHash);
|
|
188
|
+
if (alreadyRejected) {
|
|
189
|
+
this.#d.logger.info("document.inbound.already_rejected", {
|
|
190
|
+
documentId: env.document_id,
|
|
191
|
+
envelopeHash,
|
|
192
|
+
correlationId,
|
|
193
|
+
});
|
|
194
|
+
return {
|
|
195
|
+
ok: true,
|
|
196
|
+
admitted: false,
|
|
197
|
+
envelopeHash,
|
|
198
|
+
rejectionReason: alreadyRejected.reason,
|
|
199
|
+
duplicate: false,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// 6. GATE — against the LIVE doc, which the gate itself trials on a copy.
|
|
203
|
+
const live = this.#d.liveDocFor(ownerAgentId, env.document_id);
|
|
204
|
+
const verdict = this.#d.gate.validate(live, env.update, {
|
|
205
|
+
documentId: env.document_id,
|
|
206
|
+
senderAgentId: env.sender_agent_id,
|
|
207
|
+
// DERIVED from authenticated data, not from a seam nobody produces. ENVELOPE-1 puts
|
|
208
|
+
// `sender_client_id` inside the signed TBS precisely because "this envelope is where it is
|
|
209
|
+
// learned" — and the assembler was decoding it, verifying the signature over it, and then
|
|
210
|
+
// throwing it away, leaving rule (h) wired to a function with no implementation. Left as a
|
|
211
|
+
// seam it would default to wrong in whichever direction its implementer guessed: `[]`
|
|
212
|
+
// refuses every peer update, the union of everything observed admits anything.
|
|
213
|
+
//
|
|
214
|
+
// §14 forbids persisting a clientID for REUSE; recording which ones a peer has SIGNED for
|
|
215
|
+
// is the opposite — it is the binding the gate's rule exists to check.
|
|
216
|
+
senderClientIds: this.#boundClientIds(ownerAgentId, env),
|
|
217
|
+
declaredDocumentId: env.document_id,
|
|
218
|
+
declaredEncoding: env.update_encoding,
|
|
219
|
+
// THE PERSISTED PROPERTY, not a caller option defaulting to off. append_only is agreed at
|
|
220
|
+
// the handshake and stored on the row, and the gate's own header says it "is the only thing
|
|
221
|
+
// standing between a bound peer and erasure" — rule (h) provably cannot see a deletion.
|
|
222
|
+
// Taking it from an option meant a document configured append-only was unprotected unless
|
|
223
|
+
// every future call site remembered to pass the flag.
|
|
224
|
+
appendOnly: appendOnly(doc),
|
|
225
|
+
}, nowMs);
|
|
226
|
+
if (!verdict.admit) {
|
|
227
|
+
// 7a. REJECT. The bytes are held, a 0x05 leaf references them, and the peer is ANSWERED —
|
|
228
|
+
// a rejection is an ack for delivery purposes, so the sender stops retrying and supersedes.
|
|
229
|
+
const rejection = await this.#d.rejections.reject(ownerAgentId, env.document_id, {
|
|
230
|
+
rejectedEnvelopeHash: envelopeHash,
|
|
231
|
+
quarantined: verdict.quarantined,
|
|
232
|
+
reason: verdict.reason,
|
|
233
|
+
detail: verdict.detail,
|
|
234
|
+
senderAgentId: env.sender_agent_id,
|
|
235
|
+
rejectedDocPrevHash: env.doc_prev_hash,
|
|
236
|
+
rule: verdict.rule,
|
|
237
|
+
limit: verdict.limit,
|
|
238
|
+
// The OWNER signs its own rejection, over the canonical preimage (DOD-DOC-REJECT-2).
|
|
239
|
+
sign: (tbs) => this.#d.sign(ownerAgentId, tbs),
|
|
240
|
+
nowMs,
|
|
241
|
+
});
|
|
242
|
+
return {
|
|
243
|
+
ok: true,
|
|
244
|
+
admitted: false,
|
|
245
|
+
envelopeHash,
|
|
246
|
+
// The signed refusal, for the caller to transmit. Absent on a duplicate — we already told
|
|
247
|
+
// them once, and re-sending would advance their round counter for a retry that never
|
|
248
|
+
// happened, driving the document to `stalled` early.
|
|
249
|
+
...(rejection.wire ? { rejectionWire: rejection.wire } : {}),
|
|
250
|
+
rejectionReason: verdict.reason,
|
|
251
|
+
duplicate: false,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
// 7b. ADMIT. The log first, then the live document: an update applied but not logged is one
|
|
255
|
+
// the operator can read and cannot rebuild, and a rebuild is how the document survives a
|
|
256
|
+
// restart. The other order loses content; this order at worst repeats an idempotent apply.
|
|
257
|
+
this.#d.store.appendEnvelope(ownerAgentId, {
|
|
258
|
+
envelopeHash,
|
|
259
|
+
documentId: env.document_id,
|
|
260
|
+
senderAgentId: env.sender_agent_id,
|
|
261
|
+
docPrevHash: env.doc_prev_hash,
|
|
262
|
+
epochId: env.epoch_id,
|
|
263
|
+
signature: env.signature,
|
|
264
|
+
stateVector: env.state_vector,
|
|
265
|
+
payload: env.update,
|
|
266
|
+
kind: "update",
|
|
267
|
+
referencesEnvelopeHash: null,
|
|
268
|
+
// Recorded so the authorship binding for this peer is derived from what they SIGNED, not
|
|
269
|
+
// from a seam somebody has to remember to implement.
|
|
270
|
+
senderClientId: env.sender_client_id,
|
|
271
|
+
createdAtMs: nowMs,
|
|
272
|
+
});
|
|
273
|
+
this.#d.engine.applyUpdateOrThrow(live, env.update);
|
|
274
|
+
// §3.2 step 4: admit AND CLEAR THE QUARANTINE. This is the supersession closing — the held
|
|
275
|
+
// bytes are released, the chain bridge stub for the superseded envelope goes with them, and
|
|
276
|
+
// `document.supersession.admitted` finally has a producer. Without it the entries stayed held
|
|
277
|
+
// forever and the event was a name with nothing behind it.
|
|
278
|
+
if (env.doc_prev_hash !== null) {
|
|
279
|
+
this.#d.rejections.clearQuarantine(ownerAgentId, env.document_id, env.doc_prev_hash);
|
|
280
|
+
}
|
|
281
|
+
this.#d.logger.info("document.inbound.admitted", {
|
|
282
|
+
documentId: env.document_id,
|
|
283
|
+
senderAgentId: env.sender_agent_id,
|
|
284
|
+
envelopeHash,
|
|
285
|
+
correlationId,
|
|
286
|
+
});
|
|
287
|
+
return { ok: true, admitted: true, envelopeHash, duplicate: false };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
//# sourceMappingURL=document-inbound.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-inbound.js","sourceRoot":"","sources":["../src/document-inbound.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-DOC-INBOUND-2 / DOD-DOC-DELIVERY-2 — the composition of the document layer.
|
|
3
|
+
*
|
|
4
|
+
* Everything M14 built is a unit with injected seams. This is where they become one thing, and it
|
|
5
|
+
* is a module rather than a block inside `daemon.ts` for one reason: the assembly makes decisions.
|
|
6
|
+
* Which verifier, which live-doc policy, and — the one that matters — whether the layer is present
|
|
7
|
+
* at all.
|
|
8
|
+
*
|
|
9
|
+
* ── ALL OR NOTHING ────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
*
|
|
11
|
+
* The layer is built completely or not built. There is no partially-wired mode, because every
|
|
12
|
+
* half-wiring is a distinct silent failure: an inbound path with no ack producer leaves the peer
|
|
13
|
+
* retrying until their document stalls; a delivery worker with no inbound counterpart publishes
|
|
14
|
+
* envelopes nobody can answer. `DOD-DOC-DELIVERY-2` records that ordering constraint on its own
|
|
15
|
+
* line, and this is where it is enforced instead of remembered.
|
|
16
|
+
*
|
|
17
|
+
* ── THE VERIFIER IS REQUIRED, AND RESOLVES THROUGH THE CALLER ─────────────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* Signature verification needs an agent id → public key mapping, which lives with the daemon's
|
|
20
|
+
* contact and session state rather than here. It is passed in, and there is no default: a default
|
|
21
|
+
* would be a default answer to "is this authentic", and both inbound paths refuse rather than
|
|
22
|
+
* admit when it says no.
|
|
23
|
+
*/
|
|
24
|
+
import { DocumentStore } from "./document-store.js";
|
|
25
|
+
import { DocumentEngine } from "./document-engine.js";
|
|
26
|
+
import { DocumentRejections } from "./document-rejection.js";
|
|
27
|
+
import { DocumentFrameRouter } from "./document-frame-router.js";
|
|
28
|
+
import { LiveDocuments } from "./document-live-docs.js";
|
|
29
|
+
import { DocumentLifecycle } from "./document-lifecycle.js";
|
|
30
|
+
import { DocumentNotifications } from "./document-notify.js";
|
|
31
|
+
import { DocumentHandshake } from "./document-handshake.js";
|
|
32
|
+
import { DocumentWritePath } from "./document-write-path.js";
|
|
33
|
+
import type { DaemonDatabase } from "./sqlcipher-db.js";
|
|
34
|
+
import type { Logger } from "./types.js";
|
|
35
|
+
export interface DocumentLayerDeps {
|
|
36
|
+
db: DaemonDatabase;
|
|
37
|
+
logger: Logger;
|
|
38
|
+
/**
|
|
39
|
+
* The public key an agent id signs with, or null when this daemon cannot resolve one.
|
|
40
|
+
*
|
|
41
|
+
* REQUIRED. Returning null means "cannot verify", which both inbound paths treat as a refusal —
|
|
42
|
+
* the same answer as a bad signature, because admitting an envelope we cannot authenticate is
|
|
43
|
+
* the outcome the whole verify step exists to prevent.
|
|
44
|
+
*
|
|
45
|
+
* Injected rather than computed here even though M14-D5 makes a remote agent id BE its pubkey
|
|
46
|
+
* hex, because the daemon may want to refuse a peer it has no contact for, or resolve through a
|
|
47
|
+
* different binding later. `agentPublicKeyFromId` below is the default implementation.
|
|
48
|
+
*/
|
|
49
|
+
publicKeyFor(agentId: string): Uint8Array | null;
|
|
50
|
+
/**
|
|
51
|
+
* The stable owner key for a daemon agent NAME — our own K_local pubkey hex (M14-D5).
|
|
52
|
+
*
|
|
53
|
+
* The session content path knows agents by name; every document row is scoped by owner key. Both
|
|
54
|
+
* halves of the layer must agree, and the delivery sweep already uses the pubkey hex. See
|
|
55
|
+
* `DocumentFrameRouterDeps.ownerKeyFor` for what a disagreement hides.
|
|
56
|
+
*/
|
|
57
|
+
ownerKeyFor(agentName: string): string | null;
|
|
58
|
+
/** Tell the peer about a unilateral end. Injected — the transport is not this layer's. */
|
|
59
|
+
notifyPeer(documentId: string, verb: "kill" | "close"): Promise<{
|
|
60
|
+
ok: true;
|
|
61
|
+
} | {
|
|
62
|
+
ok: false;
|
|
63
|
+
reason: string;
|
|
64
|
+
}>;
|
|
65
|
+
/** Undo one envelope's operations on the live document, for withdraw. */
|
|
66
|
+
rollback(ownerAgentId: string, documentId: string, envelopeHash: string): {
|
|
67
|
+
ok: true;
|
|
68
|
+
} | {
|
|
69
|
+
ok: false;
|
|
70
|
+
reason: string;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Sign as a given agent. Takes the agent id so a rejection cannot be signed with the wrong key —
|
|
74
|
+
* the first version took no arguments and the composition root reached for whichever key
|
|
75
|
+
* provider was first in a map, which is fabricated crypto wearing a real signature.
|
|
76
|
+
*
|
|
77
|
+
* ASYNC because signing goes through a key provider, and that async is what forced the router to
|
|
78
|
+
* split synchronous CLASSIFICATION from asynchronous HANDLING.
|
|
79
|
+
*/
|
|
80
|
+
sign(ownerAgentId: string, tbs: Uint8Array): Promise<Uint8Array>;
|
|
81
|
+
/**
|
|
82
|
+
* Put an already-encoded frame on the wire to a peer — the ack, today.
|
|
83
|
+
*
|
|
84
|
+
* Injected because the transport is not this layer's, and REQUIRED for the same reason the layer
|
|
85
|
+
* is all-or-nothing: an inbound path that cannot answer leaves every sender retrying until their
|
|
86
|
+
* document stalls.
|
|
87
|
+
*/
|
|
88
|
+
/**
|
|
89
|
+
* Where documents are materialized as files. Absent disables the file surface entirely — the
|
|
90
|
+
* content verbs still work, and nothing silently half-writes.
|
|
91
|
+
*/
|
|
92
|
+
workspaceRoot?: string;
|
|
93
|
+
sendFrame(ownerAgentId: string, peerAgentId: string, bytes: Uint8Array): Promise<{
|
|
94
|
+
ok: true;
|
|
95
|
+
} | {
|
|
96
|
+
ok: false;
|
|
97
|
+
reason: string;
|
|
98
|
+
}>;
|
|
99
|
+
}
|
|
100
|
+
export interface DocumentLayer {
|
|
101
|
+
store: DocumentStore;
|
|
102
|
+
/** The file projection, or null when no workspace root was configured. */
|
|
103
|
+
writePath: DocumentWritePath | null;
|
|
104
|
+
handshake: DocumentHandshake;
|
|
105
|
+
engine: DocumentEngine;
|
|
106
|
+
live: LiveDocuments;
|
|
107
|
+
lifecycle: DocumentLifecycle;
|
|
108
|
+
notifications: DocumentNotifications;
|
|
109
|
+
rejections: DocumentRejections;
|
|
110
|
+
router: DocumentFrameRouter;
|
|
111
|
+
/**
|
|
112
|
+
* The hook `SessionNodeManager.setOnDocumentFrame` takes. Handed out ready-shaped so the
|
|
113
|
+
* composition root does not re-derive the adapter — and so the `consumed` contract has exactly
|
|
114
|
+
* one definition.
|
|
115
|
+
*/
|
|
116
|
+
onDocumentFrame(agentName: string, sessionId: string, content: Uint8Array, senderPubkey: string, correlationId?: string): {
|
|
117
|
+
consumed: boolean;
|
|
118
|
+
kind?: string;
|
|
119
|
+
ok?: boolean;
|
|
120
|
+
reason?: string;
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The default `publicKeyFor`: a remote agent's id IS its K_local public key, hex-encoded (M14-D5).
|
|
125
|
+
*
|
|
126
|
+
* `agent_id` in this daemon is a LOCAL primary key on the `agents` table — it names rows in our own
|
|
127
|
+
* database and cannot identify a remote peer. The pubkey is the only self-verifying identifier
|
|
128
|
+
* available: the signature checks against it directly, with no lookup that could be stale, missing
|
|
129
|
+
* or poisoned sitting on the critical path of every authentication.
|
|
130
|
+
*
|
|
131
|
+
* Malformed input returns null rather than throwing, and null is a refusal. An id that is not a
|
|
132
|
+
* 32-byte hex key is not "a peer we have no key for" — it is a frame that does not follow the
|
|
133
|
+
* protocol — but both must refuse, and refusing loudly at the verify step is where it belongs.
|
|
134
|
+
*/
|
|
135
|
+
export declare function agentPublicKeyFromId(agentId: string): Uint8Array | null;
|
|
136
|
+
export declare function createDocumentLayer(deps: DocumentLayerDeps): DocumentLayer;
|
|
137
|
+
//# sourceMappingURL=document-layer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-layer.d.ts","sourceRoot":"","sources":["../src/document-layer.ts"],"names":[],"mappings":"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"}
|