@cello-protocol/daemon 0.0.182 → 0.0.184

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 (137) hide show
  1. package/dist/agent-settings-keys.d.ts.map +1 -1
  2. package/dist/agent-settings-keys.js +18 -0
  3. package/dist/agent-settings-keys.js.map +1 -1
  4. package/dist/close-session-handler.d.ts.map +1 -1
  5. package/dist/close-session-handler.js +90 -16
  6. package/dist/close-session-handler.js.map +1 -1
  7. package/dist/consortium-bootstrap.d.ts.map +1 -1
  8. package/dist/consortium-bootstrap.js +117 -2
  9. package/dist/consortium-bootstrap.js.map +1 -1
  10. package/dist/content-encryption-status.d.ts +97 -0
  11. package/dist/content-encryption-status.d.ts.map +1 -0
  12. package/dist/content-encryption-status.js +122 -0
  13. package/dist/content-encryption-status.js.map +1 -0
  14. package/dist/content-park-client.d.ts +10 -0
  15. package/dist/content-park-client.d.ts.map +1 -1
  16. package/dist/content-park-client.js +84 -3
  17. package/dist/content-park-client.js.map +1 -1
  18. package/dist/content-park.d.ts.map +1 -1
  19. package/dist/content-park.js +184 -13
  20. package/dist/content-park.js.map +1 -1
  21. package/dist/daemon.d.ts.map +1 -1
  22. package/dist/daemon.js +671 -67
  23. package/dist/daemon.js.map +1 -1
  24. package/dist/document-handlers.d.ts.map +1 -1
  25. package/dist/document-handlers.js +119 -0
  26. package/dist/document-handlers.js.map +1 -1
  27. package/dist/document-layer.d.ts +0 -2
  28. package/dist/document-layer.d.ts.map +1 -1
  29. package/dist/document-layer.js.map +1 -1
  30. package/dist/frame-values.d.ts.map +1 -1
  31. package/dist/frame-values.js +14 -0
  32. package/dist/frame-values.js.map +1 -1
  33. package/dist/inclusion-proof-handlers.d.ts +43 -0
  34. package/dist/inclusion-proof-handlers.d.ts.map +1 -0
  35. package/dist/inclusion-proof-handlers.js +565 -0
  36. package/dist/inclusion-proof-handlers.js.map +1 -0
  37. package/dist/inclusion-proof.d.ts +151 -0
  38. package/dist/inclusion-proof.d.ts.map +1 -0
  39. package/dist/inclusion-proof.js +228 -0
  40. package/dist/inclusion-proof.js.map +1 -0
  41. package/dist/initiate-session-handler.d.ts.map +1 -1
  42. package/dist/initiate-session-handler.js +34 -3
  43. package/dist/initiate-session-handler.js.map +1 -1
  44. package/dist/manifest-deps.d.ts +0 -11
  45. package/dist/manifest-deps.d.ts.map +1 -1
  46. package/dist/manifest-deps.js +28 -2
  47. package/dist/manifest-deps.js.map +1 -1
  48. package/dist/manifest-validity.d.ts +2 -0
  49. package/dist/manifest-validity.d.ts.map +1 -1
  50. package/dist/manifest-validity.js +1 -1
  51. package/dist/manifest-validity.js.map +1 -1
  52. package/dist/network-directory-node.d.ts +16 -0
  53. package/dist/network-directory-node.d.ts.map +1 -1
  54. package/dist/network-directory-node.js +40 -0
  55. package/dist/network-directory-node.js.map +1 -1
  56. package/dist/notification-handlers.d.ts.map +1 -1
  57. package/dist/notification-handlers.js +71 -2
  58. package/dist/notification-handlers.js.map +1 -1
  59. package/dist/outbound-sessions.d.ts +6 -0
  60. package/dist/outbound-sessions.d.ts.map +1 -1
  61. package/dist/outbound-sessions.js +50 -7
  62. package/dist/outbound-sessions.js.map +1 -1
  63. package/dist/park-envelope.d.ts +23 -0
  64. package/dist/park-envelope.d.ts.map +1 -1
  65. package/dist/park-envelope.js +76 -0
  66. package/dist/park-envelope.js.map +1 -1
  67. package/dist/register-handler.d.ts.map +1 -1
  68. package/dist/register-handler.js +76 -0
  69. package/dist/register-handler.js.map +1 -1
  70. package/dist/relay-only.d.ts +133 -0
  71. package/dist/relay-only.d.ts.map +1 -0
  72. package/dist/relay-only.js +183 -0
  73. package/dist/relay-only.js.map +1 -0
  74. package/dist/retry-queue.d.ts +26 -1
  75. package/dist/retry-queue.d.ts.map +1 -1
  76. package/dist/retry-queue.js +18 -1
  77. package/dist/retry-queue.js.map +1 -1
  78. package/dist/seal-coordinator.d.ts +51 -1
  79. package/dist/seal-coordinator.d.ts.map +1 -1
  80. package/dist/seal-coordinator.js +248 -1
  81. package/dist/seal-coordinator.js.map +1 -1
  82. package/dist/seal-escalation.d.ts.map +1 -1
  83. package/dist/seal-escalation.js +28 -0
  84. package/dist/seal-escalation.js.map +1 -1
  85. package/dist/seal-failure-store.d.ts.map +1 -1
  86. package/dist/seal-failure-store.js +5 -1
  87. package/dist/seal-failure-store.js.map +1 -1
  88. package/dist/sealed-leaf-set.d.ts +92 -0
  89. package/dist/sealed-leaf-set.d.ts.map +1 -0
  90. package/dist/sealed-leaf-set.js +129 -0
  91. package/dist/sealed-leaf-set.js.map +1 -0
  92. package/dist/session-ceremony.d.ts +27 -1
  93. package/dist/session-ceremony.d.ts.map +1 -1
  94. package/dist/session-ceremony.js +109 -7
  95. package/dist/session-ceremony.js.map +1 -1
  96. package/dist/session-content-handlers.d.ts +12 -0
  97. package/dist/session-content-handlers.d.ts.map +1 -1
  98. package/dist/session-content-handlers.js +39 -4
  99. package/dist/session-content-handlers.js.map +1 -1
  100. package/dist/session-node-manager.d.ts +387 -4
  101. package/dist/session-node-manager.d.ts.map +1 -1
  102. package/dist/session-node-manager.js +3529 -279
  103. package/dist/session-node-manager.js.map +1 -1
  104. package/dist/session-read-handlers.d.ts.map +1 -1
  105. package/dist/session-read-handlers.js +16 -1
  106. package/dist/session-read-handlers.js.map +1 -1
  107. package/dist/session-relay-client.d.ts +163 -1
  108. package/dist/session-relay-client.d.ts.map +1 -1
  109. package/dist/session-relay-client.js +540 -6
  110. package/dist/session-relay-client.js.map +1 -1
  111. package/dist/session-salt-agreement.d.ts +58 -4
  112. package/dist/session-salt-agreement.d.ts.map +1 -1
  113. package/dist/session-salt-agreement.js +110 -3
  114. package/dist/session-salt-agreement.js.map +1 -1
  115. package/dist/signal-submission.d.ts +11 -1
  116. package/dist/signal-submission.d.ts.map +1 -1
  117. package/dist/signal-submission.js.map +1 -1
  118. package/dist/signaling-connect.d.ts +18 -1
  119. package/dist/signaling-connect.d.ts.map +1 -1
  120. package/dist/signaling-connect.js +164 -1
  121. package/dist/signaling-connect.js.map +1 -1
  122. package/dist/submission-retry.d.ts +208 -0
  123. package/dist/submission-retry.d.ts.map +1 -0
  124. package/dist/submission-retry.js +505 -0
  125. package/dist/submission-retry.js.map +1 -0
  126. package/dist/types.d.ts +33 -0
  127. package/dist/types.d.ts.map +1 -1
  128. package/dist/types.js.map +1 -1
  129. package/dist/vocabulary.d.ts +13 -3
  130. package/dist/vocabulary.d.ts.map +1 -1
  131. package/dist/vocabulary.js +14 -3
  132. package/dist/vocabulary.js.map +1 -1
  133. package/dist/wire-content-hash.d.ts +15 -3
  134. package/dist/wire-content-hash.d.ts.map +1 -1
  135. package/dist/wire-content-hash.js +15 -3
  136. package/dist/wire-content-hash.js.map +1 -1
  137. package/package.json +5 -5
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { type ContentHashAlg } from "./wire-content-hash.js";
18
18
  import { type CapacityReason } from "./refusal-reasons.js";
