@cello-protocol/daemon 0.0.227 → 0.0.229
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-admin-handlers.d.ts +56 -0
- package/dist/agent-admin-handlers.d.ts.map +1 -0
- package/dist/agent-admin-handlers.js +73 -0
- package/dist/agent-admin-handlers.js.map +1 -0
- package/dist/agent-handlers.d.ts +72 -0
- package/dist/agent-handlers.d.ts.map +1 -0
- package/dist/agent-handlers.js +604 -0
- package/dist/agent-handlers.js.map +1 -0
- package/dist/agent-id-migration.d.ts +70 -0
- package/dist/agent-id-migration.d.ts.map +1 -0
- package/dist/agent-id-migration.js +468 -0
- package/dist/agent-id-migration.js.map +1 -0
- package/dist/agent-loader.d.ts +37 -0
- package/dist/agent-loader.d.ts.map +1 -0
- package/dist/agent-loader.js +37 -0
- package/dist/agent-loader.js.map +1 -0
- package/dist/agent-selection-root.d.ts +29 -0
- package/dist/agent-selection-root.d.ts.map +1 -0
- package/dist/agent-selection-root.js +126 -0
- package/dist/agent-selection-root.js.map +1 -0
- package/dist/agent-selection.d.ts +73 -0
- package/dist/agent-selection.d.ts.map +1 -0
- package/dist/agent-selection.js +80 -0
- package/dist/agent-selection.js.map +1 -0
- package/dist/agent-settings-keys.d.ts +52 -0
- package/dist/agent-settings-keys.d.ts.map +1 -0
- package/dist/agent-settings-keys.js +114 -0
- package/dist/agent-settings-keys.js.map +1 -0
- package/dist/agent-state.d.ts +67 -0
- package/dist/agent-state.d.ts.map +1 -0
- package/dist/agent-state.js +66 -0
- package/dist/agent-state.js.map +1 -0
- package/dist/assignment-verify.d.ts +90 -0
- package/dist/assignment-verify.d.ts.map +1 -0
- package/dist/assignment-verify.js +301 -0
- package/dist/assignment-verify.js.map +1 -0
- package/dist/attendance-wiring.d.ts +23 -0
- package/dist/attendance-wiring.d.ts.map +1 -0
- package/dist/attendance-wiring.js +248 -0
- package/dist/attendance-wiring.js.map +1 -0
- package/dist/attestation-body.d.ts +47 -0
- package/dist/attestation-body.d.ts.map +1 -0
- package/dist/attestation-body.js +71 -0
- package/dist/attestation-body.js.map +1 -0
- package/dist/authorship-verification.d.ts +91 -0
- package/dist/authorship-verification.d.ts.map +1 -0
- package/dist/authorship-verification.js +532 -0
- package/dist/authorship-verification.js.map +1 -0
- package/dist/away-detection.d.ts +139 -0
- package/dist/away-detection.d.ts.map +1 -0
- package/dist/away-detection.js +186 -0
- package/dist/away-detection.js.map +1 -0
- package/dist/away-inbox-oneshot.d.ts +40 -0
- package/dist/away-inbox-oneshot.d.ts.map +1 -0
- package/dist/away-inbox-oneshot.js +311 -0
- package/dist/away-inbox-oneshot.js.map +1 -0
- package/dist/backup-restore-handlers.d.ts +10 -0
- package/dist/backup-restore-handlers.d.ts.map +1 -0
- package/dist/backup-restore-handlers.js +82 -0
- package/dist/backup-restore-handlers.js.map +1 -0
- package/dist/backup-restore.d.ts +102 -0
- package/dist/backup-restore.d.ts.map +1 -0
- package/dist/backup-restore.js +341 -0
- package/dist/backup-restore.js.map +1 -0
- package/dist/bin/cello-daemon.d.ts +13 -0
- package/dist/bin/cello-daemon.d.ts.map +1 -0
- package/dist/bin/cello-daemon.js.map +1 -0
- package/dist/boot-agents.d.ts +64 -0
- package/dist/boot-agents.d.ts.map +1 -0
- package/dist/boot-agents.js +241 -0
- package/dist/boot-agents.js.map +1 -0
- package/dist/boot-connection-state.d.ts +38 -0
- package/dist/boot-connection-state.d.ts.map +1 -0
- package/dist/boot-connection-state.js +95 -0
- package/dist/boot-connection-state.js.map +1 -0
- package/dist/boot-core.d.ts +36 -0
- package/dist/boot-core.d.ts.map +1 -0
- package/dist/boot-core.js +293 -0
- package/dist/boot-core.js.map +1 -0
- package/dist/boot-parked-content.d.ts +34 -0
- package/dist/boot-parked-content.d.ts.map +1 -0
- package/dist/boot-parked-content.js +528 -0
- package/dist/boot-parked-content.js.map +1 -0
- package/dist/boot-sweeps.d.ts +22 -0
- package/dist/boot-sweeps.d.ts.map +1 -0
- package/dist/boot-sweeps.js +51 -0
- package/dist/boot-sweeps.js.map +1 -0
- package/dist/bundled-consortium-manifest.d.ts +47 -0
- package/dist/bundled-consortium-manifest.d.ts.map +1 -0
- package/dist/bundled-consortium-manifest.js +116 -0
- package/dist/bundled-consortium-manifest.js.map +1 -0
- package/dist/cbor-blob-migration.d.ts +8 -0
- package/dist/cbor-blob-migration.d.ts.map +1 -0
- package/dist/cbor-blob-migration.js +116 -0
- package/dist/cbor-blob-migration.js.map +1 -0
- package/dist/challenge-verifier.d.ts +12 -0
- package/dist/challenge-verifier.d.ts.map +1 -0
- package/dist/challenge-verifier.js +11 -0
- package/dist/challenge-verifier.js.map +1 -0
- package/dist/channel-log-store.d.ts +80 -0
- package/dist/channel-log-store.d.ts.map +1 -0
- package/dist/channel-log-store.js +290 -0
- package/dist/channel-log-store.js.map +1 -0
- package/dist/close-commitment.d.ts +67 -0
- package/dist/close-commitment.d.ts.map +1 -0
- package/dist/close-commitment.js +78 -0
- package/dist/close-commitment.js.map +1 -0
- package/dist/close-session-handler.d.ts +94 -0
- package/dist/close-session-handler.d.ts.map +1 -0
- package/dist/close-session-handler.js +1418 -0
- package/dist/close-session-handler.js.map +1 -0
- package/dist/co-attendance.d.ts +29 -0
- package/dist/co-attendance.d.ts.map +1 -0
- package/dist/co-attendance.js +32 -0
- package/dist/co-attendance.js.map +1 -0
- package/dist/column-birth.d.ts +40 -0
- package/dist/column-birth.d.ts.map +1 -0
- package/dist/column-birth.js +61 -0
- package/dist/column-birth.js.map +1 -0
- package/dist/connect-or-start.d.ts +25 -0
- package/dist/connect-or-start.d.ts.map +1 -0
- package/dist/connect-or-start.js +175 -0
- package/dist/connect-or-start.js.map +1 -0
- package/dist/connection-agents.d.ts +16 -0
- package/dist/connection-agents.d.ts.map +1 -0
- package/dist/connection-agents.js +68 -0
- package/dist/connection-agents.js.map +1 -0
- package/dist/consent-migration.d.ts +49 -0
- package/dist/consent-migration.d.ts.map +1 -0
- package/dist/consent-migration.js +128 -0
- package/dist/consent-migration.js.map +1 -0
- package/dist/consortium-bootstrap.d.ts +138 -0
- package/dist/consortium-bootstrap.d.ts.map +1 -0
- package/dist/consortium-bootstrap.js +339 -0
- package/dist/consortium-bootstrap.js.map +1 -0
- package/dist/consortium-fingerprint.d.ts +115 -0
- package/dist/consortium-fingerprint.d.ts.map +1 -0
- package/dist/consortium-fingerprint.js +175 -0
- package/dist/consortium-fingerprint.js.map +1 -0
- package/dist/contact-handlers.d.ts +59 -0
- package/dist/contact-handlers.d.ts.map +1 -0
- package/dist/contact-handlers.js +343 -0
- package/dist/contact-handlers.js.map +1 -0
- package/dist/contact-pubkey-case.d.ts +65 -0
- package/dist/contact-pubkey-case.d.ts.map +1 -0
- package/dist/contact-pubkey-case.js +136 -0
- package/dist/contact-pubkey-case.js.map +1 -0
- package/dist/contacts-tier-migration.d.ts +90 -0
- package/dist/contacts-tier-migration.d.ts.map +1 -0
- package/dist/contacts-tier-migration.js +150 -0
- package/dist/contacts-tier-migration.js.map +1 -0
- package/dist/content-encryption-status.d.ts +111 -0
- package/dist/content-encryption-status.d.ts.map +1 -0
- package/dist/content-encryption-status.js +158 -0
- package/dist/content-encryption-status.js.map +1 -0
- package/dist/content-park-client.d.ts +90 -0
- package/dist/content-park-client.d.ts.map +1 -0
- package/dist/content-park-client.js +362 -0
- package/dist/content-park-client.js.map +1 -0
- package/dist/content-park.d.ts +52 -0
- package/dist/content-park.d.ts.map +1 -0
- package/dist/content-park.js +1309 -0
- package/dist/content-park.js.map +1 -0
- package/dist/cross-node-negotiation.d.ts +44 -0
- package/dist/cross-node-negotiation.d.ts.map +1 -0
- package/dist/cross-node-negotiation.js +34 -0
- package/dist/cross-node-negotiation.js.map +1 -0
- package/dist/daemon-handle.d.ts +57 -0
- package/dist/daemon-handle.d.ts.map +1 -0
- package/dist/daemon-handle.js +2 -0
- package/dist/daemon-handle.js.map +1 -0
- package/dist/daemon-status-report.d.ts +50 -0
- package/dist/daemon-status-report.d.ts.map +1 -0
- package/dist/daemon-status-report.js +81 -0
- package/dist/daemon-status-report.js.map +1 -0
- package/dist/daemon.d.ts +45 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +1177 -0
- package/dist/daemon.js.map +1 -0
- package/dist/db-identity-store.d.ts +159 -0
- package/dist/db-identity-store.d.ts.map +1 -0
- package/dist/db-identity-store.js +502 -0
- package/dist/db-identity-store.js.map +1 -0
- package/dist/delivery-open-registry.d.ts +92 -0
- package/dist/delivery-open-registry.d.ts.map +1 -0
- package/dist/delivery-open-registry.js +121 -0
- package/dist/delivery-open-registry.js.map +1 -0
- package/dist/delivery-session-suspects.d.ts +56 -0
- package/dist/delivery-session-suspects.d.ts.map +1 -0
- package/dist/delivery-session-suspects.js +94 -0
- package/dist/delivery-session-suspects.js.map +1 -0
- package/dist/directory-auth-posture.d.ts +87 -0
- package/dist/directory-auth-posture.d.ts.map +1 -0
- package/dist/directory-auth-posture.js +134 -0
- package/dist/directory-auth-posture.js.map +1 -0
- package/dist/directory-bootstrap.d.ts +310 -0
- package/dist/directory-bootstrap.d.ts.map +1 -0
- package/dist/directory-bootstrap.js +557 -0
- package/dist/directory-bootstrap.js.map +1 -0
- package/dist/directory-connect.d.ts +27 -0
- package/dist/directory-connect.d.ts.map +1 -0
- package/dist/directory-connect.js +106 -0
- package/dist/directory-connect.js.map +1 -0
- package/dist/disconnect-cleanup.d.ts +51 -0
- package/dist/disconnect-cleanup.d.ts.map +1 -0
- package/dist/disconnect-cleanup.js +72 -0
- package/dist/disconnect-cleanup.js.map +1 -0
- package/dist/document-amendment-store.d.ts +120 -0
- package/dist/document-amendment-store.d.ts.map +1 -0
- package/dist/document-amendment-store.js +266 -0
- package/dist/document-amendment-store.js.map +1 -0
- package/dist/document-delivery-transport.d.ts +168 -0
- package/dist/document-delivery-transport.d.ts.map +1 -0
- package/dist/document-delivery-transport.js +206 -0
- package/dist/document-delivery-transport.js.map +1 -0
- package/dist/document-engine.d.ts +134 -0
- package/dist/document-engine.d.ts.map +1 -0
- package/dist/document-engine.js +282 -0
- package/dist/document-engine.js.map +1 -0
- package/dist/document-flag.d.ts +58 -0
- package/dist/document-flag.d.ts.map +1 -0
- package/dist/document-flag.js +70 -0
- package/dist/document-flag.js.map +1 -0
- package/dist/document-frame-router.d.ts +245 -0
- package/dist/document-frame-router.d.ts.map +1 -0
- package/dist/document-frame-router.js +397 -0
- package/dist/document-frame-router.js.map +1 -0
- package/dist/document-gate-wiring.d.ts +61 -0
- package/dist/document-gate-wiring.d.ts.map +1 -0
- package/dist/document-gate-wiring.js +125 -0
- package/dist/document-gate-wiring.js.map +1 -0
- package/dist/document-gate.d.ts +149 -0
- package/dist/document-gate.d.ts.map +1 -0
- package/dist/document-gate.js +509 -0
- package/dist/document-gate.js.map +1 -0
- package/dist/document-handlers.d.ts +47 -0
- package/dist/document-handlers.d.ts.map +1 -0
- package/dist/document-handlers.js +2203 -0
- package/dist/document-handlers.js.map +1 -0
- package/dist/document-handshake.d.ts +176 -0
- package/dist/document-handshake.d.ts.map +1 -0
- package/dist/document-handshake.js +452 -0
- package/dist/document-handshake.js.map +1 -0
- package/dist/document-inbound.d.ts +162 -0
- package/dist/document-inbound.d.ts.map +1 -0
- package/dist/document-inbound.js +530 -0
- package/dist/document-inbound.js.map +1 -0
- package/dist/document-json.d.ts +120 -0
- package/dist/document-json.d.ts.map +1 -0
- package/dist/document-json.js +191 -0
- package/dist/document-json.js.map +1 -0
- package/dist/document-layer.d.ts +215 -0
- package/dist/document-layer.d.ts.map +1 -0
- package/dist/document-layer.js +1025 -0
- package/dist/document-layer.js.map +1 -0
- package/dist/document-lifecycle.d.ts +52 -0
- package/dist/document-lifecycle.d.ts.map +1 -0
- package/dist/document-lifecycle.js +134 -0
- package/dist/document-lifecycle.js.map +1 -0
- package/dist/document-live-docs.d.ts +58 -0
- package/dist/document-live-docs.d.ts.map +1 -0
- package/dist/document-live-docs.js +126 -0
- package/dist/document-live-docs.js.map +1 -0
- package/dist/document-notify.d.ts +228 -0
- package/dist/document-notify.d.ts.map +1 -0
- package/dist/document-notify.js +580 -0
- package/dist/document-notify.js.map +1 -0
- package/dist/document-profile.d.ts +61 -0
- package/dist/document-profile.d.ts.map +1 -0
- package/dist/document-profile.js +112 -0
- package/dist/document-profile.js.map +1 -0
- package/dist/document-publish.d.ts +87 -0
- package/dist/document-publish.d.ts.map +1 -0
- package/dist/document-publish.js +173 -0
- package/dist/document-publish.js.map +1 -0
- package/dist/document-reachability.d.ts +42 -0
- package/dist/document-reachability.d.ts.map +1 -0
- package/dist/document-reachability.js +80 -0
- package/dist/document-reachability.js.map +1 -0
- package/dist/document-reconcile-engine.d.ts +66 -0
- package/dist/document-reconcile-engine.d.ts.map +1 -0
- package/dist/document-reconcile-engine.js +225 -0
- package/dist/document-reconcile-engine.js.map +1 -0
- package/dist/document-reconcile-scheduler.d.ts +163 -0
- package/dist/document-reconcile-scheduler.d.ts.map +1 -0
- package/dist/document-reconcile-scheduler.js +303 -0
- package/dist/document-reconcile-scheduler.js.map +1 -0
- package/dist/document-rejection.d.ts +251 -0
- package/dist/document-rejection.d.ts.map +1 -0
- package/dist/document-rejection.js +435 -0
- package/dist/document-rejection.js.map +1 -0
- package/dist/document-screen.d.ts +114 -0
- package/dist/document-screen.d.ts.map +1 -0
- package/dist/document-screen.js +223 -0
- package/dist/document-screen.js.map +1 -0
- package/dist/document-store.d.ts +372 -0
- package/dist/document-store.d.ts.map +1 -0
- package/dist/document-store.js +931 -0
- package/dist/document-store.js.map +1 -0
- package/dist/document-surface.d.ts +32 -0
- package/dist/document-surface.d.ts.map +1 -0
- package/dist/document-surface.js +151 -0
- package/dist/document-surface.js.map +1 -0
- package/dist/document-types.d.ts +94 -0
- package/dist/document-types.d.ts.map +1 -0
- package/dist/document-types.js +90 -0
- package/dist/document-types.js.map +1 -0
- package/dist/document-watch.d.ts +69 -0
- package/dist/document-watch.d.ts.map +1 -0
- package/dist/document-watch.js +108 -0
- package/dist/document-watch.js.map +1 -0
- package/dist/document-wiring.d.ts +53 -0
- package/dist/document-wiring.d.ts.map +1 -0
- package/dist/document-wiring.js +323 -0
- package/dist/document-wiring.js.map +1 -0
- package/dist/document-write-guard.d.ts +66 -0
- package/dist/document-write-guard.d.ts.map +1 -0
- package/dist/document-write-guard.js +98 -0
- package/dist/document-write-guard.js.map +1 -0
- package/dist/document-write-path.d.ts +94 -0
- package/dist/document-write-path.d.ts.map +1 -0
- package/dist/document-write-path.js +530 -0
- package/dist/document-write-path.js.map +1 -0
- package/dist/error-message.d.ts +7 -0
- package/dist/error-message.d.ts.map +1 -0
- package/dist/error-message.js +19 -0
- package/dist/error-message.js.map +1 -0
- package/dist/file-manifest-provider.d.ts +37 -0
- package/dist/file-manifest-provider.d.ts.map +1 -0
- package/dist/file-manifest-provider.js +105 -0
- package/dist/file-manifest-provider.js.map +1 -0
- package/dist/frame-values.d.ts +3 -0
- package/dist/frame-values.d.ts.map +1 -0
- package/dist/frame-values.js +84 -0
- package/dist/frame-values.js.map +1 -0
- package/dist/frontier-mismatch.d.ts +73 -0
- package/dist/frontier-mismatch.d.ts.map +1 -0
- package/dist/frontier-mismatch.js +89 -0
- package/dist/frontier-mismatch.js.map +1 -0
- package/dist/gateway-config-handlers.d.ts +26 -0
- package/dist/gateway-config-handlers.d.ts.map +1 -0
- package/dist/gateway-config-handlers.js +454 -0
- package/dist/gateway-config-handlers.js.map +1 -0
- package/dist/held-content.d.ts +145 -0
- package/dist/held-content.d.ts.map +1 -0
- package/dist/held-content.js +389 -0
- package/dist/held-content.js.map +1 -0
- package/dist/http-manifest-poll.d.ts +67 -0
- package/dist/http-manifest-poll.d.ts.map +1 -0
- package/dist/http-manifest-poll.js +150 -0
- package/dist/http-manifest-poll.js.map +1 -0
- package/dist/identity-migration.d.ts +40 -0
- package/dist/identity-migration.d.ts.map +1 -0
- package/dist/identity-migration.js +461 -0
- package/dist/identity-migration.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-seal-request.d.ts +32 -0
- package/dist/inbound-seal-request.d.ts.map +1 -0
- package/dist/inbound-seal-request.js +228 -0
- package/dist/inbound-seal-request.js.map +1 -0
- package/dist/inbound-sessions.d.ts +291 -0
- package/dist/inbound-sessions.d.ts.map +1 -0
- package/dist/inbound-sessions.js +1609 -0
- package/dist/inbound-sessions.js.map +1 -0
- package/dist/inclusion-proof-handlers.d.ts +43 -0
- package/dist/inclusion-proof-handlers.d.ts.map +1 -0
- package/dist/inclusion-proof-handlers.js +565 -0
- package/dist/inclusion-proof-handlers.js.map +1 -0
- package/dist/inclusion-proof.d.ts +151 -0
- package/dist/inclusion-proof.d.ts.map +1 -0
- package/dist/inclusion-proof.js +228 -0
- package/dist/inclusion-proof.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js.map +1 -0
- package/dist/initiate-session-handler.d.ts +55 -0
- package/dist/initiate-session-handler.d.ts.map +1 -0
- package/dist/initiate-session-handler.js +310 -0
- package/dist/initiate-session-handler.js.map +1 -0
- package/dist/ipc-client.d.ts +31 -0
- package/dist/ipc-client.d.ts.map +1 -0
- package/dist/ipc-client.js +113 -0
- package/dist/ipc-client.js.map +1 -0
- package/dist/ipc-server.d.ts +63 -0
- package/dist/ipc-server.d.ts.map +1 -0
- package/dist/ipc-server.js +429 -0
- package/dist/ipc-server.js.map +1 -0
- package/dist/ipc-surface.d.ts +44 -0
- package/dist/ipc-surface.d.ts.map +1 -0
- package/dist/ipc-surface.js +107 -0
- package/dist/ipc-surface.js.map +1 -0
- package/dist/line-lcs.d.ts +51 -0
- package/dist/line-lcs.d.ts.map +1 -0
- package/dist/line-lcs.js +71 -0
- package/dist/line-lcs.js.map +1 -0
- package/dist/lock-file.d.ts +39 -0
- package/dist/lock-file.d.ts.map +1 -0
- package/dist/lock-file.js +120 -0
- package/dist/lock-file.js.map +1 -0
- package/dist/log-collapse.d.ts +66 -0
- package/dist/log-collapse.d.ts.map +1 -0
- package/dist/log-collapse.js +244 -0
- package/dist/log-collapse.js.map +1 -0
- package/dist/log-rotate.d.ts +67 -0
- package/dist/log-rotate.d.ts.map +1 -0
- package/dist/log-rotate.js +134 -0
- package/dist/log-rotate.js.map +1 -0
- package/dist/manifest-deps.d.ts +25 -0
- package/dist/manifest-deps.d.ts.map +1 -0
- package/dist/manifest-deps.js +151 -0
- package/dist/manifest-deps.js.map +1 -0
- package/dist/manifest-poll-scheduler.d.ts +31 -0
- package/dist/manifest-poll-scheduler.d.ts.map +1 -0
- package/dist/manifest-poll-scheduler.js +59 -0
- package/dist/manifest-poll-scheduler.js.map +1 -0
- package/dist/manifest-validity.d.ts +153 -0
- package/dist/manifest-validity.d.ts.map +1 -0
- package/dist/manifest-validity.js +268 -0
- package/dist/manifest-validity.js.map +1 -0
- package/dist/manifest-version-store-db.d.ts +24 -0
- package/dist/manifest-version-store-db.d.ts.map +1 -0
- package/dist/manifest-version-store-db.js +58 -0
- package/dist/manifest-version-store-db.js.map +1 -0
- package/dist/manifest-version-store.d.ts +16 -0
- package/dist/manifest-version-store.d.ts.map +1 -0
- package/dist/manifest-version-store.js +15 -0
- package/dist/manifest-version-store.js.map +1 -0
- package/dist/network-directory-node.d.ts +136 -0
- package/dist/network-directory-node.d.ts.map +1 -0
- package/dist/network-directory-node.js +810 -0
- package/dist/network-directory-node.js.map +1 -0
- package/dist/nonce-dedup.d.ts +68 -0
- package/dist/nonce-dedup.d.ts.map +1 -0
- package/dist/nonce-dedup.js +205 -0
- package/dist/nonce-dedup.js.map +1 -0
- package/dist/notification-dispatcher.d.ts +92 -0
- package/dist/notification-dispatcher.d.ts.map +1 -0
- package/dist/notification-dispatcher.js +210 -0
- package/dist/notification-dispatcher.js.map +1 -0
- package/dist/notification-handlers.d.ts +53 -0
- package/dist/notification-handlers.d.ts.map +1 -0
- package/dist/notification-handlers.js +461 -0
- package/dist/notification-handlers.js.map +1 -0
- package/dist/onboarding-guidance.d.ts +79 -0
- package/dist/onboarding-guidance.d.ts.map +1 -0
- package/dist/onboarding-guidance.js +95 -0
- package/dist/onboarding-guidance.js.map +1 -0
- package/dist/operator-guidance.d.ts +25 -0
- package/dist/operator-guidance.d.ts.map +1 -0
- package/dist/operator-guidance.js +50 -0
- package/dist/operator-guidance.js.map +1 -0
- package/dist/orphan-triage.d.ts +130 -0
- package/dist/orphan-triage.d.ts.map +1 -0
- package/dist/orphan-triage.js +207 -0
- package/dist/orphan-triage.js.map +1 -0
- package/dist/outbound-sessions.d.ts +126 -0
- package/dist/outbound-sessions.d.ts.map +1 -0
- package/dist/outbound-sessions.js +1052 -0
- package/dist/outbound-sessions.js.map +1 -0
- package/dist/park-envelope.d.ts +329 -0
- package/dist/park-envelope.d.ts.map +1 -0
- package/dist/park-envelope.js +509 -0
- package/dist/park-envelope.js.map +1 -0
- package/dist/park-recovery.d.ts +273 -0
- package/dist/park-recovery.d.ts.map +1 -0
- package/dist/park-recovery.js +717 -0
- package/dist/park-recovery.js.map +1 -0
- package/dist/park-refusals.d.ts +147 -0
- package/dist/park-refusals.d.ts.map +1 -0
- package/dist/park-refusals.js +331 -0
- package/dist/park-refusals.js.map +1 -0
- package/dist/quarantine-framing.d.ts +92 -0
- package/dist/quarantine-framing.d.ts.map +1 -0
- package/dist/quarantine-framing.js +111 -0
- package/dist/quarantine-framing.js.map +1 -0
- package/dist/reconnect-drain.d.ts +22 -0
- package/dist/reconnect-drain.d.ts.map +1 -0
- package/dist/reconnect-drain.js +65 -0
- package/dist/reconnect-drain.js.map +1 -0
- package/dist/recovered-position.d.ts +14 -0
- package/dist/recovered-position.d.ts.map +1 -0
- package/dist/recovered-position.js +59 -0
- package/dist/recovered-position.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 +516 -0
- package/dist/refusal-notices.js.map +1 -0
- package/dist/refusal-reasons.d.ts +245 -0
- package/dist/refusal-reasons.d.ts.map +1 -0
- package/dist/refusal-reasons.js +363 -0
- package/dist/refusal-reasons.js.map +1 -0
- package/dist/register-handler.d.ts +36 -0
- package/dist/register-handler.d.ts.map +1 -0
- package/dist/register-handler.js +285 -0
- package/dist/register-handler.js.map +1 -0
- package/dist/registration-context.d.ts +72 -0
- package/dist/registration-context.d.ts.map +1 -0
- package/dist/registration-context.js +126 -0
- package/dist/registration-context.js.map +1 -0
- package/dist/registration-manager.d.ts +94 -0
- package/dist/registration-manager.d.ts.map +1 -0
- package/dist/registration-manager.js +585 -0
- package/dist/registration-manager.js.map +1 -0
- package/dist/registration-persistence.d.ts +183 -0
- package/dist/registration-persistence.d.ts.map +1 -0
- package/dist/registration-persistence.js +263 -0
- package/dist/registration-persistence.js.map +1 -0
- package/dist/registry-poll.d.ts +52 -0
- package/dist/registry-poll.d.ts.map +1 -0
- package/dist/registry-poll.js +140 -0
- package/dist/registry-poll.js.map +1 -0
- package/dist/registry-version-store-db.d.ts +22 -0
- package/dist/registry-version-store-db.d.ts.map +1 -0
- package/dist/registry-version-store-db.js +50 -0
- package/dist/registry-version-store-db.js.map +1 -0
- package/dist/relay-endpoints.d.ts +18 -0
- package/dist/relay-endpoints.d.ts.map +1 -0
- package/dist/relay-endpoints.js +9 -0
- package/dist/relay-endpoints.js.map +1 -0
- package/dist/relay-only.d.ts +140 -0
- package/dist/relay-only.d.ts.map +1 -0
- package/dist/relay-only.js +193 -0
- package/dist/relay-only.js.map +1 -0
- package/dist/relay-receipt-store.d.ts +155 -0
- package/dist/relay-receipt-store.d.ts.map +1 -0
- package/dist/relay-receipt-store.js +284 -0
- package/dist/relay-receipt-store.js.map +1 -0
- package/dist/relay-reconnect.d.ts +32 -0
- package/dist/relay-reconnect.d.ts.map +1 -0
- package/dist/relay-reconnect.js +29 -0
- package/dist/relay-reconnect.js.map +1 -0
- package/dist/reply-lag.d.ts +11 -0
- package/dist/reply-lag.d.ts.map +1 -0
- package/dist/reply-lag.js +46 -0
- package/dist/reply-lag.js.map +1 -0
- package/dist/resolve-named-agent.d.ts +49 -0
- package/dist/resolve-named-agent.d.ts.map +1 -0
- package/dist/resolve-named-agent.js +77 -0
- package/dist/resolve-named-agent.js.map +1 -0
- package/dist/restart-seal-resolver.d.ts +110 -0
- package/dist/restart-seal-resolver.d.ts.map +1 -0
- package/dist/restart-seal-resolver.js +353 -0
- package/dist/restart-seal-resolver.js.map +1 -0
- package/dist/resume-last-seen.d.ts +15 -0
- package/dist/resume-last-seen.d.ts.map +1 -0
- package/dist/resume-last-seen.js +44 -0
- package/dist/resume-last-seen.js.map +1 -0
- package/dist/retry-queue.d.ts +203 -0
- package/dist/retry-queue.d.ts.map +1 -0
- package/dist/retry-queue.js +701 -0
- package/dist/retry-queue.js.map +1 -0
- package/dist/roster-freshness.d.ts +160 -0
- package/dist/roster-freshness.d.ts.map +1 -0
- package/dist/roster-freshness.js +250 -0
- package/dist/roster-freshness.js.map +1 -0
- package/dist/screening-status.d.ts +15 -0
- package/dist/screening-status.d.ts.map +1 -0
- package/dist/screening-status.js +65 -0
- package/dist/screening-status.js.map +1 -0
- package/dist/seal-carried-close.d.ts +37 -0
- package/dist/seal-carried-close.d.ts.map +1 -0
- package/dist/seal-carried-close.js +179 -0
- package/dist/seal-carried-close.js.map +1 -0
- package/dist/seal-certificate-pull.d.ts +79 -0
- package/dist/seal-certificate-pull.d.ts.map +1 -0
- package/dist/seal-certificate-pull.js +186 -0
- package/dist/seal-certificate-pull.js.map +1 -0
- package/dist/seal-certified-root-check.d.ts +37 -0
- package/dist/seal-certified-root-check.d.ts.map +1 -0
- package/dist/seal-certified-root-check.js +127 -0
- package/dist/seal-certified-root-check.js.map +1 -0
- package/dist/seal-coordinator.d.ts +118 -0
- package/dist/seal-coordinator.d.ts.map +1 -0
- package/dist/seal-coordinator.js +927 -0
- package/dist/seal-coordinator.js.map +1 -0
- package/dist/seal-escalation.d.ts +80 -0
- package/dist/seal-escalation.d.ts.map +1 -0
- package/dist/seal-escalation.js +283 -0
- package/dist/seal-escalation.js.map +1 -0
- package/dist/seal-evidence-root-check.d.ts +70 -0
- package/dist/seal-evidence-root-check.d.ts.map +1 -0
- package/dist/seal-evidence-root-check.js +199 -0
- package/dist/seal-evidence-root-check.js.map +1 -0
- package/dist/seal-failure-store.d.ts +139 -0
- package/dist/seal-failure-store.d.ts.map +1 -0
- package/dist/seal-failure-store.js +207 -0
- package/dist/seal-failure-store.js.map +1 -0
- package/dist/seal-flows.d.ts +107 -0
- package/dist/seal-flows.d.ts.map +1 -0
- package/dist/seal-flows.js +638 -0
- package/dist/seal-flows.js.map +1 -0
- package/dist/seal-frontier-verify.d.ts +103 -0
- package/dist/seal-frontier-verify.d.ts.map +1 -0
- package/dist/seal-frontier-verify.js +143 -0
- package/dist/seal-frontier-verify.js.map +1 -0
- package/dist/seal-leaf.d.ts +58 -0
- package/dist/seal-leaf.d.ts.map +1 -0
- package/dist/seal-leaf.js +112 -0
- package/dist/seal-leaf.js.map +1 -0
- package/dist/seal-legibility-tbs.d.ts +25 -0
- package/dist/seal-legibility-tbs.d.ts.map +1 -0
- package/dist/seal-legibility-tbs.js +77 -0
- package/dist/seal-legibility-tbs.js.map +1 -0
- package/dist/seal-local-terminus.d.ts +41 -0
- package/dist/seal-local-terminus.d.ts.map +1 -0
- package/dist/seal-local-terminus.js +170 -0
- package/dist/seal-local-terminus.js.map +1 -0
- package/dist/seal-receipt-upgrade.d.ts +30 -0
- package/dist/seal-receipt-upgrade.d.ts.map +1 -0
- package/dist/seal-receipt-upgrade.js +49 -0
- package/dist/seal-receipt-upgrade.js.map +1 -0
- package/dist/seal-relay-silence.d.ts +51 -0
- package/dist/seal-relay-silence.d.ts.map +1 -0
- package/dist/seal-relay-silence.js +76 -0
- package/dist/seal-relay-silence.js.map +1 -0
- package/dist/seal-settle.d.ts +38 -0
- package/dist/seal-settle.d.ts.map +1 -0
- package/dist/seal-settle.js +33 -0
- package/dist/seal-settle.js.map +1 -0
- package/dist/seal-upgrade.d.ts +107 -0
- package/dist/seal-upgrade.d.ts.map +1 -0
- package/dist/seal-upgrade.js +200 -0
- package/dist/seal-upgrade.js.map +1 -0
- package/dist/sealed-conversation.d.ts +36 -0
- package/dist/sealed-conversation.d.ts.map +1 -0
- package/dist/sealed-conversation.js +171 -0
- package/dist/sealed-conversation.js.map +1 -0
- package/dist/sealed-leaf-set.d.ts +92 -0
- package/dist/sealed-leaf-set.d.ts.map +1 -0
- package/dist/sealed-leaf-set.js +122 -0
- package/dist/sealed-leaf-set.js.map +1 -0
- package/dist/send-claims.d.ts +56 -0
- package/dist/send-claims.d.ts.map +1 -0
- package/dist/send-claims.js +50 -0
- package/dist/send-claims.js.map +1 -0
- package/dist/session-assignment-parser.d.ts +104 -0
- package/dist/session-assignment-parser.d.ts.map +1 -0
- package/dist/session-assignment-parser.js +345 -0
- package/dist/session-assignment-parser.js.map +1 -0
- package/dist/session-category.d.ts +19 -0
- package/dist/session-category.d.ts.map +1 -0
- package/dist/session-category.js +14 -0
- package/dist/session-category.js.map +1 -0
- package/dist/session-ceremony.d.ts +297 -0
- package/dist/session-ceremony.d.ts.map +1 -0
- package/dist/session-ceremony.js +964 -0
- package/dist/session-ceremony.js.map +1 -0
- package/dist/session-closed.d.ts +79 -0
- package/dist/session-closed.d.ts.map +1 -0
- package/dist/session-closed.js +189 -0
- package/dist/session-closed.js.map +1 -0
- package/dist/session-connection-gater.d.ts +148 -0
- package/dist/session-connection-gater.d.ts.map +1 -0
- package/dist/session-connection-gater.js +332 -0
- package/dist/session-connection-gater.js.map +1 -0
- package/dist/session-content-context.d.ts +162 -0
- package/dist/session-content-context.d.ts.map +1 -0
- package/dist/session-content-context.js +2 -0
- package/dist/session-content-context.js.map +1 -0
- package/dist/session-content-handlers.d.ts +82 -0
- package/dist/session-content-handlers.d.ts.map +1 -0
- package/dist/session-content-handlers.js +1382 -0
- package/dist/session-content-handlers.js.map +1 -0
- package/dist/session-content-ingest.d.ts +206 -0
- package/dist/session-content-ingest.d.ts.map +1 -0
- package/dist/session-content-ingest.js +2167 -0
- package/dist/session-content-ingest.js.map +1 -0
- package/dist/session-content-send.d.ts +191 -0
- package/dist/session-content-send.d.ts.map +1 -0
- package/dist/session-content-send.js +1360 -0
- package/dist/session-content-send.js.map +1 -0
- package/dist/session-delivery-acks.d.ts +169 -0
- package/dist/session-delivery-acks.d.ts.map +1 -0
- package/dist/session-delivery-acks.js +569 -0
- package/dist/session-delivery-acks.js.map +1 -0
- package/dist/session-ephemerals.d.ts +279 -0
- package/dist/session-ephemerals.d.ts.map +1 -0
- package/dist/session-ephemerals.js +591 -0
- package/dist/session-ephemerals.js.map +1 -0
- package/dist/session-leaf-records.d.ts +159 -0
- package/dist/session-leaf-records.d.ts.map +1 -0
- package/dist/session-leaf-records.js +408 -0
- package/dist/session-leaf-records.js.map +1 -0
- package/dist/session-lifecycle.d.ts +303 -0
- package/dist/session-lifecycle.d.ts.map +1 -0
- package/dist/session-lifecycle.js +1679 -0
- package/dist/session-lifecycle.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-name.d.ts +35 -0
- package/dist/session-name.d.ts.map +1 -0
- package/dist/session-name.js +60 -0
- package/dist/session-name.js.map +1 -0
- package/dist/session-node-factory.d.ts +18 -0
- package/dist/session-node-factory.d.ts.map +1 -0
- package/dist/session-node-factory.js +182 -0
- package/dist/session-node-factory.js.map +1 -0
- package/dist/session-node-manager.d.ts +877 -0
- package/dist/session-node-manager.d.ts.map +1 -0
- package/dist/session-node-manager.js +2998 -0
- package/dist/session-node-manager.js.map +1 -0
- package/dist/session-node-types.d.ts +1057 -0
- package/dist/session-node-types.d.ts.map +1 -0
- package/dist/session-node-types.js +657 -0
- package/dist/session-node-types.js.map +1 -0
- package/dist/session-notify.d.ts +46 -0
- package/dist/session-notify.d.ts.map +1 -0
- package/dist/session-notify.js +116 -0
- package/dist/session-notify.js.map +1 -0
- package/dist/session-own-chain-store.d.ts +65 -0
- package/dist/session-own-chain-store.d.ts.map +1 -0
- package/dist/session-own-chain-store.js +75 -0
- package/dist/session-own-chain-store.js.map +1 -0
- package/dist/session-queries.d.ts +476 -0
- package/dist/session-queries.d.ts.map +1 -0
- package/dist/session-queries.js +1007 -0
- package/dist/session-queries.js.map +1 -0
- package/dist/session-read-handlers.d.ts +87 -0
- package/dist/session-read-handlers.d.ts.map +1 -0
- package/dist/session-read-handlers.js +689 -0
- package/dist/session-read-handlers.js.map +1 -0
- package/dist/session-records.d.ts +341 -0
- package/dist/session-records.d.ts.map +1 -0
- package/dist/session-records.js +858 -0
- package/dist/session-records.js.map +1 -0
- package/dist/session-relay-client.d.ts +659 -0
- package/dist/session-relay-client.d.ts.map +1 -0
- package/dist/session-relay-client.js +2876 -0
- package/dist/session-relay-client.js.map +1 -0
- package/dist/session-relay.d.ts +397 -0
- package/dist/session-relay.d.ts.map +1 -0
- package/dist/session-relay.js +1636 -0
- package/dist/session-relay.js.map +1 -0
- package/dist/session-salt-agreement.d.ts +331 -0
- package/dist/session-salt-agreement.d.ts.map +1 -0
- package/dist/session-salt-agreement.js +472 -0
- package/dist/session-salt-agreement.js.map +1 -0
- package/dist/session-salts.d.ts +432 -0
- package/dist/session-salts.d.ts.map +1 -0
- package/dist/session-salts.js +1540 -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 +877 -0
- package/dist/session-schema.js.map +1 -0
- package/dist/session-seal-leaf-store.d.ts +70 -0
- package/dist/session-seal-leaf-store.d.ts.map +1 -0
- package/dist/session-seal-leaf-store.js +105 -0
- package/dist/session-seal-leaf-store.js.map +1 -0
- package/dist/session-seal.d.ts +334 -0
- package/dist/session-seal.d.ts.map +1 -0
- package/dist/session-seal.js +1017 -0
- package/dist/session-seal.js.map +1 -0
- package/dist/session-terminal-refusal.d.ts +65 -0
- package/dist/session-terminal-refusal.d.ts.map +1 -0
- package/dist/session-terminal-refusal.js +87 -0
- package/dist/session-terminal-refusal.js.map +1 -0
- package/dist/session-tree.d.ts +110 -0
- package/dist/session-tree.d.ts.map +1 -0
- package/dist/session-tree.js +144 -0
- package/dist/session-tree.js.map +1 -0
- package/dist/session-views.d.ts +47 -0
- package/dist/session-views.d.ts.map +1 -0
- package/dist/session-views.js +278 -0
- package/dist/session-views.js.map +1 -0
- package/dist/signal-handlers.d.ts +63 -0
- package/dist/signal-handlers.d.ts.map +1 -0
- package/dist/signal-handlers.js +980 -0
- package/dist/signal-handlers.js.map +1 -0
- package/dist/signal-requirement-policy.d.ts +51 -0
- package/dist/signal-requirement-policy.d.ts.map +1 -0
- package/dist/signal-requirement-policy.js +89 -0
- package/dist/signal-requirement-policy.js.map +1 -0
- package/dist/signal-revocability.d.ts +49 -0
- package/dist/signal-revocability.d.ts.map +1 -0
- package/dist/signal-revocability.js +93 -0
- package/dist/signal-revocability.js.map +1 -0
- package/dist/signal-submission.d.ts +181 -0
- package/dist/signal-submission.d.ts.map +1 -0
- package/dist/signal-submission.js +368 -0
- package/dist/signal-submission.js.map +1 -0
- package/dist/signaling-connect.d.ts +118 -0
- package/dist/signaling-connect.d.ts.map +1 -0
- package/dist/signaling-connect.js +538 -0
- package/dist/signaling-connect.js.map +1 -0
- package/dist/signaling-wiring.d.ts +105 -0
- package/dist/signaling-wiring.d.ts.map +1 -0
- package/dist/signaling-wiring.js +393 -0
- package/dist/signaling-wiring.js.map +1 -0
- package/dist/singleton-lock.d.ts +85 -0
- package/dist/singleton-lock.d.ts.map +1 -0
- package/dist/singleton-lock.js +219 -0
- package/dist/singleton-lock.js.map +1 -0
- package/dist/sqlcipher-db.d.ts +139 -0
- package/dist/sqlcipher-db.d.ts.map +1 -0
- package/dist/sqlcipher-db.js +357 -0
- package/dist/sqlcipher-db.js.map +1 -0
- package/dist/standing-receivers.d.ts +319 -0
- package/dist/standing-receivers.d.ts.map +1 -0
- package/dist/standing-receivers.js +1295 -0
- package/dist/standing-receivers.js.map +1 -0
- package/dist/start-agent.d.ts +64 -0
- package/dist/start-agent.d.ts.map +1 -0
- package/dist/start-agent.js +136 -0
- package/dist/start-agent.js.map +1 -0
- package/dist/status-handler.d.ts +54 -0
- package/dist/status-handler.d.ts.map +1 -0
- package/dist/status-handler.js +61 -0
- package/dist/status-handler.js.map +1 -0
- package/dist/submission-retry.d.ts +208 -0
- package/dist/submission-retry.d.ts.map +1 -0
- package/dist/submission-retry.js +506 -0
- package/dist/submission-retry.js.map +1 -0
- package/dist/telegram-bot-client.d.ts +34 -0
- package/dist/telegram-bot-client.d.ts.map +1 -0
- package/dist/telegram-bot-client.js +36 -0
- package/dist/telegram-bot-client.js.map +1 -0
- package/dist/telegram-doorbell.d.ts +38 -0
- package/dist/telegram-doorbell.d.ts.map +1 -0
- package/dist/telegram-doorbell.js +140 -0
- package/dist/telegram-doorbell.js.map +1 -0
- package/dist/test-handlers.d.ts +62 -0
- package/dist/test-handlers.d.ts.map +1 -0
- package/dist/test-handlers.js +240 -0
- package/dist/test-handlers.js.map +1 -0
- package/dist/testing.d.ts +10 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +10 -0
- package/dist/testing.js.map +1 -0
- package/dist/transport-composition.d.ts +31 -0
- package/dist/transport-composition.d.ts.map +1 -0
- package/dist/transport-composition.js +55 -0
- package/dist/transport-composition.js.map +1 -0
- package/dist/transport-selector.d.ts +202 -0
- package/dist/transport-selector.d.ts.map +1 -0
- package/dist/transport-selector.js +196 -0
- package/dist/transport-selector.js.map +1 -0
- package/dist/trust-signal-pickup-listener.d.ts +43 -0
- package/dist/trust-signal-pickup-listener.d.ts.map +1 -0
- package/dist/trust-signal-pickup-listener.js +48 -0
- package/dist/trust-signal-pickup-listener.js.map +1 -0
- package/dist/trust-signal-store.d.ts +406 -0
- package/dist/trust-signal-store.d.ts.map +1 -0
- package/dist/trust-signal-store.js +939 -0
- package/dist/trust-signal-store.js.map +1 -0
- package/dist/trust-signal-sweep-tick.d.ts +60 -0
- package/dist/trust-signal-sweep-tick.d.ts.map +1 -0
- package/dist/trust-signal-sweep-tick.js +101 -0
- package/dist/trust-signal-sweep-tick.js.map +1 -0
- package/dist/trust-signal-sweep.d.ts +94 -0
- package/dist/trust-signal-sweep.d.ts.map +1 -0
- package/dist/trust-signal-sweep.js +147 -0
- package/dist/trust-signal-sweep.js.map +1 -0
- package/dist/type-registry.d.ts +42 -0
- package/dist/type-registry.d.ts.map +1 -0
- package/dist/type-registry.js +37 -0
- package/dist/type-registry.js.map +1 -0
- package/dist/types.d.ts +746 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +20 -0
- package/dist/types.js.map +1 -0
- package/dist/unresolved-nodes-report.d.ts +14 -0
- package/dist/unresolved-nodes-report.d.ts.map +1 -0
- package/dist/unresolved-nodes-report.js +96 -0
- package/dist/unresolved-nodes-report.js.map +1 -0
- package/dist/vocabulary.d.ts +150 -0
- package/dist/vocabulary.d.ts.map +1 -0
- package/dist/vocabulary.js +386 -0
- package/dist/vocabulary.js.map +1 -0
- package/dist/who-label.d.ts +28 -0
- package/dist/who-label.d.ts.map +1 -0
- package/dist/who-label.js +31 -0
- package/dist/who-label.js.map +1 -0
- package/dist/who-resolver.d.ts +15 -0
- package/dist/who-resolver.d.ts.map +1 -0
- package/dist/who-resolver.js +47 -0
- package/dist/who-resolver.js.map +1 -0
- package/dist/wire-content-hash.d.ts +93 -0
- package/dist/wire-content-hash.d.ts.map +1 -0
- package/dist/wire-content-hash.js +116 -0
- package/dist/wire-content-hash.js.map +1 -0
- package/dist/withheld-content.d.ts +13 -0
- package/dist/withheld-content.d.ts.map +1 -0
- package/dist/withheld-content.js +46 -0
- package/dist/withheld-content.js.map +1 -0
- 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,1540 @@
|
|
|
1
|
+
import { extractErrorMessage } from "./error-message.js";
|
|
2
|
+
import { generateSaltContribution, SESSION_SALT_BYTES } from "@cello-protocol/crypto";
|
|
3
|
+
import { onPeerSaltFrame, SALT_ADOPTION_LABELS, SALT_FREEZE_GUIDANCE, } from "./session-salt-agreement.js";
|
|
4
|
+
import { UNSALTED_REASONS, UNSALTED_GUIDANCE, SALT_AGREEMENT_WAIT_MS, } from "./session-node-types.js";
|
|
5
|
+
import { contentHashFor, CONTENT_HASH_ALGS } from "./wire-content-hash.js";
|
|
6
|
+
import { CONTENT_ENCRYPTION_REASONS, } from "./content-encryption-status.js";
|
|
7
|
+
export class SessionSalts {
|
|
8
|
+
#ctx;
|
|
9
|
+
constructor(ctx) {
|
|
10
|
+
this.#ctx = ctx;
|
|
11
|
+
}
|
|
12
|
+
/** A getter so the moved queries still read `this.#db` and narrow exactly as they did. */
|
|
13
|
+
get #db() {
|
|
14
|
+
return this.#ctx.db();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* DOD-M15-SEALWIRE-1 bullet 6 (part A) — the salt agreement's two pieces of per-session state.
|
|
18
|
+
*
|
|
19
|
+
* `#saltContributions` — OUR random half, MINTED ONCE PER SESSION. This being a map rather than a
|
|
20
|
+
* fresh call at each send is the whole correctness of the exchange: we re-announce on every
|
|
21
|
+
* counterparty connect, and a contribution regenerated per reconnect would have both sides
|
|
22
|
+
* deriving against a moving value with the fingerprints never settling — a session that
|
|
23
|
+
* reconnects and still disagrees, which reads as a network fault rather than a bug here.
|
|
24
|
+
*
|
|
25
|
+
* `#sessionSalts` — a CACHE over `sessions.content_salt`, which is the durable copy. Both are
|
|
26
|
+
* cleared by `#evictSessionCaches`: the contribution is worthless once a salt exists, and the
|
|
27
|
+
* salt is re-read from the row on revival, which is exactly what Decision #8 persists it for.
|
|
28
|
+
*/
|
|
29
|
+
#saltContributions = new Map();
|
|
30
|
+
/**
|
|
31
|
+
* ─── B2b-2 state: what the SEND path needs that the row cannot answer ─────────────────────────
|
|
32
|
+
*
|
|
33
|
+
* `#saltPending` — an agreement that is outstanding. The first send waits on it (constraint 2).
|
|
34
|
+
*
|
|
35
|
+
* ⚠️ THIS SAID "an agreement that has ACTUALLY GONE OUT", and constraint 5 read that a park-only
|
|
36
|
+
* session never starts one at all. DOD-M15-AWAYSALT-1 amended both: an away ack hashes BEFORE the
|
|
37
|
+
* peer can attach, so it arms one SPECULATIVELY with no frame sent — otherwise it hashes unsalted,
|
|
38
|
+
* closes adoption, and the session can never seal. Corrected rather than deleted, because the
|
|
39
|
+
* constraint underneath still holds and is load-bearing: a park-only session must not be made to
|
|
40
|
+
* WAIT the full bound for a peer that will never answer. What carries it now is the `announced`
|
|
41
|
+
* flag — a speculative arm that times out reports `no_agreement_started` (nobody was connected),
|
|
42
|
+
* never `agreement_timed_out` (they ignored us), and stays silent.
|
|
43
|
+
*
|
|
44
|
+
* `#hashedWithoutSalt` — this session has already computed an unsalted content hash. Decision #8
|
|
45
|
+
* closes adoption at the moment content is HASHED, and for a session's first message that is a
|
|
46
|
+
* full network round trip before any leaf, held row or in-flight entry exists. Without this flag
|
|
47
|
+
* the frontier count reads empty for exactly the window in which adopting would split the
|
|
48
|
+
* transcript.
|
|
49
|
+
*
|
|
50
|
+
* `#unsaltedAnnounced` — the fallback has been stated for this session. Decision #15 says once per
|
|
51
|
+
* session; a per-message warning is a filter waiting to be written.
|
|
52
|
+
*
|
|
53
|
+
* All three are per-session and in-memory by design, and are dropped with the rest of a session's
|
|
54
|
+
* caches on eviction — a revived session re-reads its salt from the row, re-derives its frontier
|
|
55
|
+
* from durable state, and starts a fresh agreement if it reconnects.
|
|
56
|
+
*/
|
|
57
|
+
#saltPending = new Map();
|
|
58
|
+
/**
|
|
59
|
+
* HOW THE LAST AGREEMENT ENDED, kept after `#saltPending` is cleared.
|
|
60
|
+
*
|
|
61
|
+
* ⚠️ FOUND BY FALSIFYING MY OWN FIX. `#settleSaltPending` deletes the pending entry, so a send that
|
|
62
|
+
* arrives AFTER an agreement has already failed finds nothing pending and is told
|
|
63
|
+
* `no_agreement_started` — *"your counterparty was not connected"* — when in fact they were
|
|
64
|
+
* connected and our own dial to them failed. The outcome was observable only to a send that
|
|
65
|
+
* happened to already be waiting, which is the minority case.
|
|
66
|
+
*
|
|
67
|
+
* So the verdict outlives the wait. An ABSENT entry still means what it always meant — no
|
|
68
|
+
* agreement was ever started, the park-only case — and that distinction is the whole reason this
|
|
69
|
+
* is a separate map rather than a default.
|
|
70
|
+
*/
|
|
71
|
+
#saltLastOutcome = new Map();
|
|
72
|
+
#unsaltedAnnounced = new Set();
|
|
73
|
+
#sessionSalts = new Map();
|
|
74
|
+
/**
|
|
75
|
+
* The peer half we last answered with a repair, hex — review F14, and it is what makes the repair
|
|
76
|
+
* TERMINATE. Without it, two daemons that already hold the same salt trade contributions forever
|
|
77
|
+
* once a reconnect leaves a stale copy queued on each side. See `onPeerSaltFrame`'s
|
|
78
|
+
* `alreadyRepairedAgainstPeerHalf`.
|
|
79
|
+
*/
|
|
80
|
+
#saltRepairedAgainst = new Map();
|
|
81
|
+
/**
|
|
82
|
+
* THE MIRROR OF THE ABOVE — the peer FINGERPRINT we last answered with our half, hex.
|
|
83
|
+
*
|
|
84
|
+
* 006-CRYPTO finding 1. `#saltRepairedAgainst` terminates the salt-HOLDER's direction only. A side
|
|
85
|
+
* holding no salt answered every fingerprint with its contribution, and a latched holder answers
|
|
86
|
+
* every contribution with its fingerprint — so after one failed persist plus a reconnect, two
|
|
87
|
+
* healthy daemons repair at each other for the life of the session, one new stream and one INFO
|
|
88
|
+
* line each per round trip. Keyed on the peer's fingerprint BYTES for the same reason the other
|
|
89
|
+
* map is keyed on its half: a genuinely NEW fingerprint is new information and must still be
|
|
90
|
+
* answered; only an identical re-offer is the loop.
|
|
91
|
+
*/
|
|
92
|
+
#saltRepairedAgainstFingerprint = new Map();
|
|
93
|
+
/**
|
|
94
|
+
* `#saltSuspended` — the peer has told us it can never hold a salt, so ours must not be USED. The
|
|
95
|
+
* bytes stay on disk (`DOD-M15-SALTSPLIT-1`, the other lane's authorization argument).
|
|
96
|
+
*
|
|
97
|
+
* ⚠️ THIS REPLACED AN IMMEDIATE, IRREVERSIBLE ERASE, AND THE REFRAMING IS THE WHOLE POINT.
|
|
98
|
+
*
|
|
99
|
+
* I defended the erase as a compatibility question — a legacy peer might send the misleading frame,
|
|
100
|
+
* we are pre-launch, do not carry weight for a state nobody is in. All true, and it does not reach
|
|
101
|
+
* the question. **It is an AUTHORIZATION question:** the receiver performed an irreversible
|
|
102
|
+
* destruction of durable key material on a peer's bare assertion with nothing to check it against.
|
|
103
|
+
* Re-derived against an empty database — *would I let one side erase the other's key material on an
|
|
104
|
+
* unauthenticated claim carrying no evidence?* No. My own empty-database rule argued FOR a guard,
|
|
105
|
+
* not against one.
|
|
106
|
+
*
|
|
107
|
+
* And my own trigger was the proof I walked past: `frontier_unreadable` is not a legacy peer, it is
|
|
108
|
+
* a **healthy current peer having one bad second**. Fixing the producer made our side stop emitting
|
|
109
|
+
* it wrongly and left the receiver built to obey it — *one side of that exchange correct by
|
|
110
|
+
* construction, the other still correct by luck.*
|
|
111
|
+
*
|
|
112
|
+
* A salt that cannot be used is inert. The destruction is what turned a transient disagreement into
|
|
113
|
+
* a permanent one, so **nothing irreversible hangs on the claim any more** and proving the claim
|
|
114
|
+
* stops being load-bearing.
|
|
115
|
+
*
|
|
116
|
+
* ⚠️ IN MEMORY ON PURPOSE, AND THE ERASE IS DEFERRED RATHER THAN CANCELLED. A durable mark needs a
|
|
117
|
+
* column, and this milestone has lost data twice in the rebuild DDL. In-memory alone would split
|
|
118
|
+
* the transcript at the next restart — unsalted now, salted after a reboot — so the salt IS erased,
|
|
119
|
+
* at the first unsalted hash, which is the moment erasing becomes both harmless (nothing was hashed
|
|
120
|
+
* under it) and REQUIRED (keeping it would re-salt after a restart). Before that moment a corrected
|
|
121
|
+
* announce carrying a matching fingerprint un-suspends and the session recovers fully salted, which
|
|
122
|
+
* erasure makes impossible even in principle: the far side cannot re-derive without both halves.
|
|
123
|
+
*
|
|
124
|
+
* A restart before either outcome loses the mark, we are salted again, the peer refuses one message,
|
|
125
|
+
* and the announce re-runs and re-suspends. **One refused message, then convergence** — against a
|
|
126
|
+
* dead session.
|
|
127
|
+
*/
|
|
128
|
+
#saltSuspended = new Set();
|
|
129
|
+
#hashedWithoutSalt = new Map();
|
|
130
|
+
/**
|
|
131
|
+
* `#hashedWithSalt` — how many content hashes this session has computed UNDER its salt and not yet
|
|
132
|
+
* landed anywhere a count can see (`DOD-M15-SALTSPLIT-1`, review HIGH-2).
|
|
133
|
+
*
|
|
134
|
+
* The mirror of `#hashedWithoutSalt`, and it exists for the same window: a hash is computed, then a
|
|
135
|
+
* relay round trip happens, and only afterwards does the message appear as a leaf, a hold or an
|
|
136
|
+
* awaiting-ack entry. In between, every count reads zero.
|
|
137
|
+
*
|
|
138
|
+
* It is read by `#discardUnspentSalt` alone. "Unspent" must mean *nothing has been hashed under
|
|
139
|
+
* it*, and without this the answer is *nothing has FINISHED being hashed under it* — which is the
|
|
140
|
+
* question nobody asked, answered destructively.
|
|
141
|
+
*
|
|
142
|
+
* Never decremented on success: a salted hash that reaches the wire is spent forever, and unlike
|
|
143
|
+
* the unsalted counter there is no `abandonUnsaltedHash` equivalent to undo. It is cleared only
|
|
144
|
+
* with the rest of the session's caches. **For a discard decision, erring toward "spent" is the
|
|
145
|
+
* safe direction** — a salt kept is recoverable, a salt erased is not.
|
|
146
|
+
*/
|
|
147
|
+
#hashedWithSalt = new Map();
|
|
148
|
+
/**
|
|
149
|
+
* THE LABEL THE PEER GAVE when it closed adoption — 006-CRYPTO finding 2.
|
|
150
|
+
*
|
|
151
|
+
* The wire carries WHY, `session-salt-agreement.ts` makes it a union so a caller cannot close
|
|
152
|
+
* without saying why, and the agreement's `detail` puts it in the log. It was going no further:
|
|
153
|
+
* `#settleSaltPending(..., "closed")` recorded only that it was closed, so every one of the four
|
|
154
|
+
* reasons arrived at the operator as "they had already hashed messages".
|
|
155
|
+
*
|
|
156
|
+
* Stored raw and rendered through `#peerClosedReason`, which maps anything outside the known set
|
|
157
|
+
* to a non-asserting reason — the peer chooses these bytes.
|
|
158
|
+
*/
|
|
159
|
+
#saltPeerClosedLabel = new Map();
|
|
160
|
+
/**
|
|
161
|
+
* cello_list_sessions: every persisted session for one agent, regardless of
|
|
162
|
+
* status (active, interrupted, sealed, seal_interrupted_pending). Ordered most
|
|
163
|
+
* recently updated first so the live session surfaces at the top. This is the
|
|
164
|
+
* discovery surface that the by-id reads (cello_get_transcript /
|
|
165
|
+
* cello_get_sealed_receipt) depend on — without it an agent has no way to learn
|
|
166
|
+
* its own session ids after a restart or from a fresh MCP connection.
|
|
167
|
+
*/
|
|
168
|
+
/**
|
|
169
|
+
* DOD-M15-REFUSED-INBOUND-SILENT-1, the DECLINED PROTECTION half — a FIELD, not an alert.
|
|
170
|
+
*
|
|
171
|
+
* An unsalted session is exactly as verifiable as every session shipped before salting existed,
|
|
172
|
+
* so there is nothing to interrupt the operator with and no event to fire. What was missing is
|
|
173
|
+
* STATE: nothing let anyone tell *"unsalted because this build predates the feature"* from
|
|
174
|
+
* *"unsalted because adoption was refused"* — and only the second says something about their
|
|
175
|
+
* setup. The session's own status now answers it, which costs nothing per message and cannot
|
|
176
|
+
* become a flood.
|
|
177
|
+
*
|
|
178
|
+
* The raw salt is dropped on the way out rather than passed through. `SELECT *` was handing the
|
|
179
|
+
* BLOB to a listing surface that has no use for it; the boolean is the whole question a reader of
|
|
180
|
+
* this list is asking, and shipping key material to answer a yes/no is not a trade worth making.
|
|
181
|
+
*/
|
|
182
|
+
/**
|
|
183
|
+
* ⚠️ THE STORED COLUMN IS NOT THE ANSWER ON ITS OWN — 006-CRYPTO finding 3.
|
|
184
|
+
*
|
|
185
|
+
* A SUSPENDED salt keeps its bytes on disk deliberately (`DOD-M15-SALTSPLIT-1`: a salt kept is
|
|
186
|
+
* recoverable, a salt erased is not), while `#saltForHashing` returns null for it and every
|
|
187
|
+
* message goes out `sha256`. Reading the column alone therefore reported `true` at the exact
|
|
188
|
+
* moment the session had STOPPED salting — and because the field is emitted only when `false`,
|
|
189
|
+
* the agent saw nothing at all, which reads as "not unsalted".
|
|
190
|
+
*
|
|
191
|
+
* That is precisely the case this field was added for. Its own note above says it exists to tell
|
|
192
|
+
* *"unsalted because this build predates the feature"* from *"unsalted because adoption was
|
|
193
|
+
* refused"*, and the refused case was the one it could not report.
|
|
194
|
+
*/
|
|
195
|
+
saltStatusOf(row, agentName) {
|
|
196
|
+
const { content_salt, ...rest } = row;
|
|
197
|
+
const stored = content_salt != null && content_salt.length > 0;
|
|
198
|
+
const suspended = agentName !== null && this.#saltSuspended.has(this.#ctx.sessionKey(agentName, String(row.session_id)));
|
|
199
|
+
/**
|
|
200
|
+
* 007-CRYPTO: the REAL state, not a constant. In 006 this was hardcoded `false` with a single
|
|
201
|
+
* reason, because nothing exchanged keys and saying so was the honest answer. It exchanges now,
|
|
202
|
+
* so a hardcode would be the stale claim that unit existed to remove.
|
|
203
|
+
*
|
|
204
|
+
* A row with no live agent name cannot be looked up in memory — an orphaned session, whose key
|
|
205
|
+
* (if it ever had one) died with the process. Reported as not-agreed rather than guessed at.
|
|
206
|
+
*/
|
|
207
|
+
const enc = agentName === null
|
|
208
|
+
? { key: null, reason: CONTENT_ENCRYPTION_REASONS.NOT_YET_AGREED }
|
|
209
|
+
: this.#ctx.contentEncryptionState(agentName, String(row.session_id));
|
|
210
|
+
return {
|
|
211
|
+
...rest,
|
|
212
|
+
content_hashes_salted: stored && !suspended,
|
|
213
|
+
content_encrypted: enc.key !== null,
|
|
214
|
+
...(enc.key === null ? { content_encryption_reason: enc.reason } : {}),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
saltContributionFor(agentName, sessionId) {
|
|
218
|
+
const key = this.#ctx.sessionKey(agentName, sessionId);
|
|
219
|
+
let contribution = this.#saltContributions.get(key);
|
|
220
|
+
if (!contribution) {
|
|
221
|
+
contribution = generateSaltContribution();
|
|
222
|
+
this.#saltContributions.set(key, contribution);
|
|
223
|
+
}
|
|
224
|
+
return contribution;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Our half for a session, **without minting one** — review F1, and the distinction is the whole
|
|
228
|
+
* safety of the repair.
|
|
229
|
+
*
|
|
230
|
+
* A session that already holds a salt must never mint a fresh half. If it did, the repair would
|
|
231
|
+
* offer the peer a half the stored salt was NOT derived from, they would compute a different salt,
|
|
232
|
+
* and both sides would believe they had agreed — silently, which is the one outcome worse than
|
|
233
|
+
* refusing. So `null` from here means exactly "we hold a salt and the half behind it is gone",
|
|
234
|
+
* and that is the only state the agreement is allowed to call unrepairable.
|
|
235
|
+
*/
|
|
236
|
+
ownSaltHalf(agentName, sessionId) {
|
|
237
|
+
return this.#saltContributions.get(this.#ctx.sessionKey(agentName, sessionId)) ?? null;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Test seam: force this session's own salt half, so the LOCAL-defect path is reachable.
|
|
241
|
+
*
|
|
242
|
+
* `generateSaltContribution` cannot produce a degenerate half, which is the point of it — so the
|
|
243
|
+
* only way to exercise "our own random source is broken" end-to-end is to stand in for the broken
|
|
244
|
+
* source. Named `…ForTest` like every other seam, and it writes the same map production writes
|
|
245
|
+
* rather than a parallel one, so a test cannot pass against state the daemon never reads.
|
|
246
|
+
*
|
|
247
|
+
* ⚠️ This block stayed in `session-node-manager.ts` when the method moved here, and ended up
|
|
248
|
+
* stacked on an unrelated test seam — the fourth found doing that. `#saltContributions` above is
|
|
249
|
+
* "the same map production writes"; the sentence was true where it was written and unverifiable
|
|
250
|
+
* where it landed.
|
|
251
|
+
*/
|
|
252
|
+
setSaltContributionForTest(agentName, sessionId, contribution) {
|
|
253
|
+
this.#saltContributions.set(this.#ctx.sessionKey(agentName, sessionId), contribution);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Test seam: drop this session's own half while leaving the stored salt in place — the state every
|
|
257
|
+
* teardown produces, because `#evictSessionCaches` clears the map and the row survives.
|
|
258
|
+
*
|
|
259
|
+
* It clears the SAME map the eviction clears rather than a stand-in, so a test cannot pass against
|
|
260
|
+
* a state the daemon never reaches. Reproducing it through a real teardown/revive would also drag
|
|
261
|
+
* in node rebuild and relay reconnection, none of which this is about.
|
|
262
|
+
*/
|
|
263
|
+
forgetSaltContributionForTest(agentName, sessionId) {
|
|
264
|
+
this.#saltContributions.delete(this.#ctx.sessionKey(agentName, sessionId));
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* THE ONE PLACE THAT DECIDES HOW A SESSION'S OUTBOUND CONTENT IS HASHED —
|
|
268
|
+
* `DOD-M15-SEALWIRE-1` part B2b.
|
|
269
|
+
*
|
|
270
|
+
* Returns the hash AND the algorithm that produced it, together, because the two must not be
|
|
271
|
+
* decided separately. `wire-content-hash.ts` exists for exactly this reason and says so in its own
|
|
272
|
+
* header: the expression was written out at five call sites, the two added last got it wrong, and
|
|
273
|
+
* the failure was invisible — *"the send succeeds, `parked: false`, the sender's log says the frame
|
|
274
|
+
* left, and the receiver discards it at the authenticity check."* It took two real daemons.
|
|
275
|
+
*
|
|
276
|
+
* There are FOUR outbound sites (`session-content-handlers.ts`, two in `daemon.ts`,
|
|
277
|
+
* `document-delivery-transport.ts`). Once salting is switchable, each of them independently
|
|
278
|
+
* deciding whether to salt is that defect again with a worse failure mode — a message hashed one
|
|
279
|
+
* way and LABELLED another is refused by every peer, including a correct one.
|
|
280
|
+
*
|
|
281
|
+
* ⚠️ ASYNC, AND THAT IS THE POINT — B2b-2 constraint 2, not an implementation detail.
|
|
282
|
+
*
|
|
283
|
+
* The agreement is in flight while the operator composes their first message. Hash without waiting
|
|
284
|
+
* and it comes out unsalted, and that first unsalted hash closes adoption for the LIFE of the
|
|
285
|
+
* session (Decision #8, unit 1). Every session would fall back permanently while every log line
|
|
286
|
+
* about it stayed true — the feature present, wired, tested, and never once reached.
|
|
287
|
+
*
|
|
288
|
+
* The wait lives HERE rather than at the four call sites for the same reason `contentHashAlg` is a
|
|
289
|
+
* required parameter rather than a defaulted one: a site that forgets it must fail to compile. A
|
|
290
|
+
* caller that drops the `await` gets a `Promise` where bytes belong, which is a typecheck error;
|
|
291
|
+
* a caller that forgot to call a separate `awaitSaltSettled()` would silently send unsalted.
|
|
292
|
+
*/
|
|
293
|
+
async contentHashForSession(agentName, sessionId, content) {
|
|
294
|
+
const { salt, reason } = await this.saltForHashing(agentName, sessionId);
|
|
295
|
+
if (salt !== null) {
|
|
296
|
+
/**
|
|
297
|
+
* ⚠️ THE SALTED HASH MARKS ITSELF SPENT — `DOD-M15-SALTSPLIT-1` review pass 1, HIGH-2.
|
|
298
|
+
*
|
|
299
|
+
* The unsalted branch below has counted itself since review pass 2 F1, for a reason stated
|
|
300
|
+
* there in full: between hashing and `#trackAwaitingAck` there is a relay round trip, and in
|
|
301
|
+
* that window leaves, held content and awaiting-ack ALL read zero. **The salted direction was
|
|
302
|
+
* left with no counterpart**, which was harmless while nothing acted on the answer — and
|
|
303
|
+
* `#discardUnspentSalt` is the first code that acts on it destructively.
|
|
304
|
+
*
|
|
305
|
+
* Without this, a peer's `adoption_closed` frame arriving inside that window finds adoption
|
|
306
|
+
* "open", discards the salt, and the message already on the wire carries
|
|
307
|
+
* `content_hash_alg: hmac-salt-v1` with a hash **nobody — including this daemon — can ever
|
|
308
|
+
* recompute**. The alg is copied verbatim into the parked envelope on TTF expiry, so it
|
|
309
|
+
* survives the round trip that would otherwise have hidden it.
|
|
310
|
+
*
|
|
311
|
+
* A COUNT, not a bit, for the same reason the unsalted side is a count: two connections can be
|
|
312
|
+
* mid-send at once, and one finishing must not clear the claim the other is still relying on.
|
|
313
|
+
*/
|
|
314
|
+
const key = this.#ctx.sessionKey(agentName, sessionId);
|
|
315
|
+
this.#hashedWithSalt.set(key, (this.#hashedWithSalt.get(key) ?? 0) + 1);
|
|
316
|
+
const alg = CONTENT_HASH_ALGS.HMAC_SALT_V1;
|
|
317
|
+
return { hash: contentHashFor(content, { alg, salt }), alg };
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* ⚠️ MARKED BEFORE THE HASH IS RETURNED, and this closes a window the row cannot see.
|
|
321
|
+
*
|
|
322
|
+
* `#saltAdoptionClosed` counts leaves, held content and in-flight sends. For the FIRST message of
|
|
323
|
+
* a session none of the three exists at this moment — the leaf lands after `sendContent` returns,
|
|
324
|
+
* which is a network round trip later. A peer contribution arriving in that gap would be adopted,
|
|
325
|
+
* and the message already on the wire would become the single unsalted leaf in an otherwise
|
|
326
|
+
* salted transcript: the exact split Decision #8 forbids, reached by the one route every count
|
|
327
|
+
* reads as empty.
|
|
328
|
+
*
|
|
329
|
+
* In memory rather than in a column, and that is sufficient rather than convenient: if this
|
|
330
|
+
* process survives, the flag holds; if it does not, the message it protects either reached a
|
|
331
|
+
* durable form (leaf, held row, queued row — all of which the counts see) or never left, in which
|
|
332
|
+
* case there is nothing to split. The one remaining case — hashed, sent, and no local record —
|
|
333
|
+
* is covered from the other side, because the peer DID leaf it and closes its own adoption, and
|
|
334
|
+
* the wire state added in unit 1 tells us so.
|
|
335
|
+
*/
|
|
336
|
+
/**
|
|
337
|
+
* ⚠️ NOT FOR A TORN-DOWN SESSION — review Finding 5. `#evictSessionCaches` settles the wait and
|
|
338
|
+
* clears both of these sets; a `.add()` afterwards re-populates a map whose eviction has already
|
|
339
|
+
* run, and the entries then outlive the session they describe. There is also nothing to protect:
|
|
340
|
+
* a session that no longer exists cannot adopt a salt or split a transcript.
|
|
341
|
+
*/
|
|
342
|
+
/**
|
|
343
|
+
* ⚠️ THE DEFERRED ERASE — `DOD-M15-SALTSPLIT-1`. This is the moment a suspended salt becomes both
|
|
344
|
+
* harmless to erase and NECESSARY to erase, and it must run BEFORE the count below.
|
|
345
|
+
*
|
|
346
|
+
* Harmless: this session has hashed nothing under the salt, which is what let it be suspended.
|
|
347
|
+
* Necessary: we are about to hash unsalted, and a salt left on disk reads back fine after a
|
|
348
|
+
* restart — so the next process would hash salted and the transcript would be split down the
|
|
349
|
+
* middle by a reboot rather than by any frame.
|
|
350
|
+
*
|
|
351
|
+
* **Before the `#hashedWithoutSalt` increment on purpose.** `#discardUnspentSalt` refuses to erase
|
|
352
|
+
* once adoption is closed, and that counter is one of the things that closes it — increment first
|
|
353
|
+
* and the erase we just decided is correct gets refused by our own guard, leaving exactly the
|
|
354
|
+
* split this ordering exists to prevent.
|
|
355
|
+
*/
|
|
356
|
+
if (reason !== UNSALTED_REASONS.SESSION_TORN_DOWN && this.#saltSuspended.has(this.#ctx.sessionKey(agentName, sessionId))) {
|
|
357
|
+
/**
|
|
358
|
+
* ⚠️ GOING UNSALTED AND ERASING THE SALT ARE ONE DECISION — pass 2, F2 (HIGH), and this is my
|
|
359
|
+
* regression, not a pre-existing one.
|
|
360
|
+
*
|
|
361
|
+
* The note above claimed the ordering was sufficient because `#hashedWithoutSalt` is what
|
|
362
|
+
* closes adoption. **It is one of FOUR contributors.** Leaves, held rows and awaiting-ack close
|
|
363
|
+
* it too — and the most ordinary event in the protocol closes it: *the peer sends us its next
|
|
364
|
+
* message.* Reproduced through the real inbound path: suspend, peer's message lands as leaf 0,
|
|
365
|
+
* we hash `sha256`, and the erase is REFUSED with `already_hashing` while the bytes stay on
|
|
366
|
+
* disk. One teardown-and-revive later — no process restart required — we hash `hmac` again.
|
|
367
|
+
* That is the split transcript, produced by the fix for the split transcript.
|
|
368
|
+
*
|
|
369
|
+
* Worth naming precisely: **the immediate-erase design this replaced could NOT produce it.**
|
|
370
|
+
* There, a refused discard simply kept the session salted — one rule throughout, and loud.
|
|
371
|
+
* Suspension is what made "unsalted now, salted later" reachable. Same shape as pass 1: the fix
|
|
372
|
+
* worse than the defect on one path.
|
|
373
|
+
*
|
|
374
|
+
* So the two are atomic. If the salt cannot be erased, we do **not** go unsalted — we keep
|
|
375
|
+
* hashing under the held salt, which is one rule for the whole session, and say so at ERROR.
|
|
376
|
+
* The counterparty may refuse those messages, and that is the honest failure: a dead session
|
|
377
|
+
* beats a transcript no single rule can verify. The durable column remains the real answer.
|
|
378
|
+
*/
|
|
379
|
+
if (!this.discardUnspentSalt(agentName, sessionId)) {
|
|
380
|
+
const stillHeld = this.getSessionSalt(agentName, sessionId);
|
|
381
|
+
if (stillHeld !== null) {
|
|
382
|
+
const key = this.#ctx.sessionKey(agentName, sessionId);
|
|
383
|
+
this.#ctx.logger.error("session.salt.split", {
|
|
384
|
+
agentName, sessionId, reason: "suspended_but_unerasable",
|
|
385
|
+
impact: "this session stays SALTED even though the counterparty says it can never hold a salt, because the salt could not be erased and hashing unsalted now would leave half this transcript under each rule — verifiable by nobody. Expect the counterparty to refuse messages sent from here.",
|
|
386
|
+
guidance: "Start a new session with this counterparty: the salt agreement runs at open, before anything is hashed. This one cannot be repaired — look for session.salt.discard.refused immediately above for why the salt could not be released.",
|
|
387
|
+
});
|
|
388
|
+
this.#hashedWithSalt.set(key, (this.#hashedWithSalt.get(key) ?? 0) + 1);
|
|
389
|
+
const alg = CONTENT_HASH_ALGS.HMAC_SALT_V1;
|
|
390
|
+
return { hash: contentHashFor(content, { alg, salt: stillHeld }), alg };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (reason !== UNSALTED_REASONS.SESSION_TORN_DOWN) {
|
|
395
|
+
/**
|
|
396
|
+
* ⚠️ A COUNT, NOT A BIT — review pass 2, F1 (HIGH). It was a `Set`, and that made it ONE FLAG
|
|
397
|
+
* PER SESSION for a fact that is per MESSAGE.
|
|
398
|
+
*
|
|
399
|
+
* The `sibling_send_in_flight` refusal path exists precisely when another connection is
|
|
400
|
+
* mid-send with an unsalted hash it computed itself — and `sendContent` awaits a full relay
|
|
401
|
+
* round trip before `#trackAwaitingAck` records anything. So: connection A hashes and sets the
|
|
402
|
+
* flag; A enters that round trip, visible in no count; connection B hashes, sees A's claim,
|
|
403
|
+
* refuses, and calls `abandonUnsaltedHash` — **deleting the flag A is still relying on.** The
|
|
404
|
+
* frontier then reads entirely empty, a salt frame arriving in that window is adopted, and A's
|
|
405
|
+
* message lands as leaf 0 hashed sha256 in a session that hashes everything after it under
|
|
406
|
+
* HMAC.
|
|
407
|
+
*
|
|
408
|
+
* That is the split transcript this unit exists to prevent, through a window a relay round
|
|
409
|
+
* trip wide. A count makes each in-flight hash hold its own claim.
|
|
410
|
+
*/
|
|
411
|
+
const key = this.#ctx.sessionKey(agentName, sessionId);
|
|
412
|
+
this.#hashedWithoutSalt.set(key, (this.#hashedWithoutSalt.get(key) ?? 0) + 1);
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* NO `??` DEFAULT — review pass 2, F6. It read `reason ?? ADOPTION_CLOSED_LOCALLY`, which is the
|
|
416
|
+
* shape the closed set was built to eliminate: a seventh return path forgetting its reason would
|
|
417
|
+
* have been silently labelled *"you already hashed"* and inherited guidance about a frontier that
|
|
418
|
+
* never moved. `#saltForHashing` returns a discriminated union now, so a null salt without a
|
|
419
|
+
* reason does not compile.
|
|
420
|
+
*/
|
|
421
|
+
this.announceUnsaltedOnce(agentName, sessionId, reason);
|
|
422
|
+
const alg = CONTENT_HASH_ALGS.SHA256;
|
|
423
|
+
return { hash: contentHashFor(content, { alg, salt: null }), alg };
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* The salt to hash this session's next message under, waiting for a pending agreement if one is
|
|
427
|
+
* genuinely in flight — B2b-2 constraints 2 and 5.
|
|
428
|
+
*
|
|
429
|
+
* Three exits, and the order matters:
|
|
430
|
+
*
|
|
431
|
+
* 1. We already hold one. No wait, ever.
|
|
432
|
+
* 2. Adoption is closed — this session has hashed or leafed something already, so a salt could
|
|
433
|
+
* never be adopted now even if one arrived. Waiting would be waiting for a value we would
|
|
434
|
+
* then have to refuse.
|
|
435
|
+
* 3. Nothing is pending. **The park-only case (constraint 5)**: the announcement hangs off
|
|
436
|
+
* `onPeerConnect`, an offline counterparty never connects, so no agreement was ever started.
|
|
437
|
+
* (An away ack now ARMS one speculatively — see `markSaltPending` — so it reaches exit 4 and
|
|
438
|
+
* settles `no_agreement_started` on timeout, which is this same answer by a slower road.) Waiting the full bound there pauses every message to an offline peer and falls
|
|
439
|
+
* back anyway — a stall bought for nothing.
|
|
440
|
+
*
|
|
441
|
+
* Only a session with an agreement actually in flight waits, and only until it settles or the
|
|
442
|
+
* bound expires.
|
|
443
|
+
*/
|
|
444
|
+
async saltForHashing(agentName, sessionId) {
|
|
445
|
+
const key = this.#ctx.sessionKey(agentName, sessionId);
|
|
446
|
+
const held = this.getSessionSalt(agentName, sessionId);
|
|
447
|
+
if (held !== null) {
|
|
448
|
+
/**
|
|
449
|
+
* SUSPENDED BEATS HELD — `DOD-M15-SALTSPLIT-1`. The peer has said it can never hold a salt, so
|
|
450
|
+
* hashing under ours produces a message it must refuse. We hold one and deliberately do not
|
|
451
|
+
* use it.
|
|
452
|
+
*
|
|
453
|
+
* ⚠️ An earlier note here said `PEER_CLOSED_ADOPTION` "already carries exactly the right
|
|
454
|
+
* guidance, so no new reason is needed and none is invented." That was right about not
|
|
455
|
+
* inventing a reason and wrong about which one applies: the peer can suspend us for any of
|
|
456
|
+
* four reasons, and the one hardcoded here asserted the most flattering of them. It now asks
|
|
457
|
+
* the same mapping every other closed path asks (006-CRYPTO finding 2).
|
|
458
|
+
*/
|
|
459
|
+
if (this.#saltSuspended.has(key)) {
|
|
460
|
+
return { salt: null, reason: this.peerClosedReason(key) };
|
|
461
|
+
}
|
|
462
|
+
return { salt: held };
|
|
463
|
+
}
|
|
464
|
+
if (this.saltAdoptionClosed(agentName, sessionId).closed) {
|
|
465
|
+
return { salt: null, reason: UNSALTED_REASONS.ADOPTION_CLOSED_LOCALLY };
|
|
466
|
+
}
|
|
467
|
+
const pending = this.#saltPending.get(key);
|
|
468
|
+
if (pending === undefined) {
|
|
469
|
+
// An agreement that already ENDED is not an agreement that never started. Only the second is
|
|
470
|
+
// "your counterparty was not connected", and only an absent entry means it.
|
|
471
|
+
const last = this.#saltLastOutcome.get(key);
|
|
472
|
+
if (last !== undefined)
|
|
473
|
+
return { salt: null, reason: this.reasonForOutcome(key, last) };
|
|
474
|
+
return { salt: null, reason: UNSALTED_REASONS.NO_AGREEMENT_STARTED };
|
|
475
|
+
}
|
|
476
|
+
const settled = await pending.settled;
|
|
477
|
+
if (settled === "agreed") {
|
|
478
|
+
const agreed = this.getSessionSalt(agentName, sessionId);
|
|
479
|
+
/**
|
|
480
|
+
* A settled-`agreed` that reads back NULL is a READ failure, not a persist failure — pass 2,
|
|
481
|
+
* F4. `persist_failed` has its own outcome now, so the only way to arrive here empty is
|
|
482
|
+
* `#getSessionSalt` returning null after the salt was stored: a throwing read, or a
|
|
483
|
+
* wrong-width row, with the cache evicted in the microtask between settle and resume. Rare —
|
|
484
|
+
* and labelling it `our_persist_failed` sent the operator to look for a
|
|
485
|
+
* `session.salt.persist.failed` line that will not be there.
|
|
486
|
+
*/
|
|
487
|
+
return agreed !== null
|
|
488
|
+
? { salt: agreed }
|
|
489
|
+
: { salt: null, reason: UNSALTED_REASONS.OUR_READ_FAILED };
|
|
490
|
+
}
|
|
491
|
+
if (settled === "announce_failed") {
|
|
492
|
+
return { salt: null, reason: UNSALTED_REASONS.ANNOUNCE_FAILED };
|
|
493
|
+
}
|
|
494
|
+
if (settled === "persist_failed") {
|
|
495
|
+
// Named separately from the timeout on purpose: the peer answered in time and OUR write
|
|
496
|
+
// failed, so nothing about their build is involved and sending the operator there wastes them.
|
|
497
|
+
return { salt: null, reason: UNSALTED_REASONS.OUR_PERSIST_FAILED };
|
|
498
|
+
}
|
|
499
|
+
if (settled === "closed") {
|
|
500
|
+
/**
|
|
501
|
+
* Two very different things reach `closed`, and only one of them is about the counterparty.
|
|
502
|
+
*
|
|
503
|
+
* `#handleSaltFrame`'s terminal branch — the peer told us it cannot adopt — is a settled
|
|
504
|
+
* bilateral outcome and the session is fine. `#evictSessionCaches` — this session is being
|
|
505
|
+
* torn down underneath us — is not: there is no session left to be unsalted, and a caller that
|
|
506
|
+
* marks `#hashedWithoutSalt` for it re-populates a map whose eviction has already run
|
|
507
|
+
* (review Finding 5). `#saltPending` is gone by the time we look, so the live node is what
|
|
508
|
+
* distinguishes them.
|
|
509
|
+
*/
|
|
510
|
+
return {
|
|
511
|
+
salt: null,
|
|
512
|
+
reason: this.#ctx.activeEntry(key) !== undefined
|
|
513
|
+
? UNSALTED_REASONS.PEER_CLOSED_ADOPTION
|
|
514
|
+
: UNSALTED_REASONS.SESSION_TORN_DOWN,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
if (settled === "timeout" && !pending.announced) {
|
|
518
|
+
/**
|
|
519
|
+
* DOD-M15-AWAYSALT-1 review HIGH-2 — NOTHING WAS EVER ANNOUNCED, so this is not a counterparty
|
|
520
|
+
* who failed to answer. It is the park-only case: the operator is leaving a message for someone
|
|
521
|
+
* who is offline, which is the DESIGNED benign path. Reporting `agreement_timed_out` here
|
|
522
|
+
* substitutes a diagnosis — its guidance sends the operator to ask a perfectly up-to-date
|
|
523
|
+
* counterparty to upgrade — and, because it fires on the normal case, buries the one occurrence
|
|
524
|
+
* that means something. Silent for the same reason: `session.content.unsalted` already says it.
|
|
525
|
+
*/
|
|
526
|
+
return { salt: null, reason: UNSALTED_REASONS.NO_AGREEMENT_STARTED };
|
|
527
|
+
}
|
|
528
|
+
if (settled === "timeout") {
|
|
529
|
+
/**
|
|
530
|
+
* A DECISION, NOT A RETRY. Logged once, here, because this is the moment the session became
|
|
531
|
+
* permanently unsalted — and an operator reading a later `session.content.unsalted` needs to
|
|
532
|
+
* be able to find out WHY this session has no salt when their others do.
|
|
533
|
+
*/
|
|
534
|
+
this.#ctx.logger.warn("session.salt.agreement.timeout", {
|
|
535
|
+
agentName, sessionId, waitedMs: pending.boundMs,
|
|
536
|
+
impact: "the counterparty did not answer the salt agreement in time, so this session is unsalted FOR ITS LIFE — the message is being sent now rather than held any longer. Nothing is lost and nothing is degraded relative to any shipped release.",
|
|
537
|
+
// Review F4: `session.salt.persist.failed` reaches this same timeout by a completely
|
|
538
|
+
// different route — the peer answered promptly and OUR OWN write failed, so we returned
|
|
539
|
+
// before announcing and nothing came back. Omitting it sent that operator to ask their
|
|
540
|
+
// counterparty about a version mismatch that was never involved.
|
|
541
|
+
guidance: "Most often the counterparty is on a build that predates the salt agreement, in which case this is expected and permanent for this session — a newer one will agree normally. If you know they are on the same version, look for session.salt.persist.failed on THIS side first (our own write failing produces this same timeout), then session.salt.announce.failed on either side.",
|
|
542
|
+
});
|
|
543
|
+
return { salt: null, reason: UNSALTED_REASONS.AGREEMENT_TIMED_OUT };
|
|
544
|
+
}
|
|
545
|
+
return { salt: null, reason: UNSALTED_REASONS.AGREEMENT_TIMED_OUT };
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Decision #15's fallback announcement — ONCE per session, never per message.
|
|
549
|
+
*
|
|
550
|
+
* A warning that fires on every message of every unsalted session is not a signal, it is a reason
|
|
551
|
+
* to build a filter; and the operator who filters it also filters the one session where it meant
|
|
552
|
+
* something. Stated once, with what the session actually loses.
|
|
553
|
+
*/
|
|
554
|
+
announceUnsaltedOnce(agentName, sessionId, reason) {
|
|
555
|
+
const key = this.#ctx.sessionKey(agentName, sessionId);
|
|
556
|
+
if (this.#unsaltedAnnounced.has(key))
|
|
557
|
+
return;
|
|
558
|
+
this.#unsaltedAnnounced.add(key);
|
|
559
|
+
this.#ctx.logger.info("session.content.unsalted", {
|
|
560
|
+
agentName, sessionId,
|
|
561
|
+
// The REASON is the field that makes this line diagnosable, and it was the missing one. The
|
|
562
|
+
// impact is the same for all six; what to do about it is not.
|
|
563
|
+
reason,
|
|
564
|
+
impact: "this session hashes its messages the way every build before this feature did. Nothing is degraded relative to any shipped release and no message is affected — it only means a relay holding the hashes could confirm a guess at a short message in THIS conversation, which a salt would have prevented.",
|
|
565
|
+
guidance: UNSALTED_GUIDANCE[reason],
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Register that a salt agreement is IN FLIGHT for this session, so the first send waits for it.
|
|
570
|
+
*
|
|
571
|
+
* Called where we announce our own state — not at session creation. That distinction is
|
|
572
|
+
* constraint 5: an agreement exists to be waited for only once a frame has actually gone out.
|
|
573
|
+
*/
|
|
574
|
+
/**
|
|
575
|
+
* ─── DOD-M15-AWAYSALT-1: WHO IS ALLOWED TO SAY "AN AGREEMENT IS COMING" ────────────────────────
|
|
576
|
+
*
|
|
577
|
+
* `saltForHashing` waits for an agreement ONLY when one is marked pending here. For a long time the
|
|
578
|
+
* only production caller was `#sendSaltFrame`, which fires on peer ATTACH — and that is later than
|
|
579
|
+
* the inbound session REQUEST which triggers an away auto-reply. So the away ack hashed with
|
|
580
|
+
* nothing pending, took `NO_AGREEMENT_STARTED`, and hashed UNSALTED.
|
|
581
|
+
*
|
|
582
|
+
* One unsalted leaf closes salt adoption for the entire session (`already_hashing`), permanently.
|
|
583
|
+
* The initiator, meanwhile, derives a salt, cannot erase it (`session.salt.split`,
|
|
584
|
+
* `suspended_but_unerasable`) and keeps labelling content `hmac-sha256-salt-v1` — which this side
|
|
585
|
+
* can then only refuse. Every message from the initiator is quarantined
|
|
586
|
+
* (`content_hash_salt_unavailable`), and the session can never seal.
|
|
587
|
+
*
|
|
588
|
+
* Measured 2026-09-07 on session 436c92f4, on the DEFAULT path (initiate to an unattended agent):
|
|
589
|
+
* the ack hashed at 22:48:03.752, the peer announced at 22:48:04.561 — 0.8s late — and the
|
|
590
|
+
* operator's first real message was quarantined. Nothing recovers it; the guidance on
|
|
591
|
+
* `session.salt.split` says to start a new session, and a new session repeats the race.
|
|
592
|
+
*
|
|
593
|
+
* So `expectSaltAgreement` on the manager now calls this BEFORE any early hash. It is idempotent
|
|
594
|
+
* (a second call is a no-op while one is outstanding) and bounded by the same `boundMs` timer as
|
|
595
|
+
* every other pending agreement, so a counterparty that never announces settles `timeout` and the
|
|
596
|
+
* content hashes unsalted exactly as it does today — the wait is a delay, never a failure.
|
|
597
|
+
*
|
|
598
|
+
* ⚠️ STILL OPEN: this covers the away reply, which is the only content this daemon emits before a
|
|
599
|
+
* peer can attach. Any FUTURE caller that hashes that early needs the same line, and nothing here
|
|
600
|
+
* enforces that.
|
|
601
|
+
*/
|
|
602
|
+
markSaltPending(agentName, sessionId, boundMs = SALT_AGREEMENT_WAIT_MS, announced = false) {
|
|
603
|
+
const key = this.#ctx.sessionKey(agentName, sessionId);
|
|
604
|
+
const outstanding = this.#saltPending.get(key);
|
|
605
|
+
if (outstanding !== undefined) {
|
|
606
|
+
/**
|
|
607
|
+
* ⚠️ AN OUTSTANDING ENTRY IS UPGRADED, NEVER IGNORED — DOD-M15-AWAYSALT-1 review HIGH-1.
|
|
608
|
+
*
|
|
609
|
+
* This used to early-return. Once a speculative arm existed, the REAL announce that followed
|
|
610
|
+
* was a no-op — so the agreement inherited the speculative bound instead of its own, and a
|
|
611
|
+
* healthy-but-slow peer could time out inside it. That reproduces the exact defect this unit
|
|
612
|
+
* removes: unsalted ack, adoption closed, every later message quarantined, session unsealable.
|
|
613
|
+
*
|
|
614
|
+
* A bound may only GROW, and `announced` may only go true. Both are the safe direction: the
|
|
615
|
+
* cost of waiting longer is a delayed machine acknowledgement, the cost of waiting less is a
|
|
616
|
+
* session that can never seal.
|
|
617
|
+
*/
|
|
618
|
+
if (announced && !outstanding.announced)
|
|
619
|
+
outstanding.announced = true;
|
|
620
|
+
if (boundMs > outstanding.boundMs) {
|
|
621
|
+
clearTimeout(outstanding.timer);
|
|
622
|
+
outstanding.boundMs = boundMs;
|
|
623
|
+
outstanding.timer = setTimeout(() => this.settleSaltPending(agentName, sessionId, "timeout"), boundMs);
|
|
624
|
+
if (typeof outstanding.timer.unref === "function")
|
|
625
|
+
outstanding.timer.unref();
|
|
626
|
+
}
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
let resolve = () => { };
|
|
630
|
+
const settled = new Promise((r) => { resolve = r; });
|
|
631
|
+
const timer = setTimeout(() => this.settleSaltPending(agentName, sessionId, "timeout"), boundMs);
|
|
632
|
+
// The daemon must be able to exit with this outstanding — a pending agreement is not a reason to
|
|
633
|
+
// hold the process open.
|
|
634
|
+
if (typeof timer.unref === "function")
|
|
635
|
+
timer.unref();
|
|
636
|
+
this.#saltPending.set(key, { settled, resolve, timer, boundMs, announced });
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* WHICH of the four terminal answers the peer actually gave — 006-CRYPTO finding 2.
|
|
640
|
+
*
|
|
641
|
+
* The default is the NON-ASSERTING reason, not the most common one. An unknown label means a build
|
|
642
|
+
* we do not understand, and rendering that as "they had already hashed messages" states something
|
|
643
|
+
* about a counterparty that may be untrue — which is what sends an operator to raise a
|
|
644
|
+
* non-problem with them. The label is peer-supplied, so nothing outside the known set is repeated
|
|
645
|
+
* back as our own diagnosis.
|
|
646
|
+
*
|
|
647
|
+
* A missing entry maps to the already-hashing case: `PEER_CLOSED_FIRST` and an absent label both
|
|
648
|
+
* mean the peer is answering a closure of OURS, and `#saltForHashing` answers that with
|
|
649
|
+
* `ADOPTION_CLOSED_LOCALLY` one branch earlier — this is only the fallback if it did not.
|
|
650
|
+
*/
|
|
651
|
+
peerClosedReason(key) {
|
|
652
|
+
const label = this.#saltPeerClosedLabel.get(key);
|
|
653
|
+
if (label === undefined || label === SALT_ADOPTION_LABELS.PEER_CLOSED_FIRST) {
|
|
654
|
+
return UNSALTED_REASONS.PEER_CLOSED_ADOPTION;
|
|
655
|
+
}
|
|
656
|
+
if (label === SALT_ADOPTION_LABELS.ALREADY_HASHING)
|
|
657
|
+
return UNSALTED_REASONS.PEER_CLOSED_ADOPTION;
|
|
658
|
+
if (label === SALT_ADOPTION_LABELS.FRONTIER_UNREADABLE)
|
|
659
|
+
return UNSALTED_REASONS.PEER_FRONTIER_UNREADABLE;
|
|
660
|
+
if (label === SALT_ADOPTION_LABELS.EXCHANGE_STALLED)
|
|
661
|
+
return UNSALTED_REASONS.PEER_EXCHANGE_STALLED;
|
|
662
|
+
return UNSALTED_REASONS.PEER_CLOSED_UNSPECIFIED;
|
|
663
|
+
}
|
|
664
|
+
/** Resolve a pending agreement. Idempotent: the first outcome wins and the timer is cleared. */
|
|
665
|
+
settleSaltPending(agentName, sessionId, outcome) {
|
|
666
|
+
const key = this.#ctx.sessionKey(agentName, sessionId);
|
|
667
|
+
const pending = this.#saltPending.get(key);
|
|
668
|
+
if (pending === undefined)
|
|
669
|
+
return;
|
|
670
|
+
this.#saltPending.delete(key);
|
|
671
|
+
// `agreed` is not recorded: the salt itself is the record, and `#getSessionSalt` answers first.
|
|
672
|
+
if (outcome !== "agreed")
|
|
673
|
+
this.#saltLastOutcome.set(key, outcome);
|
|
674
|
+
clearTimeout(pending.timer);
|
|
675
|
+
pending.resolve(outcome);
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* THIS SESSION'S UNSALTED HASH NEVER BECAME A MESSAGE — release the permanent closure it caused.
|
|
679
|
+
*
|
|
680
|
+
* ⚠️ REVIEW FINDING 3, and it is the opposite of the direction the flag was written to defend.
|
|
681
|
+
* `#hashedWithoutSalt` closes adoption at hash time, because for a session's first message the
|
|
682
|
+
* leaf is a network round trip away and every frontier count reads zero in between. Correct — but
|
|
683
|
+
* `cello_send` has three paths that compute the hash and then produce NOTHING: a sibling send
|
|
684
|
+
* holding the in-flight claim, the frontier moving under the send, and a non-durable send failure
|
|
685
|
+
* whose bytes go to a queue with no production consumer.
|
|
686
|
+
*
|
|
687
|
+
* In all three the session was permanently unsalted for a message that exists nowhere: no leaf, no
|
|
688
|
+
* wire, no copy at the peer. And B2b-2 made two of them MORE likely on a first message, because
|
|
689
|
+
* the five-second wait widens the very window the frontier re-check is watching.
|
|
690
|
+
*
|
|
691
|
+
* Only safe because it is called on paths that provably sent nothing. It deliberately does NOT
|
|
692
|
+
* clear `#unsaltedAnnounced`: the announcement was true when it fired and re-announcing on the
|
|
693
|
+
* retry would be the per-message flood Decision #15 forbids.
|
|
694
|
+
*
|
|
695
|
+
* ─── THREE OTHER SITES HASH AND MAY SEND NOTHING, AND ARE EXEMPT ON PURPOSE (pass 2, F8) ──────
|
|
696
|
+
*
|
|
697
|
+
* `daemon.ts`'s one-shot rejection and away reply, and `document-delivery-transport.ts`'s frame
|
|
698
|
+
* send, can all fail after hashing. None of them needs to abandon, and the reason is the same in
|
|
699
|
+
* each: every one is a REPLY. The inbound message that triggered it has already been leafed on
|
|
700
|
+
* this side, so `#saltAdoptionClosed` is already closed by the leaf count and would stay closed
|
|
701
|
+
* whatever this flag said. Calling abandon there would be a no-op that looks like a guarantee.
|
|
702
|
+
*
|
|
703
|
+
* Written down rather than left to be re-derived: the next reader's first question is why the
|
|
704
|
+
* list is three and not six.
|
|
705
|
+
*/
|
|
706
|
+
abandonUnsaltedHash(agentName, sessionId) {
|
|
707
|
+
const key = this.#ctx.sessionKey(agentName, sessionId);
|
|
708
|
+
const held = this.#hashedWithoutSalt.get(key) ?? 0;
|
|
709
|
+
if (held === 0)
|
|
710
|
+
return;
|
|
711
|
+
// DECREMENT, never delete — F1. Deleting released a sibling's claim along with this one.
|
|
712
|
+
if (held > 1) {
|
|
713
|
+
this.#hashedWithoutSalt.set(key, held - 1);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
this.#hashedWithoutSalt.delete(key);
|
|
717
|
+
/**
|
|
718
|
+
* INFO, not DEBUG — review pass 2, F3. `session.content.unsalted` has already told this operator
|
|
719
|
+
* at INFO that the session is unsalted *"permanently… start a new session if you want the
|
|
720
|
+
* protection."* That statement is now false, and a retraction logged below the level of the
|
|
721
|
+
* claim it retracts is not a retraction. The announcement itself is deliberately NOT re-armed —
|
|
722
|
+
* re-announcing on the retry is the per-message flood Decision #15 forbids.
|
|
723
|
+
*/
|
|
724
|
+
this.#ctx.logger.info("session.content.unsalted.retracted", {
|
|
725
|
+
agentName, sessionId,
|
|
726
|
+
impact: "a hash computed unsalted never became a message — no leaf, nothing on the wire, no copy at the counterparty — so this session CAN still adopt a salt. An earlier session.content.unsalted line said the session was permanently unsalted; that no longer applies.",
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* PUBLIC read of a session's agreed salt — `DOD-M15-SEALWIRE-1` part B2a.
|
|
731
|
+
*
|
|
732
|
+
* `content-park.ts` runs a SECOND, independent content-hash verifier (the park signature does not
|
|
733
|
+
* cover the envelope content, so it checks before `ingestReceivedContent` is ever reached), and it
|
|
734
|
+
* hardcoded `sha256`. It needs the salt to verify a v3 envelope, and it is outside this class.
|
|
735
|
+
*
|
|
736
|
+
* Read-only and cache-backed, so exposing it adds no way to CHANGE the salt from outside — the
|
|
737
|
+
* only writer remains `#persistSessionSalt`, behind the one-salt-per-session predicate.
|
|
738
|
+
*/
|
|
739
|
+
/**
|
|
740
|
+
* IS THIS SESSION ACTUALLY PROTECTED BY ITS SALT RIGHT NOW — pass 2, F3.
|
|
741
|
+
*
|
|
742
|
+
* Distinct from `getSessionContentSalt`, which is POSSESSION and is what the verifier needs: a
|
|
743
|
+
* message parked before suspension was hashed under this salt and must still be checkable against
|
|
744
|
+
* it, so that accessor must keep answering with the bytes.
|
|
745
|
+
*
|
|
746
|
+
* This one answers the OPERATOR's question, and it is a different question. A suspended session
|
|
747
|
+
* holds a salt it will not use, so every hash it produces is `sha256` — reporting `contentSalted:
|
|
748
|
+
* true` there is not a gap, it is an affirmatively false security claim on the surface whose own
|
|
749
|
+
* comment reads *"a security property must not be inferable from a gap."* Same predicate
|
|
750
|
+
* `#saltForHashing` uses, so the flag cannot drift from the behaviour it describes.
|
|
751
|
+
*/
|
|
752
|
+
isContentSaltActive(agentName, sessionId) {
|
|
753
|
+
if (this.#saltSuspended.has(this.#ctx.sessionKey(agentName, sessionId)))
|
|
754
|
+
return false;
|
|
755
|
+
return this.getSessionSalt(agentName, sessionId) !== null;
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* This session's agreed salt, or null. Reads the durable row through a cache, because Decision #8
|
|
759
|
+
* persists it for exactly one reason: *"a restart silently splits the transcript"* if the lookup
|
|
760
|
+
* misses and a fresh salt is minted.
|
|
761
|
+
*
|
|
762
|
+
* A read failure returns null WITH a log rather than throwing — except the bare `!this.#db` guard,
|
|
763
|
+
* which is this file's convention at 60+ sites and only reachable during shutdown. Null means "we
|
|
764
|
+
* hold no salt", which drives the agreement to offer a contribution — and against a peer that does
|
|
765
|
+
* hold one that is a named, loud `salt_state_divergent` refusal. So the degraded path ends in a
|
|
766
|
+
* diagnosis, not in a session that quietly hashes under the wrong value.
|
|
767
|
+
*
|
|
768
|
+
* ⚠️ THIS PARAGRAPH SPENT A UNIT STRANDED 180 LINES AWAY, directly above `contentHashForSession`
|
|
769
|
+
* and followed by that method's own block — so a reader hovering the hash decision got prose about
|
|
770
|
+
* salt read failures. Harmless and exactly the kind of drift that makes a comment stop being read.
|
|
771
|
+
*/
|
|
772
|
+
getSessionSalt(agentName, sessionId) {
|
|
773
|
+
const key = this.#ctx.sessionKey(agentName, sessionId);
|
|
774
|
+
const cached = this.#sessionSalts.get(key);
|
|
775
|
+
if (cached)
|
|
776
|
+
return cached;
|
|
777
|
+
if (!this.#db)
|
|
778
|
+
return null;
|
|
779
|
+
try {
|
|
780
|
+
const row = this.#db
|
|
781
|
+
.prepare("SELECT content_salt FROM sessions WHERE agent_id = ? AND session_id = ?")
|
|
782
|
+
.get(this.#ctx.requireAgentId(agentName), sessionId);
|
|
783
|
+
const stored = row?.content_salt;
|
|
784
|
+
if (!stored || stored.length === 0)
|
|
785
|
+
return null;
|
|
786
|
+
/**
|
|
787
|
+
* A WRONG-WIDTH ROW IS NOT A SALT — review F8.
|
|
788
|
+
*
|
|
789
|
+
* Any non-empty blob used to be accepted, so a truncated row became "our salt", the digests
|
|
790
|
+
* then differed, and the operator was told *"one of you is running an older build — compare
|
|
791
|
+
* versions with them"*: sent to their counterparty over corruption on their own disk. Refusing
|
|
792
|
+
* it here makes this side hold NO salt, which re-offers a contribution and repairs.
|
|
793
|
+
*/
|
|
794
|
+
if (stored.length !== SESSION_SALT_BYTES) {
|
|
795
|
+
this.#ctx.logger.error("session.salt.read.failed", {
|
|
796
|
+
agentName, sessionId, storedBytes: stored.length, expected: SESSION_SALT_BYTES,
|
|
797
|
+
reason: "wrong_width",
|
|
798
|
+
impact: "the stored salt is the wrong size, so it is not used; this session is treated as holding no salt and will re-agree one with the counterparty rather than comparing a corrupt value and blaming their build",
|
|
799
|
+
});
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
const salt = new Uint8Array(stored);
|
|
803
|
+
this.#sessionSalts.set(key, salt);
|
|
804
|
+
return salt;
|
|
805
|
+
}
|
|
806
|
+
catch (err) {
|
|
807
|
+
this.#ctx.logger.error("session.salt.read.failed", {
|
|
808
|
+
agentName, sessionId, error: extractErrorMessage(err),
|
|
809
|
+
impact: "this session is treated as holding no salt, so it will offer a fresh contribution; against a counterparty that still holds theirs the agreement refuses by name rather than hashing under a value only one side has",
|
|
810
|
+
});
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Is this session past the point where a salt can be adopted? — Decision #8, part B2b-2.
|
|
816
|
+
*
|
|
817
|
+
* ⚠️ THE PREDICATE IS "HAS ANYTHING BEEN HASHED", NOT "IS THERE A LEAF" — review F5. A leaf is
|
|
818
|
+
* APPENDED after `await sendContent(...)` returns, so a message can be hashed, put on the wire, and
|
|
819
|
+
* still be invisible to `tree.size()`. Adopting inside that window makes leaf 0 unsalted and the
|
|
820
|
+
* rest salted — the exact split this exists to prevent, with the guard green.
|
|
821
|
+
*
|
|
822
|
+
* ⚠️ HELD CONTENT COUNTS, AND MUST BE HYDRATED FIRST — review F6. `#ensureHeldRestored` is lazy and
|
|
823
|
+
* is not called at session-node creation, so a revived session whose first inbound frame is the
|
|
824
|
+
* salt frame reads a frontier that excludes durable `held_content` rows — rows already hashed
|
|
825
|
+
* unsalted, which `#releaseHeld` will append moments later. Every other frontier reader in this
|
|
826
|
+
* file hydrates first, for this reason. `release: false`, because a salt frame must never deliver
|
|
827
|
+
* messages as a side effect.
|
|
828
|
+
*
|
|
829
|
+
* ⚠️ "CANNOT TELL" IS CLOSED, NOT OPEN. `#requireAgentId` throws for a retired agent, and inferring
|
|
830
|
+
* "zero leaves" from a failure to count them is how a guard becomes a formality. The cost of
|
|
831
|
+
* refusing is an unsalted session; the cost of permitting is a transcript neither rule can verify.
|
|
832
|
+
*/
|
|
833
|
+
saltAdoptionClosed(agentName, sessionId) {
|
|
834
|
+
try {
|
|
835
|
+
this.#ctx.ensureHeldRestored(agentName, sessionId, { release: false });
|
|
836
|
+
}
|
|
837
|
+
catch { /* hydration is best-effort; the counts below still refuse on their own failure */ }
|
|
838
|
+
try {
|
|
839
|
+
const key = this.#ctx.sessionKey(agentName, sessionId);
|
|
840
|
+
const leaves = this.#ctx.getSessionTree(agentName, sessionId).size();
|
|
841
|
+
const held = this.#ctx.heldContentFor(key)?.size ?? 0;
|
|
842
|
+
const inFlight = this.#ctx.awaitingAck.get(key)?.size ?? 0;
|
|
843
|
+
/**
|
|
844
|
+
* ⚠️ THE HASH ITSELF COUNTS — B2b-2, and none of the three counts above can see it.
|
|
845
|
+
*
|
|
846
|
+
* Decision #8 closes adoption when content is HASHED. For a session's first message the leaf
|
|
847
|
+
* lands after `sendContent` returns, a network round trip later; there is no held row and no
|
|
848
|
+
* in-flight entry yet either. So between the hash and the leaf every count reads zero, and a
|
|
849
|
+
* peer contribution arriving in that window would be adopted — leaving the message already on
|
|
850
|
+
* the wire as the one unsalted leaf in a salted transcript.
|
|
851
|
+
*/
|
|
852
|
+
const hashed = this.#hashedWithoutSalt.get(key) ?? 0;
|
|
853
|
+
const total = leaves + held + inFlight + hashed;
|
|
854
|
+
return {
|
|
855
|
+
closed: total > 0,
|
|
856
|
+
// The label crosses the WIRE, so it carries no counts and no error text — only which of the
|
|
857
|
+
// two refusals this is. The counts stay in `why`, which stays local.
|
|
858
|
+
label: SALT_ADOPTION_LABELS.ALREADY_HASHING,
|
|
859
|
+
leafCount: total,
|
|
860
|
+
why: `leaves=${leaves} held=${held} awaiting_ack=${inFlight} hashed=${hashed}`,
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
catch (err) {
|
|
864
|
+
return {
|
|
865
|
+
closed: true,
|
|
866
|
+
label: SALT_ADOPTION_LABELS.FRONTIER_UNREADABLE,
|
|
867
|
+
leafCount: -1,
|
|
868
|
+
why: `frontier_unreadable: ${extractErrorMessage(err)}`,
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Persist the agreed salt, and DO NOT ANNOUNCE ONE WE FAILED TO STORE.
|
|
874
|
+
*
|
|
875
|
+
* The caller sends its fingerprint only if this returns true. A salt held in memory and not on
|
|
876
|
+
* disk would confirm agreement to the counterparty and then be gone at the next restart — turning
|
|
877
|
+
* a loud `salt_state_divergent` refusal, which is the whole point of Decision #10, into the silent
|
|
878
|
+
* split it exists to prevent, one restart later.
|
|
879
|
+
*
|
|
880
|
+
* ⚠️ SECOND ORPHAN OF THE SAME KIND. This paragraph was stranded above `#saltAdoptionClosed` and
|
|
881
|
+
* followed by that method's own block, exactly like the `#getSessionSalt` one re-homed in the
|
|
882
|
+
* previous pass — which walked straight past this one sixty lines below it. Two in one file is not
|
|
883
|
+
* coincidence: inserting a method between a doc block and its subject leaves no error, no lint,
|
|
884
|
+
* and no test, so the drift is invisible until someone reads for it.
|
|
885
|
+
*/
|
|
886
|
+
persistSessionSalt(agentName, sessionId, salt) {
|
|
887
|
+
if (!this.#db) {
|
|
888
|
+
// NOT a silent return — review F7. The other two persist failures each emit an event, so a
|
|
889
|
+
// derive that could not store because the handle is closed was the ONE salt path producing no
|
|
890
|
+
// record at all. Only reachable during shutdown, which is exactly when a lone unexplained
|
|
891
|
+
// gap in the log is hardest to account for later.
|
|
892
|
+
this.#ctx.logger.error("session.salt.persist.failed", {
|
|
893
|
+
agentName, sessionId, reason: "db_closed",
|
|
894
|
+
impact: "the salt was NOT stored and is not announced; the agreement stays open and re-runs on the next connect",
|
|
895
|
+
});
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
try {
|
|
899
|
+
/**
|
|
900
|
+
* ORDER MATTERS HERE, and getting it wrong cost three findings — review F3, F4, F7.
|
|
901
|
+
*
|
|
902
|
+
* The adoption guard used to run FIRST, above `!this.#db` and outside this `try`. That:
|
|
903
|
+
* - short-circuited the `salt_already_stored` discrimination below, so a session that DOES
|
|
904
|
+
* hold a valid salt was told it "stays unsalted FOR THE LIFE of the session" after a
|
|
905
|
+
* transient read failure — a refusal asserting something false about the row (F4);
|
|
906
|
+
* - put `getSessionTree`'s `#requireAgentId` throw outside the `try`, where it surfaced as
|
|
907
|
+
* *"the stream read failed"* instead of a named salt-persist failure (F7).
|
|
908
|
+
*
|
|
909
|
+
* So the row's own state is established first, and only a session with no salt at all reaches
|
|
910
|
+
* the adoption question.
|
|
911
|
+
*/
|
|
912
|
+
const existingRow = this.#db
|
|
913
|
+
.prepare("SELECT length(content_salt) AS n FROM sessions WHERE agent_id = ? AND session_id = ?")
|
|
914
|
+
.get(this.#ctx.requireAgentId(agentName), sessionId);
|
|
915
|
+
if (existingRow?.n === SESSION_SALT_BYTES) {
|
|
916
|
+
this.#ctx.logger.error("session.salt.persist.failed", {
|
|
917
|
+
agentName, sessionId, reason: "salt_already_stored",
|
|
918
|
+
impact: "this session already has a salt and it was NOT replaced — Decision #8 is one salt per session. Reaching here means a read failure made this side believe it had none; the stored salt is intact, nothing was announced, and the agreement re-runs against it on the next connect.",
|
|
919
|
+
});
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
const adoption = this.saltAdoptionClosed(agentName, sessionId);
|
|
923
|
+
if (adoption.closed) {
|
|
924
|
+
this.#ctx.logger.warn("session.salt.adoption.refused", {
|
|
925
|
+
agentName, sessionId, reason: "already_hashing", leafCount: adoption.leafCount, frontier: adoption.why,
|
|
926
|
+
impact: "this session has already hashed content under the unsalted rule, so the salt was NOT adopted — it stays unsalted FOR THE LIFE of the session. Adopting now would hash the rest of the conversation differently and leave a transcript that neither rule can verify end to end.",
|
|
927
|
+
guidance: "Nothing is broken and no message was lost: an unsalted session is exactly as verifiable as every session before this feature existed. It only means a relay holding the hashes could confirm a guess at a short message in THIS conversation. If you want the protection, start a new session — the agreement runs at open, before anything is hashed.",
|
|
928
|
+
});
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* THE ROW COUNT IS THE CHECK, and without it this method reported success for a write that
|
|
933
|
+
* stored nothing.
|
|
934
|
+
*
|
|
935
|
+
* An `UPDATE` that matches no row does not throw — it returns `changes: 0`. So a session whose
|
|
936
|
+
* row is missing (retired agent, a row that failed to write at creation, an id that does not
|
|
937
|
+
* line up) took the success branch, cached the salt in memory, and announced our fingerprint
|
|
938
|
+
* to the counterparty. Agreement confirmed, nothing on disk, and the failure surfaces at the
|
|
939
|
+
* next restart as the divergence this whole design exists to make loud — except one restart
|
|
940
|
+
* late and with both sides believing they had agreed.
|
|
941
|
+
*
|
|
942
|
+
* Found by a mutant that removed the caller's `if (!persisted) return`: the suite stayed green,
|
|
943
|
+
* because nothing could produce a false from here.
|
|
944
|
+
*/
|
|
945
|
+
/**
|
|
946
|
+
* ONE SALT PER SESSION, ENFORCED AT THE WRITE — review F18.
|
|
947
|
+
*
|
|
948
|
+
* This `UPDATE` was unconditional, so it could replace an already-stored VALID salt. The path
|
|
949
|
+
* is real: `#getSessionSalt` returns null on a transient read failure, which sends this side
|
|
950
|
+
* down the derive path, which then overwrote the perfectly good salt on disk. The read error
|
|
951
|
+
* was logged; the destruction of the durable value was not — and the read log actively said
|
|
952
|
+
* the wrong thing, promising only that we would "offer a fresh contribution".
|
|
953
|
+
*
|
|
954
|
+
* The predicate has to allow ONE overwrite: a wrong-width blob is refused by `#getSessionSalt`
|
|
955
|
+
* (F8) precisely so a corrupt row can be replaced rather than stranding the session forever.
|
|
956
|
+
* So: write when there is nothing there, or when what is there is not a salt.
|
|
957
|
+
*/
|
|
958
|
+
const written = this.#db
|
|
959
|
+
.prepare("UPDATE sessions SET content_salt = ? WHERE agent_id = ? AND session_id = ? " +
|
|
960
|
+
"AND (content_salt IS NULL OR length(content_salt) <> ?)")
|
|
961
|
+
.run(Buffer.from(salt), this.#ctx.requireAgentId(agentName), sessionId, SESSION_SALT_BYTES);
|
|
962
|
+
if (Number(written.changes) !== 1) {
|
|
963
|
+
// WHICH of the two it was. "No row" is a broken session record; "a salt is already there" is
|
|
964
|
+
// this guard doing its job, and telling an operator the row is missing when it is not would
|
|
965
|
+
// send them to look at the wrong thing.
|
|
966
|
+
// The `salt_already_stored` case is decided above now, before the adoption question, so
|
|
967
|
+
// reaching here with a valid salt in the row is not possible. Re-read anyway rather than
|
|
968
|
+
// assume: a wrong-width blob also fails the predicate and must not be reported as a missing
|
|
969
|
+
// row, which would send the operator to look at session state for a corrupt value.
|
|
970
|
+
const existing = this.#db
|
|
971
|
+
.prepare("SELECT length(content_salt) AS n FROM sessions WHERE agent_id = ? AND session_id = ?")
|
|
972
|
+
.get(this.#ctx.requireAgentId(agentName), sessionId);
|
|
973
|
+
const alreadyStored = existing?.n === SESSION_SALT_BYTES;
|
|
974
|
+
this.#ctx.logger.error("session.salt.persist.failed", {
|
|
975
|
+
agentName, sessionId, changes: Number(written.changes),
|
|
976
|
+
reason: alreadyStored ? "salt_already_stored" : "no_session_row",
|
|
977
|
+
impact: alreadyStored
|
|
978
|
+
? "this session already has a salt and it was NOT replaced — Decision #8 is one salt per session. Reaching here means a read failure made this side believe it had none; the stored salt is intact, nothing was announced, and the agreement re-runs against it on the next connect."
|
|
979
|
+
: "the salt was NOT stored — no session row matched — so it is not announced either; the agreement stays open rather than being confirmed against a value that exists only in memory",
|
|
980
|
+
});
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
983
|
+
this.#sessionSalts.set(this.#ctx.sessionKey(agentName, sessionId), salt);
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
986
|
+
catch (err) {
|
|
987
|
+
this.#ctx.logger.error("session.salt.persist.failed", {
|
|
988
|
+
agentName, sessionId, error: extractErrorMessage(err),
|
|
989
|
+
impact: "the salt was NOT stored, so it is not announced to the counterparty either; the agreement stays open rather than being confirmed against a value that would vanish at the next restart",
|
|
990
|
+
});
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* DROP AN UNSPENT SALT — `DOD-M15-SALTSPLIT-1`. The second writer of `content_salt`, and the only
|
|
996
|
+
* one that clears it.
|
|
997
|
+
*
|
|
998
|
+
* Reached when the counterparty tells us it can never adopt a salt for this session. Keeping ours
|
|
999
|
+
* would mean every message we send from here is refused by them with
|
|
1000
|
+
* `content_hash_salt_unavailable` — a conversation that dies while looking merely quiet, which is
|
|
1001
|
+
* the failure this exists to prevent.
|
|
1002
|
+
*
|
|
1003
|
+
* ⚠️ THE ADOPTION CHECK IS REPEATED HERE ON PURPOSE, not because the caller is untrusted.
|
|
1004
|
+
*
|
|
1005
|
+
* The caller has already computed `adoption`, so this looks redundant — and it is, for today's one
|
|
1006
|
+
* call site. It stays because the cost of a future caller getting it wrong is a transcript that no
|
|
1007
|
+
* single rule can verify: leaves hashed under a salt that has just been erased, with nothing
|
|
1008
|
+
* recording that they were. A guard whose failure mode is silent and permanent belongs next to the
|
|
1009
|
+
* destructive act, not only at the place that currently decides to perform it. Same reasoning that
|
|
1010
|
+
* made `placeOwnLeaf`'s authorship parameter required rather than optional.
|
|
1011
|
+
*
|
|
1012
|
+
* Returns true only if a salt was actually cleared.
|
|
1013
|
+
*/
|
|
1014
|
+
/**
|
|
1015
|
+
* SUSPEND, don't destroy — `DOD-M15-SALTSPLIT-1`, the authorization argument. Returns true if a
|
|
1016
|
+
* salt is now suspended (or already was).
|
|
1017
|
+
*
|
|
1018
|
+
* This is the frame handler's entry point. It runs the same two refusals as the erase below —
|
|
1019
|
+
* a spent salt and one mid-flight are not ours to set aside either, because the messages already
|
|
1020
|
+
* hashed under them would become unverifiable the moment we stop using it — and where they do not
|
|
1021
|
+
* fire it records the suspension instead of doing anything irreversible.
|
|
1022
|
+
*/
|
|
1023
|
+
suspendSalt(agentName, sessionId, correlationId) {
|
|
1024
|
+
const key = this.#ctx.sessionKey(agentName, sessionId);
|
|
1025
|
+
if (this.getSessionSalt(agentName, sessionId) === null)
|
|
1026
|
+
return false;
|
|
1027
|
+
if (this.#saltSuspended.has(key))
|
|
1028
|
+
return true;
|
|
1029
|
+
const inFlight = this.#hashedWithSalt.get(key) ?? 0;
|
|
1030
|
+
const adoption = this.saltAdoptionClosed(agentName, sessionId);
|
|
1031
|
+
if (inFlight > 0 || adoption.closed) {
|
|
1032
|
+
/**
|
|
1033
|
+
* SPENT, or mid-send. Suspending is not destructive, but it IS a split: content already hashed
|
|
1034
|
+
* under this salt stays hashed under it while everything after would be hashed the other way,
|
|
1035
|
+
* in one session, with nothing recording where the change happened. That is the one thing
|
|
1036
|
+
* Decision #8 forbids outright, so the salt keeps being used and the session stays honestly
|
|
1037
|
+
* broken rather than becoming dishonestly half-verifiable.
|
|
1038
|
+
*/
|
|
1039
|
+
this.#ctx.logger.info("session.salt.suspend.refused", {
|
|
1040
|
+
agentName, sessionId, correlationId,
|
|
1041
|
+
reason: inFlight > 0 ? "salted_hash_in_flight" : adoption.label,
|
|
1042
|
+
...(inFlight > 0 ? { inFlight } : { frontier: adoption.why }),
|
|
1043
|
+
impact: "the salt stays IN USE, because content in this session is already hashed under it and switching now would split the transcript — half verifiable by one rule, half by another. The counterparty cannot hold this salt, so it will keep refusing messages sent from here. See session.salt.split.",
|
|
1044
|
+
});
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
this.#saltSuspended.add(key);
|
|
1048
|
+
this.#ctx.logger.info("session.salt.suspended", {
|
|
1049
|
+
agentName, sessionId, correlationId,
|
|
1050
|
+
impact: "the counterparty can never adopt a salt for this session, so this side has STOPPED USING its own — messages are hashed the way every build before content salting hashed them, and every message continues to be accepted. Nothing was hashed under it, so nothing is split.",
|
|
1051
|
+
guidance: "No action. The salt bytes are kept, not erased: if the counterparty was merely unable to read its own state for a moment, its next announcement carrying a matching fingerprint restores this session to salted automatically. The bytes are erased only when this session actually hashes a message unsalted, which is the point after which keeping them would re-salt the session at the next restart.",
|
|
1052
|
+
});
|
|
1053
|
+
return true;
|
|
1054
|
+
}
|
|
1055
|
+
/** Un-suspend: the peer answered with a fingerprint matching the salt we kept. */
|
|
1056
|
+
resumeSalt(agentName, sessionId, correlationId) {
|
|
1057
|
+
const key = this.#ctx.sessionKey(agentName, sessionId);
|
|
1058
|
+
if (!this.#saltSuspended.has(key))
|
|
1059
|
+
return;
|
|
1060
|
+
/**
|
|
1061
|
+
* ⚠️ REFUSE THE RESUME IF THIS SESSION HAS ALREADY HASHED UNSALTED — pass 2, F1 (HIGH).
|
|
1062
|
+
*
|
|
1063
|
+
* `#resumeSalt` deleted the mark unconditionally, and the reviewer produced the counter-example
|
|
1064
|
+
* in ONE process with no restart: suspend, the peer keeps talking so a leaf lands, we send `m1`
|
|
1065
|
+
* under `sha256`, the peer's frontier recovers and announces `fingerprint(S)`, we resume, and
|
|
1066
|
+
* `m2` goes out under `hmac`. Two rules, one session — and `session.salt.resumed` asserted
|
|
1067
|
+
* *"No message was hashed while suspended, so the transcript is uniform"* while it was happening.
|
|
1068
|
+
* **The code never checked the thing its own log line claimed**, which is this milestone's
|
|
1069
|
+
* signature defect committed inside the fix for it.
|
|
1070
|
+
*
|
|
1071
|
+
* `#unsaltedAnnounced` is exactly that fact and is already maintained, so the check costs a
|
|
1072
|
+
* lookup. Once it is set the salt can never be used again, so it is erased here rather than left
|
|
1073
|
+
* to be found by a later restart.
|
|
1074
|
+
*/
|
|
1075
|
+
if (this.#unsaltedAnnounced.has(key)) {
|
|
1076
|
+
this.#ctx.logger.warn("session.salt.resume.refused", {
|
|
1077
|
+
agentName, sessionId, correlationId,
|
|
1078
|
+
impact: "the counterparty now confirms a salt this side is holding, but this session has ALREADY hashed at least one message unsalted. Resuming would put half the transcript under each rule, which no single rule can verify — so the session stays unsalted for its whole life and the salt is released.",
|
|
1079
|
+
guidance: "Nothing to do here, and nothing is lost: the transcript stays uniform and every message is intact. If you want the salt protection with this counterparty, start a new session — the agreement runs at open, before anything is hashed.",
|
|
1080
|
+
});
|
|
1081
|
+
this.#saltSuspended.delete(key);
|
|
1082
|
+
this.discardUnspentSalt(agentName, sessionId, correlationId);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
this.#saltSuspended.delete(key);
|
|
1086
|
+
/**
|
|
1087
|
+
* THE RECOVERY THE ERASE MADE IMPOSSIBLE. Keeping the bytes is what allows this line to exist:
|
|
1088
|
+
* the peer's earlier terminal frame was wrong (a frontier it could not read for a moment), it can
|
|
1089
|
+
* read again, and the fingerprints match — so the session resumes salted with nothing lost. An
|
|
1090
|
+
* erased salt cannot be re-derived from one side.
|
|
1091
|
+
*/
|
|
1092
|
+
this.#ctx.logger.info("session.salt.resumed", {
|
|
1093
|
+
agentName, sessionId, correlationId,
|
|
1094
|
+
impact: "the counterparty now confirms the same salt this side kept, so this session is salted again. It was suspended earlier because the counterparty reported it could never hold one; that has resolved. No message was hashed while suspended, so the transcript is uniform.",
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
discardUnspentSalt(agentName, sessionId, correlationId) {
|
|
1098
|
+
const held = this.getSessionSalt(agentName, sessionId);
|
|
1099
|
+
if (held === null)
|
|
1100
|
+
return false;
|
|
1101
|
+
/**
|
|
1102
|
+
* ⚠️ IN-FLIGHT FIRST — `DOD-M15-SALTSPLIT-1` review HIGH-2. `#saltAdoptionClosed` cannot see a
|
|
1103
|
+
* hash that has been computed under the salt but has not yet become a leaf, a hold or an
|
|
1104
|
+
* awaiting-ack entry, and that gap is a full relay round trip wide.
|
|
1105
|
+
*/
|
|
1106
|
+
/**
|
|
1107
|
+
* ⚠️ MEASURED UNREACHABLE FROM TODAY'S CALLERS, AND KEPT ANYWAY — pass 2 test-teeth, survivor 2.
|
|
1108
|
+
*
|
|
1109
|
+
* Deleting this block leaves the whole salt suite GREEN. That is the definition this unit has
|
|
1110
|
+
* used all along for *"not a guard, a comment that happens to execute"*, so it is labelled rather
|
|
1111
|
+
* than quietly left to look load-bearing. `#suspendSalt` refuses on `inFlight > 0` before a
|
|
1112
|
+
* session can ever be marked, and both callers of this method require the mark — so the deferred
|
|
1113
|
+
* erase cannot observe a non-zero count.
|
|
1114
|
+
*
|
|
1115
|
+
* It stays for one reason: **it sits at an irreversible write.** The earlier instance of this
|
|
1116
|
+
* question in this same unit was resolved by making the guard the actual decision-maker, and that
|
|
1117
|
+
* option does not exist here — `#suspendSalt` genuinely must refuse early, so the duplication is
|
|
1118
|
+
* structural rather than a mistake about where responsibility lives. For a destructive act, the
|
|
1119
|
+
* safe direction is to keep a check that cannot fire over removing one that turns out it could.
|
|
1120
|
+
*
|
|
1121
|
+
* What must NOT happen is claiming it as coverage. It is not tested and it is not testable from
|
|
1122
|
+
* outside; if a third caller ever reaches this method without the suspension mark, this becomes
|
|
1123
|
+
* reachable and needs a test in the same commit.
|
|
1124
|
+
*/
|
|
1125
|
+
const inFlight = this.#hashedWithSalt.get(this.#ctx.sessionKey(agentName, sessionId)) ?? 0;
|
|
1126
|
+
if (inFlight > 0) {
|
|
1127
|
+
this.#ctx.logger.info("session.salt.discard.refused", {
|
|
1128
|
+
agentName, sessionId, correlationId, reason: "salted_hash_in_flight", inFlight,
|
|
1129
|
+
impact: "the salt was NOT dropped: a message has already been hashed under it and is mid-send, so erasing it now would put a hash on the wire that nothing — including this daemon — could ever recompute. The session stays salted and the counterparty, which cannot adopt, will refuse what is in flight.",
|
|
1130
|
+
});
|
|
1131
|
+
return false;
|
|
1132
|
+
}
|
|
1133
|
+
const adoption = this.saltAdoptionClosed(agentName, sessionId);
|
|
1134
|
+
if (adoption.closed) {
|
|
1135
|
+
/**
|
|
1136
|
+
* SPENT. Something is already hashed under this salt, so it is not ours to drop.
|
|
1137
|
+
*
|
|
1138
|
+
* INFO, not ERROR, and the level is a judgement rather than a downgrade: this is the guard
|
|
1139
|
+
* doing its job correctly, and the FAILURE it accompanies — the session is split and unusable
|
|
1140
|
+
* — is reported by `session.salt.split` at ERROR from the caller that has the operator-facing
|
|
1141
|
+
* detail. Two ERRORs for one condition trains people to read neither. This line stays so the
|
|
1142
|
+
* refusal itself is correlatable when someone asks why the salt is still on disk.
|
|
1143
|
+
*/
|
|
1144
|
+
this.#ctx.logger.info("session.salt.discard.refused", {
|
|
1145
|
+
agentName, sessionId, correlationId, reason: adoption.label, frontier: adoption.why,
|
|
1146
|
+
impact: "the salt was NOT dropped, because content in this session is already hashed under it and erasing it would leave a transcript no single rule can verify. The session stays split: the counterparty holds no salt and refuses everything sent from here.",
|
|
1147
|
+
});
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
if (!this.#db) {
|
|
1151
|
+
this.#ctx.logger.error("session.salt.discard.failed", {
|
|
1152
|
+
agentName, sessionId, correlationId, reason: "db_closed",
|
|
1153
|
+
impact: "the salt is still stored, so after the next restart this side hashes salted while the counterparty refuses every message. Only reachable during shutdown; the agreement re-runs on the next connect, which discards it then.",
|
|
1154
|
+
});
|
|
1155
|
+
return false;
|
|
1156
|
+
}
|
|
1157
|
+
try {
|
|
1158
|
+
const cleared = this.#db
|
|
1159
|
+
.prepare("UPDATE sessions SET content_salt = NULL WHERE agent_id = ? AND session_id = ?")
|
|
1160
|
+
.run(this.#ctx.requireAgentId(agentName), sessionId);
|
|
1161
|
+
if (Number(cleared.changes) !== 1) {
|
|
1162
|
+
// The row-count check that `#persistSessionSalt` learned the hard way: an UPDATE matching no
|
|
1163
|
+
// row does not throw, and reporting success here would leave the durable salt in place while
|
|
1164
|
+
// the cache said otherwise — salted after a restart, unsalted before one.
|
|
1165
|
+
this.#ctx.logger.error("session.salt.discard.failed", {
|
|
1166
|
+
agentName, sessionId, correlationId, changes: Number(cleared.changes), reason: "no_session_row",
|
|
1167
|
+
impact: "the stored salt was NOT cleared, so this side hashes unsalted now and salted again after a restart — the transcript splits at the restart rather than here",
|
|
1168
|
+
});
|
|
1169
|
+
return false;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
catch (err) {
|
|
1173
|
+
this.#ctx.logger.error("session.salt.discard.failed", {
|
|
1174
|
+
agentName, sessionId, correlationId, error: extractErrorMessage(err),
|
|
1175
|
+
impact: "the stored salt was NOT cleared, so this side hashes unsalted now and salted again after a restart — the transcript splits at the restart rather than here",
|
|
1176
|
+
});
|
|
1177
|
+
return false;
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* CACHE AFTER ROW, and both or the session is worse off than before.
|
|
1181
|
+
*
|
|
1182
|
+
* `#saltForHashing` reads the cache on its first line and never consults the row, so clearing
|
|
1183
|
+
* one without the other produces a session that hashes one way in this process and the other way
|
|
1184
|
+
* in the next — the split transcript, arriving at a daemon restart instead of at a frame.
|
|
1185
|
+
*/
|
|
1186
|
+
this.#sessionSalts.delete(this.#ctx.sessionKey(agentName, sessionId));
|
|
1187
|
+
/**
|
|
1188
|
+
* ⚠️ THE MARK GOES WITH THE BYTES — pass 2, F5. Leaving the key in `#saltSuspended` after a
|
|
1189
|
+
* successful erase means a LATER agreed salt is silently never used: `#persistSessionSalt`'s
|
|
1190
|
+
* predicate explicitly allows a write when the column is NULL, and `abandonUnsaltedHash` can
|
|
1191
|
+
* re-open adoption — so the session would log `session.salt.agreed`, surface as protected, and
|
|
1192
|
+
* hash `sha256` for the rest of its life. A stale suppression is indistinguishable from a
|
|
1193
|
+
* feature that does not work.
|
|
1194
|
+
*/
|
|
1195
|
+
this.#saltSuspended.delete(this.#ctx.sessionKey(agentName, sessionId));
|
|
1196
|
+
this.#ctx.logger.info("session.salt.discarded", {
|
|
1197
|
+
agentName, sessionId, correlationId,
|
|
1198
|
+
impact: "the counterparty can never adopt a salt for this session, so this side dropped its own before spending it. Both sides now hash unsalted — exactly as verifiable as every session shipped before content salting existed, and every message continues to be accepted. Nothing was hashed under the discarded salt.",
|
|
1199
|
+
});
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
/** Apply one inbound salt-agreement frame. The verdict is the pure function's; this executes it. */
|
|
1203
|
+
async handleSaltFrame(agentName, sessionId, frame, correlationId) {
|
|
1204
|
+
const key = this.#ctx.sessionKey(agentName, sessionId);
|
|
1205
|
+
const peerHalfHex = frame.contribution ? Buffer.from(frame.contribution).toString("hex") : null;
|
|
1206
|
+
const peerFingerprintHex = frame.fingerprint ? Buffer.from(frame.fingerprint).toString("hex") : null;
|
|
1207
|
+
// WHY the peer closed, kept for the operator-facing reason — 006-CRYPTO finding 2. Recorded here
|
|
1208
|
+
// rather than in the `adoption_closed` handler because that action fires for OUR closure too,
|
|
1209
|
+
// and only the frame says what the PEER said.
|
|
1210
|
+
if (typeof frame.adoptionClosed === "string") {
|
|
1211
|
+
this.#saltPeerClosedLabel.set(key, frame.adoptionClosed);
|
|
1212
|
+
}
|
|
1213
|
+
const adoption = this.saltAdoptionClosed(agentName, sessionId);
|
|
1214
|
+
const action = onPeerSaltFrame({
|
|
1215
|
+
...this.saltState(agentName, sessionId),
|
|
1216
|
+
// Review F2: the frontier is what decides whether THIS side can still adopt, and only the
|
|
1217
|
+
// caller can count it. Without this the state machine derives, the persist refuses, and the
|
|
1218
|
+
// peer never learns — which is how the two sides end up on opposite verdicts.
|
|
1219
|
+
ownAdoption: adoption.closed
|
|
1220
|
+
? { closed: true, label: adoption.label, why: adoption.why }
|
|
1221
|
+
: { closed: false },
|
|
1222
|
+
// Keyed on the peer's BYTES, not on a repair counter: a genuinely NEW half from the peer must
|
|
1223
|
+
// still get our contribution back, and only an identical re-offer is the loop (review F14).
|
|
1224
|
+
alreadyRepairedAgainstPeerHalf: peerHalfHex !== null && this.#saltRepairedAgainst.get(key) === peerHalfHex,
|
|
1225
|
+
// The mirror, 006-CRYPTO finding 1: without it a saltless side answers a latched holder's
|
|
1226
|
+
// fingerprint forever. Same keying rule — an identical re-offer is the loop, a new one is not.
|
|
1227
|
+
alreadyRepairedAgainstPeerFingerprint: peerFingerprintHex !== null && this.#saltRepairedAgainstFingerprint.get(key) === peerFingerprintHex,
|
|
1228
|
+
frame,
|
|
1229
|
+
});
|
|
1230
|
+
if (action.action === "confirmed") {
|
|
1231
|
+
// DOD-M15-SALTSPLIT-1: the peer confirms the salt we KEPT while suspended — resume before logging
|
|
1232
|
+
// agreement, so a resumed session is never reported as agreed while still suspended.
|
|
1233
|
+
this.resumeSalt(agentName, sessionId, correlationId);
|
|
1234
|
+
this.#ctx.logger.info("session.salt.agreed", {
|
|
1235
|
+
agentName, sessionId, correlationId, via: "fingerprint_match",
|
|
1236
|
+
});
|
|
1237
|
+
// B2b-2: release a first send that is waiting on this agreement. Both `confirmed` and
|
|
1238
|
+
// `derive_and_announce` end with a salt this side can hash under, so both settle the wait.
|
|
1239
|
+
this.settleSaltPending(agentName, sessionId, "agreed");
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
if (action.action === "derive_and_announce") {
|
|
1243
|
+
/**
|
|
1244
|
+
* ⚠️ I DEFENDED THE OPPOSITE OF THIS TWICE, AND BOTH DEFENCES WERE WRONG. The code now does
|
|
1245
|
+
* what the "surviving mutant" did; recording that rather than quietly switching, because the
|
|
1246
|
+
* reasoning is the useful part.
|
|
1247
|
+
*
|
|
1248
|
+
* A failed persist used to fall through with no settle, so a waiting first send sat out the
|
|
1249
|
+
* FULL FIVE SECONDS and was then told, by the timeout path, to go and check its counterparty's
|
|
1250
|
+
* build version — for a fault that was this machine's own disk.
|
|
1251
|
+
*
|
|
1252
|
+
* Defence #1 said releasing the waiter "would hand it a null it would hash unsalted under."
|
|
1253
|
+
* True, and not a consequence: that is exactly what the timeout does. Defence #2 said the
|
|
1254
|
+
* remaining bound gave a repair a chance to land — and the review showed that essentially
|
|
1255
|
+
* cannot fire. This branch returns BEFORE the announce, so nothing goes out and nothing comes
|
|
1256
|
+
* back; all five of `#sendSaltFrame`'s callers are triggered by a peer connect or an inbound
|
|
1257
|
+
* frame. Only a counterparty reconnect inside those seconds could do it.
|
|
1258
|
+
*
|
|
1259
|
+
* So the real trade was a rare reconnect-within-five-seconds repair against five seconds of
|
|
1260
|
+
* visible latency on the operator's first message AND a diagnosis pointing at the wrong
|
|
1261
|
+
* machine. The repair loses. Settle immediately under its own name, so `#saltForHashing` can
|
|
1262
|
+
* say *our own write failed* instead of *they did not answer*.
|
|
1263
|
+
*/
|
|
1264
|
+
if (!this.persistSessionSalt(agentName, sessionId, action.salt)) {
|
|
1265
|
+
this.settleSaltPending(agentName, sessionId, "persist_failed");
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
this.#ctx.logger.info("session.salt.agreed", {
|
|
1269
|
+
agentName, sessionId, correlationId, via: "derived",
|
|
1270
|
+
});
|
|
1271
|
+
this.settleSaltPending(agentName, sessionId, "agreed");
|
|
1272
|
+
// `void`, not `await` — review F10. This runs inside the INBOUND content-stream handler, so
|
|
1273
|
+
// awaiting an outbound `newStream` here lets a stalled dial hold up the stream we are reading.
|
|
1274
|
+
// The connect-side call is `void`-ed for the same reason and this is now consistent with it.
|
|
1275
|
+
void this.#ctx.sendSaltFrame(agentName, sessionId, correlationId);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if (action.action === "adoption_closed") {
|
|
1279
|
+
// B2b-2: terminal means there is nothing left to wait for. A send still holding on the bound
|
|
1280
|
+
// would otherwise sit out the full five seconds for an answer that has already arrived and
|
|
1281
|
+
// said no — the slowest possible way to reach a decision both sides already agree on.
|
|
1282
|
+
this.settleSaltPending(agentName, sessionId, "closed");
|
|
1283
|
+
/**
|
|
1284
|
+
* Terminal, and NOT a freeze — review F1/F2. Both sides stay unsalted, which is exactly as
|
|
1285
|
+
* verifiable as every session shipped before the salt existed; the thing that was broken was
|
|
1286
|
+
* them disagreeing about it silently.
|
|
1287
|
+
*
|
|
1288
|
+
* WHICH SIDE DECLINED decides the level, and it is not decoration.
|
|
1289
|
+
*
|
|
1290
|
+
* If WE closed, an operator has lost a protection they could otherwise have had, and there is
|
|
1291
|
+
* something they can do about it — that is a WARN under `session.salt.adoption.refused`, which
|
|
1292
|
+
* keeps meaning what it has always meant.
|
|
1293
|
+
*
|
|
1294
|
+
* If we are merely LEARNING the peer closed, nothing about this machine is at fault and there
|
|
1295
|
+
* is nothing for its operator to do. Logging that at WARN would fire on the innocent side of
|
|
1296
|
+
* every such session and train them to ignore the name.
|
|
1297
|
+
*/
|
|
1298
|
+
/**
|
|
1299
|
+
* DOD-M15-SALTSPLIT-1 — ONE PLACE DECIDES WHETHER THE SALT GOES, and it is not here.
|
|
1300
|
+
*
|
|
1301
|
+
* ⚠️ THIS CALL WAS INSIDE THE `else` BELOW, AND THE REVERT TEST CAUGHT IT.
|
|
1302
|
+
*
|
|
1303
|
+
* Guarding it by `adoption.closed` here meant `#discardUnspentSalt`'s own adoption check could
|
|
1304
|
+
* never be reached, so deleting that check left all three tests GREEN — the survivor. A guard
|
|
1305
|
+
* nothing can redden is not a guard; it is a comment that happens to execute, which is the
|
|
1306
|
+
* shape this milestone keeps finding.
|
|
1307
|
+
*
|
|
1308
|
+
* Called unconditionally now. The method owns the spent/unspent decision, both outcomes run
|
|
1309
|
+
* through it, and deleting its check reddens the spent test immediately. That also removes the
|
|
1310
|
+
* duplicated condition: two places deciding the same thing is one place being wrong later.
|
|
1311
|
+
*/
|
|
1312
|
+
/**
|
|
1313
|
+
* The return is CONSUMED, not decorative — review LOW-5. `true` means a salt was actually
|
|
1314
|
+
* cleared, which settles the question below without a second read; `false` is ambiguous (we
|
|
1315
|
+
* held none, or we refused to drop one), so that case still asks.
|
|
1316
|
+
*/
|
|
1317
|
+
const suspended = this.suspendSalt(agentName, sessionId, correlationId);
|
|
1318
|
+
/**
|
|
1319
|
+
* "Still holds a salt it is USING" — suspension is what settles it, not possession. A suspended
|
|
1320
|
+
* session keeps the bytes on disk deliberately, and reporting that as an unrecoverable split
|
|
1321
|
+
* would fire the ERROR below on the one case that recovers by itself.
|
|
1322
|
+
*/
|
|
1323
|
+
const stillHoldsSalt = !suspended && this.getSessionSalt(agentName, sessionId) !== null;
|
|
1324
|
+
const shared = {
|
|
1325
|
+
agentName, sessionId, correlationId, detail: action.detail,
|
|
1326
|
+
/**
|
|
1327
|
+
* ⚠️ *"no message is affected"* IS FALSE WHEN WE ARE STILL HOLDING A SALT — review MEDIUM-4,
|
|
1328
|
+
* second instance. The sentence was written for a session where neither side ever adopted
|
|
1329
|
+
* one, and it stayed attached to a branch that now also covers the case where this side
|
|
1330
|
+
* kept a spent salt and every message it sends is about to be refused. Two log lines from
|
|
1331
|
+
* one event contradicting each other is worse than either alone.
|
|
1332
|
+
*/
|
|
1333
|
+
impact: stillHoldsSalt
|
|
1334
|
+
? "the counterparty will not use a content salt, and this side is still holding one it cannot drop — see session.salt.split on the next line for what that costs and what to do about it."
|
|
1335
|
+
: "neither side will use a content salt for this session, and both now know it. Messages are hashed the way every build before this feature hashed them — nothing is degraded relative to any shipped release, and no message is affected.",
|
|
1336
|
+
};
|
|
1337
|
+
if (adoption.closed) {
|
|
1338
|
+
/**
|
|
1339
|
+
* ⚠️ TWO REFUSALS, TWO DIFFERENT THINGS TO DO — and this used to report both as
|
|
1340
|
+
* `already_hashing`.
|
|
1341
|
+
*
|
|
1342
|
+
* A session that has already sent messages is the feature working: the fix is a new session,
|
|
1343
|
+
* and it will work. A frontier this side could not READ is local storage trouble: a new
|
|
1344
|
+
* session will refuse in exactly the same way, so sending the operator to open one is
|
|
1345
|
+
* sending them somewhere that cannot help. `frontier` carries the counts (or the error) so
|
|
1346
|
+
* the two are separable from the log alone.
|
|
1347
|
+
*/
|
|
1348
|
+
const unreadable = adoption.label === SALT_ADOPTION_LABELS.FRONTIER_UNREADABLE;
|
|
1349
|
+
this.#ctx.logger.warn("session.salt.adoption.refused", {
|
|
1350
|
+
...shared,
|
|
1351
|
+
reason: adoption.label,
|
|
1352
|
+
leafCount: adoption.leafCount,
|
|
1353
|
+
frontier: adoption.why,
|
|
1354
|
+
guidance: unreadable
|
|
1355
|
+
? "This side could not read its own message frontier, so it refused the salt rather than risk hashing half the session one way and half the other. Starting a new session will NOT help — it will refuse the same way. Look for session.content.held.restore.failed or other storage errors around this line; the conversation still works and every message is intact, it is just unsalted."
|
|
1356
|
+
: "Nothing is broken and no message was lost: an unsalted session is exactly as verifiable as every session before this feature existed. It only means a relay holding the hashes could confirm a guess at a short message in THIS conversation. If you want the protection, start a new session — the agreement runs at open, before anything is hashed.",
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
else {
|
|
1360
|
+
/**
|
|
1361
|
+
* DOD-M15-SALTSPLIT-1 — CARRY OUT THE CLAIM ABOVE INSTEAD OF ONLY STATING IT.
|
|
1362
|
+
*
|
|
1363
|
+
* `shared.impact` says *"neither side will use a content salt for this session, and both now
|
|
1364
|
+
* know it."* Nothing made that true: a salt already agreed on this side stayed on disk and in
|
|
1365
|
+
* the cache, and `#saltForHashing` returns it before it ever looks at adoption. Our adoption
|
|
1366
|
+
* is still open here, so nothing has been hashed under it and dropping it is free.
|
|
1367
|
+
*
|
|
1368
|
+
* Ordering matters — discard BEFORE the log, so the line cannot claim an outcome that the
|
|
1369
|
+
* write then failed to produce.
|
|
1370
|
+
*/
|
|
1371
|
+
this.#ctx.logger.info("session.salt.adoption.closed", shared);
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* ⚠️ OUTSIDE THE ADOPTION BRANCH — pass 2, F4. This used to live inside `if (adoption.closed)`,
|
|
1375
|
+
* so the one case that needed it most never got it: suspension refused for
|
|
1376
|
+
* `salted_hash_in_flight` while adoption is still OPEN leaves us holding a salt the peer can
|
|
1377
|
+
* never accept, and it took the `else` path. Measured on that exact scenario:
|
|
1378
|
+
* `suspend.refused = 1`, `adoption.closed = 1`, **`split = 0`** — while two other log lines
|
|
1379
|
+
* told the operator to *"see session.salt.split on the next line"*, a line that was never
|
|
1380
|
+
* written. Guidance pointing at an event that does not fire is worse than no guidance: it
|
|
1381
|
+
* spends the reader's trust and their time.
|
|
1382
|
+
*
|
|
1383
|
+
* The condition was always `stillHoldsSalt`; only its placement disagreed.
|
|
1384
|
+
*
|
|
1385
|
+
* ─── What this event means, moved here with the code it describes ─────────────────────────
|
|
1386
|
+
*
|
|
1387
|
+
* We hold a salt AND the peer has told us it can never hold one. Either our frontier closed
|
|
1388
|
+
* with the salt already spent, or a salted hash is mid-flight — both mean the salt cannot be
|
|
1389
|
+
* released, so the peer will refuse every message we send with `content_hash_salt_unavailable`.
|
|
1390
|
+
*
|
|
1391
|
+
* `session.salt.adoption.refused` may fire alongside, saying *"nothing is degraded relative to
|
|
1392
|
+
* any shipped release, and no message is affected"* — true for the ordinary refusal and FALSE
|
|
1393
|
+
* here, at the exact moment every message stops being accepted. Hence its own event at ERROR
|
|
1394
|
+
* rather than a tightened sentence on that one: an operator filtering for the refusal is
|
|
1395
|
+
* looking at a benign condition, and this is not it.
|
|
1396
|
+
*/
|
|
1397
|
+
if (stillHoldsSalt) {
|
|
1398
|
+
/**
|
|
1399
|
+
* ⚠️ TWO REASONS REACH `adoption.closed`, AND ONLY ONE IS ABOUT CONTENT — review MEDIUM-4.
|
|
1400
|
+
*
|
|
1401
|
+
* This fired for both with a single impact asserting *"content here is already hashed
|
|
1402
|
+
* under a salt"*. For `frontier_unreadable` that is a claim about content made from a
|
|
1403
|
+
* database read that FAILED — we do not know what was hashed; that is the whole condition.
|
|
1404
|
+
*
|
|
1405
|
+
* The WARN twenty lines above was explicitly corrected for this exact collapse — its
|
|
1406
|
+
* comment reads *"TWO REFUSALS, TWO DIFFERENT THINGS TO DO — and this used to report both
|
|
1407
|
+
* as `already_hashing`"* — and I reintroduced it one severity level up, with the guidance
|
|
1408
|
+
* that WARN was fixed to stop giving. Branching on the label the way it already does.
|
|
1409
|
+
*/
|
|
1410
|
+
const unreadable = adoption.label === SALT_ADOPTION_LABELS.FRONTIER_UNREADABLE;
|
|
1411
|
+
this.#ctx.logger.error("session.salt.split", {
|
|
1412
|
+
agentName, sessionId, correlationId, reason: adoption.label, frontier: adoption.why,
|
|
1413
|
+
impact: unreadable
|
|
1414
|
+
? "this side holds a salt, the counterparty can never hold one, and this side could NOT read its own message frontier — so whether anything has been hashed under that salt is unknown. The salt is kept rather than dropped, because dropping one that HAS been spent leaves a transcript no single rule can verify. Until the read succeeds, expect the counterparty to refuse messages sent from here."
|
|
1415
|
+
: "this session cannot continue. Content here is already hashed under a salt the counterparty can never hold, so they refuse every message sent from this side — the conversation looks quiet rather than broken, and the session can never be sealed because the two transcripts no longer agree on a leaf.",
|
|
1416
|
+
guidance: unreadable
|
|
1417
|
+
? "Do NOT start a new session yet — it would refuse in exactly the same way, because the fault is this side's storage rather than this conversation. Look for session.content.held.restore.failed or other storage errors around this line. Once the frontier reads again, this resolves to either an ordinary salted session or the split case, and the log will say which."
|
|
1418
|
+
: "Start a new session with this counterparty: the salt agreement runs at open, before anything is hashed, so a fresh session agrees or declines cleanly on both sides. This one cannot be repaired — the salt cannot be dropped without leaving a transcript no single rule can verify, and it cannot be shared with a peer that has already closed adoption.",
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
if (action.announce) {
|
|
1422
|
+
void this.#ctx.sendSaltFrame(agentName, sessionId, correlationId, action.announce);
|
|
1423
|
+
}
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
if (action.action === "repair") {
|
|
1427
|
+
/**
|
|
1428
|
+
* THE REPAIR — review F1. The two sides are out of step and CAN converge, so re-send our half
|
|
1429
|
+
* rather than destroying the session.
|
|
1430
|
+
*
|
|
1431
|
+
* At INFO because it is a real event an operator may need to correlate with a
|
|
1432
|
+
* `session.salt.announce.failed` or `session.salt.persist.failed` on either machine, and
|
|
1433
|
+
* because a session that repairs REPEATEDLY is a signal even though each repair is benign.
|
|
1434
|
+
*/
|
|
1435
|
+
this.#ctx.logger.info("session.salt.repair", {
|
|
1436
|
+
agentName, sessionId, correlationId, detail: action.detail,
|
|
1437
|
+
answeredWith: action.frame.contribution ? "contribution" : "fingerprint",
|
|
1438
|
+
});
|
|
1439
|
+
// Recorded ONLY for a repair that sent our half, because that is the one a second identical
|
|
1440
|
+
// offer must not repeat (review F14).
|
|
1441
|
+
if (peerHalfHex && action.frame.contribution)
|
|
1442
|
+
this.#saltRepairedAgainst.set(key, peerHalfHex);
|
|
1443
|
+
// AND THE MIRROR (006-CRYPTO finding 1): we answered the peer's FINGERPRINT with our half. An
|
|
1444
|
+
// earlier note here said recording this "says nothing, that branch is already terminal for the
|
|
1445
|
+
// peer" — it is terminal only for a peer that HOLDS a salt, and the loop is the case where we
|
|
1446
|
+
// do not. A second identical fingerprint now closes adoption instead of repairing again.
|
|
1447
|
+
if (peerFingerprintHex && action.frame.contribution) {
|
|
1448
|
+
this.#saltRepairedAgainstFingerprint.set(key, peerFingerprintHex);
|
|
1449
|
+
}
|
|
1450
|
+
void this.#ctx.sendSaltFrame(agentName, sessionId, correlationId, action.frame);
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
// `detail` is the primitive's own sentence wherever the primitive produced it — never a code of
|
|
1454
|
+
// ours substituted for it (Invariant 2). `guidance` is what the operator can DO, and it comes
|
|
1455
|
+
// from the total map so a reason can never reach a log without one.
|
|
1456
|
+
this.#ctx.logger.error("session.salt.disagreement", {
|
|
1457
|
+
agentName, sessionId, correlationId,
|
|
1458
|
+
reason: action.reason,
|
|
1459
|
+
detail: action.detail,
|
|
1460
|
+
guidance: SALT_FREEZE_GUIDANCE[action.reason],
|
|
1461
|
+
});
|
|
1462
|
+
/**
|
|
1463
|
+
* TELL THE PEER BEFORE TEARING DOWN — review F1's mirror.
|
|
1464
|
+
*
|
|
1465
|
+
* Only the fingerprint mismatch carries a notice, and only it can: the peer holds everything
|
|
1466
|
+
* needed to run the identical comparison and has simply not been given our side of it. Without
|
|
1467
|
+
* this the session stops answering and the far operator gets no reason at all, while ours gets a
|
|
1468
|
+
* full explanation — Decision #10 asks for BOTH sides to refuse by name.
|
|
1469
|
+
*
|
|
1470
|
+
* Awaited, unlike the other sends, because `destroySessionNode` on the next line takes the node
|
|
1471
|
+
* away and an un-awaited write would race its own transport. A failure is already handled
|
|
1472
|
+
* inside — the refusal here has happened either way.
|
|
1473
|
+
*/
|
|
1474
|
+
if (action.notifyPeer) {
|
|
1475
|
+
await this.#ctx.sendSaltFrame(agentName, sessionId, correlationId, action.notifyPeer);
|
|
1476
|
+
}
|
|
1477
|
+
await this.#ctx.freezeSession(agentName, sessionId, action.reason, {
|
|
1478
|
+
event: "session.salt.frozen",
|
|
1479
|
+
observation: `the salt agreement could not be completed with this counterparty: ${action.detail}`,
|
|
1480
|
+
impact: "the session was stopped rather than left to hash under a value the two sides do not share; no message was lost and the transcript is unaffected — only a NEW session moves this forward",
|
|
1481
|
+
reviveReason: `session_frozen_${action.reason}`,
|
|
1482
|
+
// The operator-facing sentence comes from the TOTAL guidance map, so a reason can never reach
|
|
1483
|
+
// this refusal without one — and it is what stops a salt disagreement being reported to them
|
|
1484
|
+
// as their counterparty failing a key check.
|
|
1485
|
+
reviveGuidance: SALT_FREEZE_GUIDANCE[action.reason],
|
|
1486
|
+
}, correlationId);
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Forget every salt fact this module holds for one session.
|
|
1490
|
+
*
|
|
1491
|
+
* ⚠️ ELEVEN MAPS BEHIND ONE CALL, and that is the point. These were eleven of the twenty-four
|
|
1492
|
+
* per-session containers the manager's cache eviction cleared by hand — so forgetting a session
|
|
1493
|
+
* meant knowing every map that might hold a piece of it, and adding a twelfth meant remembering to
|
|
1494
|
+
* add a twelfth delete. Missing one leaves a per-session entry alive for the life of the process.
|
|
1495
|
+
* The list of what to forget now lives beside the code that fills them.
|
|
1496
|
+
*/
|
|
1497
|
+
evictSession(agentName, sessionId) {
|
|
1498
|
+
const key = this.#ctx.sessionKey(agentName, sessionId);
|
|
1499
|
+
this.#saltContributions.delete(key);
|
|
1500
|
+
this.#sessionSalts.delete(key);
|
|
1501
|
+
this.#saltRepairedAgainst.delete(key);
|
|
1502
|
+
this.#saltRepairedAgainstFingerprint.delete(key);
|
|
1503
|
+
this.#saltPeerClosedLabel.delete(key);
|
|
1504
|
+
this.#hashedWithoutSalt.delete(key);
|
|
1505
|
+
this.#hashedWithSalt.delete(key);
|
|
1506
|
+
this.#saltSuspended.delete(key);
|
|
1507
|
+
this.#unsaltedAnnounced.delete(key);
|
|
1508
|
+
this.#saltLastOutcome.delete(key);
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* The pair the agreement reasons over: our salt, and the half that goes with it.
|
|
1512
|
+
*
|
|
1513
|
+
* Minting is deliberate and conditional. With NO salt we are certain to need a half — to offer, or
|
|
1514
|
+
* to derive with — so minting here is what makes the exchange work at all. WITH a salt we must
|
|
1515
|
+
* never mint; see `#ownSaltHalf`.
|
|
1516
|
+
*/
|
|
1517
|
+
saltState(agentName, sessionId) {
|
|
1518
|
+
const ownSalt = this.getSessionSalt(agentName, sessionId);
|
|
1519
|
+
return {
|
|
1520
|
+
ownSalt,
|
|
1521
|
+
ownContribution: ownSalt
|
|
1522
|
+
? this.ownSaltHalf(agentName, sessionId)
|
|
1523
|
+
: this.saltContributionFor(agentName, sessionId),
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* ONE mapping from a settled outcome to the operator-facing reason, so the send that WAITED and the
|
|
1528
|
+
* send that arrived afterwards cannot disagree about what happened.
|
|
1529
|
+
*/
|
|
1530
|
+
reasonForOutcome(key, outcome) {
|
|
1531
|
+
if (outcome === "announce_failed")
|
|
1532
|
+
return UNSALTED_REASONS.ANNOUNCE_FAILED;
|
|
1533
|
+
if (outcome === "persist_failed")
|
|
1534
|
+
return UNSALTED_REASONS.OUR_PERSIST_FAILED;
|
|
1535
|
+
if (outcome === "closed")
|
|
1536
|
+
return this.peerClosedReason(key);
|
|
1537
|
+
return UNSALTED_REASONS.AGREEMENT_TIMED_OUT;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
//# sourceMappingURL=session-salts.js.map
|