@cello-protocol/daemon 0.0.192 → 0.0.194
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/agent-id-migration.d.ts.map +1 -1
- package/dist/agent-id-migration.js +6 -0
- package/dist/agent-id-migration.js.map +1 -1
- package/dist/daemon.js +12 -5
- package/dist/daemon.js.map +1 -1
- package/dist/inbound-sessions.d.ts +1 -1
- package/dist/inbound-sessions.d.ts.map +1 -1
- package/dist/inbound-sessions.js +124 -5
- package/dist/inbound-sessions.js.map +1 -1
- package/dist/initiate-session-handler.d.ts.map +1 -1
- package/dist/initiate-session-handler.js +19 -0
- package/dist/initiate-session-handler.js.map +1 -1
- package/dist/park-envelope.d.ts +17 -0
- package/dist/park-envelope.d.ts.map +1 -1
- package/dist/park-envelope.js.map +1 -1
- package/dist/refusal-reasons.d.ts +23 -0
- package/dist/refusal-reasons.d.ts.map +1 -1
- package/dist/refusal-reasons.js +66 -0
- package/dist/refusal-reasons.js.map +1 -1
- package/dist/retry-queue.d.ts +12 -1
- package/dist/retry-queue.d.ts.map +1 -1
- package/dist/retry-queue.js +14 -5
- package/dist/retry-queue.js.map +1 -1
- package/dist/session-node-manager.d.ts +30 -19
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +657 -102
- package/dist/session-node-manager.js.map +1 -1
- package/dist/session-own-chain-store.d.ts +65 -0
- package/dist/session-own-chain-store.d.ts.map +1 -0
- package/dist/session-own-chain-store.js +75 -0
- package/dist/session-own-chain-store.js.map +1 -0
- package/dist/session-relay-client.d.ts +35 -3
- package/dist/session-relay-client.d.ts.map +1 -1
- package/dist/session-relay-client.js +211 -30
- package/dist/session-relay-client.js.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `DOD-M15-SELFCHAIN-1` — what THIS agent last said in a session, so the next thing it says can
|
|
3
|
+
* link to it.
|
|
4
|
+
*
|
|
5
|
+
* ─── Why this exists at all ───────────────────────────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* A sender signs `last_seen_hash`: the hash of the last message they RECEIVED. That chains across
|
|
8
|
+
* the two parties and never chains a sender to themselves — so when one party sends twice in a
|
|
9
|
+
* row, both of their messages carry the same acknowledgement, because nothing arrived in between.
|
|
10
|
+
* Nothing in the signed bytes tells them apart.
|
|
11
|
+
*
|
|
12
|
+
* The relay-assigned position cannot fill the gap: it is assigned AFTER the sender signs, so a
|
|
13
|
+
* sender can never sign their own position. The relay's receipt does pin it — but the receipt goes
|
|
14
|
+
* to the SENDER, so whoever hands a conversation to a new relay holds no receipt for the
|
|
15
|
+
* counterparty's messages and can reorder any run of them.
|
|
16
|
+
*
|
|
17
|
+
* `prev_own_hash` closes it, and this store is the thing that can answer what to put in it.
|
|
18
|
+
*
|
|
19
|
+
* ─── Why a dedicated table rather than reading one that exists ────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* Three candidates were examined and each fails for a different reason:
|
|
22
|
+
*
|
|
23
|
+
* - **`SessionTree`** stores leaf hashes and no authorship, so it cannot say which leaves are
|
|
24
|
+
* ours.
|
|
25
|
+
* - **`transcript`** knows `direction` but carries no content hash column.
|
|
26
|
+
* - **`session_seal_leaves`** knows the sender and the signed bytes, and is keyed by the RELAY's
|
|
27
|
+
* `sequence_number` — so it holds nothing for a message that was never witnessed. `033-ACKEMIT`
|
|
28
|
+
* Part 0b measured exactly that gap and it is still open.
|
|
29
|
+
*
|
|
30
|
+
* The self link must hold on the unwitnessed path too — a conversation that ran while the relay was
|
|
31
|
+
* down is precisely the one whose order gets disputed later — so the producer cannot depend on a
|
|
32
|
+
* relay having been there. One row per (agent, session), written wherever this daemon signs.
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ PERSISTED, because the chain must survive a restart. An in-memory tracker would silently start
|
|
35
|
+
* a new chain after a daemon restart mid-conversation, and the counterparty would refuse every
|
|
36
|
+
* message after it with no way to tell that from tampering.
|
|
37
|
+
*/
|
|
38
|
+
import type { DaemonDatabase } from "./sqlcipher-db.js";
|
|
39
|
+
import type { Logger } from "./types.js";
|
|
40
|
+
export declare class SessionOwnChainStore {
|
|
41
|
+
#private;
|
|
42
|
+
constructor(db: DaemonDatabase, logger: Logger);
|
|
43
|
+
/**
|
|
44
|
+
* The content hash of this agent's own previous message in this session, or `null` when it has
|
|
45
|
+
* not sent one yet.
|
|
46
|
+
*
|
|
47
|
+
* ⚠️ `null` MEANS "I HAVE NOT SPOKEN HERE", AND THE CALLER MUST NOT TREAT IT AS "SKIP THE LINK".
|
|
48
|
+
* The first message of a session carries the session GENESIS as its self link — a defined value,
|
|
49
|
+
* derived per session. Substituting an absent field, or a shared constant like 32 zero bytes,
|
|
50
|
+
* would give a first message a link that is presentable for any conversation.
|
|
51
|
+
*/
|
|
52
|
+
lastOwnHash(agentPubkeyHex: string, sessionIdHex: string): Uint8Array | null;
|
|
53
|
+
/**
|
|
54
|
+
* Record what this agent just sent, so the next message links to it.
|
|
55
|
+
*
|
|
56
|
+
* ⚠️ CALLED AFTER THE SEND SUCCEEDS, NEVER BEFORE. Recording first and failing to send would skip
|
|
57
|
+
* a link over a message that never existed, and every later message would be refused by the
|
|
58
|
+
* counterparty for a reason that names tampering. Recording after means a retry re-uses the same
|
|
59
|
+
* predecessor, which is exactly right — a retransmission is the same message.
|
|
60
|
+
*/
|
|
61
|
+
record(agentPubkeyHex: string, sessionIdHex: string, contentHash: Uint8Array, atMs: number): void;
|
|
62
|
+
/** Drop a session's chain record. Called when the session is destroyed. */
|
|
63
|
+
forget(agentPubkeyHex: string, sessionIdHex: string): void;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=session-own-chain-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-own-chain-store.d.ts","sourceRoot":"","sources":["../src/session-own-chain-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAYzC,qBAAa,oBAAoB;;gBAInB,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM;IAM9C;;;;;;;;OAQG;IACH,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IA2B5E;;;;;;;OAOG;IACH,MAAM,CAAC,cAAc,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAUjG,2EAA2E;IAC3E,MAAM,CAAC,cAAc,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI;CAK3D"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const CREATE_SQL = `
|
|
2
|
+
CREATE TABLE IF NOT EXISTS session_own_chain (
|
|
3
|
+
agent_pubkey TEXT NOT NULL,
|
|
4
|
+
session_id TEXT NOT NULL,
|
|
5
|
+
last_own_hash TEXT NOT NULL,
|
|
6
|
+
updated_at INTEGER NOT NULL,
|
|
7
|
+
PRIMARY KEY (agent_pubkey, session_id)
|
|
8
|
+
);
|
|
9
|
+
`;
|
|
10
|
+
export class SessionOwnChainStore {
|
|
11
|
+
#db;
|
|
12
|
+
#logger;
|
|
13
|
+
constructor(db, logger) {
|
|
14
|
+
this.#db = db;
|
|
15
|
+
this.#logger = logger;
|
|
16
|
+
this.#db.exec(CREATE_SQL);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The content hash of this agent's own previous message in this session, or `null` when it has
|
|
20
|
+
* not sent one yet.
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ `null` MEANS "I HAVE NOT SPOKEN HERE", AND THE CALLER MUST NOT TREAT IT AS "SKIP THE LINK".
|
|
23
|
+
* The first message of a session carries the session GENESIS as its self link — a defined value,
|
|
24
|
+
* derived per session. Substituting an absent field, or a shared constant like 32 zero bytes,
|
|
25
|
+
* would give a first message a link that is presentable for any conversation.
|
|
26
|
+
*/
|
|
27
|
+
lastOwnHash(agentPubkeyHex, sessionIdHex) {
|
|
28
|
+
const row = this.#db
|
|
29
|
+
.prepare(`SELECT last_own_hash FROM session_own_chain WHERE agent_pubkey = ? AND session_id = ?`)
|
|
30
|
+
.get(agentPubkeyHex, sessionIdHex);
|
|
31
|
+
if (!row || typeof row.last_own_hash !== "string")
|
|
32
|
+
return null;
|
|
33
|
+
const bytes = Buffer.from(row.last_own_hash, "hex");
|
|
34
|
+
if (bytes.length !== 32) {
|
|
35
|
+
/**
|
|
36
|
+
* A stored value that is not a hash is a corrupt local record, and the honest answer is to say
|
|
37
|
+
* so and behave as if nothing were stored — the next message then chains to the session
|
|
38
|
+
* genesis, which the counterparty will refuse, which is the correct loud outcome. Returning
|
|
39
|
+
* the malformed bytes would sign a link nobody can check; returning null silently would hide
|
|
40
|
+
* a corrupt database behind a first-message claim.
|
|
41
|
+
*/
|
|
42
|
+
this.#logger.error("session.selfchain.stored_hash_malformed", {
|
|
43
|
+
session: sessionIdHex,
|
|
44
|
+
storedWidth: bytes.length,
|
|
45
|
+
impact: "this agent's own chain record for this session is corrupt, so the next message it sends " +
|
|
46
|
+
"will not link to the previous one and the counterparty will refuse it. The conversation " +
|
|
47
|
+
"cannot continue on this machine until the session is restarted.",
|
|
48
|
+
});
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return new Uint8Array(bytes);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Record what this agent just sent, so the next message links to it.
|
|
55
|
+
*
|
|
56
|
+
* ⚠️ CALLED AFTER THE SEND SUCCEEDS, NEVER BEFORE. Recording first and failing to send would skip
|
|
57
|
+
* a link over a message that never existed, and every later message would be refused by the
|
|
58
|
+
* counterparty for a reason that names tampering. Recording after means a retry re-uses the same
|
|
59
|
+
* predecessor, which is exactly right — a retransmission is the same message.
|
|
60
|
+
*/
|
|
61
|
+
record(agentPubkeyHex, sessionIdHex, contentHash, atMs) {
|
|
62
|
+
this.#db
|
|
63
|
+
.prepare(`INSERT INTO session_own_chain (agent_pubkey, session_id, last_own_hash, updated_at)
|
|
64
|
+
VALUES (?, ?, ?, ?)
|
|
65
|
+
ON CONFLICT(agent_pubkey, session_id) DO UPDATE SET last_own_hash = excluded.last_own_hash, updated_at = excluded.updated_at`)
|
|
66
|
+
.run(agentPubkeyHex, sessionIdHex, Buffer.from(contentHash).toString("hex"), atMs);
|
|
67
|
+
}
|
|
68
|
+
/** Drop a session's chain record. Called when the session is destroyed. */
|
|
69
|
+
forget(agentPubkeyHex, sessionIdHex) {
|
|
70
|
+
this.#db
|
|
71
|
+
.prepare(`DELETE FROM session_own_chain WHERE agent_pubkey = ? AND session_id = ?`)
|
|
72
|
+
.run(agentPubkeyHex, sessionIdHex);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=session-own-chain-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-own-chain-store.js","sourceRoot":"","sources":["../src/session-own-chain-store.ts"],"names":[],"mappings":"AAwCA,MAAM,UAAU,GAAG;;;;;;;;CAQlB,CAAC;AAEF,MAAM,OAAO,oBAAoB;IACtB,GAAG,CAAiB;IACpB,OAAO,CAAS;IAEzB,YAAY,EAAkB,EAAE,MAAc;QAC5C,IAAI,CAAC,GAAG,GAAG,EAAE,CAAC;QACd,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC5B,CAAC;IAED;;;;;;;;OAQG;IACH,WAAW,CAAC,cAAsB,EAAE,YAAoB;QACtD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG;aACjB,OAAO,CAAC,uFAAuF,CAAC;aAChG,GAAG,CAAC,cAAc,EAAE,YAAY,CAA2C,CAAC;QAC/E,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,CAAC,aAAa,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC/D,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;QACpD,IAAI,KAAK,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;YACxB;;;;;;eAMG;YACH,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,yCAAyC,EAAE;gBAC5D,OAAO,EAAE,YAAY;gBACrB,WAAW,EAAE,KAAK,CAAC,MAAM;gBACzB,MAAM,EACJ,0FAA0F;oBAC1F,0FAA0F;oBAC1F,iEAAiE;aACpE,CAAC,CAAC;YACH,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,cAAsB,EAAE,YAAoB,EAAE,WAAuB,EAAE,IAAY;QACxF,IAAI,CAAC,GAAG;aACL,OAAO,CACN;;sIAE8H,CAC/H;aACA,GAAG,CAAC,cAAc,EAAE,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,CAAC;IACvF,CAAC;IAED,2EAA2E;IAC3E,MAAM,CAAC,cAAsB,EAAE,YAAoB;QACjD,IAAI,CAAC,GAAG;aACL,OAAO,CAAC,yEAAyE,CAAC;aAClF,GAAG,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;IACvC,CAAC;CACF"}
|
|
@@ -3,6 +3,7 @@ import { type KeyProvider } from "@cello-protocol/crypto";
|
|
|
3
3
|
import type { Logger } from "./types.js";
|
|
4
4
|
import { type RelayReceiptStore } from "./relay-receipt-store.js";
|
|
5
5
|
import type { SessionSealLeafStore } from "./session-seal-leaf-store.js";
|
|
6
|
+
import type { SessionOwnChainStore } from "./session-own-chain-store.js";
|
|
6
7
|
export declare const RELAY_PROTOCOL_ID = "/cello/relay/1.0.0";
|
|
7
8
|
export declare const RELAY_AUTH_DOMAIN = "CELLO-RELAY-AUTH-v1";
|
|
8
9
|
/** Structure 1 leaf kind: 0x00 = message, 0x02 = control (matches the relay). */
|
|
@@ -147,6 +148,12 @@ export interface AgentRelayClientOpts {
|
|
|
147
148
|
* directory's OFFLINE tree rebuild. Optional — when absent, no leaf log is kept.
|
|
148
149
|
*/
|
|
149
150
|
sealLeafStore?: SessionSealLeafStore;
|
|
151
|
+
/**
|
|
152
|
+
* `DOD-M15-SELFCHAIN-1` — what this agent last said in each session, so the next message links to
|
|
153
|
+
* it. Optional on the type because test harnesses construct a client without one; when it is
|
|
154
|
+
* absent this daemon cannot chain to itself and says so rather than signing an unlinked claim.
|
|
155
|
+
*/
|
|
156
|
+
ownChainStore?: SessionOwnChainStore;
|
|
150
157
|
/**
|
|
151
158
|
* DOD-M15-RELAYSLOTS-1: the current directory-issued online token for this agent, or `undefined`
|
|
152
159
|
* when the directory has not issued one (not connected yet, or this key is not a registered
|
|
@@ -272,9 +279,13 @@ export declare class AgentRelayClient {
|
|
|
272
279
|
* Supplied by the caller because `session-node-manager` is where the session record lives. When
|
|
273
280
|
* it is absent an ASSIGNMENT can still produce it (both participant keys and the session
|
|
274
281
|
* timestamp are on the carry), and that covers re-registration of a session whose row predates
|
|
275
|
-
* the column.
|
|
276
|
-
*
|
|
277
|
-
*
|
|
282
|
+
* the column.
|
|
283
|
+
*
|
|
284
|
+
* ⚠️ WHEN NEITHER IS AVAILABLE THE SESSION CANNOT SUBMIT AT ALL — and this sentence used to say
|
|
285
|
+
* the opposite, that the first submit would claim position 0 with no hash. That shape no longer
|
|
286
|
+
* exists: `DOD-M15-SELFCHAIN-1` made both chain links required, so a session with no starting
|
|
287
|
+
* point has nothing for them to anchor to and every submit on it is refused by name. Registering
|
|
288
|
+
* without a seed is therefore a real fault, not a degraded mode.
|
|
278
289
|
*/
|
|
279
290
|
genesisPrevRoot?: Uint8Array): void;
|
|
280
291
|
/**
|
|
@@ -463,6 +474,27 @@ export declare class AgentRelayClient {
|
|
|
463
474
|
seq: number;
|
|
464
475
|
hash: Uint8Array;
|
|
465
476
|
} | undefined;
|
|
477
|
+
/**
|
|
478
|
+
* This agent's OWN last message on a session — what its next message's self link must name.
|
|
479
|
+
*
|
|
480
|
+
* ⚠️ EXPOSED BECAUSE THERE MUST BE ONE CHAIN, NOT TWO. The unwitnessed send path lives in
|
|
481
|
+
* `session-node-manager` and was reading only the durable store, while the witnessed path reads
|
|
482
|
+
* this in-memory map first. A session that mixed the two — which is every session where the relay
|
|
483
|
+
* comes and goes — was walking two different chains, and the one that lagged produced a link the
|
|
484
|
+
* counterparty refuses.
|
|
485
|
+
*
|
|
486
|
+
* `undefined` means this agent has not spoken on this session yet, which is the session GENESIS
|
|
487
|
+
* and not an absence. The caller supplies it; this class does not guess.
|
|
488
|
+
*/
|
|
489
|
+
lastOwnHash(sessionIdHex: string): Uint8Array | undefined;
|
|
490
|
+
/**
|
|
491
|
+
* Record what this agent just sent on a path this client did not carry — the UNWITNESSED send.
|
|
492
|
+
*
|
|
493
|
+
* The two paths share one chain (see `lastOwnHash`), so a direct send has to advance it here too.
|
|
494
|
+
* Without this, a conversation that ran while the relay was down advanced nothing, and the first
|
|
495
|
+
* witnessed message after it linked to something long superseded.
|
|
496
|
+
*/
|
|
497
|
+
noteOwnLeaf(sessionIdHex: string, contentHash: Uint8Array): void;
|
|
466
498
|
close(): void;
|
|
467
499
|
}
|
|
468
500
|
//# sourceMappingURL=session-relay-client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-relay-client.d.ts","sourceRoot":"","sources":["../src/session-relay-client.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"session-relay-client.d.ts","sourceRoot":"","sources":["../src/session-relay-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -262,6 +262,35 @@ export class AgentRelayClient {
|
|
|
262
262
|
#logger;
|
|
263
263
|
#receiptStore;
|
|
264
264
|
#sealLeafStore;
|
|
265
|
+
#ownChainStore;
|
|
266
|
+
/**
|
|
267
|
+
* `DOD-M15-SELFCHAIN-1` — this agent's own last message per session, in memory.
|
|
268
|
+
*
|
|
269
|
+
* ⚠️ THE MAP IS THE CHAIN; THE STORE IS ITS DURABILITY. Separating them is deliberate. Within a
|
|
270
|
+
* running process the map is authoritative and always available, so a send never depends on a
|
|
271
|
+
* database being wired. Across a RESTART only the store can answer, and without it this daemon
|
|
272
|
+
* would silently start a new chain mid-conversation — the counterparty refusing every message
|
|
273
|
+
* after it, with no way to tell that from tampering.
|
|
274
|
+
*
|
|
275
|
+
* So a missing store is a durability gap, and never a reason to refuse a send: it is caused by our
|
|
276
|
+
* own wiring, never by anything a peer does, and refusing there would break the product to close a
|
|
277
|
+
* hole the peer cannot reach. It IS reported — once per client, at construction, by the error
|
|
278
|
+
* below. An earlier version of this comment promised that report and nothing emitted it, which is
|
|
279
|
+
* the shape that lets a wiring gap sit invisible until a restart days later.
|
|
280
|
+
*/
|
|
281
|
+
#ownChain = new Map();
|
|
282
|
+
/**
|
|
283
|
+
* `DOD-M15-SELFCHAIN-1` — each session's GENESIS, kept apart from `#lastSeen`, and keeping them
|
|
284
|
+
* apart is the entire point of this field.
|
|
285
|
+
*
|
|
286
|
+
* `#lastSeen` is seeded with the genesis at registration and then ADVANCES as the counterparty
|
|
287
|
+
* speaks. So after the counterparty's first message it holds THEIR last content hash, not the
|
|
288
|
+
* starting point. Reading it as the self link's fallback handed a sender the other party's hash
|
|
289
|
+
* for their own first message, which the relay then refused — and, worse, told the innocent
|
|
290
|
+
* counterparty their chain was broken. This map never advances, so it is always the right answer
|
|
291
|
+
* to "what does a party who has not spoken here yet link to?".
|
|
292
|
+
*/
|
|
293
|
+
#genesis = new Map();
|
|
265
294
|
/** DOD-M15-RELAYSLOTS-1 — read fresh at every auth. See `AgentRelayClientOpts.onlineToken`. */
|
|
266
295
|
#onlineToken;
|
|
267
296
|
/** DOD-M15-CORROBORATE-1 — where a relay's witness alert goes. See the opt of the same name. */
|
|
@@ -338,6 +367,22 @@ export class AgentRelayClient {
|
|
|
338
367
|
this.#logger = opts.logger;
|
|
339
368
|
this.#receiptStore = opts.receiptStore;
|
|
340
369
|
this.#sealLeafStore = opts.sealLeafStore;
|
|
370
|
+
this.#ownChainStore = opts.ownChainStore;
|
|
371
|
+
if (!this.#ownChainStore) {
|
|
372
|
+
/**
|
|
373
|
+
* ONCE PER CLIENT, AT CONSTRUCTION, and at ERROR because the consequence is invisible until a
|
|
374
|
+
* restart that may be days away — at which point this daemon silently starts a new chain
|
|
375
|
+
* mid-conversation and the counterparty refuses everything after it, for a reason that names
|
|
376
|
+
* tampering.
|
|
377
|
+
*/
|
|
378
|
+
this.#logger.error("session.relay.own_chain.store_absent", {
|
|
379
|
+
relayPeerId: opts.relayPeerId,
|
|
380
|
+
impact: "this relay client was wired without a durable record of what this agent has said. " +
|
|
381
|
+
"Conversations work until the daemon restarts; after a restart, mid-conversation, this " +
|
|
382
|
+
"side starts a new chain and the counterparty refuses every message after it as though " +
|
|
383
|
+
"the record had been altered.",
|
|
384
|
+
});
|
|
385
|
+
}
|
|
341
386
|
this.#onlineToken = opts.onlineToken;
|
|
342
387
|
this.#onWitnessAlert = opts.onWitnessAlert;
|
|
343
388
|
this.#onWitnessUnreadable = opts.onWitnessUnreadable;
|
|
@@ -359,9 +404,13 @@ export class AgentRelayClient {
|
|
|
359
404
|
* Supplied by the caller because `session-node-manager` is where the session record lives. When
|
|
360
405
|
* it is absent an ASSIGNMENT can still produce it (both participant keys and the session
|
|
361
406
|
* timestamp are on the carry), and that covers re-registration of a session whose row predates
|
|
362
|
-
* the column.
|
|
363
|
-
*
|
|
364
|
-
*
|
|
407
|
+
* the column.
|
|
408
|
+
*
|
|
409
|
+
* ⚠️ WHEN NEITHER IS AVAILABLE THE SESSION CANNOT SUBMIT AT ALL — and this sentence used to say
|
|
410
|
+
* the opposite, that the first submit would claim position 0 with no hash. That shape no longer
|
|
411
|
+
* exists: `DOD-M15-SELFCHAIN-1` made both chain links required, so a session with no starting
|
|
412
|
+
* point has nothing for them to anchor to and every submit on it is refused by name. Registering
|
|
413
|
+
* without a seed is therefore a real fault, not a degraded mode.
|
|
365
414
|
*/
|
|
366
415
|
genesisPrevRoot) {
|
|
367
416
|
const existing = this.#sessions.get(sessionIdHex);
|
|
@@ -383,10 +432,12 @@ export class AgentRelayClient {
|
|
|
383
432
|
* nothing" for a conversation that is well underway — and the relay would then answer the next
|
|
384
433
|
* submit from a position we had already passed.
|
|
385
434
|
*/
|
|
435
|
+
const genesis = genesisPrevRoot ?? genesisFromAssignment(sessionIdHex, carriedAssignment);
|
|
436
|
+
if (genesis)
|
|
437
|
+
this.#genesis.set(sessionIdHex, genesis);
|
|
386
438
|
if (!this.#lastSeen.has(sessionIdHex)) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
this.#lastSeen.set(sessionIdHex, { seq: 0, hash: seed });
|
|
439
|
+
if (genesis)
|
|
440
|
+
this.#lastSeen.set(sessionIdHex, { seq: 0, hash: genesis });
|
|
390
441
|
}
|
|
391
442
|
// Eagerly present the assignment so the relay records the session (binds peer IDs, creates the
|
|
392
443
|
// session entry) BEFORE the first hash_submit or the counterparty's leaves arrive — the relay
|
|
@@ -508,6 +559,7 @@ export class AgentRelayClient {
|
|
|
508
559
|
unregisterSession(sessionIdHex) {
|
|
509
560
|
this.#sessions.delete(sessionIdHex);
|
|
510
561
|
this.#lastSeen.delete(sessionIdHex);
|
|
562
|
+
this.#genesis.delete(sessionIdHex);
|
|
511
563
|
}
|
|
512
564
|
hasSessions() {
|
|
513
565
|
return this.#sessions.size > 0;
|
|
@@ -1755,28 +1807,15 @@ export class AgentRelayClient {
|
|
|
1755
1807
|
* AUTHOR's, already made. Logging "this submit acknowledges nothing" about it would be false,
|
|
1756
1808
|
* and this daemon's own seed is irrelevant to a claim it did not write.
|
|
1757
1809
|
*/
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
* not the unbacked number this unit exists to stop signing.
|
|
1768
|
-
*
|
|
1769
|
-
* It does not refuse the submit, and an earlier version did. Sessions brokered without a
|
|
1770
|
-
* relay assignment are real, and refusing there left them unable to be witnessed at all.
|
|
1771
|
-
*/
|
|
1772
|
-
this.#logger.info("session.relay.submit.unacknowledged", {
|
|
1773
|
-
relayPeerId: this.#relayPeerId,
|
|
1774
|
-
session: sessionIdHex,
|
|
1775
|
-
impact: "this submit acknowledges nothing: the session has no recorded starting point and no " +
|
|
1776
|
-
"counterparty leaf has arrived on it. The leaf is witnessed as normal; the claim simply " +
|
|
1777
|
-
"makes no assertion about what this agent has received.",
|
|
1778
|
-
});
|
|
1779
|
-
}
|
|
1810
|
+
/**
|
|
1811
|
+
* ⚠️ THE `session.relay.submit.unacknowledged` LOG THAT WAS HERE IS GONE, AND ITS ABSENCE IS
|
|
1812
|
+
* DELIBERATE — `DOD-M15-SELFCHAIN-1`.
|
|
1813
|
+
*
|
|
1814
|
+
* It fired on exactly the condition the refusal below fires on (no seed, not a carried leaf)
|
|
1815
|
+
* and told the operator "the leaf is witnessed as normal". That is now false: nothing is
|
|
1816
|
+
* witnessed, the submit is refused. Two lines about one condition, one of them wrong, is how a
|
|
1817
|
+
* reader ends up trusting the wrong one — so the refusal below is the only thing that speaks.
|
|
1818
|
+
*/
|
|
1780
1819
|
// The published encoder from protocol-types — the ONE definition of the field order, pinned by
|
|
1781
1820
|
// `structure1-canonical.json` (v1) and `structure1-v2-canonical.json` (v2). A second local copy
|
|
1782
1821
|
// lived here until 020-ACKHASH; it drifted, and the drift was invisible because both copies
|
|
@@ -1799,13 +1838,97 @@ export class AgentRelayClient {
|
|
|
1799
1838
|
* deliberately NOT consulted: `last_seen_seq` and `last_seen_hash` inside those bytes are the
|
|
1800
1839
|
* AUTHOR's account of what THEY had seen, and they are not ours to restate.
|
|
1801
1840
|
*/
|
|
1841
|
+
/**
|
|
1842
|
+
* ─── BOTH CHAIN LINKS, UNCONDITIONALLY — `DOD-M15-SELFCHAIN-1` ───────────────────────────────
|
|
1843
|
+
*
|
|
1844
|
+
* `lastSeenHash` chains this sender to their COUNTERPARTY. `prevOwnHash` chains them to
|
|
1845
|
+
* THEMSELVES. Neither is optional: a claim carrying one link is not a shape this protocol has,
|
|
1846
|
+
* and `encodeStructure1` has no branch that emits one.
|
|
1847
|
+
*
|
|
1848
|
+
* ⚠️ THE CONDITIONAL SPREAD THAT USED TO BE HERE DEFEATED THE TYPE. Writing
|
|
1849
|
+
* `...(x ? { lastSeenHash: x } : {})` satisfies a REQUIRED field as far as the compiler is
|
|
1850
|
+
* concerned, so the one guard that should have made an unlinked claim impossible to write was
|
|
1851
|
+
* silently inert. Pass both by value; let `tsc` do its job.
|
|
1852
|
+
*
|
|
1853
|
+
* ⚠️ NO SEED MEANS NO SEND. The seed is this session's genesis — the agreed starting point both
|
|
1854
|
+
* links fall back to before anything has been said. A session registered with neither a genesis
|
|
1855
|
+
* nor an assignment to derive one from has no such point, and the honest outcome is to refuse
|
|
1856
|
+
* rather than sign a chain anchored to nothing. There is no degraded shape to fall back to and
|
|
1857
|
+
* deliberately so: an unlinked message is invisible until the conversation's order is disputed,
|
|
1858
|
+
* which is far too late for anyone to act on it.
|
|
1859
|
+
*/
|
|
1860
|
+
const seed = lastSeen?.hash;
|
|
1861
|
+
const genesis = this.#genesis.get(sessionIdHex);
|
|
1862
|
+
if (!carried && !seed) {
|
|
1863
|
+
this.#logger.error("session.relay.submit.unchainable", {
|
|
1864
|
+
relayPeerId: this.#relayPeerId,
|
|
1865
|
+
session: sessionIdHex,
|
|
1866
|
+
impact: "this session has no recorded starting point on this machine, so a message sent on it " +
|
|
1867
|
+
"could not link to anything and its place in the conversation could never be proven. " +
|
|
1868
|
+
"The message was NOT sent. Restart the session so it is registered with its genesis.",
|
|
1869
|
+
});
|
|
1870
|
+
return { ok: false, reason: "session_unchainable" };
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* This agent's own previous message, or the session genesis when it has not spoken here yet.
|
|
1874
|
+
* "I have not spoken" is a VALUE — derived per session, so it cannot be presented for a
|
|
1875
|
+
* different conversation — never an absent field.
|
|
1876
|
+
*
|
|
1877
|
+
* The in-memory map first, then the durable store for the restart case, then the genesis. See
|
|
1878
|
+
* `#ownChain` for why a missing store degrades durability rather than blocking the send.
|
|
1879
|
+
*/
|
|
1880
|
+
let prevOwn;
|
|
1881
|
+
if (!carried) {
|
|
1882
|
+
/**
|
|
1883
|
+
* ⚠️ THE LAST FALLBACK IS THE GENESIS, NOT `seed` — and `seed` is what it used to be.
|
|
1884
|
+
*
|
|
1885
|
+
* `seed` is `#lastSeen`, which ADVANCES as the counterparty speaks. So the moment the
|
|
1886
|
+
* counterparty had said anything, a party sending their FIRST message linked to the
|
|
1887
|
+
* counterparty's message instead of to the session's starting point. The relay expects the
|
|
1888
|
+
* genesis for a sender's first leaf, so it refused — and then told the counterparty, who had
|
|
1889
|
+
* done nothing, that this side's chain was broken. Every two-party conversation died on its
|
|
1890
|
+
* second message and blamed the wrong party for it.
|
|
1891
|
+
*/
|
|
1892
|
+
prevOwn = this.#ownChain.get(sessionIdHex)
|
|
1893
|
+
?? this.#ownChainStore?.lastOwnHash(this.senderPubkeyHex, sessionIdHex)
|
|
1894
|
+
?? genesis;
|
|
1895
|
+
if (!prevOwn) {
|
|
1896
|
+
/**
|
|
1897
|
+
* Unreachable while a seed exists — both come from the same registration — but stated as a
|
|
1898
|
+
* refusal rather than a non-null assertion. The one thing that must never happen here is
|
|
1899
|
+
* signing a self link chosen because it was the nearest value to hand.
|
|
1900
|
+
*/
|
|
1901
|
+
this.#logger.error("session.relay.submit.unchainable", {
|
|
1902
|
+
relayPeerId: this.#relayPeerId,
|
|
1903
|
+
session: sessionIdHex,
|
|
1904
|
+
impact: "this session has an acknowledgement but no recorded starting point, so this agent's " +
|
|
1905
|
+
"own first message has nothing to link to. The message was NOT sent. Restart the " +
|
|
1906
|
+
"session so it is registered with its genesis.",
|
|
1907
|
+
});
|
|
1908
|
+
return { ok: false, reason: "session_unchainable" };
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* ⚠️ **A CARRIED LEAF IS SENT VERBATIM AND SIGNED BY NOBODY HERE — 034-CARRYLEAF.**
|
|
1913
|
+
*
|
|
1914
|
+
* When this agent is witnessing something it RECEIVED, the claim already exists: its author
|
|
1915
|
+
* built it, signed it, and put it on the content frame. Re-encoding it would change the signed
|
|
1916
|
+
* bytes and the relay would refuse a leaf that is perfectly valid. Signing it ourselves would be
|
|
1917
|
+
* worse — it would turn their statement into ours, which is the one thing that must never happen
|
|
1918
|
+
* to a record whose whole value is that each party's words are their own.
|
|
1919
|
+
*
|
|
1920
|
+
* So this branch takes the bytes as they arrived, and this agent's own chain state is
|
|
1921
|
+
* deliberately NOT consulted: both links inside those bytes are the AUTHOR's account, and they
|
|
1922
|
+
* are not ours to restate.
|
|
1923
|
+
*/
|
|
1802
1924
|
const structure1 = carried ? carried.structure1Cbor : encodeStructure1({
|
|
1803
1925
|
contentHash,
|
|
1804
1926
|
senderPubkey: this.#senderPubkey,
|
|
1805
1927
|
sessionId,
|
|
1806
|
-
lastSeenSeq: lastSeen
|
|
1928
|
+
lastSeenSeq: lastSeen.seq,
|
|
1807
1929
|
timestamp: Date.now(),
|
|
1808
|
-
|
|
1930
|
+
lastSeenHash: seed,
|
|
1931
|
+
prevOwnHash: prevOwn,
|
|
1809
1932
|
});
|
|
1810
1933
|
const signature = carried ? carried.senderSignature : await this.#keyProvider.sign(structure1);
|
|
1811
1934
|
const frame = encodeCbor({
|
|
@@ -1875,6 +1998,36 @@ export class AgentRelayClient {
|
|
|
1875
1998
|
// On timeout, reset the stream so a late ack can't settle a LATER submit (FIFO desync).
|
|
1876
1999
|
if (!result.ok && result.reason === "relay_submit_timeout")
|
|
1877
2000
|
this.#resetStream();
|
|
2001
|
+
/**
|
|
2002
|
+
* ─── ADVANCE THE SELF CHAIN, AND ONLY ON SUCCESS — `DOD-M15-SELFCHAIN-1` ──────────────────
|
|
2003
|
+
*
|
|
2004
|
+
* Recorded AFTER the relay acknowledged, never before. Advancing first and then failing to
|
|
2005
|
+
* send would leave the chain pointing at a message that never existed, and every later message
|
|
2006
|
+
* would be refused by the counterparty with a reason that names tampering — an outage
|
|
2007
|
+
* reported as an attack.
|
|
2008
|
+
*
|
|
2009
|
+
* A retry therefore re-reads the same predecessor, which is exactly right: a retransmission is
|
|
2010
|
+
* the same message, not the next one.
|
|
2011
|
+
*
|
|
2012
|
+
* NOT for a carried leaf. Those bytes are the AUTHOR's and this agent did not write them, so
|
|
2013
|
+
* they are no part of this agent's own chain.
|
|
2014
|
+
*/
|
|
2015
|
+
if (result.ok && !carried) {
|
|
2016
|
+
// The map is the chain — always advanced. The store is its durability, advanced when wired.
|
|
2017
|
+
this.#ownChain.set(sessionIdHex, contentHash);
|
|
2018
|
+
try {
|
|
2019
|
+
this.#ownChainStore?.record(this.senderPubkeyHex, sessionIdHex, contentHash, Date.now());
|
|
2020
|
+
}
|
|
2021
|
+
catch (err) {
|
|
2022
|
+
this.#logger.error("session.selfchain.record.failed", {
|
|
2023
|
+
session: sessionIdHex,
|
|
2024
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2025
|
+
impact: "this message was witnessed, but this agent could not record it as the link for its " +
|
|
2026
|
+
"next message — so the next one will chain to the wrong predecessor and the " +
|
|
2027
|
+
"counterparty will refuse it. Restart the session to re-anchor the chain.",
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
1878
2031
|
return result;
|
|
1879
2032
|
}
|
|
1880
2033
|
finally {
|
|
@@ -1928,6 +2081,34 @@ export class AgentRelayClient {
|
|
|
1928
2081
|
lastSeenAck(sessionIdHex) {
|
|
1929
2082
|
return this.#lastSeen.get(sessionIdHex);
|
|
1930
2083
|
}
|
|
2084
|
+
/**
|
|
2085
|
+
* This agent's OWN last message on a session — what its next message's self link must name.
|
|
2086
|
+
*
|
|
2087
|
+
* ⚠️ EXPOSED BECAUSE THERE MUST BE ONE CHAIN, NOT TWO. The unwitnessed send path lives in
|
|
2088
|
+
* `session-node-manager` and was reading only the durable store, while the witnessed path reads
|
|
2089
|
+
* this in-memory map first. A session that mixed the two — which is every session where the relay
|
|
2090
|
+
* comes and goes — was walking two different chains, and the one that lagged produced a link the
|
|
2091
|
+
* counterparty refuses.
|
|
2092
|
+
*
|
|
2093
|
+
* `undefined` means this agent has not spoken on this session yet, which is the session GENESIS
|
|
2094
|
+
* and not an absence. The caller supplies it; this class does not guess.
|
|
2095
|
+
*/
|
|
2096
|
+
lastOwnHash(sessionIdHex) {
|
|
2097
|
+
return this.#ownChain.get(sessionIdHex)
|
|
2098
|
+
?? this.#ownChainStore?.lastOwnHash(this.senderPubkeyHex, sessionIdHex)
|
|
2099
|
+
?? undefined;
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* Record what this agent just sent on a path this client did not carry — the UNWITNESSED send.
|
|
2103
|
+
*
|
|
2104
|
+
* The two paths share one chain (see `lastOwnHash`), so a direct send has to advance it here too.
|
|
2105
|
+
* Without this, a conversation that ran while the relay was down advanced nothing, and the first
|
|
2106
|
+
* witnessed message after it linked to something long superseded.
|
|
2107
|
+
*/
|
|
2108
|
+
noteOwnLeaf(sessionIdHex, contentHash) {
|
|
2109
|
+
this.#ownChain.set(sessionIdHex, contentHash);
|
|
2110
|
+
this.#ownChainStore?.record(this.senderPubkeyHex, sessionIdHex, contentHash, Date.now());
|
|
2111
|
+
}
|
|
1931
2112
|
close() {
|
|
1932
2113
|
this.#closed = true;
|
|
1933
2114
|
this.#settlePending({ ok: false, reason: "relay_client_closed" });
|