@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.
Files changed (103) hide show
  1. package/dist/assignment-verify.d.ts +39 -11
  2. package/dist/assignment-verify.d.ts.map +1 -1
  3. package/dist/assignment-verify.js +133 -17
  4. package/dist/assignment-verify.js.map +1 -1
  5. package/dist/authorship-verification.d.ts +70 -0
  6. package/dist/authorship-verification.d.ts.map +1 -0
  7. package/dist/authorship-verification.js +441 -0
  8. package/dist/authorship-verification.js.map +1 -0
  9. package/dist/db-identity-store.d.ts +1 -0
  10. package/dist/db-identity-store.d.ts.map +1 -1
  11. package/dist/db-identity-store.js +19 -1
  12. package/dist/db-identity-store.js.map +1 -1
  13. package/dist/held-content.d.ts +141 -0
  14. package/dist/held-content.d.ts.map +1 -0
  15. package/dist/held-content.js +388 -0
  16. package/dist/held-content.js.map +1 -0
  17. package/dist/inbound-refusals.d.ts +283 -0
  18. package/dist/inbound-refusals.d.ts.map +1 -0
  19. package/dist/inbound-refusals.js +919 -0
  20. package/dist/inbound-refusals.js.map +1 -0
  21. package/dist/inbound-sessions.d.ts.map +1 -1
  22. package/dist/inbound-sessions.js +65 -27
  23. package/dist/inbound-sessions.js.map +1 -1
  24. package/dist/initiate-session-handler.d.ts.map +1 -1
  25. package/dist/initiate-session-handler.js +63 -0
  26. package/dist/initiate-session-handler.js.map +1 -1
  27. package/dist/outbound-sessions.d.ts.map +1 -1
  28. package/dist/outbound-sessions.js +108 -8
  29. package/dist/outbound-sessions.js.map +1 -1
  30. package/dist/park-recovery.d.ts +205 -0
  31. package/dist/park-recovery.d.ts.map +1 -0
  32. package/dist/park-recovery.js +600 -0
  33. package/dist/park-recovery.js.map +1 -0
  34. package/dist/refusal-notices.d.ts +196 -0
  35. package/dist/refusal-notices.d.ts.map +1 -0
  36. package/dist/refusal-notices.js +496 -0
  37. package/dist/refusal-notices.js.map +1 -0
  38. package/dist/refusal-reasons.d.ts +11 -0
  39. package/dist/refusal-reasons.d.ts.map +1 -1
  40. package/dist/refusal-reasons.js +18 -0
  41. package/dist/refusal-reasons.js.map +1 -1
  42. package/dist/registration-manager.d.ts.map +1 -1
  43. package/dist/registration-manager.js +126 -5
  44. package/dist/registration-manager.js.map +1 -1
  45. package/dist/registration-persistence.d.ts +18 -0
  46. package/dist/registration-persistence.d.ts.map +1 -1
  47. package/dist/registration-persistence.js +6 -0
  48. package/dist/registration-persistence.js.map +1 -1
  49. package/dist/session-assignment-parser.d.ts.map +1 -1
  50. package/dist/session-assignment-parser.js +23 -1
  51. package/dist/session-assignment-parser.js.map +1 -1
  52. package/dist/session-ceremony.d.ts +12 -7
  53. package/dist/session-ceremony.d.ts.map +1 -1
  54. package/dist/session-ceremony.js +12 -7
  55. package/dist/session-ceremony.js.map +1 -1
  56. package/dist/session-ephemerals.d.ts +271 -0
  57. package/dist/session-ephemerals.d.ts.map +1 -0
  58. package/dist/session-ephemerals.js +546 -0
  59. package/dist/session-ephemerals.js.map +1 -0
  60. package/dist/session-leaf-records.d.ts +132 -0
  61. package/dist/session-leaf-records.d.ts.map +1 -0
  62. package/dist/session-leaf-records.js +320 -0
  63. package/dist/session-leaf-records.js.map +1 -0
  64. package/dist/session-liveness.d.ts +135 -0
  65. package/dist/session-liveness.d.ts.map +1 -0
  66. package/dist/session-liveness.js +347 -0
  67. package/dist/session-liveness.js.map +1 -0
  68. package/dist/session-node-manager.d.ts +300 -1670
  69. package/dist/session-node-manager.d.ts.map +1 -1
  70. package/dist/session-node-manager.js +871 -9951
  71. package/dist/session-node-manager.js.map +1 -1
  72. package/dist/session-node-types.d.ts +933 -0
  73. package/dist/session-node-types.d.ts.map +1 -0
  74. package/dist/session-node-types.js +576 -0
  75. package/dist/session-node-types.js.map +1 -0
  76. package/dist/session-queries.d.ts +476 -0
  77. package/dist/session-queries.d.ts.map +1 -0
  78. package/dist/session-queries.js +1006 -0
  79. package/dist/session-queries.js.map +1 -0
  80. package/dist/session-records.d.ts +320 -0
  81. package/dist/session-records.d.ts.map +1 -0
  82. package/dist/session-records.js +792 -0
  83. package/dist/session-records.js.map +1 -0
  84. package/dist/session-salts.d.ts +390 -0
  85. package/dist/session-salts.d.ts.map +1 -0
  86. package/dist/session-salts.js +1457 -0
  87. package/dist/session-salts.js.map +1 -0
  88. package/dist/session-schema.d.ts +30 -0
  89. package/dist/session-schema.d.ts.map +1 -0
  90. package/dist/session-schema.js +786 -0
  91. package/dist/session-schema.js.map +1 -0
  92. package/dist/standing-receivers.d.ts +280 -0
  93. package/dist/standing-receivers.d.ts.map +1 -0
  94. package/dist/standing-receivers.js +1100 -0
  95. package/dist/standing-receivers.js.map +1 -0
  96. package/dist/transport-selector.d.ts +13 -0
  97. package/dist/transport-selector.d.ts.map +1 -1
  98. package/dist/transport-selector.js.map +1 -1
  99. package/dist/witness-alerts.d.ts +40 -0
  100. package/dist/witness-alerts.d.ts.map +1 -0
  101. package/dist/witness-alerts.js +102 -0
  102. package/dist/witness-alerts.js.map +1 -0
  103. 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"}