19
+ import { type SaltAgreementFrame } from "./session-salt-agreement.js";
19
20
  import { type DaemonDatabase } from "./sqlcipher-db.js";
20
21
  import type { Stream } from "@libp2p/interface";
21
22
  import type { Logger, SessionRecord, SealReadinessView } from "./types.js";
@@ -23,11 +24,32 @@ import { SessionConnectionGater } from "./session-connection-gater.js";
23
24
  import { SessionTree, type WritableSessionTreeLeafKind } from "./session-tree.js";
24
25
  import { type CelloNode, type IAutoNatService } from "@cello-protocol/transport";
25
26
  import type { KeyProvider } from "@cello-protocol/crypto";
27
+ import { type SessionEphemeral } from "@cello-protocol/crypto";
26
28
  import { type ParkEnvelope } from "./park-envelope.js";
27
- import { AgentRelayClient, type RelayAssignmentCarry } from "./session-relay-client.js";
29
+ import { AgentRelayClient, type RelayAssignmentCarry, type RelayAuthRefusal, type RelayWitnessAlert } from "./session-relay-client.js";
30
+ /**
31
+ * One row in an agent's witness-alert list — DOD-M15-CORROBORATE-1 review F1. Deduped on
32
+ * `(witness relay, session)`, so a repeated observation raises `occurrences` rather than taking
33
+ * another slot in a bounded list.
34
+ */
35
+ export interface WitnessAlertNotice {
36
+ /** `${relayId}::${sessionIdHex}` — the dedupe key, not shown to anyone. */
37
+ key: string;
38
+ alert: RelayWitnessAlert;
39
+ occurrences: number;
40
+ /**
41
+ * When this witness first said it. Held SEPARATELY from `alert.observedAt`, because a later
42
+ * repeat can replace `alert` (a provable one supersedes an unprovable one) and that must not
43
+ * silently move the first sighting forward — an operator reading "first observed" wants to know
44
+ * when this started, not when the strongest version of it arrived.
45
+ */
46
+ firstObservedAt: number;
47
+ lastObservedAt: number;
48
+ }
28
49
  import { RelayReceiptStore, type RelayReceipt } from "./relay-receipt-store.js";
