@cello-protocol/daemon 0.0.194 → 0.0.195
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/assignment-verify.d.ts +39 -11
- package/dist/assignment-verify.d.ts.map +1 -1
- package/dist/assignment-verify.js +133 -17
- package/dist/assignment-verify.js.map +1 -1
- package/dist/authorship-verification.d.ts +70 -0
- package/dist/authorship-verification.d.ts.map +1 -0
- package/dist/authorship-verification.js +441 -0
- package/dist/authorship-verification.js.map +1 -0
- package/dist/db-identity-store.d.ts +1 -0
- package/dist/db-identity-store.d.ts.map +1 -1
- package/dist/db-identity-store.js +19 -1
- package/dist/db-identity-store.js.map +1 -1
- package/dist/held-content.d.ts +141 -0
- package/dist/held-content.d.ts.map +1 -0
- package/dist/held-content.js +388 -0
- package/dist/held-content.js.map +1 -0
- package/dist/inbound-refusals.d.ts +283 -0
- package/dist/inbound-refusals.d.ts.map +1 -0
- package/dist/inbound-refusals.js +919 -0
- package/dist/inbound-refusals.js.map +1 -0
- package/dist/inbound-sessions.d.ts.map +1 -1
- package/dist/inbound-sessions.js +65 -27
- package/dist/inbound-sessions.js.map +1 -1
- package/dist/initiate-session-handler.d.ts.map +1 -1
- package/dist/initiate-session-handler.js +63 -0
- package/dist/initiate-session-handler.js.map +1 -1
- package/dist/outbound-sessions.d.ts.map +1 -1
- package/dist/outbound-sessions.js +108 -8
- package/dist/outbound-sessions.js.map +1 -1
- package/dist/park-recovery.d.ts +205 -0
- package/dist/park-recovery.d.ts.map +1 -0
- package/dist/park-recovery.js +600 -0
- package/dist/park-recovery.js.map +1 -0
- package/dist/refusal-notices.d.ts +196 -0
- package/dist/refusal-notices.d.ts.map +1 -0
- package/dist/refusal-notices.js +496 -0
- package/dist/refusal-notices.js.map +1 -0
- package/dist/refusal-reasons.d.ts +11 -0
- package/dist/refusal-reasons.d.ts.map +1 -1
- package/dist/refusal-reasons.js +18 -0
- package/dist/refusal-reasons.js.map +1 -1
- package/dist/registration-manager.d.ts.map +1 -1
- package/dist/registration-manager.js +126 -5
- package/dist/registration-manager.js.map +1 -1
- package/dist/registration-persistence.d.ts +18 -0
- package/dist/registration-persistence.d.ts.map +1 -1
- package/dist/registration-persistence.js +6 -0
- package/dist/registration-persistence.js.map +1 -1
- package/dist/session-assignment-parser.d.ts.map +1 -1
- package/dist/session-assignment-parser.js +23 -1
- package/dist/session-assignment-parser.js.map +1 -1
- package/dist/session-ceremony.d.ts +12 -7
- package/dist/session-ceremony.d.ts.map +1 -1
- package/dist/session-ceremony.js +12 -7
- package/dist/session-ceremony.js.map +1 -1
- package/dist/session-ephemerals.d.ts +271 -0
- package/dist/session-ephemerals.d.ts.map +1 -0
- package/dist/session-ephemerals.js +546 -0
- package/dist/session-ephemerals.js.map +1 -0
- package/dist/session-leaf-records.d.ts +132 -0
- package/dist/session-leaf-records.d.ts.map +1 -0
- package/dist/session-leaf-records.js +320 -0
- package/dist/session-leaf-records.js.map +1 -0
- package/dist/session-liveness.d.ts +135 -0
- package/dist/session-liveness.d.ts.map +1 -0
- package/dist/session-liveness.js +347 -0
- package/dist/session-liveness.js.map +1 -0
- package/dist/session-node-manager.d.ts +300 -1670
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +871 -9951
- package/dist/session-node-manager.js.map +1 -1
- package/dist/session-node-types.d.ts +933 -0
- package/dist/session-node-types.d.ts.map +1 -0
- package/dist/session-node-types.js +576 -0
- package/dist/session-node-types.js.map +1 -0
- package/dist/session-queries.d.ts +476 -0
- package/dist/session-queries.d.ts.map +1 -0
- package/dist/session-queries.js +1006 -0
- package/dist/session-queries.js.map +1 -0
- package/dist/session-records.d.ts +320 -0
- package/dist/session-records.d.ts.map +1 -0
- package/dist/session-records.js +792 -0
- package/dist/session-records.js.map +1 -0
- package/dist/session-salts.d.ts +390 -0
- package/dist/session-salts.d.ts.map +1 -0
- package/dist/session-salts.js +1457 -0
- package/dist/session-salts.js.map +1 -0
- package/dist/session-schema.d.ts +30 -0
- package/dist/session-schema.d.ts.map +1 -0
- package/dist/session-schema.js +786 -0
- package/dist/session-schema.js.map +1 -0
- package/dist/standing-receivers.d.ts +280 -0
- package/dist/standing-receivers.d.ts.map +1 -0
- package/dist/standing-receivers.js +1100 -0
- package/dist/standing-receivers.js.map +1 -0
- package/dist/transport-selector.d.ts +13 -0
- package/dist/transport-selector.d.ts.map +1 -1
- package/dist/transport-selector.js.map +1 -1
- package/dist/witness-alerts.d.ts +40 -0
- package/dist/witness-alerts.d.ts.map +1 -0
- package/dist/witness-alerts.js +102 -0
- package/dist/witness-alerts.js.map +1 -0
- package/package.json +5 -5
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CELLO Daemon — THE TWO DURABLE FACTS ABOUT A SESSION'S LEAVES
|
|
3
|
+
*
|
|
4
|
+
* Split out of `session-node-manager.ts` by 037-SESSIONCORE. Both are DURABLE PER-SESSION LEAF
|
|
5
|
+
* FACTS that outlive the process, which is why they are here together rather than with the
|
|
6
|
+
* in-memory tree:
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ NOT "both are read at seal time" — that was the first wording and it is false of both halves.
|
|
9
|
+
* The genesis is read at session OPEN and at SEND (`#signOwnContentClaim`), never at seal; the
|
|
10
|
+
* certified leaf set is written at seal and read at PROOF time. The distinction matters because
|
|
11
|
+
* this header is the rule the next extraction obeys when deciding what belongs here.
|
|
12
|
+
*
|
|
13
|
+
* - the GENESIS prev-root — where this two-party chain starts. It is a defined 32 bytes derived
|
|
14
|
+
* from both keys, the session id and the session timestamp, NOT 32 zeros: a constant identical
|
|
15
|
+
* across every session is one an attacker could present for any session, which would make the
|
|
16
|
+
* one position most exposed to a forged acknowledgement the only one nobody could check.
|
|
17
|
+
* - the CERTIFIED LEAF SET — which leaves the directory actually attested, and the state of that
|
|
18
|
+
* answer when it could not be obtained.
|
|
19
|
+
*
|
|
20
|
+
* Moved verbatim, comments included.
|
|
21
|
+
*/
|
|
22
|
+
import type { Logger } from "./types.js";
|
|
23
|
+
import type { SessionQueries } from "./session-queries.js";
|
|
24
|
+
import type { ActiveSessionEntry } from "./session-node-types.js";
|
|
25
|
+
import type { DaemonDatabase } from "./sqlcipher-db.js";
|
|
26
|
+
import type { RelayAssignmentCarry } from "./session-relay-client.js";
|
|
27
|
+
import type { SealFrontierLeaf } from "./seal-frontier-verify.js";
|
|
28
|
+
/** What the leaf records need from the manager — six things, none of them session state. */
|
|
29
|
+
export interface SessionLeafRecordContext {
|
|
30
|
+
readonly logger: Logger;
|
|
31
|
+
readonly queries: SessionQueries;
|
|
32
|
+
/** A function: the manager opens its database after construction. Re-exposed below as `#db`. */
|
|
33
|
+
db(): DaemonDatabase | null;
|
|
34
|
+
sessionKey(agentName: string, sessionId: string): string;
|
|
35
|
+
requireAgentId(agentName: string): string;
|
|
36
|
+
activeEntry(key: string): ActiveSessionEntry | undefined;
|
|
37
|
+
}
|
|
38
|
+
export declare class SessionLeafRecords {
|
|
39
|
+
#private;
|
|
40
|
+
constructor(ctx: SessionLeafRecordContext);
|
|
41
|
+
/**
|
|
42
|
+
* The session's genesis prev_root — what its FIRST message acknowledges, before anything has been
|
|
43
|
+
* received (033-ACKEMIT).
|
|
44
|
+
*
|
|
45
|
+
* DERIVED FIRST, STORED SECOND — and this docblock used to say "derived, never stored", which
|
|
46
|
+
* stopped being true inside this same unit. Rewritten rather than deleted: a reader who believed
|
|
47
|
+
* the first sentence would delete the column read below as redundant, and take the restart case
|
|
48
|
+
* with it.
|
|
49
|
+
*
|
|
50
|
+
* The live assignment is authoritative, because it is the thing the value is defined by. The
|
|
51
|
+
* stored column covers the one case the derivation cannot: a session restored after a restart
|
|
52
|
+
* re-registers with no assignment, and the session TIMESTAMP the genesis needs lives nowhere
|
|
53
|
+
* else.
|
|
54
|
+
*
|
|
55
|
+
* `undefined` when neither is available. The callers do not paper over that — they say, in the
|
|
56
|
+
* log and in the claim itself, that this session acknowledges nothing yet.
|
|
57
|
+
*/
|
|
58
|
+
sessionGenesisPrevRoot(agentName: string, sessionId: string): Uint8Array | undefined;
|
|
59
|
+
persistGenesisPrevRoot(agentName: string, sessionId: string, assignment: RelayAssignmentCarry): void;
|
|
60
|
+
/**
|
|
61
|
+
* Persist the session's genesis prev_root, once, at the moment the assignment arrives.
|
|
62
|
+
*
|
|
63
|
+
* `WHERE genesis_prev_root IS NULL` rather than a plain update: the value cannot legitimately
|
|
64
|
+
* change for the life of a session, so the second writer is either redundant or wrong, and the
|
|
65
|
+
* first write is the one derived closest to the assignment that opened the session.
|
|
66
|
+
*/
|
|
67
|
+
/**
|
|
68
|
+
* Record the session's starting point from the ASSIGNMENT — `DOD-M15-SELFCHAIN-1`.
|
|
69
|
+
*
|
|
70
|
+
* ⚠️ **CALL THIS BEFORE `createSessionNode` / `acceptSession`, NOT AFTER.** Registering the
|
|
71
|
+
* session is what seeds the relay client's acknowledgement state, so the value has to exist by
|
|
72
|
+
* then; recorded afterwards, the first message of the session has nothing to chain to and is
|
|
73
|
+
* refused. Both the initiator and the responder derive this from the same FROST-signed assignment
|
|
74
|
+
* before they build anything.
|
|
75
|
+
*
|
|
76
|
+
* ⚠️ THE ANCHOR BELONGS TO THE SESSION, NOT TO THE RELAY, and treating it as the relay's was a
|
|
77
|
+
* real gap. It was derived only when a relay assignment CARRY was present — and that carry is
|
|
78
|
+
* built only for a relay-mode assignment that also carries a per-node relay signature. So a
|
|
79
|
+
* direct-mode session, brokered and FROST-signed exactly like any other, recorded no starting
|
|
80
|
+
* point at all, and every message on it had nothing to chain to.
|
|
81
|
+
*
|
|
82
|
+
* Both transport modes get their assignment from the same ceremony and derive the same value from
|
|
83
|
+
* it. The relay is how the conversation travels; it is not what makes the conversation provable.
|
|
84
|
+
*/
|
|
85
|
+
recordSessionGenesis(agentName: string, sessionId: string, participantA: Uint8Array, participantB: Uint8Array, sessionTimestamp: number): void;
|
|
86
|
+
/**
|
|
87
|
+
* Test seam: put the session's genesis prev_root where a completed session open leaves it —
|
|
88
|
+
* 033-ACKEMIT.
|
|
89
|
+
*
|
|
90
|
+
* ⚠️ THE STATE IS THE PRODUCTION ONE; ONLY HOW IT GOT THERE IS SHORT-CIRCUITED, exactly as
|
|
91
|
+
* `setSessionContentKeyForTest` short-circuits the key exchange next door.
|
|
92
|
+
*
|
|
93
|
+
* In production this value is derived from the directory-signed relay assignment and written to
|
|
94
|
+
* the session row the moment the session learns it, so every real session has one. A fixture that
|
|
95
|
+
* builds a session node directly never sees an assignment — so without this seam every content
|
|
96
|
+
* test built on the fixture would be exercising the "no starting point" REFUSAL path instead of
|
|
97
|
+
* the thing it was written for, and would report that as a pass or a mysterious failure depending
|
|
98
|
+
* on which side of the send it sat on.
|
|
99
|
+
*/
|
|
100
|
+
setSessionGenesisForTest(agentName: string, sessionId: string, genesis: Uint8Array): void;
|
|
101
|
+
/**
|
|
102
|
+
* DOD-M15-INCLUSION-1: keep the leaf set the certificate is signed over, so one message can later
|
|
103
|
+
* be proved to sit under it.
|
|
104
|
+
*
|
|
105
|
+
* REFUSES unless the hashes reproduce `sealedRootHex` — `certifiedLeafSetFrom` does that check and
|
|
106
|
+
* this method never bypasses it. That is what separates "the leaves the directory sent" from "the
|
|
107
|
+
* leaves the consortium signed", and only the second is worth storing: a proof built on the first
|
|
108
|
+
* would inherit whatever the directory chose to say.
|
|
109
|
+
*
|
|
110
|
+
* Idempotent (INSERT OR REPLACE keyed on leaf_index) so a re-delivered seal frame, or a unilateral
|
|
111
|
+
* seal later upgraded to bilateral, rewrites the same rows instead of failing or doubling them.
|
|
112
|
+
*
|
|
113
|
+
* @returns whether the set was accepted and stored.
|
|
114
|
+
*/
|
|
115
|
+
recordCertifiedLeafSet(agentName: string, sessionId: string, signedLeaves: readonly SealFrontierLeaf[], sealedRootHex: string, correlationId?: string): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Record WHY this session does or does not have a certified leaf set.
|
|
118
|
+
*
|
|
119
|
+
* Public for the one case the manager cannot see: a seal frame that carried no signed leaves at
|
|
120
|
+
* all never reaches `recordCertifiedLeafSet`, and that absence is a permanent fact about the
|
|
121
|
+
* session for the party that observed it.
|
|
122
|
+
*/
|
|
123
|
+
noteCertifiedLeafSetUnavailable(agentName: string, sessionId: string, state: "not_carried_absent_party" | "not_carried_present_party", detail: string): void;
|
|
124
|
+
getCertifiedLeafSet(agentName: string, sessionId: string): string[] | null;
|
|
125
|
+
getCertifiedLeafSetState(agentName: string, sessionId: string): {
|
|
126
|
+
state: string;
|
|
127
|
+
detail: string | null;
|
|
128
|
+
} | null;
|
|
129
|
+
/** The in-memory genesis prev-root for a session, when one has been recorded. */
|
|
130
|
+
genesisFor(agentName: string, sessionId: string): Uint8Array | undefined;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=session-leaf-records.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-leaf-records.d.ts","sourceRoot":"","sources":["../src/session-leaf-records.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAKlE,4FAA4F;AAC5F,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,gGAAgG;IAChG,EAAE,IAAI,cAAc,GAAG,IAAI,CAAC;IAC5B,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IACzD,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1C,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAAC;CAC1D;AAED,qBAAa,kBAAkB;;gBAGjB,GAAG,EAAE,wBAAwB;IA6BzC;;;;;;;;;;;;;;;;OAgBG;IACH,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAsCpF,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,oBAAoB,GAAG,IAAI;IAmDpG;;;;;;OAMG;IACH;;;;;;;;;;;;;;;;;OAiBG;IACH,oBAAoB,CAClB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,UAAU,EACxB,YAAY,EAAE,UAAU,EACxB,gBAAgB,EAAE,MAAM,GACvB,IAAI;IAKP;;;;;;;;;;;;;OAaG;IACH,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI;IA0BzF;;;;;;;;;;;;;OAaG;IACH,sBAAsB,CACpB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,SAAS,gBAAgB,EAAE,EACzC,aAAa,EAAE,MAAM,EACrB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO;IAqFV;;;;;;OAMG;IACH,+BAA+B,CAC7B,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,0BAA0B,GAAG,2BAA2B,EAC/D,MAAM,EAAE,MAAM,GACb,IAAI;IAGP,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI;IAC1E,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI;IAE/G,iFAAiF;IACjF,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;CAGzE"}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { computeGenesisPrevRoot } from "@cello-protocol/protocol-types";
|
|
2
|
+
import { extractErrorMessage } from "./error-message.js";
|
|
3
|
+
import { certifiedLeafSetFrom } from "./sealed-leaf-set.js";
|
|
4
|
+
export class SessionLeafRecords {
|
|
5
|
+
#ctx;
|
|
6
|
+
constructor(ctx) {
|
|
7
|
+
this.#ctx = ctx;
|
|
8
|
+
}
|
|
9
|
+
/** A getter so the moved queries still read `this.#db` and narrow exactly as they did. */
|
|
10
|
+
get #db() {
|
|
11
|
+
return this.#ctx.db();
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The starting point of each live session's chain, in memory.
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ NOT A CACHE OF THE DATABASE — it is the only copy that exists at the moment the value is
|
|
17
|
+
* first needed. It is recorded before the session node is built, and the session ROW does not
|
|
18
|
+
* exist until that build inserts it (`#insertSessionRow` writes the column from here). The row is
|
|
19
|
+
* what survives a restart; this is what the session open itself reads.
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ **DELIBERATELY NOT EVICTED, and this module ships no `evictSession` because of it.** Every
|
|
22
|
+
* other collaborator has one, so the absence reads as an oversight — it is not. Dropping this on
|
|
23
|
+
* teardown would lose the in-memory genesis for any session whose `UPDATE sessions SET
|
|
24
|
+
* genesis_prev_root` FAILED, which is exactly the case `persistGenesisPrevRoot`'s error log says
|
|
25
|
+
* survives until a restart. Adding an `evictSession` here for symmetry would delete the only copy.
|
|
26
|
+
*
|
|
27
|
+
* ⚠️ IT THEREFORE GROWS FOR THE LIFE OF THE PROCESS — pre-existing, and stated rather than fixed.
|
|
28
|
+
* "each live session's chain" above is what it is FOR, not what it holds: entries are set and read
|
|
29
|
+
* and never removed, so it is roughly 200 bytes per session ever opened. Bounding it is not
|
|
30
|
+
* obviously safe for the reason directly above, so it wants a decision rather than a tidy-up.
|
|
31
|
+
*/
|
|
32
|
+
#sessionGenesis = new Map();
|
|
33
|
+
/**
|
|
34
|
+
* The session's genesis prev_root — what its FIRST message acknowledges, before anything has been
|
|
35
|
+
* received (033-ACKEMIT).
|
|
36
|
+
*
|
|
37
|
+
* DERIVED FIRST, STORED SECOND — and this docblock used to say "derived, never stored", which
|
|
38
|
+
* stopped being true inside this same unit. Rewritten rather than deleted: a reader who believed
|
|
39
|
+
* the first sentence would delete the column read below as redundant, and take the restart case
|
|
40
|
+
* with it.
|
|
41
|
+
*
|
|
42
|
+
* The live assignment is authoritative, because it is the thing the value is defined by. The
|
|
43
|
+
* stored column covers the one case the derivation cannot: a session restored after a restart
|
|
44
|
+
* re-registers with no assignment, and the session TIMESTAMP the genesis needs lives nowhere
|
|
45
|
+
* else.
|
|
46
|
+
*
|
|
47
|
+
* `undefined` when neither is available. The callers do not paper over that — they say, in the
|
|
48
|
+
* log and in the claim itself, that this session acknowledges nothing yet.
|
|
49
|
+
*/
|
|
50
|
+
sessionGenesisPrevRoot(agentName, sessionId) {
|
|
51
|
+
const assignment = this.#ctx.activeEntry(this.#ctx.sessionKey(agentName, sessionId))?.relayAssignment;
|
|
52
|
+
if (assignment) {
|
|
53
|
+
return computeGenesisPrevRoot(assignment.participantA, assignment.participantB, Uint8Array.from(Buffer.from(sessionId, "hex")), assignment.sessionTimestamp);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* ⚠️ THE IN-MEMORY RECORD, READ BEFORE THE DATABASE — and this ordering is the fix, not a
|
|
57
|
+
* cache.
|
|
58
|
+
*
|
|
59
|
+
* `recordSessionGenesis` is called BEFORE the session node exists, because registering the
|
|
60
|
+
* session is what seeds the relay client's acknowledgement state and the seed has to be
|
|
61
|
+
* available by then. At that moment there is no session ROW to write to — `createSessionNode`
|
|
62
|
+
* inserts it — so a database-only record would still be empty at the one moment it is read.
|
|
63
|
+
* The row is written from this map when the insert happens, and read back after a restart.
|
|
64
|
+
*/
|
|
65
|
+
const recorded = this.#sessionGenesis.get(this.#ctx.sessionKey(agentName, sessionId));
|
|
66
|
+
if (recorded)
|
|
67
|
+
return recorded;
|
|
68
|
+
/**
|
|
69
|
+
* THE RESTART CASE. A session restored from the database re-registers with no assignment, so
|
|
70
|
+
* the derivation above has nothing to work from and the stored copy is the only answer. Read
|
|
71
|
+
* second, never first: the live assignment is authoritative, and a stored value that ever
|
|
72
|
+
* disagreed with it would be the more dangerous of the two to prefer.
|
|
73
|
+
*/
|
|
74
|
+
const row = this.#db
|
|
75
|
+
?.prepare("SELECT genesis_prev_root FROM sessions WHERE agent_id = ? AND session_id = ?")
|
|
76
|
+
.get(this.#ctx.requireAgentId(agentName), sessionId);
|
|
77
|
+
const stored = row?.genesis_prev_root;
|
|
78
|
+
const bytes = stored instanceof Uint8Array ? stored : Buffer.isBuffer(stored) ? new Uint8Array(stored) : null;
|
|
79
|
+
// A stored value of the wrong width is not a genesis. Refusing it here sends the caller down its
|
|
80
|
+
// own named refusal, which is a better outcome than signing an acknowledgement of 17 bytes.
|
|
81
|
+
if (bytes && bytes.length === 32)
|
|
82
|
+
return bytes;
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
persistGenesisPrevRoot(agentName, sessionId, assignment) {
|
|
86
|
+
let genesis;
|
|
87
|
+
try {
|
|
88
|
+
genesis = computeGenesisPrevRoot(assignment.participantA, assignment.participantB, Uint8Array.from(Buffer.from(sessionId, "hex")), assignment.sessionTimestamp);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
/**
|
|
92
|
+
* The DERIVATION failed, which is a different failure from the write below and must not be
|
|
93
|
+
* reported as one. It means the assignment's own fields are not what this function needs, and
|
|
94
|
+
* no amount of database health would help.
|
|
95
|
+
*/
|
|
96
|
+
this.#ctx.logger.error("session.genesis.derive.failed", {
|
|
97
|
+
agentName, sessionId,
|
|
98
|
+
error: err instanceof Error ? err.message : String(err),
|
|
99
|
+
impact: "this session's starting point could not be computed from its assignment, so nothing " +
|
|
100
|
+
"sent on it can be chained and every send will be refused by name. The session open " +
|
|
101
|
+
"continues; the conversation cannot.",
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// The in-memory record FIRST, and unconditionally: it is what the session open reads, and it
|
|
106
|
+
// must not depend on a database write that may not have anywhere to land yet.
|
|
107
|
+
this.#sessionGenesis.set(this.#ctx.sessionKey(agentName, sessionId), genesis);
|
|
108
|
+
if (!this.#db)
|
|
109
|
+
return;
|
|
110
|
+
try {
|
|
111
|
+
this.#db
|
|
112
|
+
.prepare("UPDATE sessions SET genesis_prev_root = ? WHERE agent_id = ? AND session_id = ? AND genesis_prev_root IS NULL")
|
|
113
|
+
.run(Buffer.from(genesis), this.#ctx.requireAgentId(agentName), sessionId);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
/**
|
|
117
|
+
* LOUD, AND IT DOES NOT BLOCK. Losing this row costs the session its acknowledgements after a
|
|
118
|
+
* restart — sends are then refused by name until the counterparty speaks — and that is a far
|
|
119
|
+
* smaller harm than failing the session open that is in progress. Reported at ERROR because
|
|
120
|
+
* the failure is invisible until a restart that may be days away.
|
|
121
|
+
*/
|
|
122
|
+
this.#ctx.logger.error("session.genesis.persist.failed", {
|
|
123
|
+
agentName, sessionId,
|
|
124
|
+
error: err instanceof Error ? err.message : String(err),
|
|
125
|
+
impact: "this session's starting point was not written to the database. Everything works until " +
|
|
126
|
+
"this daemon restarts; after that, a send on this session is refused until the " +
|
|
127
|
+
"counterparty has sent something, because the daemon cannot say what its first message " +
|
|
128
|
+
"acknowledges.",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Persist the session's genesis prev_root, once, at the moment the assignment arrives.
|
|
134
|
+
*
|
|
135
|
+
* `WHERE genesis_prev_root IS NULL` rather than a plain update: the value cannot legitimately
|
|
136
|
+
* change for the life of a session, so the second writer is either redundant or wrong, and the
|
|
137
|
+
* first write is the one derived closest to the assignment that opened the session.
|
|
138
|
+
*/
|
|
139
|
+
/**
|
|
140
|
+
* Record the session's starting point from the ASSIGNMENT — `DOD-M15-SELFCHAIN-1`.
|
|
141
|
+
*
|
|
142
|
+
* ⚠️ **CALL THIS BEFORE `createSessionNode` / `acceptSession`, NOT AFTER.** Registering the
|
|
143
|
+
* session is what seeds the relay client's acknowledgement state, so the value has to exist by
|
|
144
|
+
* then; recorded afterwards, the first message of the session has nothing to chain to and is
|
|
145
|
+
* refused. Both the initiator and the responder derive this from the same FROST-signed assignment
|
|
146
|
+
* before they build anything.
|
|
147
|
+
*
|
|
148
|
+
* ⚠️ THE ANCHOR BELONGS TO THE SESSION, NOT TO THE RELAY, and treating it as the relay's was a
|
|
149
|
+
* real gap. It was derived only when a relay assignment CARRY was present — and that carry is
|
|
150
|
+
* built only for a relay-mode assignment that also carries a per-node relay signature. So a
|
|
151
|
+
* direct-mode session, brokered and FROST-signed exactly like any other, recorded no starting
|
|
152
|
+
* point at all, and every message on it had nothing to chain to.
|
|
153
|
+
*
|
|
154
|
+
* Both transport modes get their assignment from the same ceremony and derive the same value from
|
|
155
|
+
* it. The relay is how the conversation travels; it is not what makes the conversation provable.
|
|
156
|
+
*/
|
|
157
|
+
recordSessionGenesis(agentName, sessionId, participantA, participantB, sessionTimestamp) {
|
|
158
|
+
this.persistGenesisPrevRoot(agentName, sessionId, {
|
|
159
|
+
participantA, participantB, sessionTimestamp,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Test seam: put the session's genesis prev_root where a completed session open leaves it —
|
|
164
|
+
* 033-ACKEMIT.
|
|
165
|
+
*
|
|
166
|
+
* ⚠️ THE STATE IS THE PRODUCTION ONE; ONLY HOW IT GOT THERE IS SHORT-CIRCUITED, exactly as
|
|
167
|
+
* `setSessionContentKeyForTest` short-circuits the key exchange next door.
|
|
168
|
+
*
|
|
169
|
+
* In production this value is derived from the directory-signed relay assignment and written to
|
|
170
|
+
* the session row the moment the session learns it, so every real session has one. A fixture that
|
|
171
|
+
* builds a session node directly never sees an assignment — so without this seam every content
|
|
172
|
+
* test built on the fixture would be exercising the "no starting point" REFUSAL path instead of
|
|
173
|
+
* the thing it was written for, and would report that as a pass or a mysterious failure depending
|
|
174
|
+
* on which side of the send it sat on.
|
|
175
|
+
*/
|
|
176
|
+
setSessionGenesisForTest(agentName, sessionId, genesis) {
|
|
177
|
+
/**
|
|
178
|
+
* ⚠️ WRITES THE SAME MAP PRODUCTION WRITES, deliberately — `DOD-M15-SELFCHAIN-1`.
|
|
179
|
+
*
|
|
180
|
+
* This seam exists because a fixture builds a session below the paths that derive a starting
|
|
181
|
+
* point from a directory assignment; it does NOT exist to install a second, quieter source of
|
|
182
|
+
* the value. Sharing the map means a fixture and a real session read through exactly the same
|
|
183
|
+
* lookup, so a change to that lookup cannot pass the tests while breaking production.
|
|
184
|
+
*
|
|
185
|
+
* ⚠️ CALL IT BEFORE `createSessionNode`, the same rule production follows: registering the
|
|
186
|
+
* session is what seeds the relay client's acknowledgement state, and a value recorded after
|
|
187
|
+
* that leaves the first send with nothing to chain to.
|
|
188
|
+
*/
|
|
189
|
+
this.#sessionGenesis.set(this.#ctx.sessionKey(agentName, sessionId), Uint8Array.from(genesis));
|
|
190
|
+
/**
|
|
191
|
+
* The durable half is BEST EFFORT. Some fixtures run against a database whose schema was never
|
|
192
|
+
* created, and a seam that threw there would turn "this fixture has no sessions table" into a
|
|
193
|
+
* failure of whatever it was actually testing. Harmless when the row does not exist yet either:
|
|
194
|
+
* `#insertSessionRow` writes the column from the map above.
|
|
195
|
+
*/
|
|
196
|
+
try {
|
|
197
|
+
this.#db
|
|
198
|
+
?.prepare("UPDATE sessions SET genesis_prev_root = ? WHERE agent_id = ? AND session_id = ?")
|
|
199
|
+
.run(Buffer.from(genesis), this.#ctx.requireAgentId(agentName), sessionId);
|
|
200
|
+
}
|
|
201
|
+
catch { /* see above — the in-memory half is the load-bearing one */ }
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* DOD-M15-INCLUSION-1: keep the leaf set the certificate is signed over, so one message can later
|
|
205
|
+
* be proved to sit under it.
|
|
206
|
+
*
|
|
207
|
+
* REFUSES unless the hashes reproduce `sealedRootHex` — `certifiedLeafSetFrom` does that check and
|
|
208
|
+
* this method never bypasses it. That is what separates "the leaves the directory sent" from "the
|
|
209
|
+
* leaves the consortium signed", and only the second is worth storing: a proof built on the first
|
|
210
|
+
* would inherit whatever the directory chose to say.
|
|
211
|
+
*
|
|
212
|
+
* Idempotent (INSERT OR REPLACE keyed on leaf_index) so a re-delivered seal frame, or a unilateral
|
|
213
|
+
* seal later upgraded to bilateral, rewrites the same rows instead of failing or doubling them.
|
|
214
|
+
*
|
|
215
|
+
* @returns whether the set was accepted and stored.
|
|
216
|
+
*/
|
|
217
|
+
recordCertifiedLeafSet(agentName, sessionId, signedLeaves, sealedRootHex, correlationId) {
|
|
218
|
+
if (!this.#db)
|
|
219
|
+
return false;
|
|
220
|
+
const resolved = certifiedLeafSetFrom(signedLeaves, sealedRootHex);
|
|
221
|
+
if (!resolved.ok) {
|
|
222
|
+
// The CAUSE is written where the proof surface can read it — fallback-finder finding 1. Without
|
|
223
|
+
// this row, `sealed_leaves_root_disagrees` (a directory contradicting its own FROST signature)
|
|
224
|
+
// and "this side was simply absent" are the same `null` downstream, and the operator is told
|
|
225
|
+
// the second.
|
|
226
|
+
this.#ctx.queries.noteCertifiedLeafState(agentName, sessionId, resolved.reason, resolved.detail);
|
|
227
|
+
// LOUD, and it names which of the two it is. `sealed_leaves_root_disagrees` in particular is
|
|
228
|
+
// the directory shipping a leaf set that is not the one it signed — the receipt still stands
|
|
229
|
+
// (its own signature is checked elsewhere), but nothing in this session can be proved at
|
|
230
|
+
// message granularity until a set that reproduces the root arrives.
|
|
231
|
+
this.#ctx.logger.error("seal.certified_leaves.refused", {
|
|
232
|
+
agentName,
|
|
233
|
+
sessionId,
|
|
234
|
+
reason: resolved.reason,
|
|
235
|
+
detail: resolved.detail,
|
|
236
|
+
correlationId,
|
|
237
|
+
impact: "the leaf set shipped with this seal is not the one the certificate is signed over, so no " +
|
|
238
|
+
"inclusion proof can be issued for this session; the sealed receipt itself is unaffected",
|
|
239
|
+
guidance: "cello_get_inclusion_proof will refuse this session by name (certified_leaves_unavailable). " +
|
|
240
|
+
"Nothing local repairs it — the set has to arrive with a seal frame that reproduces the " +
|
|
241
|
+
"signed root.",
|
|
242
|
+
});
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
const now = Date.now();
|
|
246
|
+
try {
|
|
247
|
+
const agentId = this.#ctx.requireAgentId(agentName);
|
|
248
|
+
/**
|
|
249
|
+
* DELETE THEN INSERT, INSIDE A TRANSACTION — fallback-finder finding 5.
|
|
250
|
+
*
|
|
251
|
+
* `INSERT OR REPLACE` alone is idempotent only for a set of the SAME length: a shorter
|
|
252
|
+
* re-delivery overwrites 0..k-1 and leaves stale rows at k..n-1, and an un-transacted loop that
|
|
253
|
+
* throws halfway leaves a truncated set that `getCertifiedLeafSet` still returns (it tests
|
|
254
|
+
* `rows.length > 0`, not completeness). Both produce a set that no longer hashes to the
|
|
255
|
+
* certified root — caught on read, but reported to the operator as *"the local copy has
|
|
256
|
+
* changed since the seal"*, which points at tampering for a write that never finished.
|
|
257
|
+
*/
|
|
258
|
+
// `BEGIN` / `COMMIT` / `ROLLBACK` via exec — this file's and `db-identity-store.ts`'s idiom.
|
|
259
|
+
// `DaemonDatabase` has no `transaction()` helper (node:sqlite's handle does not provide one),
|
|
260
|
+
// and reaching for better-sqlite3's would compile against the adapter and fail on the other.
|
|
261
|
+
this.#db.exec("BEGIN");
|
|
262
|
+
try {
|
|
263
|
+
this.#db.prepare("DELETE FROM session_certified_leaves WHERE agent_id = ? AND session_id = ?")
|
|
264
|
+
.run(agentId, sessionId);
|
|
265
|
+
const stmt = this.#db.prepare(`INSERT INTO session_certified_leaves
|
|
266
|
+
(agent_id, session_id, leaf_index, content_hash_hex, recorded_at)
|
|
267
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
268
|
+
for (let i = 0; i < resolved.leafHashes.length; i++) {
|
|
269
|
+
stmt.run(agentId, sessionId, i, resolved.leafHashes[i], now);
|
|
270
|
+
}
|
|
271
|
+
this.#db.exec("COMMIT");
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
try {
|
|
275
|
+
this.#db.exec("ROLLBACK");
|
|
276
|
+
}
|
|
277
|
+
catch { /* the failing statement may have aborted it already */ }
|
|
278
|
+
throw err;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
this.#ctx.queries.noteCertifiedLeafState(agentName, sessionId, "persist_failed", extractErrorMessage(err));
|
|
283
|
+
this.#ctx.logger.error("seal.certified_leaves.persist.failed", {
|
|
284
|
+
agentName,
|
|
285
|
+
sessionId,
|
|
286
|
+
reason: extractErrorMessage(err),
|
|
287
|
+
correlationId,
|
|
288
|
+
impact: "this session's certified leaf set was verified but not written, so cello_get_inclusion_proof " +
|
|
289
|
+
"will refuse it by name until a later seal frame re-delivers the set",
|
|
290
|
+
});
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
this.#ctx.queries.noteCertifiedLeafState(agentName, sessionId, "stored", null);
|
|
294
|
+
this.#ctx.logger.info("seal.certified_leaves.recorded", {
|
|
295
|
+
agentName,
|
|
296
|
+
sessionId,
|
|
297
|
+
leafCount: resolved.leafHashes.length,
|
|
298
|
+
sealedRoot: sealedRootHex,
|
|
299
|
+
correlationId,
|
|
300
|
+
});
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Record WHY this session does or does not have a certified leaf set.
|
|
305
|
+
*
|
|
306
|
+
* Public for the one case the manager cannot see: a seal frame that carried no signed leaves at
|
|
307
|
+
* all never reaches `recordCertifiedLeafSet`, and that absence is a permanent fact about the
|
|
308
|
+
* session for the party that observed it.
|
|
309
|
+
*/
|
|
310
|
+
noteCertifiedLeafSetUnavailable(agentName, sessionId, state, detail) {
|
|
311
|
+
this.#ctx.queries.noteCertifiedLeafState(agentName, sessionId, state, detail);
|
|
312
|
+
}
|
|
313
|
+
getCertifiedLeafSet(agentName, sessionId) { return this.#ctx.queries.getCertifiedLeafSet(agentName, sessionId); }
|
|
314
|
+
getCertifiedLeafSetState(agentName, sessionId) { return this.#ctx.queries.getCertifiedLeafSetState(agentName, sessionId); }
|
|
315
|
+
/** The in-memory genesis prev-root for a session, when one has been recorded. */
|
|
316
|
+
genesisFor(agentName, sessionId) {
|
|
317
|
+
return this.#sessionGenesis.get(this.#ctx.sessionKey(agentName, sessionId));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
//# sourceMappingURL=session-leaf-records.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-leaf-records.js","sourceRoot":"","sources":["../src/session-leaf-records.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CELLO Daemon — IS THIS SESSION STILL ALIVE, AND IF NOT, WHY
|
|
3
|
+
*
|
|
4
|
+
* Split out of `session-node-manager.ts` by 037-SESSIONCORE. The connection-level liveness of a
|
|
5
|
+
* session, and the IMPAIRMENT that explains a degraded one — which matters more than it sounds,
|
|
6
|
+
* because "impaired, and here is the cause" is what an operator gets instead of silence when a
|
|
7
|
+
* session is half-working.
|
|
8
|
+
*
|
|
9
|
+
* Moved verbatim, comments included.
|
|
10
|
+
*/
|
|
11
|
+
import type { Logger } from "./types.js";
|
|
12
|
+
import type { CelloNode } from "@cello-protocol/transport";
|
|
13
|
+
import type { ActiveSessionEntry, SessionImpairment } from "./session-node-types.js";
|
|
14
|
+
import type { SessionQueries } from "./session-queries.js";
|
|
15
|
+
import type { RefusalNotices } from "./refusal-notices.js";
|
|
16
|
+
import type { SessionEphemerals } from "./session-ephemerals.js";
|
|
17
|
+
import type { SaltAgreementFrame } from "./session-salt-agreement.js";
|
|
18
|
+
/** What liveness needs from the manager. */
|
|
19
|
+
export interface SessionLivenessContext {
|
|
20
|
+
readonly logger: Logger;
|
|
21
|
+
readonly queries: SessionQueries;
|
|
22
|
+
readonly notices: RefusalNotices;
|
|
23
|
+
readonly ephemerals: SessionEphemerals;
|
|
24
|
+
readonly counterpartyAddrs: Map<string, string[]>;
|
|
25
|
+
sessionKey(agentName: string, sessionId: string): string;
|
|
26
|
+
activeEntry(key: string): ActiveSessionEntry | undefined;
|
|
27
|
+
sendSaltFrame(agentName: string, sessionId: string, correlationId?: string, override?: SaltAgreementFrame): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare class SessionLiveness {
|
|
30
|
+
#private;
|
|
31
|
+
constructor(ctx: SessionLivenessContext);
|
|
32
|
+
/**
|
|
33
|
+
* M7-SESSION-003 AC-004: wire a session node's peer-connect / peer-disconnect
|
|
34
|
+
* events to per-session direct-path liveness. onPeerConnect → 'alive',
|
|
35
|
+
* onPeerDisconnect → 'gone', emitting session.liveness.changed at WARN. Combined
|
|
36
|
+
* with the transport keepalive (AC-005), a peer that vanished without a clean
|
|
37
|
+
* close still surfaces a disconnect and drives 'gone'.
|
|
38
|
+
*
|
|
39
|
+
* THE EVENT MUST BE FILTERED BY PEER (DOD-RELAY-KEEPALIVE-1 review, F2). The
|
|
40
|
+
* original wiring acted on EVERY peer event this node saw, justified by "the
|
|
41
|
+
* session node's gater restricts connections to the designated counterparty".
|
|
42
|
+
* That stopped being true: the session node also dials the RELAY as its
|
|
43
|
+
* Structure-2 witness (#connectSessionRelay), and the gater allows those peers
|
|
44
|
+
* outbound. So a relay link dropping declared the counterparty dead — at WARN,
|
|
45
|
+
* feeding the unilateral-seal gate — while the counterparty was sitting there
|
|
46
|
+
* perfectly alive. During the 2026-08-04 incident, when the relay link churned
|
|
47
|
+
* every 60-90 seconds, that fired continuously.
|
|
48
|
+
*
|
|
49
|
+
* `counterpartySessionPeerId` is the authority when known. When it is not (the
|
|
50
|
+
* peer id can be absent on a session whose assignment has not landed yet),
|
|
51
|
+
* every peer is honoured EXCEPT ones known to be relays for this session —
|
|
52
|
+
* degrading to the old over-eager behaviour minus its one known false positive,
|
|
53
|
+
* rather than to silence, because a liveness detector that never fires is worse
|
|
54
|
+
* than one that fires too often.
|
|
55
|
+
*/
|
|
56
|
+
wireSessionLiveness(agentName: string, sessionId: string, node: CelloNode, counterpartyPubkey: string, correlationId: string, counterpartySessionPeerId?: string): void;
|
|
57
|
+
/**
|
|
58
|
+
* M7-SESSION-003: read the direct-path counterparty liveness for a session.
|
|
59
|
+
* 'unknown' when no session node observation has occurred yet.
|
|
60
|
+
*
|
|
61
|
+
* DOD-M12B-ACK-1: 'impaired' is DAEMON-LOCAL and deliberately not on the relay's
|
|
62
|
+
* SessionLiveness wire type — the relay answers a different question (does it hold the
|
|
63
|
+
* recipient's standing connection) and its three states are a deployed bilateral contract.
|
|
64
|
+
*/
|
|
65
|
+
getSessionLiveness(agentName: string, sessionId: string): "alive" | "impaired" | "gone" | "unknown";
|
|
66
|
+
/**
|
|
67
|
+
* DOD-M12B-ACK-1 — the connection is up and delivery on it is not working.
|
|
68
|
+
*
|
|
69
|
+
* Liveness is otherwise driven ONLY by libp2p peer-connect/peer-disconnect, so it answers "is
|
|
70
|
+
* there a connection object?" while every surface that prints it is read as "can I talk to them?".
|
|
71
|
+
* Measured 2026-08-17: one session reported `alive` for 70 minutes after every write had started
|
|
72
|
+
* failing, another never stopped.
|
|
73
|
+
*
|
|
74
|
+
* ONLY 'gone' is protected, and 'gone' is NOT protected because a seal gate reads it — nothing in
|
|
75
|
+
* the code does. It is protected because the receive surface turns 'gone' into "call
|
|
76
|
+
* cello_close_session", and a failed write must never be able to produce that instruction.
|
|
77
|
+
*
|
|
78
|
+
* 'unknown' is DOWNGRADED just like 'alive', which is not obvious and is the point. A session
|
|
79
|
+
* whose recorded `counterpartySessionPeerId` has gone stale never sees a matching peer-connect,
|
|
80
|
+
* so it sits at 'unknown' while every send fails forever — the exact case documented at
|
|
81
|
+
* #wireSessionLiveness — and the receive surface renders 'unknown' as healthy-and-quiet, which is
|
|
82
|
+
* the 70-minute lie relocated one lane over. 'unknown' claims nothing; the surface built on it does.
|
|
83
|
+
*/
|
|
84
|
+
markSessionImpaired(agentName: string, sessionId: string, opts: {
|
|
85
|
+
cause: "direct_send" | "delivery_ack" | "content_key";
|
|
86
|
+
error: string;
|
|
87
|
+
correlationId?: string;
|
|
88
|
+
}): void;
|
|
89
|
+
/**
|
|
90
|
+
* DOD-M12B-ACK-1 — what became of the content whose send caused the impairment.
|
|
91
|
+
*
|
|
92
|
+
* The receive surface has no memory of the last send, so without this it can only guess — and the
|
|
93
|
+
* guess it would make ("it was parked, do not resend") is FALSE in the two cases that matter
|
|
94
|
+
* most: a refused park whose durable enqueue was dropped, and one that threw. In both the message
|
|
95
|
+
* is gone and `cello_send` has already told the caller to send it again, so a receive that says
|
|
96
|
+
* "do not resend" contradicts it later, while the agent is sitting there waiting.
|
|
97
|
+
*/
|
|
98
|
+
noteImpairmentRetention(agentName: string, sessionId: string, retained: "parked" | "durable" | "lost"): void;
|
|
99
|
+
/** DOD-M12B-ACK-1: why this session is impaired, for the surface that has to explain it. Null
|
|
100
|
+
* when it is not impaired — a caller must not narrate a failure that is not current. */
|
|
101
|
+
getSessionImpairment(agentName: string, sessionId: string): SessionImpairment | null;
|
|
102
|
+
/**
|
|
103
|
+
* DOD-M12B-ACK-1 — a delivery landed, so the impairment is over.
|
|
104
|
+
*
|
|
105
|
+
* Without this an `impaired` flag is a one-way door: one bad write would make a session report a
|
|
106
|
+
* broken conversation for the rest of its life, which is the same class of lie in the other
|
|
107
|
+
* direction. Called from BOTH send paths — an agent that mostly listens sends content rarely and
|
|
108
|
+
* ACKs constantly, so clearing only on content would leave exactly those sessions impaired
|
|
109
|
+
* forever. Only clears 'impaired': a successful write says nothing about a connection libp2p has
|
|
110
|
+
* already declared 'gone'.
|
|
111
|
+
*/
|
|
112
|
+
clearSessionImpairment(agentName: string, sessionId: string, observedBy: "direct_send" | "delivery_ack", correlationId?: string): void;
|
|
113
|
+
/** Test seam (same spirit as getDb()): seed per-session direct-path liveness, which is otherwise
|
|
114
|
+
* only set by the live node's onPeerConnect/onPeerDisconnect (#wireSessionLiveness). Lets a
|
|
115
|
+
* DB-seeded test exercise the CC-5 reaper's "alive counterparty must survive" gate without standing
|
|
116
|
+
* up a real libp2p peer connection. */
|
|
117
|
+
markSessionLivenessForTest(agentName: string, sessionId: string, state: "alive" | "impaired" | "gone"): void;
|
|
118
|
+
/**
|
|
119
|
+
* Drop the liveness verdict for a torn-down session.
|
|
120
|
+
*
|
|
121
|
+
* The direct-path liveness flag goes because the seal gate has already read its verdict, so a
|
|
122
|
+
* destroyed or retired session must not retain a stale alive/gone state that a later read could
|
|
123
|
+
* mistake for a live one.
|
|
124
|
+
*
|
|
125
|
+
* ⚠️ **`#impairmentCause` IS DELIBERATELY LEFT ALONE — clearing it here loses an operator notice.**
|
|
126
|
+
* It was added for symmetry and reverted. `getSessionImpairment` gates on liveness, so the cause
|
|
127
|
+
* looks unreachable — but `noteImpairmentRetention` reads it UNGATED, and on the path where
|
|
128
|
+
* `markSessionImpaired` declines because liveness is already `gone`, the retained cause is what
|
|
129
|
+
* produces the `outbound_message_lost` notice: *"your message is gone, send it again."* Clearing
|
|
130
|
+
* it here means the operator is told nothing instead. The cause is cleared where it should be,
|
|
131
|
+
* in `clearSessionImpairment`.
|
|
132
|
+
*/
|
|
133
|
+
evictSession(agentName: string, sessionId: string): void;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=session-liveness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-liveness.d.ts","sourceRoot":"","sources":["../src/session-liveness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAG3D,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACrF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEtE,4CAA4C;AAC5C,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,iBAAiB,CAAC;IACvC,QAAQ,CAAC,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAClD,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IACzD,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAAC;IACzD,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3H;AAED,qBAAa,eAAe;;gBAGd,GAAG,EAAE,sBAAsB;IAMvC;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,mBAAmB,CACjB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,SAAS,EACf,kBAAkB,EAAE,MAAM,EAC1B,aAAa,EAAE,MAAM,EACrB,yBAAyB,CAAC,EAAE,MAAM,GACjC,IAAI;IA+IP;;;;;;;OAOG;IACH,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,SAAS;IAGnG;;;;;;;;;;;;;;;;;OAiBG;IACH,mBAAmB,CACjB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE;QAAE,KAAK,EAAE,aAAa,GAAG,cAAc,GAAG,aAAa,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GACrG,IAAI;IA+BP;;;;;;;;OAQG;IACH,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI;IAuC5G;6FACyF;IACzF,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAKpF;;;;;;;;;OASG;IACH,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,GAAG,cAAc,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI;IAetI;;;4CAGwC;IACxC,0BAA0B,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,IAAI;IAI5G;;;;;;;;;;;;;;OAcG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;CAGzD"}
|