@cello-protocol/daemon 0.0.23 → 0.0.25
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/bin/cello-daemon.js +16 -65
- package/dist/bin/cello-daemon.js.map +1 -1
- package/dist/bundled-consortium-manifest.d.ts +45 -0
- package/dist/bundled-consortium-manifest.d.ts.map +1 -0
- package/dist/bundled-consortium-manifest.js +79 -0
- package/dist/bundled-consortium-manifest.js.map +1 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +176 -12
- package/dist/daemon.js.map +1 -1
- package/dist/directory-bootstrap.d.ts +65 -0
- package/dist/directory-bootstrap.d.ts.map +1 -1
- package/dist/directory-bootstrap.js +105 -2
- package/dist/directory-bootstrap.js.map +1 -1
- package/dist/file-manifest-provider.d.ts +19 -0
- package/dist/file-manifest-provider.d.ts.map +1 -1
- package/dist/file-manifest-provider.js +32 -0
- package/dist/file-manifest-provider.js.map +1 -1
- package/dist/manifest-deps.d.ts +36 -0
- package/dist/manifest-deps.d.ts.map +1 -0
- package/dist/manifest-deps.js +111 -0
- package/dist/manifest-deps.js.map +1 -0
- package/dist/seal-frontier-verify.d.ts +45 -0
- package/dist/seal-frontier-verify.d.ts.map +1 -1
- package/dist/seal-frontier-verify.js +49 -0
- package/dist/seal-frontier-verify.js.map +1 -1
- 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/session-node-manager.d.ts +11 -0
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +21 -0
- package/dist/session-node-manager.js.map +1 -1
- package/package.json +3 -3
package/dist/bin/cello-daemon.js
CHANGED
|
@@ -13,9 +13,7 @@ import { homedir } from "node:os";
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { startDaemon } from "../daemon.js";
|
|
15
15
|
import { createDirectoryEndpointResolver } from "../directory-bootstrap.js";
|
|
16
|
-
import {
|
|
17
|
-
import { RandomizedPollScheduler } from "../manifest-poll-scheduler.js";
|
|
18
|
-
import { ManifestDirectoryChallengeVerifier } from "@cello-protocol/transport";
|
|
16
|
+
import { buildManifestDeps } from "../manifest-deps.js";
|
|
19
17
|
const MAX_CONNECTIONS = 16;
|
|
20
18
|
// Composition root: stdout JSON logger
|
|
21
19
|
const logger = {
|
|
@@ -40,73 +38,26 @@ const celloDir = process.env.CELLO_DIR || join(homedir(), ".cello");
|
|
|
40
38
|
const socketPath = join(celloDir, "daemon.sock");
|
|
41
39
|
const lockFilePath = join(celloDir, "daemon.lock");
|
|
42
40
|
const version = process.env.CELLO_VERSION || "0.0.1";
|
|
43
|
-
/**
|
|
44
|
-
* Build the optional consortium-manifest deps from the environment (M7 J-AUTH).
|
|
45
|
-
*
|
|
46
|
-
* CELLO_CONSORTIUM_MANIFEST absolute path to the manifest JSON
|
|
47
|
-
* CELLO_CONSORTIUM_ROOT_KEYS comma-separated officer root pubkeys (hex)
|
|
48
|
-
* CELLO_CONSORTIUM_THRESHOLD minimum officer signatures (integer)
|
|
49
|
-
*
|
|
50
|
-
* When the manifest path is unset, returns {} — step-6 stays off (M6 compat).
|
|
51
|
-
* One FileManifestProvider instance is shared between startDaemon's loadAndVerify
|
|
52
|
-
* call and the ManifestDirectoryChallengeVerifier, so step-6 reads the same cached,
|
|
53
|
-
* verified manifest.
|
|
54
|
-
*/
|
|
55
|
-
function buildManifestDeps(logger) {
|
|
56
|
-
const manifestPath = process.env.CELLO_CONSORTIUM_MANIFEST;
|
|
57
|
-
if (!manifestPath)
|
|
58
|
-
return {};
|
|
59
|
-
const rootKeysRaw = process.env.CELLO_CONSORTIUM_ROOT_KEYS ?? "";
|
|
60
|
-
const manifestRootKeys = rootKeysRaw.split(",").map((k) => k.trim()).filter((k) => k.length > 0);
|
|
61
|
-
const manifestThreshold = Number.parseInt(process.env.CELLO_CONSORTIUM_THRESHOLD ?? "", 10);
|
|
62
|
-
if (manifestRootKeys.length === 0 || Number.isNaN(manifestThreshold)) {
|
|
63
|
-
throw new Error("CELLO_CONSORTIUM_MANIFEST is set but CELLO_CONSORTIUM_ROOT_KEYS / CELLO_CONSORTIUM_THRESHOLD are missing or invalid");
|
|
64
|
-
}
|
|
65
|
-
const manifestProvider = new FileManifestProvider({ path: manifestPath });
|
|
66
|
-
const challengeVerifier = new ManifestDirectoryChallengeVerifier(manifestProvider);
|
|
67
|
-
// Anti-rollback (DOD-AUTH-2 / TUF): the last-verified manifest version is persisted to refuse a
|
|
68
|
-
// manifest whose version regressed across restarts. PERSIST-002 (AC-008): this now lives in the
|
|
69
|
-
// encrypted manifest_state table (a manifest-version.json file is no longer written) — startDaemon
|
|
70
|
-
// constructs the DB-backed store itself after opening the DB, so the bin injects nothing here.
|
|
71
|
-
// DOD-AUTH-2: background manifest poll. The directory is re-polled on a randomized
|
|
72
|
-
// 6–12h interval (thundering-herd avoidance) and a newer signed manifest is adopted.
|
|
73
|
-
// The interval is env-injectable so the live binary test can poll sub-second instead
|
|
74
|
-
// of waiting hours; production leaves these unset → the 6–12h default window.
|
|
75
|
-
// Both-or-neither, positive, min <= max — a partial/invalid override fails LOUDLY
|
|
76
|
-
// rather than silently reverting to 6–12h or producing a negative (tight-loop) delay.
|
|
77
|
-
const rawPollMin = process.env.CELLO_MANIFEST_POLL_MIN_MS;
|
|
78
|
-
const rawPollMax = process.env.CELLO_MANIFEST_POLL_MAX_MS;
|
|
79
|
-
let pollOpts;
|
|
80
|
-
if (rawPollMin !== undefined || rawPollMax !== undefined) {
|
|
81
|
-
const minMs = Number.parseInt(rawPollMin ?? "", 10);
|
|
82
|
-
const maxMs = Number.parseInt(rawPollMax ?? "", 10);
|
|
83
|
-
if (Number.isNaN(minMs) || Number.isNaN(maxMs) || minMs <= 0 || maxMs < minMs) {
|
|
84
|
-
throw new Error("CELLO_MANIFEST_POLL_MIN_MS / _MAX_MS must BOTH be set to positive integers with min <= max");
|
|
85
|
-
}
|
|
86
|
-
pollOpts = { minMs, maxMs };
|
|
87
|
-
}
|
|
88
|
-
const manifestPollScheduler = new RandomizedPollScheduler(pollOpts);
|
|
89
|
-
logger.info("daemon.manifest.configured", {
|
|
90
|
-
manifestPath,
|
|
91
|
-
rootKeyCount: manifestRootKeys.length,
|
|
92
|
-
threshold: manifestThreshold,
|
|
93
|
-
pollMinMs: pollOpts?.minMs ?? null,
|
|
94
|
-
pollMaxMs: pollOpts?.maxMs ?? null,
|
|
95
|
-
});
|
|
96
|
-
return { manifestProvider, manifestRootKeys, manifestThreshold, challengeVerifier, manifestPollScheduler };
|
|
97
|
-
}
|
|
98
41
|
async function main() {
|
|
99
42
|
// M7 Keystone (Part 1): give the daemon its door to the directory. The resolver
|
|
100
43
|
// discovers the directory endpoint via GET ${CELLO_DIRECTORY_URL}/bootstrap (the
|
|
101
44
|
// proven M6 path); startDaemon builds the real signalingConnect from it + the
|
|
102
45
|
// primary agent identity.
|
|
103
|
-
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
//
|
|
46
|
+
// FINDING-4: this resolver is wrapped by the daemon's roster-aware failover resolver, which owns
|
|
47
|
+
// ALL fallback semantics (roster + sticky). staleFallback:false makes it report a dead primary as
|
|
48
|
+
// null on a fresh /bootstrap failure — WITHOUT it, the wrapper would keep receiving the stale dead
|
|
49
|
+
// endpoint and never fail over (the exact live kill-primary bug). The roster is the real fallback.
|
|
50
|
+
const directoryEndpointResolver = createDirectoryEndpointResolver({ logger, staleFallback: false });
|
|
51
|
+
// M7 J-AUTH (DOD-AUTH-1/2) + FINDING-4: consortium-manifest deps. buildManifestDeps chooses:
|
|
52
|
+
// - DEFAULT (no CELLO_CONSORTIUM_MANIFEST) — load the COMPILED-IN production roster + step-6
|
|
53
|
+
// directory identity auth, so a cold-boot daemon knows every directory and can fail over to a
|
|
54
|
+
// reachable one (redundancy invariant). Gated on CELLO_DIRECTORY_URL actually being a bundled
|
|
55
|
+
// node — a daemon pointed at a local/non-bundled directory (local dev, e2e spine harness) gets
|
|
56
|
+
// the M6 backward-compat path (no roster, no step-6) instead of wrongly failing step-6.
|
|
57
|
+
// - OVERRIDE (CELLO_CONSORTIUM_MANIFEST set) — operator-supplied manifest FILE + env root keys /
|
|
58
|
+
// threshold + optional /manifest poll (the pre-FINDING-4 opt-in path).
|
|
59
|
+
// When a manifest is active, the daemon verifies the directory's step-6 identity proof against the
|
|
60
|
+
// node pubkeys in it. Both dialers (keystone + per-agent) receive the verifier via startDaemon.
|
|
110
61
|
const manifest = buildManifestDeps(logger);
|
|
111
62
|
const handle = await startDaemon({
|
|
112
63
|
celloDir,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cello-daemon.js","sourceRoot":"","sources":["../../src/bin/cello-daemon.ts"],"names":[],"mappings":";AACA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,+BAA+B,EAAE,MAAM,2BAA2B,CAAC;AAC5E,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"cello-daemon.js","sourceRoot":"","sources":["../../src/bin/cello-daemon.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FINDING-4 — the bundled consortium roster (sovereign-node REDUNDANCY invariant).
|
|
3
|
+
*
|
|
4
|
+
* This is the signed list of the sovereign directory nodes that make up the CELLO consortium,
|
|
5
|
+
* COMPILED INTO the client. It is loaded by default (when the operator does not override it via
|
|
6
|
+
* CELLO_CONSORTIUM_MANIFEST) so that a cold-boot daemon already knows every directory — and can
|
|
7
|
+
* fail over to a reachable one — even when its configured/primary directory is unreachable. Without
|
|
8
|
+
* a bundled roster the failover resolver has nothing to route to (the exact FINDING-4 gap: the
|
|
9
|
+
* roster-aware dialer was correct but the roster was empty), so a single directory being down
|
|
10
|
+
* stranded the client at startup, violating the redundancy invariant.
|
|
11
|
+
*
|
|
12
|
+
* Trust model:
|
|
13
|
+
* - The manifest is threshold-signed by the consortium officer key(s). At load the client
|
|
14
|
+
* RE-VERIFIES it against BUNDLED_CONSORTIUM_ROOT_KEYS (a self-consistency / anti-corruption
|
|
15
|
+
* gate — the manifest and root keys ship together, so this catches a bad regeneration, not a
|
|
16
|
+
* network adversary).
|
|
17
|
+
* - The node `pubkey`s are the directories' Ed25519 node keys. They are the trust anchor for
|
|
18
|
+
* step-6 directory identity auth (M7-MANIFEST-002): when the client connects to (or fails over
|
|
19
|
+
* to) a directory, the directory must sign the client's challenge with the matching node key,
|
|
20
|
+
* proving it is the real consortium node for that nodeId — defeating a MITM on the plaintext
|
|
21
|
+
* /bootstrap that would otherwise redirect the client to a rogue directory. THIS is the
|
|
22
|
+
* adversarial defense; it is enabled by default alongside the roster.
|
|
23
|
+
*
|
|
24
|
+
* nodeId MUST equal the directory's reported NODE_ID (its AWS region string); the step-6 verifier
|
|
25
|
+
* looks the node up by nodeId. pubkey MUST equal /cello/<env>/directory/manifest-signer-pubkey for
|
|
26
|
+
* that region (derived from the directory's node-private-key). endpoint is the HTTP /bootstrap base.
|
|
27
|
+
*
|
|
28
|
+
* Regeneration: on directory node-key or officer-key rotation, re-sign with
|
|
29
|
+
* infra/scripts/sign-consortium-manifest and update this constant + BUNDLED_CONSORTIUM_ROOT_KEYS.
|
|
30
|
+
* The officer signing key lives in Secrets Manager (cello/<env>/consortium/officer-key-0); only its
|
|
31
|
+
* PUBLIC key is embedded here.
|
|
32
|
+
*
|
|
33
|
+
* Crypto reference: RFC 8032 (Ed25519 threshold signatures, verified by verifyManifest).
|
|
34
|
+
*/
|
|
35
|
+
import type { ConsortiumManifestInput } from "@cello-protocol/crypto";
|
|
36
|
+
/** The signed consortium manifest (roster of sovereign directory nodes). */
|
|
37
|
+
export declare const BUNDLED_CONSORTIUM_MANIFEST: ConsortiumManifestInput;
|
|
38
|
+
/**
|
|
39
|
+
* Pinned consortium officer root public key(s). Officer 0's Ed25519 public key (hex). The client
|
|
40
|
+
* verifies BUNDLED_CONSORTIUM_MANIFEST's threshold signatures against this set. Public data.
|
|
41
|
+
*/
|
|
42
|
+
export declare const BUNDLED_CONSORTIUM_ROOT_KEYS: readonly string[];
|
|
43
|
+
/** Minimum number of distinct valid officer signatures required (dev: single officer, threshold 1). */
|
|
44
|
+
export declare const BUNDLED_CONSORTIUM_THRESHOLD = 1;
|
|
45
|
+
//# sourceMappingURL=bundled-consortium-manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundled-consortium-manifest.d.ts","sourceRoot":"","sources":["../src/bundled-consortium-manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEtE,4EAA4E;AAC5E,eAAO,MAAM,2BAA2B,EAAE,uBAkCzC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,EAAE,SAAS,MAAM,EAEzD,CAAC;AAEF,uGAAuG;AACvG,eAAO,MAAM,4BAA4B,IAAI,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FINDING-4 — the bundled consortium roster (sovereign-node REDUNDANCY invariant).
|
|
3
|
+
*
|
|
4
|
+
* This is the signed list of the sovereign directory nodes that make up the CELLO consortium,
|
|
5
|
+
* COMPILED INTO the client. It is loaded by default (when the operator does not override it via
|
|
6
|
+
* CELLO_CONSORTIUM_MANIFEST) so that a cold-boot daemon already knows every directory — and can
|
|
7
|
+
* fail over to a reachable one — even when its configured/primary directory is unreachable. Without
|
|
8
|
+
* a bundled roster the failover resolver has nothing to route to (the exact FINDING-4 gap: the
|
|
9
|
+
* roster-aware dialer was correct but the roster was empty), so a single directory being down
|
|
10
|
+
* stranded the client at startup, violating the redundancy invariant.
|
|
11
|
+
*
|
|
12
|
+
* Trust model:
|
|
13
|
+
* - The manifest is threshold-signed by the consortium officer key(s). At load the client
|
|
14
|
+
* RE-VERIFIES it against BUNDLED_CONSORTIUM_ROOT_KEYS (a self-consistency / anti-corruption
|
|
15
|
+
* gate — the manifest and root keys ship together, so this catches a bad regeneration, not a
|
|
16
|
+
* network adversary).
|
|
17
|
+
* - The node `pubkey`s are the directories' Ed25519 node keys. They are the trust anchor for
|
|
18
|
+
* step-6 directory identity auth (M7-MANIFEST-002): when the client connects to (or fails over
|
|
19
|
+
* to) a directory, the directory must sign the client's challenge with the matching node key,
|
|
20
|
+
* proving it is the real consortium node for that nodeId — defeating a MITM on the plaintext
|
|
21
|
+
* /bootstrap that would otherwise redirect the client to a rogue directory. THIS is the
|
|
22
|
+
* adversarial defense; it is enabled by default alongside the roster.
|
|
23
|
+
*
|
|
24
|
+
* nodeId MUST equal the directory's reported NODE_ID (its AWS region string); the step-6 verifier
|
|
25
|
+
* looks the node up by nodeId. pubkey MUST equal /cello/<env>/directory/manifest-signer-pubkey for
|
|
26
|
+
* that region (derived from the directory's node-private-key). endpoint is the HTTP /bootstrap base.
|
|
27
|
+
*
|
|
28
|
+
* Regeneration: on directory node-key or officer-key rotation, re-sign with
|
|
29
|
+
* infra/scripts/sign-consortium-manifest and update this constant + BUNDLED_CONSORTIUM_ROOT_KEYS.
|
|
30
|
+
* The officer signing key lives in Secrets Manager (cello/<env>/consortium/officer-key-0); only its
|
|
31
|
+
* PUBLIC key is embedded here.
|
|
32
|
+
*
|
|
33
|
+
* Crypto reference: RFC 8032 (Ed25519 threshold signatures, verified by verifyManifest).
|
|
34
|
+
*/
|
|
35
|
+
/** The signed consortium manifest (roster of sovereign directory nodes). */
|
|
36
|
+
export const BUNDLED_CONSORTIUM_MANIFEST = {
|
|
37
|
+
version: 1,
|
|
38
|
+
not_before: "2026-01-01T00:00:00Z",
|
|
39
|
+
expires: "2030-01-01T00:00:00Z",
|
|
40
|
+
nodes: [
|
|
41
|
+
{
|
|
42
|
+
nodeId: "us-east-1",
|
|
43
|
+
pubkey: "167ca6b145bfdd3696af8f4befd883c3dc610f4a9c8d52a30f6a22f669dc27b5",
|
|
44
|
+
region: "us-east-1",
|
|
45
|
+
provider: "aws",
|
|
46
|
+
endpoint: "http://directory-us1.cello.mygentic.ai",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
nodeId: "eu-central-1",
|
|
50
|
+
pubkey: "8105b180b753d97b50039a7e94433fd2b419f43d61f9ad7caf2ac15ad5cd1b45",
|
|
51
|
+
region: "eu-central-1",
|
|
52
|
+
provider: "aws",
|
|
53
|
+
endpoint: "http://directory-eu1.cello.mygentic.ai",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
nodeId: "ap-northeast-1",
|
|
57
|
+
pubkey: "9b4b673a16487ba47363e3eaff844bf68f19736d82967918fb896b813e39b984",
|
|
58
|
+
region: "ap-northeast-1",
|
|
59
|
+
provider: "aws",
|
|
60
|
+
endpoint: "http://directory-ap1.cello.mygentic.ai",
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
signatures: [
|
|
64
|
+
{
|
|
65
|
+
officerIndex: 0,
|
|
66
|
+
signature: "1a372676e83ae323be8dbc6f8b786b3424706999efcccd70027798cbca91fe0b60cb35ac1e1af21078f24618d1f740b61bdcf06da03c5f90acc48d954039a104",
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Pinned consortium officer root public key(s). Officer 0's Ed25519 public key (hex). The client
|
|
72
|
+
* verifies BUNDLED_CONSORTIUM_MANIFEST's threshold signatures against this set. Public data.
|
|
73
|
+
*/
|
|
74
|
+
export const BUNDLED_CONSORTIUM_ROOT_KEYS = [
|
|
75
|
+
"8e9b99e5aa5505f2f50ec9f933f497a64956614575edce3427ec8a096c864199",
|
|
76
|
+
];
|
|
77
|
+
/** Minimum number of distinct valid officer signatures required (dev: single officer, threshold 1). */
|
|
78
|
+
export const BUNDLED_CONSORTIUM_THRESHOLD = 1;
|
|
79
|
+
//# sourceMappingURL=bundled-consortium-manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundled-consortium-manifest.js","sourceRoot":"","sources":["../src/bundled-consortium-manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAIH,4EAA4E;AAC5E,MAAM,CAAC,MAAM,2BAA2B,GAA4B;IAClE,OAAO,EAAE,CAAC;IACV,UAAU,EAAE,sBAAsB;IAClC,OAAO,EAAE,sBAAsB;IAC/B,KAAK,EAAE;QACL;YACE,MAAM,EAAE,WAAW;YACnB,MAAM,EAAE,kEAAkE;YAC1E,MAAM,EAAE,WAAW;YACnB,QAAQ,EAAE,KAAK;YACf,QAAQ,EAAE,wCAAwC;SACnD;QACD;YACE,MAAM,EAAE,cAAc;YACtB,MAAM,EAAE,kEAAkE;YAC1E,MAAM,EAAE,cAAc;YACtB,QAAQ,EAAE,KAAK;YACf,QAAQ,EAAE,wCAAwC;SACnD;QACD;YACE,MAAM,EAAE,gBAAgB;YACxB,MAAM,EAAE,kEAAkE;YAC1E,MAAM,EAAE,gBAAgB;YACxB,QAAQ,EAAE,KAAK;YACf,QAAQ,EAAE,wCAAwC;SACnD;KACF;IACD,UAAU,EAAE;QACV;YACE,YAAY,EAAE,CAAC;YACf,SAAS,EACP,kIAAkI;SACrI;KACF;CACF,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAsB;IAC7D,kEAAkE;CACnE,CAAC;AAEF,uGAAuG;AACvG,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAC"}
|
package/dist/daemon.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EASrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAM/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EASrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAM/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAsB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAM/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA0InF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,oBAAoB,CAAC;IAClC;;;;OAIG;IACH,qBAAqB,IAAI,kBAAkB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;CACtC;AAyCD,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAwwI7E"}
|
package/dist/daemon.js
CHANGED
|
@@ -45,6 +45,7 @@ import { DbRegistrationPersistence, DbIdentityStore } from "./db-identity-store.
|
|
|
45
45
|
import { DbManifestVersionStore } from "./manifest-version-store-db.js";
|
|
46
46
|
import { verify as ed25519Verify, sealToRecipient, generateKLocalSeed, InMemoryKeyProvider, hash as cryptoHash } from "@cello-protocol/crypto";
|
|
47
47
|
import { attemptSealUpgrade as attemptSealUpgradeImpl, verifyUpgradeConfirmedCert } from "./seal-upgrade.js";
|
|
48
|
+
import { upgradeAbsentToRecovered, hasAbsentParticipant } from "./seal-receipt-upgrade.js";
|
|
48
49
|
// CELLO-M7-MSG-001 (AC-013/AC-018): the single application content-size cap, enforced
|
|
49
50
|
// at the send point here (the receive point lives in the transport content decode).
|
|
50
51
|
import { MAX_CONTENT_BYTES, computeGenesisPrevRoot, buildAgentRevocationTbs } from "@cello-protocol/protocol-types";
|
|
@@ -52,10 +53,10 @@ import { resolveCelloEnv, createTransportSelector, isProductionVariant, } from "
|
|
|
52
53
|
import { selectAdvertisedAddress } from "./transport-selector.js";
|
|
53
54
|
import { parseSessionAssignment, sessionRequestErrorReason } from "./session-assignment-parser.js";
|
|
54
55
|
import { wireSessionCeremonyHandler, wireSessionOfferHandler, wireSealCeremonyHandler, verifyUnilateralCertificate, verifyBilateralSealCertificate, runAgentRefresh } from "./session-ceremony.js";
|
|
55
|
-
import { reDeriveFrontiers, findInflatedFrontier } from "./seal-frontier-verify.js";
|
|
56
|
+
import { reDeriveFrontiers, findInflatedFrontier, checkUnilateralFrontier } from "./seal-frontier-verify.js";
|
|
56
57
|
import { LocalAutoNatStub } from "@cello-protocol/transport";
|
|
57
58
|
import { startHttpManifestPoll } from "./http-manifest-poll.js";
|
|
58
|
-
import { resolveDirectoryUrl, manifestNodesToEndpoints } from "./directory-bootstrap.js";
|
|
59
|
+
import { resolveDirectoryUrl, manifestNodesToEndpoints, createRosterAwareEndpointResolver, } from "./directory-bootstrap.js";
|
|
59
60
|
/**
|
|
60
61
|
* M7-SESSION-001 (H-1): canonical byte encoding of a SEAL-INTERRUPTED leaf for
|
|
61
62
|
* Ed25519 signing/verification. Field order is fixed and deterministic. Both the
|
|
@@ -330,6 +331,25 @@ export async function startDaemon(config) {
|
|
|
330
331
|
const m = manifestProvider?.getCurrentManifest();
|
|
331
332
|
return m ? await manifestNodesToEndpoints(m.nodes, { logger }) : null;
|
|
332
333
|
};
|
|
334
|
+
// FINDING-4: roster-aware directory failover (bootstrap SPOF fix). The injected
|
|
335
|
+
// directoryEndpointResolver only ever probes the single configured node (CELLO_DIRECTORY_URL),
|
|
336
|
+
// so a down primary stranded the client at startup even though it held the other nodes'
|
|
337
|
+
// addresses. Wrap it with the consortium roster so the signaling dialer — and the ceremony
|
|
338
|
+
// endpoint that shares this same instance — route AROUND a down primary (primary-first,
|
|
339
|
+
// sticky-until-fail, randomized fallback), honoring the sovereign-node REDUNDANCY invariant.
|
|
340
|
+
// ONE instance → shared sticky state so signaling + ceremonies stay on the SAME directory node
|
|
341
|
+
// and fail over together. Undefined on the in-process test path (no directoryEndpointResolver),
|
|
342
|
+
// where the shared manager uses an injected signalingConnect instead.
|
|
343
|
+
const failoverEndpointResolver = directoryEndpointResolver
|
|
344
|
+
? createRosterAwareEndpointResolver({
|
|
345
|
+
primaryResolver: directoryEndpointResolver,
|
|
346
|
+
getConsortiumRoster: resolveConsortiumRoster,
|
|
347
|
+
logger,
|
|
348
|
+
})
|
|
349
|
+
: undefined;
|
|
350
|
+
// Null-safe accessor for the ceremony/handler getDirectoryEndpoint sites (mirrors the old
|
|
351
|
+
// `directoryEndpointResolver ? ... : null` wrapper, now routed through the failover resolver).
|
|
352
|
+
const getFailoverEndpoint = async () => failoverEndpointResolver ? await failoverEndpointResolver() : null;
|
|
333
353
|
// CELLO-M7-CONN-001 (DOD-CONN-3): start the daemon-level HTTP manifest poll. It fetches
|
|
334
354
|
// ${directoryHttpUrl}/manifest, verifies the threshold signature against the locally-pinned
|
|
335
355
|
// root keys, applies anti-rollback + expiry, and adopts a newer manifest — independent of any
|
|
@@ -484,7 +504,10 @@ export async function startDaemon(config) {
|
|
|
484
504
|
if (!directoryEndpointResolver) {
|
|
485
505
|
throw new Error("getAgentSignaling: no directory endpoint resolver configured (and no shared manager)");
|
|
486
506
|
}
|
|
487
|
-
|
|
507
|
+
// FINDING-4: dial through the roster-aware failover resolver so this agent's signaling
|
|
508
|
+
// stream routes around a down primary node. (failoverEndpointResolver is defined here
|
|
509
|
+
// because it is built iff directoryEndpointResolver is — guarded non-null just above.)
|
|
510
|
+
const resolver = failoverEndpointResolver ?? directoryEndpointResolver;
|
|
488
511
|
let nodeRef = null;
|
|
489
512
|
const connect = createSignalingConnect({
|
|
490
513
|
getDirectoryEndpoint: resolver,
|
|
@@ -513,7 +536,7 @@ export async function startDaemon(config) {
|
|
|
513
536
|
persistence: getPersistence(agentName),
|
|
514
537
|
agentPubkeyHex,
|
|
515
538
|
getNode: entry.getNode,
|
|
516
|
-
getDirectoryEndpoint:
|
|
539
|
+
getDirectoryEndpoint: getFailoverEndpoint,
|
|
517
540
|
getConsortiumEndpoints: resolveConsortiumRoster,
|
|
518
541
|
signaling: mgr,
|
|
519
542
|
logger,
|
|
@@ -524,7 +547,7 @@ export async function startDaemon(config) {
|
|
|
524
547
|
persistence: getPersistence(agentName),
|
|
525
548
|
agentPubkeyHex,
|
|
526
549
|
getNode: entry.getNode,
|
|
527
|
-
getDirectoryEndpoint:
|
|
550
|
+
getDirectoryEndpoint: getFailoverEndpoint,
|
|
528
551
|
getConsortiumEndpoints: resolveConsortiumRoster,
|
|
529
552
|
signaling: mgr,
|
|
530
553
|
logger,
|
|
@@ -591,7 +614,7 @@ export async function startDaemon(config) {
|
|
|
591
614
|
persistence: getPersistence(agent.name),
|
|
592
615
|
agentPubkeyHex: agent.pubkey,
|
|
593
616
|
getNode: getDirectoryNode,
|
|
594
|
-
getDirectoryEndpoint:
|
|
617
|
+
getDirectoryEndpoint: getFailoverEndpoint,
|
|
595
618
|
getConsortiumEndpoints: resolveConsortiumRoster,
|
|
596
619
|
signaling: mgr,
|
|
597
620
|
logger,
|
|
@@ -601,7 +624,7 @@ export async function startDaemon(config) {
|
|
|
601
624
|
persistence: getPersistence(agent.name),
|
|
602
625
|
agentPubkeyHex: agent.pubkey,
|
|
603
626
|
getNode: getDirectoryNode,
|
|
604
|
-
getDirectoryEndpoint:
|
|
627
|
+
getDirectoryEndpoint: getFailoverEndpoint,
|
|
605
628
|
getConsortiumEndpoints: resolveConsortiumRoster,
|
|
606
629
|
signaling: mgr,
|
|
607
630
|
logger,
|
|
@@ -1444,9 +1467,16 @@ export async function startDaemon(config) {
|
|
|
1444
1467
|
registrationInProgress = true;
|
|
1445
1468
|
try {
|
|
1446
1469
|
// Resolve the directory endpoint once for this registration (the context's
|
|
1447
|
-
// getDirectoryEndpoint is synchronous; the daemon's resolver is async
|
|
1448
|
-
//
|
|
1449
|
-
//
|
|
1470
|
+
// getDirectoryEndpoint is synchronous; the daemon's resolver is async).
|
|
1471
|
+
// FINDING-4 scope note: this uses the PRIMARY resolver directly (not the roster-aware
|
|
1472
|
+
// failover resolver — registration is deliberately out of FINDING-4's "signaling +
|
|
1473
|
+
// ceremony" scope). Because that primary is now built with staleFallback:false (so the
|
|
1474
|
+
// failover wrapper sees a dead primary as null), a transient /bootstrap blip here resolves
|
|
1475
|
+
// to null → directory_unreachable rather than riding through on a stale last-known-good;
|
|
1476
|
+
// registration is a rare, manual, retryable op, so failing fast (retry) is acceptable. With
|
|
1477
|
+
// a manifest configured the DKG fans out over the independently-probed roster regardless of
|
|
1478
|
+
// this endpoint. The endpoint is stable for one registration — if it changed mid-flow the
|
|
1479
|
+
// DKG streams would break anyway.
|
|
1450
1480
|
const ep = await directoryEndpointResolver();
|
|
1451
1481
|
if (!ep || !ep.multiaddr) {
|
|
1452
1482
|
// FROST DKG must dial the directory's /cello/frost/1.0.0 — a dialable
|
|
@@ -1769,6 +1799,84 @@ export async function startDaemon(config) {
|
|
|
1769
1799
|
// seal still completes — surfaced loudly so this can never masquerade as a produced receipt.
|
|
1770
1800
|
const rootHex = Buffer.from(sealedRoot).toString("hex");
|
|
1771
1801
|
const legibility = normalizeLegibility(frame["legibility"]);
|
|
1802
|
+
// M8B FINDING-5 (SI-002): before persisting, INDEPENDENTLY re-derive each CLIENT-VERIFIABLE
|
|
1803
|
+
// ('live', the present party) frontier from the signed frontier_leaves and OVERRIDE an inflated
|
|
1804
|
+
// directory-published value DOWN to the provable one (the directory cannot forge signed leaves,
|
|
1805
|
+
// so the re-derived value is truth). We OVERRIDE, never reject — the unilateral seal has a
|
|
1806
|
+
// directory-side dedup guard that makes a client rejection unrecoverable (a retry close is
|
|
1807
|
+
// silently ignored → no receipt ever, worse than FINDING-3; cascade-2 reviewer Critical 2). The
|
|
1808
|
+
// absent party's frontier stays directory-attested (its remainder is not re-derivable here). No
|
|
1809
|
+
// frontier_leaves (pre-FINDING-5 directory) → the cert stays directory-attested (FINDING-3).
|
|
1810
|
+
// A receipt is ALWAYS persisted; the close never dead-ends here.
|
|
1811
|
+
if (legibility !== undefined) {
|
|
1812
|
+
const rawParticipants = legibility.participants ?? [];
|
|
1813
|
+
const guardParticipants = rawParticipants
|
|
1814
|
+
.filter((p) => typeof p.pubkey === "string" && typeof p.attestation_mode === "string")
|
|
1815
|
+
.map((p) => ({
|
|
1816
|
+
pubkey: p.pubkey,
|
|
1817
|
+
content_frontier_seq: typeof p.content_frontier_seq === "number" ? p.content_frontier_seq : 0,
|
|
1818
|
+
attestation_mode: p.attestation_mode,
|
|
1819
|
+
}));
|
|
1820
|
+
const frontierLeavesRaw = frame["frontier_leaves"];
|
|
1821
|
+
const toU8f = (v) => (v instanceof Uint8Array ? v : new Uint8Array(v));
|
|
1822
|
+
const parsedFrontierLeaves = Array.isArray(frontierLeavesRaw) && frontierLeavesRaw.length > 0
|
|
1823
|
+
? frontierLeavesRaw.map((l) => {
|
|
1824
|
+
const o = l;
|
|
1825
|
+
return {
|
|
1826
|
+
structure1_cbor: toU8f(o["structure1_cbor"]),
|
|
1827
|
+
sender_pubkey: toU8f(o["sender_pubkey"]),
|
|
1828
|
+
sender_signature: toU8f(o["sender_signature"]),
|
|
1829
|
+
};
|
|
1830
|
+
})
|
|
1831
|
+
: undefined;
|
|
1832
|
+
const check = checkUnilateralFrontier(guardParticipants, parsedFrontierLeaves, sessionId);
|
|
1833
|
+
// Apply any frontier corrections to the object we persist — override inflated 'live'
|
|
1834
|
+
// frontier(s) DOWN to the provable value, so the stored receipt never claims more than the
|
|
1835
|
+
// signed leaves support. Runs for both 'corrected' (valid leaves) and 'leaves_invalid'
|
|
1836
|
+
// (forged leaves → corrected to 0). Never rejects → the close never dead-ends.
|
|
1837
|
+
if (check.corrections.size > 0) {
|
|
1838
|
+
for (const p of rawParticipants) {
|
|
1839
|
+
if (typeof p.pubkey === "string" && check.corrections.has(p.pubkey.toLowerCase())) {
|
|
1840
|
+
p.content_frontier_seq = check.corrections.get(p.pubkey.toLowerCase());
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
if (check.status === "corrected") {
|
|
1845
|
+
logger.error("seal.certificate.frontier.overridden", {
|
|
1846
|
+
sessionId: sidHex,
|
|
1847
|
+
corrections: [...check.corrections.entries()].map(([party, seq]) => ({ party, correctedTo: seq })),
|
|
1848
|
+
path: "unilateral",
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
else if (check.status === "verified") {
|
|
1852
|
+
logger.info("seal.certificate.frontier.verified", {
|
|
1853
|
+
sessionId: sidHex,
|
|
1854
|
+
parties: guardParticipants.filter((p) => p.attestation_mode === "live").length,
|
|
1855
|
+
path: "unilateral",
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
else if (check.status === "leaves_invalid") {
|
|
1859
|
+
// Forged / cross-session leaves — a tamper signal. The 'live' frontier(s) have been
|
|
1860
|
+
// corrected to 0 above (zero trustworthy evidence); persist that (never reject → never
|
|
1861
|
+
// dead-end). Surfaced loudly for audit.
|
|
1862
|
+
logger.error("seal.certificate.frontier.leaves_invalid", {
|
|
1863
|
+
sessionId: sidHex,
|
|
1864
|
+
reason: check.reason,
|
|
1865
|
+
corrections: [...check.corrections.entries()].map(([party, seq]) => ({ party, correctedTo: seq })),
|
|
1866
|
+
path: "unilateral",
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
else {
|
|
1870
|
+
// directory_attested: no frontier_leaves shipped (pre-FINDING-5 directory). Frontiers stay
|
|
1871
|
+
// DIRECTORY-attested (already marked per-participant) — visible/auditable, never silently
|
|
1872
|
+
// presented as client-verified.
|
|
1873
|
+
logger.warn("seal.certificate.frontier.directory_attested", {
|
|
1874
|
+
sessionId: sidHex,
|
|
1875
|
+
reason: "no_frontier_leaves",
|
|
1876
|
+
path: "unilateral",
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1772
1880
|
if (legibility !== undefined) {
|
|
1773
1881
|
try {
|
|
1774
1882
|
sessionNodeManager.recordSealCertificate(agentName, sidHex, rootHex, JSON.stringify(legibility));
|
|
@@ -1819,7 +1927,7 @@ export async function startDaemon(config) {
|
|
|
1819
1927
|
return;
|
|
1820
1928
|
sealUpgradeInFlight.add(key);
|
|
1821
1929
|
try {
|
|
1822
|
-
await attemptSealUpgradeImpl({
|
|
1930
|
+
const result = await attemptSealUpgradeImpl({
|
|
1823
1931
|
logger, agentName, agentPubkeyHex,
|
|
1824
1932
|
getReadiness: (a, s) => sessionNodeManager.getSealUpgradeReadiness(a, s),
|
|
1825
1933
|
getContentLeafCount: (a, s) => sessionNodeManager.getSessionTree(a, s).size(),
|
|
@@ -1827,6 +1935,44 @@ export async function startDaemon(config) {
|
|
|
1827
1935
|
getKeyProvider: (a) => keyProviders.get(a),
|
|
1828
1936
|
sendRaw: (f) => signaling.sendRaw(f),
|
|
1829
1937
|
}, sidHex, frame);
|
|
1938
|
+
// M8B FINDING-6 (3b): the ABSENT party (B) persists its UNILATERAL receipt from the
|
|
1939
|
+
// notification's legibility — but ONLY after the KERNEL content-recovery/verify gate passed.
|
|
1940
|
+
// `result.sent` is true iff attemptSealUpgradeImpl recovered + integrity-verified the content
|
|
1941
|
+
// behind R1 and sent the ratification request; a tampered/unrecoverable/incomplete case returns
|
|
1942
|
+
// sent:false → NO receipt (never a receipt for content B could not verify). B may hold no local
|
|
1943
|
+
// `sessions` row (recordSealCertificateEnsuringRow inserts a stub, counterparty = A's pubkey).
|
|
1944
|
+
// The notification carries no frontier_leaves (FINDING-5 ships those only on the present party's
|
|
1945
|
+
// confirm frame), so B's legibility is directory-attested — consistent with FINDING-5's
|
|
1946
|
+
// directory_attested path; B trusts its own KERNEL-verified content, not a re-derivation.
|
|
1947
|
+
if (result.sent) {
|
|
1948
|
+
try {
|
|
1949
|
+
// ONE-WAY RATCHET (cascade-2 FINDING-6 review): a re-delivered seal_unilateral_notification
|
|
1950
|
+
// (reconnect burst) re-runs this path and would re-persist the notification's ORIGINAL
|
|
1951
|
+
// legibility (counterparty 'absent'). If a prior upgrade already flipped the stored receipt
|
|
1952
|
+
// to 'recovered' (no 'absent' participant), re-persisting would REGRESS it — and no later
|
|
1953
|
+
// event restores 'recovered' (the directory dedups the duplicate as already_bilateral →
|
|
1954
|
+
// seal_upgrade_rejected, which only logs). So skip the re-persist once the cert is upgraded.
|
|
1955
|
+
const existing = sessionNodeManager.getSealCertificate(agentName, sidHex);
|
|
1956
|
+
if (existing && !hasAbsentParticipant(existing.legibility)) {
|
|
1957
|
+
logger.debug("session.unilateral.receipt.persist.skipped", { sessionId: sidHex, reason: "already_upgraded" });
|
|
1958
|
+
}
|
|
1959
|
+
else {
|
|
1960
|
+
const legibility = normalizeLegibility(frame["legibility"]);
|
|
1961
|
+
const rootHex = frameValueToHex(frame["sealed_root"]);
|
|
1962
|
+
const counterpartyHex = frameValueToHex(frame["present_pubkey"]); // A — the present party
|
|
1963
|
+
if (legibility !== undefined && rootHex && counterpartyHex) {
|
|
1964
|
+
sessionNodeManager.recordSealCertificateEnsuringRow(agentName, sidHex, counterpartyHex, rootHex, JSON.stringify(legibility));
|
|
1965
|
+
logger.info("session.unilateral.receipt.persisted", { sessionId: sidHex, sealedRoot: rootHex, party: "absent" });
|
|
1966
|
+
}
|
|
1967
|
+
else if (legibility === undefined) {
|
|
1968
|
+
logger.warn("session.unilateral.receipt.absent", { sessionId: sidHex, reason: "no_legibility_on_notification" });
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
catch (error) {
|
|
1973
|
+
logger.warn("seal.certificate.persist.failed", { sessionId: sidHex, reason: error instanceof Error ? error.message : String(error) });
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1830
1976
|
}
|
|
1831
1977
|
finally {
|
|
1832
1978
|
// Clear the guard so a later reconnect can retry if the request never reached the directory;
|
|
@@ -1844,6 +1990,24 @@ export async function startDaemon(config) {
|
|
|
1844
1990
|
if (!result.ok)
|
|
1845
1991
|
return; // cert.invalid already logged inside; do NOT accept as bilateral.
|
|
1846
1992
|
logger.info("session.seal.upgraded", { sessionId: sidHex, agentName, party: result.party });
|
|
1993
|
+
// M8B FINDING-6 (3a): the seal is now BILATERAL (verified) — upgrade THIS party's own stored
|
|
1994
|
+
// receipt so the counterparty recorded 'absent' becomes 'recovered'. Client-side: the directory
|
|
1995
|
+
// ships no bilateral legibility at upgrade time (the seal leaves aren't persisted there), so each
|
|
1996
|
+
// party rebuilds from the cert it already holds — the present party's from its unilateral close
|
|
1997
|
+
// (FINDING-3), the returning party's from the notification it persisted after the KERNEL gate
|
|
1998
|
+
// (3b). The seal signatures do not bind the (unsigned) legibility, so mutating it is sound; only
|
|
1999
|
+
// the attestation flips (frontiers unchanged — never overstated). No-op if no cert is stored yet.
|
|
2000
|
+
const stored = sessionNodeManager.getSealCertificate(agentName, sidHex);
|
|
2001
|
+
if (stored) {
|
|
2002
|
+
try {
|
|
2003
|
+
const upgraded = upgradeAbsentToRecovered(stored.legibility);
|
|
2004
|
+
sessionNodeManager.recordSealCertificate(agentName, sidHex, stored.sealed_root, JSON.stringify(upgraded));
|
|
2005
|
+
logger.info("session.seal.receipt.upgraded", { sessionId: sidHex, agentName, party: result.party });
|
|
2006
|
+
}
|
|
2007
|
+
catch (error) {
|
|
2008
|
+
logger.warn("seal.certificate.persist.failed", { sessionId: sidHex, reason: error instanceof Error ? error.message : String(error) });
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
1847
2011
|
void sessionNodeManager.destroySessionNode(agentName, sidHex, "sealed");
|
|
1848
2012
|
}
|
|
1849
2013
|
/**
|
|
@@ -1901,7 +2065,7 @@ export async function startDaemon(config) {
|
|
|
1901
2065
|
persistence: getPersistence(agentName),
|
|
1902
2066
|
agentPubkeyHex: loaded.pubkey,
|
|
1903
2067
|
getNode: entry.getNode,
|
|
1904
|
-
getDirectoryEndpoint:
|
|
2068
|
+
getDirectoryEndpoint: getFailoverEndpoint,
|
|
1905
2069
|
getConsortiumEndpoints: resolveConsortiumRoster,
|
|
1906
2070
|
signaling: entry.signaling,
|
|
1907
2071
|
logger,
|