29
50
  import { SessionSealLeafStore, type SealCarryLeaf } from "./session-seal-leaf-store.js";
30
51
  import type { SealUpgradeReadiness } from "./seal-upgrade.js";
52
+ import type { SealFrontierLeaf } from "./seal-frontier-verify.js";
31
53
  import { type SecurityGatewayClient } from "@cello-protocol/gateway";
32
54
  /**
33
55
  * DOD-CAP-SELF-HEAL-1 — how long an interrupted session keeps consuming a cap slot.
@@ -57,6 +79,15 @@ import { type SecurityGatewayClient } from "@cello-protocol/gateway";
57
79
  * span about 2.5 hours.
58
80
  */
59
81
  export declare const SR_RESERVATION_MAX_RETRIES = 5;
82
+ /**
83
+ * DOD-M15-RELAYSLOTS-1 — how long an agent skips a relay that refused it for a relay-side fault.
84
+ *
85
+ * Ten minutes: long enough that the agent is not re-asking a relay that cannot serve it every time
86
+ * its receiver rebuilds, short enough that when someone fixes that relay the agent finds it again
87
+ * without needing its own restart. The fault is on somebody else's machine and nobody tells us when
88
+ * it is fixed, so this has to expire on its own.
89
+ */
90
+ export declare const RELAY_QUARANTINE_MS: number;
60
91
  export declare const CAP_INTERRUPTED_TTL_MS: number;
61
92
  /**
62
93
  * DOD-M12B-ABANDON-NOTIFY-1 — what happened when we tried to tell the counterparty we hung up.
@@ -154,6 +185,18 @@ export interface ISessionNodeFactory {
154
185
  export interface SessionNodeConfig {
155
186
  sessionId: string;
156
187
  connectionGater?: SessionConnectionGater;
188
+ /**
189
+ * DOD-M15-RELAYONLY-1: this agent has asked never to be directly reachable, so the factory must
190
+ * omit dcutr from the node's service set.
191
+ *
192
+ * ⚠️ NOT a duplicate of filtering the published addresses. Those two things stop DIFFERENT
193
+ * disclosures: the filter controls what the DIRECTORY is told, and dcutr talks to the peer
194
+ * directly. Its whole job is to upgrade a relayed connection into a direct one, and the inbound
195
+ * side starts the upgrade — which is exactly a standing receiver. Leaving it on means the agent
196
+ * routes over the relay precisely as asked and then hole-punches to a direct connection anyway,
197
+ * with every test still green because the leak happens inside libp2p after the assertions.
198
+ */
199
+ relayOnly?: boolean;
157
200
  /**
158
201
  * CELLO-M7-TRANSPORT-001: role of the node, forwarded to createNode to tune the
159
202
  * libp2p service set (dcutr is included for 'session' dialers, omitted for the
@@ -257,12 +300,63 @@ export declare const REVIVE_RESERVATION_CANDIDATES = 2;
257
300
  * the healthy direct latency and far below anything a person would notice.
258
301
  */
259
302
  export declare const LEAF_FETCH_GRACE_MS = 2000;
303
+ /**
304
+ * DOD-M15-SEALWIRE-1 bullet 5, SENT half — our own authorship proof for a message we sent.
305
+ *
306
+ * Deliberately the SAME shape as the received half's `verifiedAuthorship`, because the transcript
307
+ * column pair is the same and a second shape would invite a second meaning. What differs is the
308
+ * ATTRIBUTION the row records: a sent row is `self_authored` (we PRODUCED this signature), never
309
+ * `verified_signature` (we CHECKED someone else's). Same bytes, different claim.
310
+ */
311
+ export interface SentAuthorship {
312
+ senderPubkey: Uint8Array;
313
+ senderSig: Uint8Array;
314
+ }
260
315
  export declare class SessionNodeManager {
261
316
  #private;
262
317
  setDetachedRelayClientBuilder(fn: (agentName: string, relayPeerId: string, relayAddrs: string[], stores: {
263
318
  receiptStore?: RelayReceiptStore;
264
319
  sealLeafStore?: SessionSealLeafStore;
320
+ onlineToken: () => Uint8Array | undefined;
265
321
  }) => AgentRelayClient | undefined): void;
322
+ /**
323
+ * Record what one relay says it saw on one of this agent's sessions, for the operator to read.
324
+ *
325
+ * ⚠️ **IT DOES NOT FREEZE THE SESSION, AND THAT IS THE DESIGN.** A client freezing on its OWN
326
+ * verification is safe: it limits only what that client trusts. Freezing on a REMOTE party's
327
+ * say-so hands any single relay the power to end any conversation it carries, and to write an
328
+ * accusatory record about a counterparty who did nothing. The identity freeze stays where it is —
329
+ * on this daemon's own check of an inbound frame — and this surfaces a second, independent
330
+ * observation next to it. One witness reports; it does not rule.
331
+ */
332
+ recordRelayWitnessAlert(agentName: string, alert: RelayWitnessAlert): void;
333
+ /** The witness alerts an agent has been told about, oldest first, one row per witness+session. */
334
+ getWitnessAlerts(agentName: string): ReadonlyArray<WitnessAlertNotice>;
335
+ /** Whether this agent's alert list hit its cap, so the inbox can say the list is incomplete. */
336
+ witnessAlertsTruncated(agentName: string): boolean;
337
+ /**
338
+ * Review F7: a relay sent a witness alert this build could not read or could not verify.
339
+ *
340
+ * Recorded so a version skew that silently kills the witness layer is visible to the operator
341
+ * instead of living only in a log file. Carries no session and no party by construction.
342
+ */
343
+ recordRelayWitnessUnreadable(agentName: string, relayPeerId: string, why: string): void;
344
+ /** Relays whose witness alerts this build could not read, for the agent's inbox. */
345
+ getWitnessUnreadable(agentName: string): ReadonlyArray<{
346
+ relayPeerId: string;
347
+ why: string;
348
+ count: number;
349
+ }>;
350
+ /**
351
+ * Why this agent's standing receiver could not hold a reservation, in words the person running it
352
+ * can act on — or null when the last attempt succeeded or none has been made.
353
+ *
354
+ * This is the surface DoD clause 7 is about: the assertion that matters is what the CLIENT can
355
+ * show someone, not what the relay wrote in its own log.
356
+ */
357
+ getStandingReceiverRefusal(agentName: string): (RelayAuthRefusal & {
358
+ relayPeerId: string;
359
+ }) | null;
266
360
  /**
267
361
  * Record that this session's tree and the relay's counter have provably parted.
268
362
  *
@@ -387,6 +481,7 @@ export declare class SessionNodeManager {
387
481
  ok: false;
388
482
  reason: string;
389
483
  cause?: string;
484
+ retryAfterMs?: number;
390
485
  }>): void;
391
486
  /**
392
487
  * DOD-PARK-DRAIN-1: inject the parked-mailbox drain (daemon.ts → contentPark.autoRecoverForAgent).
@@ -420,6 +515,16 @@ export declare class SessionNodeManager {
420
515
  * encrypted DB file.
421
516
  */
422
517
  getDb(): DaemonDatabase;
518
+ /**
519
+ * DOD-M15-RELAYONLY-1: is the settings store readable RIGHT NOW?
520
+ *
521
+ * ⚠️ Exists because `getSetting` cannot answer it. That method returns `null` for BOTH "the key is
522
+ * unset" and "there is no database", and a security setting must tell those apart: unset-means-off
523
+ * is correct, db-gone-means-off publishes the operator's real address during the shutdown window.
524
+ * `getDb()` cannot stand in either — it THROWS when there is no database, which on a catch-less
525
+ * ceremony path is worse than the wrong answer.
526
+ */
527
+ hasDatabase(): boolean;
423
528
  /**
424
529
  * RELAYSIG-1: the durably-stored, signature-verified relay ordering-record receipts for an agent
425
530
  * (optionally a single session). Empty when no receipts have been recorded yet. Read-only.
@@ -465,6 +570,30 @@ export declare class SessionNodeManager {
465
570
  verdict: "cannot_judge";
466
571
  reason: string;
467
572
  };
573
+ /**
574
+ * WHERE THE MUTUALLY-SIGNED PREFIX ENDS, DERIVED FROM THIS DAEMON'S OWN LEAVES —
575
+ * `DOD-M15-UNILATERAL-1`, review F2.
576
+ *
577
+ * ⚠️ **THE FIRST VERSION COMPUTED THIS FROM THE CERTIFICATE'S OWN PARTICIPANT LIST, AND CALLED
578
+ * THAT "recomputed, cannot be steered".** It could be steered. On the SOLO path the certificate's
579
+ * TBS binds no legibility at all, and the client verifies only the *live* party's frontier — so
580
+ * the absent party's `content_frontier_seq` and every `last_authored_seq` arrived unchecked. One
581
+ * directory node could publish the absent party's frontier as 3 and the receipt would say
582
+ * "mutually signed through 3" over a transcript that party never signed for. That is the precise
583
+ * conflation this field exists to prevent, reintroduced by the field itself.
584
+ *
585
+ * The carry answers it without trusting anybody. This daemon holds the counterparty's own leaves,
586
+ * each carrying, inside the bytes THEY signed, both the sequence they authored and the
587
+ * `last_seen_seq` they acknowledged. So a party's commitment reaches
588
+ * `max(highest sequence they authored, highest sequence they acknowledged)`, and the transcript is
589
+ * mutually signed only as far as the LEAST-committed party reaches.
590
+ *
591
+ * Fewer than two distinct authors ⇒ `0`: nobody countersigned anything, which is the honest floor
592
+ * for a conversation where the other side only ever received. `null` when the carry is empty or
593
+ * unreadable — the caller must then publish NO boundary rather than fall back to a number
594
+ * somebody else supplied.
595
+ */
596
+ countersignedThroughSeqFromCarry(agentPubkeyHex: string, sessionIdHex: string): number | null;
468
597
  getSealCarry(agentPubkeyHex: string, sessionIdHex: string): SealCarryLeaf[];
469
598
  /**
470
599
  * DOD-LOG-1 / PERSIST-002 (AC-010): append one readable message to the durable transcript, keyed
@@ -1280,6 +1409,46 @@ export declare class SessionNodeManager {
1280
1409
  sealed_root: string;
1281
1410
  legibility: unknown;
1282
1411
  } | null;
1412
+ /**
1413
+ * DOD-M15-INCLUSION-1: keep the leaf set the certificate is signed over, so one message can later
1414
+ * be proved to sit under it.
1415
+ *
1416
+ * REFUSES unless the hashes reproduce `sealedRootHex` — `certifiedLeafSetFrom` does that check and
1417
+ * this method never bypasses it. That is what separates "the leaves the directory sent" from "the
1418
+ * leaves the consortium signed", and only the second is worth storing: a proof built on the first
1419
+ * would inherit whatever the directory chose to say.
1420
+ *
1421
+ * Idempotent (INSERT OR REPLACE keyed on leaf_index) so a re-delivered seal frame, or a unilateral
1422
+ * seal later upgraded to bilateral, rewrites the same rows instead of failing or doubling them.
1423
+ *
1424
+ * @returns whether the set was accepted and stored.
1425
+ */
1426
+ recordCertifiedLeafSet(agentName: string, sessionId: string, signedLeaves: readonly SealFrontierLeaf[], sealedRootHex: string, correlationId?: string): boolean;
1427
+ /**
1428
+ * Record WHY this session does or does not have a certified leaf set.
1429
+ *
1430
+ * Public for the one case the manager cannot see: a seal frame that carried no signed leaves at
1431
+ * all never reaches `recordCertifiedLeafSet`, and that absence is a permanent fact about the
1432
+ * session for the party that observed it.
1433
+ */
1434
+ noteCertifiedLeafSetUnavailable(agentName: string, sessionId: string, state: "not_carried_absent_party" | "not_carried_present_party", detail: string): void;
1435
+ /**
1436
+ * The last thing that happened to this session's certified leaf set, or null if nothing has.
1437
+ *
1438
+ * Null here and a null from `getCertifiedLeafSet` together mean "no seal has been processed on
1439
+ * this side yet" — which is a different sentence again from any of the recorded states.
1440
+ */
1441
+ getCertifiedLeafSetState(agentName: string, sessionId: string): {
1442
+ state: string;
1443
+ detail: string | null;
1444
+ } | null;
1445
+ /**
1446
+ * The certified leaf set, in order, or null when none was stored for this session.
1447
+ *
1448
+ * Null is a REFUSAL upstream, never a fallback to the local tree: the two cover different leaves
1449
+ * and substituting one for the other is how a proof comes to land on a root nobody signed.
1450
+ */
1451
+ getCertifiedLeafSet(agentName: string, sessionId: string): string[] | null;
1283
1452
  /**
1284
1453
  * M7-SESSION-001: Mark a session as interrupted with message count and timestamp.
1285
1454
  * Called when a relay session_interrupted frame arrives or a relay stream closes.
@@ -1289,7 +1458,15 @@ export declare class SessionNodeManager {
1289
1458
  * @param messageCount Number of message leaves at interruption
1290
1459
  * @param source 'relay_frame' | 'stream_close'
1291
1460
  */
1292
- markInterruptedWithDetails(agentName: string, sessionId: string, messageCount: number, source: "relay_frame" | "stream_close"): Promise<void>;
1461
+ markInterruptedWithDetails(agentName: string, sessionId: string, messageCount: number,
1462
+ /**
1463
+ * WHAT ACTUALLY HAPPENED, and it is written to the row — review F3.
1464
+ *
1465
+ * `key_refused` is its own source rather than a borrowed `stream_close`, because the row's
1466
+ * `interrupted_by` is what an operator reads days later: labelling a key-authentication refusal
1467
+ * `relay_stream_close` sends them to the relay fleet for a fault in the payload.
1468
+ */
1469
+ source: "relay_frame" | "stream_close" | "key_refused"): Promise<boolean>;
1293
1470
  /**
1294
1471
  * M7-SESSION-001 (H-1): persist a verified bilateral SEAL-INTERRUPTED
1295
1472
  * commitment and transition the session to 'seal_interrupted_pending'.
@@ -1483,12 +1660,25 @@ export declare class SessionNodeManager {
1483
1660
  ok: true;
1484
1661
  delivered: true;
1485
1662
  sequenceNumber?: number;
1663
+ authorship?: SentAuthorship;
1486
1664
  } | {
1487
1665
  ok: true;
1488
1666
  delivered: false;
1489
1667
  parked: true;
1490
1668
  sequenceNumber?: number;
1491
- } | {
1669
+ authorship?: SentAuthorship;
1670
+ }
1671
+ /**
1672
+ * ⚠️ `authorship` RIDES THE FAILURE PATH TOO — review pass 2, and it is the same reasoning that
1673
+ * already put `sequenceNumber` here.
1674
+ *
1675
+ * A DURABLY QUEUED message was witnessed and SIGNED before delivery was attempted; only the
1676
+ * direct hand-off failed. Omitting the proof here meant every relay-degraded-but-alive send —
1677
+ * the common case — wrote a transcript row indistinguishable from one the relay never saw, while
1678
+ * the signature for it sat in scope and was discarded. The position survives a failed delivery
1679
+ * for exactly this reason; so does the proof.
1680
+ */
1681
+ | {
1492
1682
  ok: false;
1493
1683
  reason: string;
1494
1684
  error: string;
@@ -1496,6 +1686,7 @@ export declare class SessionNodeManager {
1496
1686
  cause?: string;
1497
1687
  guidance?: string;
1498
1688
  sequenceNumber?: number;
1689
+ authorship?: SentAuthorship;
1499
1690
  }>;
1500
1691
  /**
1501
1692
  * M7 DOD-SPINE-7: submit THIS party's SEAL ctrl leaf (0x02) to the relay witness.
@@ -1789,7 +1980,35 @@ export declare class SessionNodeManager {
1789
1980
  * and it appends in arrival order as before: with no position there is no discipline to enforce,
1790
1981
  * and refusing would take messaging down whenever the relay is unreachable.
1791
1982
  */
1792
- placeOwnLeaf(agentName: string, sessionId: string, contentHashHex: string, sentBytes: Uint8Array, assignedSeq: number | undefined, correlationId?: string, kind?: WritableSessionTreeLeafKind): {
1983
+ placeOwnLeaf(agentName: string, sessionId: string, contentHashHex: string, sentBytes: Uint8Array, assignedSeq: number | undefined, correlationId: string | undefined,
1984
+ /**
1985
+ * ⚠️ NO DEFAULT, for the same reason `authorship` has none.
1986
+ *
1987
+ * `kind = "msg"` meant a caller writing a `doc` or a `ctrl` leaf got a `msg` leaf by saying
1988
+ * nothing, and the tree recorded a leaf kind the author never chose. TypeScript also forbids a
1989
+ * required parameter after a defaulted one, so leaving the default here would have forced
1990
+ * `authorship` back to optional — which is the defect above. Every one of the seven call sites
1991
+ * already passed a kind or wanted "msg"; making it explicit cost nothing and removes a second
1992
+ * silent answer from the same signature.
1993
+ */
1994
+ kind: WritableSessionTreeLeafKind,
1995
+ /**
1996
+ * DOD-M15-SEALWIRE-1 bullet 5 — the proof for THIS send, so a held row keeps it.
1997
+ *
1998
+ * ⚠️ REQUIRED, AND `undefined` IS A VALID ANSWER — the two are not the same thing.
1999
+ *
2000
+ * This was `authorship?:` for exactly one review cycle, and in that cycle THREE of the seven
2001
+ * call sites omitted it: `daemon.ts` 1440, 1671, 1685 — the away-reply path, which is the
2002
+ * highest-traffic sent-writer in the daemon and the one with no human watching it. All three
2003
+ * had the proof **already in a local variable one line below**, handed to
2004
+ * `recordTranscriptMessage` and not to this method. Nothing went red, because an optional
2005
+ * parameter's whole behaviour on omission is to look deliberate.
2006
+ *
2007
+ * An unwitnessed send genuinely has no proof, so absence must stay expressible. Requiring the
2008
+ * parameter keeps that while making the caller SAY it: omission is now a type error, and
2009
+ * `undefined` is a claim the author made rather than one the signature made for them.
2010
+ */
2011
+ authorship: SentAuthorship | undefined): {
1793
2012
  placed: true;
1794
2013
  leafIndex: number;
1795
2014
  diverged?: true;
@@ -1882,6 +2101,19 @@ export declare class SessionNodeManager {
1882
2101
  * record (the recover handler lives in daemon.ts, which has no access to the private method).
1883
2102
  */
1884
2103
  recordOrderingRecord(agentName: string, sessionId: string, structure1Cbor: Uint8Array, structure2Cbor: Uint8Array, contentHash: Uint8Array, correlationId?: string): number | null;
2104
+ /**
2105
+ * Test seam: see every decoded inbound content frame, as it arrived.
2106
+ *
2107
+ * Review F4. The "bytes on the wire are ciphertext" claim needs the ACTUAL frame; asserting on a
2108
+ * freshly sealed stand-in tests the crypto primitive and stays green when the send path is
2109
+ * reverted to putting plaintext on the wire. There is no other way to reach the decoded frame from
2110
+ * outside — the handler consumes it and hands ingest the plaintext.
2111
+ *
2112
+ * Read-only by construction: the callback receives the frame and cannot influence routing.
2113
+ */
2114
+ observeInboundContentFramesForTest(cb: (frame: Record<string, unknown>) => void): void;
2115
+ /** Injected by the daemon once its per-agent key providers exist. See `#keyProviderResolver`. */
2116
+ setKeyProviderResolver(resolver: (agentName: string) => KeyProvider | undefined): void;
1885
2117
  /**
1886
2118
  * Test seam: force this session's own salt half, so the LOCAL-defect path is reachable.
1887
2119
  *
@@ -1907,6 +2139,97 @@ export declare class SessionNodeManager {
1907
2139
  */
1908
2140
  runAutoAcknowledgeGateForTest(agentName: string, sessionId: string, correlationId?: string): void;
1909
2141
  setSaltContributionForTest(agentName: string, sessionId: string, contribution: Uint8Array): void;
2142
+ /**
2143
+ * Test seams: re-enter the mint path, and read back the PUBLIC half — 006-CRYPTO.
2144
+ *
2145
+ * `#mintSessionEphemeral` is idempotent because a reconnect can re-enter an activation path, and a
2146
+ * second keypair mid-session would leave the two sides deriving against a moving value. Proving
2147
+ * that needs the path called TWICE, and the alternative — driving a real reconnect — drags in node
2148
+ * rebuild and relay reconnection, none of which the property is about. Same justification as
2149
+ * `forgetSaltContributionForTest` above.
2150
+ *
2151
+ * It calls the REAL private method, so a test cannot pass against a decision production does not
2152
+ * make. The reader returns the public half ONLY: a seam that could hand out the secret is a way
2153
+ * for the secret to leave this object, which is the one thing the whole unit is about.
2154
+ */
2155
+ mintSessionEphemeralForTest(agentName: string, sessionId: string): void;
2156
+ sessionEphemeralPublicForTest(agentName: string, sessionId: string): Uint8Array | null;
2157
+ /**
2158
+ * Test seam: INSTALL a keypair the caller already holds — the only way to prove ZEROING.
2159
+ *
2160
+ * Presence is easy to assert and is not the property. `destroySessionEphemeral` overwrites the
2161
+ * buffer before the entry is dropped, and a mutant that drops without overwriting leaves the
2162
+ * secret wherever the collector last moved it while passing every presence check — which is
2163
+ * exactly what happened: the shutdown zeroing shipped with a surviving mutant, and the transport
2164
+ * seeds four lines above it have the same untested gap today.
2165
+ *
2166
+ * The direction matters. Nothing here HANDS OUT a secret — the test supplies an object it already
2167
+ * owns and then inspects its own reference. A reader that returned the live keypair would be a
2168
+ * path for the secret to leave this object, which is the one thing this unit exists to prevent.
2169
+ */
2170
+ setSessionEphemeralForTest(agentName: string, sessionId: string, ephemeral: SessionEphemeral): void;
2171
+ /**
2172
+ * Test seam: put a session into the state a COMPLETED exchange leaves it in — 007-CRYPTO.
2173
+ *
2174
+ * A live send now requires an agreed key, because there is no plaintext path to fall back to. In
2175
+ * production the exchange completes on connect, before any send. A fixture with no real peer never
2176
+ * completes it, so without this every content test in the repo would be exercising the refusal
2177
+ * path instead of the thing it was written for.
2178
+ *
2179
+ * ⚠️ IT SHORT-CIRCUITS HOW THE KEY GOT THERE, NEVER WHAT THE KEY IS FOR. The state it produces —
2180
+ * a session holding an agreed content key — is exactly the production state, which is what makes
2181
+ * it legitimate; `setSaltContributionForTest` exists for the same reason. Tests of the EXCHANGE
2182
+ * itself drive the real signed frames and must not use this.
2183
+ */
2184
+ setSessionContentKeyForTest(agentName: string, sessionId: string, key: Uint8Array): void;
2185
+ /**
2186
+ * Test seam: drop the agreed key while leaving the session up — the state before an exchange
2187
+ * completes, and after a teardown evicts one. Its mirror above is what a completed exchange
2188
+ * leaves; both are needed, or a status field stuck in one position passes either test alone.
2189
+ */
2190
+ forgetSessionContentKeyForTest(agentName: string, sessionId: string): void;
2191
+ /**
2192
+ * Test seam: produce THIS side's signed ephemeral, using the manager's own identity resolver.
2193
+ *
2194
+ * For harnesses whose connectivity is one-directional — one side dials, so only one announce ever
2195
+ * lands. Carrying the other side's half across with a REAL signature is what completes the
2196
+ * exchange, and it beats seeding a key: a seeded key has no peer half recorded against it, so the
2197
+ * first genuine announce replaces it and the two ends drift apart (which is correct behaviour —
2198
+ * see the re-key guard — and exactly what made seeding fragile here).
2199
+ *
2200
+ * It signs with the same provider production signs with, so a test cannot pass against a signature
2201
+ * production would have refused.
2202
+ */
2203
+ signOwnEphemeralForTest(agentName: string, sessionId: string): Promise<{
2204
+ ephemeralPublic: Uint8Array;
2205
+ signature: Uint8Array;
2206
+ } | null>;
2207
+ /**
2208
+ * Test seam: deliver a peer's signed ephemeral, exactly as the content-stream decoder does.
2209
+ *
2210
+ * For harnesses whose connectivity is one-directional — one side dials, so only one announce ever
2211
+ * lands — this is what completes the exchange instead of stuffing a key in. It runs the REAL
2212
+ * verification and the REAL derivation, so a test cannot pass against a signature production would
2213
+ * have refused.
2214
+ */
2215
+ handleEphemeralFrameForTest(agentName: string, sessionId: string, frame: {
2216
+ ephemeralPublic?: Uint8Array;
2217
+ signature?: Uint8Array;
2218
+ }, correlationId?: string): Promise<void>;
2219
+ /**
2220
+ * Test seam: deliver an inbound salt frame, exactly as the content-stream decoder does.
2221
+ *
2222
+ * 006-CRYPTO finding 2. WHICH of the four reasons the peer gave decides what the operator is told,
2223
+ * and reaching that decision from a test otherwise needs a second live daemon that has closed
2224
+ * adoption for a specific reason — which is not something a counterparty can be asked to do on
2225
+ * demand. The four labels are the whole point of the finding, so they need to be reachable.
2226
+ *
2227
+ * It calls the REAL private handler rather than reproducing its routing, so a test cannot pass
2228
+ * against a decision production does not make. It takes the DECODED frame, so it deliberately
2229
+ * does NOT stand in for the decoder above it — the length and vocabulary checks there have their
2230
+ * own tests driving `handleContentFrameForTest`.
2231
+ */
2232
+ handleSaltFrameForTest(agentName: string, sessionId: string, frame: SaltAgreementFrame, correlationId?: string): Promise<void>;
1910
2233
  /**
1911
2234
  * Test seam: drop this session's own half while leaving the stored salt in place — the state every
1912
2235
  * teardown produces, because `#evictSessionCaches` clears the map and the row survives.
@@ -1995,7 +2318,49 @@ export declare class SessionNodeManager {
1995
2318
  * Read-only and cache-backed, so exposing it adds no way to CHANGE the salt from outside — the
1996
2319
  * only writer remains `#persistSessionSalt`, behind the one-salt-per-session predicate.
1997
2320
  */
2321
+ /**
2322
+ * IS THIS SESSION ACTUALLY PROTECTED BY ITS SALT RIGHT NOW — pass 2, F3.
2323
+ *
2324
+ * Distinct from `getSessionContentSalt`, which is POSSESSION and is what the verifier needs: a
2325
+ * message parked before suspension was hashed under this salt and must still be checkable against
2326
+ * it, so that accessor must keep answering with the bytes.
2327
+ *
2328
+ * This one answers the OPERATOR's question, and it is a different question. A suspended session
2329
+ * holds a salt it will not use, so every hash it produces is `sha256` — reporting `contentSalted:
2330
+ * true` there is not a gap, it is an affirmatively false security claim on the surface whose own
2331
+ * comment reads *"a security property must not be inferable from a gap."* Same predicate
2332
+ * `#saltForHashing` uses, so the flag cannot drift from the behaviour it describes.
2333
+ */
2334
+ isContentSaltActive(agentName: string, sessionId: string): boolean;
1998
2335
  getSessionContentSalt(agentName: string, sessionId: string): Uint8Array | null;
2336
+ /**
2337
+ * THE SALT, OR WHY THERE ISN'T ONE — `DOD-M15-INCLUSION-1`, fallback-finder finding 2.
2338
+ *
2339
+ * `getSessionContentSalt` above answers `null` for THREE different situations, and a caller that
2340
+ * turns that null into a sentence for an operator gets two of them wrong:
2341
+ *
2342
+ * `none` — no salt was ever agreed. The session really is unsalted.
2343
+ * `unreadable` — a salt row EXISTS and could not be used: the wrong width (corruption on this
2344
+ * operator's own disk), or the read threw.
2345
+ *
2346
+ * The distinction is not cosmetic. `unreadable` means the session's leaves WERE hashed under a
2347
+ * salt, so telling its operator *"this session's content hashes are UNSALTED … start a session
2348
+ * while you are both connected"* is an affirmatively false statement about a security property
2349
+ * their session has, and it points them at their counterparty over damage to their own database.
2350
+ * That is the same defect `#getSessionSalt`'s own F8 note was written to end, re-committed one
2351
+ * surface out.
2352
+ *
2353
+ * ⚠️ IT DELEGATES — there is no second read here. Calling `#getSessionSalt` first means the salt
2354
+ * this reports is the salt the hashing path uses, including its cache and its wrong-width refusal.
2355
+ * A parallel query would be free to disagree with it, which is the whole failure this returns a
2356
+ * reason to prevent.
2357
+ */
2358
+ getSessionContentSaltState(agentName: string, sessionId: string): {
2359
+ salt: Uint8Array;
2360
+ } | {
2361
+ salt: null;
2362
+ reason: "none" | "unreadable";
2363
+ };
1999
2364
  /**
2000
2365
  * M7-SESSION-001 AC-004/AC-005: Register a relay stream for an active session.
2001
2366
  * Starts a background reader that watches for session_interrupted frames and
@@ -2012,6 +2377,19 @@ export declare class SessionNodeManager {
2012
2377
  * receipt may differ, but this is the value available at stream setup time)
2013
2378
  */
2014
2379
  registerRelayStream(agentName: string, sessionId: string, stream: Stream, messageCount?: number): void;
2380
+ /**
2381
+ * DOD-M15-RELAYSLOTS-1: accept the directory's online token for an agent. Called on every
2382
+ * signaling connect and reconnect, which is what keeps it fresh.
2383
+ */
2384
+ setDirectoryOnlineToken(agentName: string, token: Uint8Array): void;
2385
+ setDirectoryOnlineTokenAbsent(agentName: string, reason: "not_registered_here" | "issue_failed" | undefined): void;
2386
+ /**
2387
+ * The current token, or `undefined` when the directory has not issued one — either no directory
2388
+ * connection yet, or this key has no agent profile there. Undefined is a real answer, not a
2389
+ * missing one: the relay refuses without a token, which is the intended outcome for a key the
2390
+ * directory does not recognise.
2391
+ */
2392
+ getDirectoryOnlineToken(agentName: string): Uint8Array | undefined;
2015
2393
  /**
2016
2394
  * DOD-NAT-REACHABILITY-1 (Phase 2): accept the directory's relay-pool endpoints
2017
2395
  * for an agent (arrives with signaling_auth_ok, i.e. on every connect AND every
@@ -2024,6 +2402,11 @@ export declare class SessionNodeManager {
2024
2402
  relayPeerId: string;
2025
2403
  relayAddrs: string[];
2026
2404
  }>): void;
2405
+ /**
2406
+ * Is this agent currently skipping this relay? The observable half of the failover decision — a
2407
+ * test that asserts only on the classifier's boolean proves nothing about what the daemon does.
2408
+ */
2409
+ isRelayQuarantined(agentName: string, relayPeerId: string): boolean;
2027
2410
  /**
2028
2411
  * DOD-M12B-SESSION-SEED-1 — bring an interrupted session back on the peer id it already has.
2029
2412
  *