@cotal-ai/cli 0.57.0 → 0.59.0
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/commands/agents.d.ts +29 -26
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +103 -38
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/backup.js +2 -1
- package/dist/commands/backup.js.map +1 -1
- package/dist/commands/down-manifest.js +0 -4
- package/dist/commands/down-manifest.js.map +1 -1
- package/dist/commands/down.d.ts +3 -2
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +8 -7
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/endpoints.js +2 -2
- package/dist/commands/endpoints.js.map +1 -1
- package/dist/commands/meshes-add.d.ts +0 -3
- package/dist/commands/meshes-add.d.ts.map +1 -1
- package/dist/commands/meshes-add.js +0 -3
- package/dist/commands/meshes-add.js.map +1 -1
- package/dist/commands/mint.d.ts +0 -9
- package/dist/commands/mint.d.ts.map +1 -1
- package/dist/commands/mint.js +9 -9
- package/dist/commands/mint.js.map +1 -1
- package/dist/commands/service.d.ts.map +1 -1
- package/dist/commands/service.js +91 -29
- package/dist/commands/service.js.map +1 -1
- package/dist/commands/setup.d.ts +2 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +29 -6
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/spawn.d.ts +18 -0
- package/dist/commands/spawn.d.ts.map +1 -1
- package/dist/commands/spawn.js +30 -20
- package/dist/commands/spawn.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +28 -28
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/up.d.ts +0 -4
- package/dist/commands/up.d.ts.map +1 -1
- package/dist/commands/up.js +260 -73
- package/dist/commands/up.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/assist.d.ts +10 -7
- package/dist/lib/assist.d.ts.map +1 -1
- package/dist/lib/assist.js +20 -31
- package/dist/lib/assist.js.map +1 -1
- package/dist/lib/auth-proc.d.ts.map +1 -1
- package/dist/lib/auth-proc.js +22 -19
- package/dist/lib/auth-proc.js.map +1 -1
- package/dist/lib/control.d.ts +3 -2
- package/dist/lib/control.d.ts.map +1 -1
- package/dist/lib/control.js.map +1 -1
- package/dist/lib/delivery-proc.d.ts.map +1 -1
- package/dist/lib/delivery-proc.js +13 -13
- package/dist/lib/delivery-proc.js.map +1 -1
- package/dist/lib/delivery-responder.d.ts +12 -2
- package/dist/lib/delivery-responder.d.ts.map +1 -1
- package/dist/lib/delivery-responder.js +3 -13
- package/dist/lib/delivery-responder.js.map +1 -1
- package/dist/lib/ext-mutation.d.ts.map +1 -1
- package/dist/lib/ext-mutation.js.map +1 -1
- package/dist/lib/manager-proc.d.ts +4 -4
- package/dist/lib/manager-proc.d.ts.map +1 -1
- package/dist/lib/manager-proc.js +22 -20
- package/dist/lib/manager-proc.js.map +1 -1
- package/dist/lib/restore.d.ts.map +1 -1
- package/dist/lib/restore.js +2 -2
- package/dist/lib/restore.js.map +1 -1
- package/dist/lib/self-exec.d.ts.map +1 -1
- package/dist/lib/self-exec.js +38 -13
- package/dist/lib/self-exec.js.map +1 -1
- package/dist/lib/status.d.ts +19 -9
- package/dist/lib/status.d.ts.map +1 -1
- package/dist/lib/status.js +41 -54
- package/dist/lib/status.js.map +1 -1
- package/dist/lib/steps.d.ts +7 -3
- package/dist/lib/steps.d.ts.map +1 -1
- package/dist/lib/steps.js +22 -10
- package/dist/lib/steps.js.map +1 -1
- package/dist/lib/theme.d.ts.map +1 -1
- package/dist/lib/theme.js +3 -2
- package/dist/lib/theme.js.map +1 -1
- package/dist/seed/reconcile.d.ts.map +1 -1
- package/dist/seed/reconcile.js +13 -9
- package/dist/seed/reconcile.js.map +1 -1
- package/dist/ui.d.ts +13 -0
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +21 -0
- package/dist/ui.js.map +1 -1
- package/package.json +7 -7
package/dist/commands/up.js
CHANGED
|
@@ -2,11 +2,13 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { createConnection, createServer } from "node:net";
|
|
4
4
|
import { hostname } from "node:os";
|
|
5
|
-
import { mkdirSync, writeFileSync, readFileSync, existsSync, openSync, statSync, readSync, closeSync, lstatSync, rmSync, realpathSync, } from "node:fs";
|
|
5
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, openSync, statSync, readSync, closeSync, lstatSync, rmSync, realpathSync, readdirSync, } from "node:fs";
|
|
6
6
|
import { dirname, join, resolve } from "node:path";
|
|
7
|
-
import { isReachable, DEFAULT_SERVER, createSpaceAuth, serverConfig, openServerConfig, validateTlsMaterial, mintCreds, mintLifecycleUid, DEV_OWNER, mintConnectionEvictorCreds, mintMembershipObserverCreds, newIdentity, setupSpaceStreams, reconcileSpaceTtls, standaloneConnectOpts, seedChannelRegistry, ensureDefaultDeliveryClass, mkSecretDir, writeSecretFile, } from "@cotal-ai/core";
|
|
7
|
+
import { isReachable, DEFAULT_SERVER, createSpaceAuth, serverConfig, openServerConfig, validateTlsMaterial, mintCreds, mintLifecycleUid, DEV_OWNER, mintConnectionEvictorCreds, mintMembershipObserverCreds, newIdentity, setupSpaceStreams, reconcileSpaceTtls, ttlBuckets, standaloneConnectOpts, requireBrokerFloor, parseServerVersion, presenceBucket, seedChannelRegistry, ensureDefaultDeliveryClass, mkSecretDir, writeSecretFile, } from "@cotal-ai/core";
|
|
8
8
|
import { connect } from "@nats-io/transport-node";
|
|
9
|
-
import {
|
|
9
|
+
import { jetstreamManager, StorageType } from "@nats-io/jetstream";
|
|
10
|
+
import { Kvm } from "@nats-io/kv";
|
|
11
|
+
import { assertSingleSpaceBroker, assertUserAuthInfo, authDir, getSoleSpaceAuth, getSpaceAuth, hasUserAuthState, listSpaceAccounts, preloadSpaceAccounts, putSpaceAuth, clearCurrent, findMesh, getCurrent, loadMeshes, connectionEvictorCredsKey, membershipConfigPath, membershipObserverCredsKey, membershipRwCredsKey, MEMBERSHIP_CONFIG_KIND, MEMBERSHIP_RW_CREDS_KIND, parsePositiveIntegerFlag, recordMesh, meshesForRoot, removeMesh, rotateSystemCreds, setCurrent, staleSystemCreds, SYSTEM_CREDS_FILES, userAuthStateDir, userAuthSpacesOnDisk, workspaceSecretStore, acquireMaintenanceLock, assertStoreIdentity, assessRestoreClaim, beginOrdinaryResume, bindOrdinaryResumeListener, consumeRetiredMaintenance, markOrdinaryResumeActive, markOrdinaryResumeDegraded, replaceDeadOrdinaryResumeListener, localProcessOwnerStatus, readMaintenanceJournal, readMaintenanceResumeDocument, seatCheckpointDir, readStoreIdentity, recordOrdinaryResumeManagerCommit, releaseMaintenanceLock, retireOrdinaryResume, sameStoreIdentity, readBrokerPolicy, writeBrokerPolicy, removePidPair, writePidPair, localProcessPath, MANAGER_PIDFILE, assertManagerCanSpare, verifyIdentityPin, isLoopbackHost, connectUserControlOrThrow, userViewAuth, } from "@cotal-ai/workspace";
|
|
10
12
|
import { ensureAuthService, resolveAuthProvider, stopAuthService } from "../lib/auth-proc.js";
|
|
11
13
|
import { resolveSpace } from "../lib/status.js";
|
|
12
14
|
import { c } from "../ui.js";
|
|
@@ -584,8 +586,6 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
584
586
|
// Gated on the ATTEMPT, not on holding the lock. The lock is now always held, and this refuses
|
|
585
587
|
// whenever a journal exists — which is precisely the state a resume re-entry is in, so gating
|
|
586
588
|
// it on the lock would refuse the very resume the lock is here to protect.
|
|
587
|
-
if (!resumeAttempt)
|
|
588
|
-
assertOrdinaryUpAllowed(cotalRoot(), values["store-dir"] ? resolve(values["store-dir"]) : cotalPath("nats"));
|
|
589
589
|
let server = values.server ?? DEFAULT_SERVER;
|
|
590
590
|
const host = values.host ?? "127.0.0.1";
|
|
591
591
|
// `--host` is the BIND address; `server` is the URL the readiness probe, the mesh registry, and
|
|
@@ -596,6 +596,46 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
596
596
|
// pair rather than starting a broker nothing can reach.
|
|
597
597
|
if (values.host)
|
|
598
598
|
server = reconcileHostAndServer(values.host, values.server);
|
|
599
|
+
// A same-root repair reopens the store the mesh record names, ONCE, before the ordinary-up
|
|
600
|
+
// assertion below reads --store-dir (#2218): an ordinary `up` on a space whose broker died must
|
|
601
|
+
// not silently fall back to the root's default store while the record still names a custom one.
|
|
602
|
+
// Never runs during a resume attempt, which keeps its own store checks. The reachability probe
|
|
603
|
+
// here is reused as `listenerReachable` further down instead of probing twice.
|
|
604
|
+
let repairProbe;
|
|
605
|
+
if (!resumeAttempt) {
|
|
606
|
+
const repairSpace = values.space ?? resolveSpace(process.cwd());
|
|
607
|
+
const repairRoot = cotalRoot();
|
|
608
|
+
const heldForRepair = loadMeshes().find((m) => m.origin !== "manual" && m.origin !== "catalog" && m.server === server && m.root === repairRoot && m.space === repairSpace);
|
|
609
|
+
if (heldForRepair) {
|
|
610
|
+
repairProbe = await isReachable(server);
|
|
611
|
+
if (!repairProbe) {
|
|
612
|
+
if (values["store-dir"] !== undefined) {
|
|
613
|
+
const requestedStoreDir = resolve(values["store-dir"]);
|
|
614
|
+
if (heldForRepair.storeDir !== undefined && requestedStoreDir !== heldForRepair.storeDir) {
|
|
615
|
+
console.error(c.red(`✗ mesh "${repairSpace}" is recorded at ${server} with store ${heldForRepair.storeDir} - a stopped broker can't change --store-dir (it is fixed at start); repair it with \`cotal up --store-dir ${heldForRepair.storeDir}\`, or \`cotal down\` it first`));
|
|
616
|
+
process.exit(1);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
else if (heldForRepair.storeDir !== undefined) {
|
|
620
|
+
values["store-dir"] = heldForRepair.storeDir;
|
|
621
|
+
console.log(c.dim(`reopening the recorded store ${heldForRepair.storeDir}`));
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
const defaultStore = cotalPath("nats");
|
|
625
|
+
let hasEntries = false;
|
|
626
|
+
try {
|
|
627
|
+
hasEntries = readdirSync(join(defaultStore, "jetstream")).length > 0;
|
|
628
|
+
}
|
|
629
|
+
catch { /* absent: no jetstream dir yet */ }
|
|
630
|
+
if (!hasEntries) {
|
|
631
|
+
console.error(c.yellow(`! mesh "${repairSpace}" was recorded before the store directory was kept, so this repair opens ${defaultStore}; if the previous broker used --store-dir, stop this one with \`cotal down\` and repair with that flag (its Store Directory: line is in ${repairRoot}/.cotal/nats.log)`));
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (!resumeAttempt)
|
|
638
|
+
assertOrdinaryUpAllowed(cotalRoot(), values["store-dir"] ? resolve(values["store-dir"]) : cotalPath("nats"));
|
|
599
639
|
const restoredAttempt = resumeAttempt ? pendingRestores.get(resumeAttempt) : undefined;
|
|
600
640
|
if (restoredAttempt?.reentry) {
|
|
601
641
|
if (!restoredAttempt.listenerProof)
|
|
@@ -622,7 +662,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
622
662
|
}
|
|
623
663
|
if (ordinaryAttempt?.journalState === "resume-committed")
|
|
624
664
|
throw new Error(`resume attempt ${resumeAttempt} is manager-committed but has no adoptable bound listener; preserving the commit token and retained suppression`);
|
|
625
|
-
const listenerReachable = await isReachable(server);
|
|
665
|
+
const listenerReachable = repairProbe !== undefined ? repairProbe : await isReachable(server);
|
|
626
666
|
if (resumeAttempt && listenerReachable)
|
|
627
667
|
throw new Error(`resume attempt ${resumeAttempt} refuses the unproven occupied listener at ${server}`);
|
|
628
668
|
if (listenerReachable) {
|
|
@@ -678,6 +718,13 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
678
718
|
console.error(c.red(`✗ mesh "${held.space}" is already running at ${server} - a running broker can't change --max-file-store (it is fixed at start); \`cotal down\` it first, then \`cotal up --max-file-store ${maxFileStore}\``));
|
|
679
719
|
process.exit(1);
|
|
680
720
|
}
|
|
721
|
+
// The store directory is fixed the same way: an explicit --store-dir on a live refresh can
|
|
722
|
+
// only be refused when it disagrees with what the running broker actually opened. Absent
|
|
723
|
+
// `held.storeDir` (a pre-field record) has nothing to compare against, so it is not checked.
|
|
724
|
+
if (values["store-dir"] !== undefined && held.storeDir !== undefined && resolve(values["store-dir"]) !== held.storeDir) {
|
|
725
|
+
console.error(c.red(`✗ mesh "${held.space}" is already running at ${server} - a running broker can't change --store-dir (it is fixed at start); \`cotal down\` it first, then \`cotal up --store-dir ${resolve(values["store-dir"])}\``));
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
681
728
|
// `--max-sessions` is fixed when the manager starts. A refresh reuses a live manager as-is
|
|
682
729
|
// (`ensureManager` returns immediately), then used to persist the requested ceiling anyway.
|
|
683
730
|
// That printed `✓ already running` over an unchanged plane and left MeshEntry lying about it.
|
|
@@ -746,6 +793,9 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
746
793
|
// a same-root caller holds the space's signing material either way.
|
|
747
794
|
reconcileCreds = await mintCreds(spaceAuth, newIdentity(), "provisioner");
|
|
748
795
|
}
|
|
796
|
+
const v = await brokerVersion(server, held.space, reconcileCreds);
|
|
797
|
+
if (belowPresenceSafeBroker(v.version) && v.presenceFileBacked)
|
|
798
|
+
console.error(c.yellow(`! nats-server ${v.version} is below 2.14.5 and the presence bucket of "${held.space}" is file-backed: it can latch after a stall and close the mesh to new joins (#1356); put a nats-server 2.14.5 or newer on PATH (it wins over the bundled one)`));
|
|
749
799
|
for (const r of await reconcileSpaceTtls({ servers: server, space: held.space, creds: reconcileCreds }))
|
|
750
800
|
console.log(c.dim(` reconciled ${r.stream} TTL ${r.fromMs === 0 ? "none" : `${r.fromMs}ms`} -> ${r.toMs}ms`));
|
|
751
801
|
}
|
|
@@ -826,7 +876,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
826
876
|
// no transport decision to record — and `recordOurMesh` writes the entry whole, so omitting
|
|
827
877
|
// the field here would erase the requirement on every bare refresh, exactly the way dropping
|
|
828
878
|
// `attachHost` would silently demote the mesh to loopback.
|
|
829
|
-
recordOurMesh({ space: held.space, server, root, mode: held.mode, ...(held.tlsRequired !== undefined ? { tlsRequired: held.tlsRequired } : {}), ...(userAuth ? { userAuth } : {}), ...(heldAttachHost ? { attachHost: heldAttachHost } : {}), ...(heldMaxSessions !== undefined ? { maxSessions: heldMaxSessions } : {}), ...(held.maxFileStore !== undefined ? { maxFileStore: held.maxFileStore } : {}), ts: new Date().toISOString() }, "refresh");
|
|
879
|
+
recordOurMesh({ space: held.space, server, root, mode: held.mode, ...(held.tlsRequired !== undefined ? { tlsRequired: held.tlsRequired } : {}), ...(userAuth ? { userAuth } : {}), ...(heldAttachHost ? { attachHost: heldAttachHost } : {}), ...(heldMaxSessions !== undefined ? { maxSessions: heldMaxSessions } : {}), ...(held.maxFileStore !== undefined ? { maxFileStore: held.maxFileStore } : {}), ...(held.storeDir !== undefined ? { storeDir: held.storeDir } : {}), ts: new Date().toISOString() }, "refresh");
|
|
830
880
|
return;
|
|
831
881
|
}
|
|
832
882
|
const who = held ? `mesh "${held.space}" (${held.root})` : "a broker not started here";
|
|
@@ -947,9 +997,9 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
947
997
|
const listenerStartedAt = new Date().toISOString();
|
|
948
998
|
const child = spawn(bin, natsArgs, { stdio: "inherit" });
|
|
949
999
|
let activationFinished = !resumeAttempt;
|
|
950
|
-
if (child.pid)
|
|
951
|
-
|
|
952
|
-
|
|
1000
|
+
if (!child.pid)
|
|
1001
|
+
throw new Error("nats-server spawned with no pid");
|
|
1002
|
+
writePidPair(cotalPath("nats.pid"), child.pid); // #969/#1238: publish the pair by rename, never torn
|
|
953
1003
|
if (restored && process.env.COTAL_SMOKE_EXIT_AFTER_RESTORE_LISTENER_SPAWN === "1")
|
|
954
1004
|
process.exit(87);
|
|
955
1005
|
if (restored)
|
|
@@ -1041,28 +1091,46 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
1041
1091
|
process.on("SIGTERM", stop);
|
|
1042
1092
|
// The broker is gone — drop it from the registry (and the `current` pointer if it was the default)
|
|
1043
1093
|
// so a later `cotal spawn` doesn't try to join a dead mesh.
|
|
1044
|
-
child.on("exit", async (code) => {
|
|
1045
|
-
|
|
1046
|
-
rmSync(cotalPath("nats.pid"), { force: true });
|
|
1094
|
+
child.on("exit", async (code, signal) => {
|
|
1095
|
+
removePidPair(cotalPath("nats.pid"), String(child.pid));
|
|
1047
1096
|
// Logged, never silently swallowed; the daemon kill runs in stopDelivery's finally regardless.
|
|
1048
1097
|
await stopDelivery(undefined, undefined, space).catch((e) => console.error(`! delivery teardown: ${e.message}`));
|
|
1049
1098
|
await stopManager(undefined, undefined, undefined, space).catch((e) => console.error(`! manager teardown: ${e.message}`));
|
|
1050
1099
|
await stopAuthService(space).catch((e) => console.error(`! auth teardown: ${e.message}`));
|
|
1051
|
-
//
|
|
1052
|
-
//
|
|
1053
|
-
//
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1100
|
+
// Unrecording (and the exit code below) both depend on WHY the broker is gone. `stopping` is
|
|
1101
|
+
// true only for the intended shutdown `stop()` drives (Ctrl-C, SIGTERM) — everything else here
|
|
1102
|
+
// (a crash, an OOM kill, a signal from outside `up`) leaves the record and its store in place.
|
|
1103
|
+
if (stopping) {
|
|
1104
|
+
// Only unrecord if the registry still points at THIS broker. A newer broker for the same space
|
|
1105
|
+
// (a concurrent `up`, or a different-port re-up that recorded after us) may have replaced our
|
|
1106
|
+
// record — removing by name would clobber the live winner and hide it from the registry.
|
|
1107
|
+
// …and only if it is still OUR kind of record. A concurrent `cotal meshes add --force` can
|
|
1108
|
+
// replace it with a hand-registered one carrying the same server + root; that record outlives
|
|
1109
|
+
// this broker by design (it is the operator's, and only they remove it), so unrecording it on
|
|
1110
|
+
// our exit would delete a registration this process never owned.
|
|
1111
|
+
const mine = findMesh(space);
|
|
1112
|
+
if (mine && mine.origin !== "manual" && mine.server === server && mine.root === cotalRoot()) {
|
|
1113
|
+
removeMesh(space);
|
|
1114
|
+
if (getCurrent() === space)
|
|
1115
|
+
clearCurrent();
|
|
1116
|
+
}
|
|
1117
|
+
if (activationFinished)
|
|
1118
|
+
process.exit(code ?? 0);
|
|
1119
|
+
}
|
|
1120
|
+
else {
|
|
1121
|
+
// The record is kept ON PURPOSE: it still names the store this broker opened, so a repair
|
|
1122
|
+
// `up` reopens that SAME store instead of falling back to the root's default, and a
|
|
1123
|
+
// supervising unit's restart takes the refresh path against the recorded store rather than
|
|
1124
|
+
// racing a second broker into existence. Foreground stdio is inherited (its own log lines
|
|
1125
|
+
// are already on this terminal, above), so there is no `.cotal/nats.log` to name here.
|
|
1126
|
+
const detail = signal ? `signal ${signal}` : `code ${code}`;
|
|
1127
|
+
console.error(c.red(`✗ nats-server exited unexpectedly (${detail}); the mesh "${space}" stays recorded (\`cotal meshes\` lists it offline); log: see the terminal output above; repair: \`cotal up --server ${server} --space ${space}\``));
|
|
1128
|
+
// A foreground broker under `Restart=on-failure` must exit non-zero on a crash so the unit
|
|
1129
|
+
// restarts it; exiting 0 here (as an unconditional `code ?? 0` used to) looked like a clean
|
|
1130
|
+
// stop and left the unit sitting dead.
|
|
1131
|
+
if (activationFinished)
|
|
1132
|
+
process.exit(code ?? 1);
|
|
1063
1133
|
}
|
|
1064
|
-
if (activationFinished)
|
|
1065
|
-
process.exit(code ?? 0);
|
|
1066
1134
|
});
|
|
1067
1135
|
const ready = await waitReady(server, setup?.creds);
|
|
1068
1136
|
if (!ready) {
|
|
@@ -1073,6 +1141,9 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
1073
1141
|
markPendingResumeDegraded(resumeAttempt ?? "", reason, startupLock);
|
|
1074
1142
|
throw new Error(reason);
|
|
1075
1143
|
}
|
|
1144
|
+
const v = await brokerVersion(server, space, setup?.creds);
|
|
1145
|
+
if (belowPresenceSafeBroker(v.version) && v.presenceFileBacked)
|
|
1146
|
+
console.error(c.yellow(`! nats-server ${v.version} is below 2.14.5 and the presence bucket of "${space}" is file-backed: it can latch after a stall and close the mesh to new joins (#1356); put a nats-server 2.14.5 or newer on PATH (it wins over the bundled one)`));
|
|
1076
1147
|
if (restored)
|
|
1077
1148
|
await provePreparedRestoreListener(restored);
|
|
1078
1149
|
// Listener ready — commit the transport decision (S5: not before start).
|
|
@@ -1080,6 +1151,8 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
1080
1151
|
{
|
|
1081
1152
|
if (!resumeAttempt)
|
|
1082
1153
|
await postStart(server, space, setup, seedFile);
|
|
1154
|
+
else
|
|
1155
|
+
await recreateMemoryBuckets(server, space, setup);
|
|
1083
1156
|
// USER MODE: the auth service comes up FIRST among the daemons — until its callout answers,
|
|
1084
1157
|
// every user-mode connect to this broker is denied, so `up` must not report a usable user mesh
|
|
1085
1158
|
// (nor let agents race it) on a half-started auth plane. (Foreground `up` doesn't exit here, so
|
|
@@ -1110,6 +1183,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
1110
1183
|
...(effectiveAttachHost ? { attachHost: effectiveAttachHost } : {}),
|
|
1111
1184
|
...(effectiveMaxSessions !== undefined ? { maxSessions: effectiveMaxSessions } : {}),
|
|
1112
1185
|
...(maxFileStore !== undefined ? { maxFileStore } : {}),
|
|
1186
|
+
storeDir,
|
|
1113
1187
|
ts: new Date().toISOString(),
|
|
1114
1188
|
}, "started");
|
|
1115
1189
|
// Bring up the delivery daemon WITH the server (auth mode only — it self-gates on `.cotal/auth`).
|
|
@@ -1204,6 +1278,28 @@ async function resumeControlAuth(root, mode) {
|
|
|
1204
1278
|
epCaller: { owner: DEV_OWNER, actor: identity.id, uid },
|
|
1205
1279
|
};
|
|
1206
1280
|
}
|
|
1281
|
+
/** The caller for the resume's admin-gated manager asks on a user mesh. The gate reads the CURRENT
|
|
1282
|
+
* ledger, so they ride the logged-in operator's manager view, the principal the preserve cut used:
|
|
1283
|
+
* a static instrument carries a caller the ledger has no row for. The view selects a registered
|
|
1284
|
+
* manager instance, so it is taken after the readiness wait. Each ask is its own connection and a
|
|
1285
|
+
* bearer lives no longer than the IdP proof behind it, so every call after the first takes a fresh
|
|
1286
|
+
* bearer from the view's source, pinned to the same principal and manager instance. */
|
|
1287
|
+
async function userManagerAuth(space) {
|
|
1288
|
+
const conn = await connectUserControlOrThrow({ space });
|
|
1289
|
+
const view = await userViewAuth(conn, "manager-caller");
|
|
1290
|
+
let first = view.bearer;
|
|
1291
|
+
return async () => {
|
|
1292
|
+
const bearer = first ?? await view.source();
|
|
1293
|
+
first = undefined;
|
|
1294
|
+
return {
|
|
1295
|
+
bearer,
|
|
1296
|
+
sentinelCreds: view.sentinelCreds,
|
|
1297
|
+
tls: conn.tls,
|
|
1298
|
+
epCaller: { owner: view.owner, actor: view.actor, uid: view.lifecycleUid },
|
|
1299
|
+
managerInstanceId: view.managerInstanceId,
|
|
1300
|
+
};
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1207
1303
|
function restoreListenerOwner(pid, nonce, startedAt) {
|
|
1208
1304
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
1209
1305
|
throw new Error("restore listener spawn returned no pid");
|
|
@@ -1476,6 +1572,7 @@ async function resumeProvenOrdinaryListener(pending, heldLock) {
|
|
|
1476
1572
|
...(adoptAttachHost ? { attachHost: adoptAttachHost } : {}),
|
|
1477
1573
|
...(adoptMaxSessions !== undefined ? { maxSessions: adoptMaxSessions } : {}),
|
|
1478
1574
|
...(pending.maxFileStore !== undefined ? { maxFileStore: pending.maxFileStore } : {}),
|
|
1575
|
+
storeDir: resolve(pending.storeDir),
|
|
1479
1576
|
ts: new Date().toISOString(),
|
|
1480
1577
|
}, "started");
|
|
1481
1578
|
const controlPlane = await startDeliveryWithBroker(pending.space, pending.server, adoptedTlsRequired(pending.root), {
|
|
@@ -1521,6 +1618,7 @@ async function resumeProvenRestoreListener(prepared, heldLock) {
|
|
|
1521
1618
|
...(svc.userAuth ? { userAuth: svc.userAuth } : {}),
|
|
1522
1619
|
...(restoreAttachHost ? { attachHost: restoreAttachHost } : {}),
|
|
1523
1620
|
...(restoreMaxSessions !== undefined ? { maxSessions: restoreMaxSessions } : {}),
|
|
1621
|
+
storeDir: resolve(prepared.targetPath),
|
|
1524
1622
|
ts: new Date().toISOString(),
|
|
1525
1623
|
}, "started");
|
|
1526
1624
|
const controlPlane = await startDeliveryWithBroker(prepared.space, prepared.server, adoptedTlsRequired(prepared.root), {
|
|
@@ -1599,12 +1697,28 @@ async function completeResumeActivation(attemptId, healthy, reason, server, held
|
|
|
1599
1697
|
}
|
|
1600
1698
|
await new Promise((resolveWait) => setTimeout(resolveWait, 150));
|
|
1601
1699
|
}
|
|
1700
|
+
let userAuth;
|
|
1701
|
+
// The auth for one admin-gated ask. A failed user-mode renewal degrades the journal only where a
|
|
1702
|
+
// refusal of that ask would.
|
|
1703
|
+
const askAuth = async (degrade) => {
|
|
1704
|
+
if (pending.mode !== "user")
|
|
1705
|
+
return auth;
|
|
1706
|
+
try {
|
|
1707
|
+
userAuth ??= await userManagerAuth(pending.space);
|
|
1708
|
+
return await userAuth();
|
|
1709
|
+
}
|
|
1710
|
+
catch (e) {
|
|
1711
|
+
if (degrade)
|
|
1712
|
+
markPendingResumeDegraded(attemptId, e instanceof Error ? e.message : String(e), heldLock);
|
|
1713
|
+
throw e;
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1602
1716
|
const resumed = await askManager(pending.space, server, "resumePreserved", {
|
|
1603
1717
|
attemptId,
|
|
1604
1718
|
inventory: restored?.selection === "registry"
|
|
1605
1719
|
? { ...pending.inventory, agents: [] }
|
|
1606
1720
|
: pending.inventory,
|
|
1607
|
-
},
|
|
1721
|
+
}, await askAuth(true), "any", 10 * 60 * 1000);
|
|
1608
1722
|
if (!resumed.ok) {
|
|
1609
1723
|
const detail = resumed.data ? ` (${JSON.stringify(resumed.data)})` : "";
|
|
1610
1724
|
const message = `${resumed.error ?? "retained-agent resume failed"}${detail}`;
|
|
@@ -1633,7 +1747,7 @@ async function completeResumeActivation(attemptId, healthy, reason, server, held
|
|
|
1633
1747
|
}
|
|
1634
1748
|
}
|
|
1635
1749
|
if (!managerCommit) {
|
|
1636
|
-
const committed = await askManager(pending.space, server, "commitResume", { attemptId },
|
|
1750
|
+
const committed = await askManager(pending.space, server, "commitResume", { attemptId }, await askAuth(true), "any", 40_000);
|
|
1637
1751
|
if (!committed.ok) {
|
|
1638
1752
|
const message = committed.error ?? "manager resume commit failed";
|
|
1639
1753
|
markPendingResumeDegraded(attemptId, message, heldLock);
|
|
@@ -1664,7 +1778,7 @@ async function completeResumeActivation(attemptId, healthy, reason, server, held
|
|
|
1664
1778
|
process.exit(89);
|
|
1665
1779
|
}
|
|
1666
1780
|
else {
|
|
1667
|
-
const recommitted = await askManager(pending.space, server, "commitResume", { attemptId },
|
|
1781
|
+
const recommitted = await askManager(pending.space, server, "commitResume", { attemptId }, await askAuth(false), "any", 40_000);
|
|
1668
1782
|
// A surviving manager that already finalized legitimately answers {state:"active"} with the
|
|
1669
1783
|
// exact durable token: accept both committed shapes idempotently, then reissue the token-bound
|
|
1670
1784
|
// finalize (itself idempotent) below.
|
|
@@ -1676,7 +1790,7 @@ async function completeResumeActivation(attemptId, healthy, reason, server, held
|
|
|
1676
1790
|
if (!exactToken)
|
|
1677
1791
|
throw new Error(`replacement manager did not recover the durable commit token for attempt ${attemptId}`);
|
|
1678
1792
|
}
|
|
1679
|
-
const finalized = await askManager(pending.space, server, "finalizeResume", { attemptId, durableCommitToken: managerCommit.durableCommitToken },
|
|
1793
|
+
const finalized = await askManager(pending.space, server, "finalizeResume", { attemptId, durableCommitToken: managerCommit.durableCommitToken }, await askAuth(false), "any", 40_000);
|
|
1680
1794
|
if (!finalized.ok)
|
|
1681
1795
|
throw new Error(finalized.error ?? `manager resume finalize failed for attempt ${attemptId}`);
|
|
1682
1796
|
if (!isManagerFinalizeResult(finalized.data, attemptId))
|
|
@@ -2041,8 +2155,9 @@ opts) {
|
|
|
2041
2155
|
const child = spawn(bin, args, { detached: true, stdio: ["ignore", fd, fd] });
|
|
2042
2156
|
closeSync(fd);
|
|
2043
2157
|
if (opts.boundListener) {
|
|
2044
|
-
|
|
2045
|
-
|
|
2158
|
+
if (!child.pid)
|
|
2159
|
+
throw new Error("nats-server spawned with no pid");
|
|
2160
|
+
writePidPair(cotalPath("nats.pid"), child.pid); // #969/#1238: publish the pair by rename, never torn
|
|
2046
2161
|
if (process.env.COTAL_SMOKE_EXIT_AFTER_RESTORE_LISTENER_SPAWN === "1")
|
|
2047
2162
|
process.exit(87);
|
|
2048
2163
|
try {
|
|
@@ -2062,14 +2177,25 @@ opts) {
|
|
|
2062
2177
|
tailing = false;
|
|
2063
2178
|
if (!ready) {
|
|
2064
2179
|
child.kill("SIGTERM");
|
|
2065
|
-
|
|
2066
|
-
removeIdentityPin(cotalPath("nats.pid"));
|
|
2067
|
-
rmSync(cotalPath("nats.pid"), { force: true });
|
|
2180
|
+
removePidPair(cotalPath("nats.pid"), String(child.pid));
|
|
2068
2181
|
throw new Error(`nats-server did not become reachable at ${server} - see ${logPath}`);
|
|
2069
2182
|
}
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2183
|
+
let brokerVer;
|
|
2184
|
+
try {
|
|
2185
|
+
brokerVer = await brokerVersion(server, space, setup?.creds);
|
|
2186
|
+
}
|
|
2187
|
+
catch (e) {
|
|
2188
|
+
child.kill("SIGTERM");
|
|
2189
|
+
removePidPair(cotalPath("nats.pid"), String(child.pid));
|
|
2190
|
+
throw e;
|
|
2191
|
+
}
|
|
2192
|
+
if (belowPresenceSafeBroker(brokerVer.version) && brokerVer.presenceFileBacked)
|
|
2193
|
+
console.error(c.yellow(`! nats-server ${brokerVer.version} is below 2.14.5 and the presence bucket of "${space}" is file-backed: it can latch after a stall and close the mesh to new joins (#1356); put a nats-server 2.14.5 or newer on PATH (it wins over the bundled one)`));
|
|
2194
|
+
if (!opts.boundListener) {
|
|
2195
|
+
if (!child.pid)
|
|
2196
|
+
throw new Error("nats-server spawned with no pid");
|
|
2197
|
+
writePidPair(cotalPath("nats.pid"), child.pid); // #969/#1238: publish the pair by rename, never torn
|
|
2198
|
+
}
|
|
2073
2199
|
if (opts.boundListener)
|
|
2074
2200
|
await opts.boundListener.verify();
|
|
2075
2201
|
// POST-START MUST NOT LEAVE AN ORPHAN LISTENER.
|
|
@@ -2097,13 +2223,15 @@ opts) {
|
|
|
2097
2223
|
}
|
|
2098
2224
|
catch { /* already gone */ }
|
|
2099
2225
|
try {
|
|
2100
|
-
|
|
2101
|
-
rmSync(cotalPath("nats.pid"), { force: true });
|
|
2226
|
+
removePidPair(cotalPath("nats.pid"), String(child.pid));
|
|
2102
2227
|
}
|
|
2103
2228
|
catch { /* best effort */ }
|
|
2104
2229
|
throw e;
|
|
2105
2230
|
}
|
|
2106
2231
|
}
|
|
2232
|
+
else {
|
|
2233
|
+
await recreateMemoryBuckets(server, space, setup);
|
|
2234
|
+
}
|
|
2107
2235
|
// USER MODE: the auth service comes up FIRST among the daemons (see the foreground path).
|
|
2108
2236
|
const svc = await startUserAuthService(space, server, setup, opts.publicExchange);
|
|
2109
2237
|
// Record BEFORE the control plane: the manager's fail-closed mode detection needs the
|
|
@@ -2125,6 +2253,7 @@ opts) {
|
|
|
2125
2253
|
...(effectiveAttachHost ? { attachHost: effectiveAttachHost } : {}),
|
|
2126
2254
|
...(effectiveMaxSessions !== undefined ? { maxSessions: effectiveMaxSessions } : {}),
|
|
2127
2255
|
...(opts.maxFileStore !== undefined ? { maxFileStore: opts.maxFileStore } : {}),
|
|
2256
|
+
storeDir,
|
|
2128
2257
|
ts: new Date().toISOString(),
|
|
2129
2258
|
}, "started");
|
|
2130
2259
|
// Commit policy BEFORE delivery launch (S9). Listener is proved; refuse paths never reach here.
|
|
@@ -2303,6 +2432,10 @@ function journaledMaxFileStore(value) {
|
|
|
2303
2432
|
* asserted directly by the registry smoke, since reaching each branch through a full `up` would
|
|
2304
2433
|
* need a live broker per case. */
|
|
2305
2434
|
export const recordOurMeshForTest = (m, provenance) => recordOurMesh(m, provenance);
|
|
2435
|
+
/** Record this mesh in the registry, and set it as the `current` default when there's no usable one
|
|
2436
|
+
* — i.e. the first mesh, OR when `current` dangles at a space that's no longer in the registry (a
|
|
2437
|
+
* ghost pointer is not a default). Never silently redirect a `current` that still resolves to a live
|
|
2438
|
+
* mesh; just say another is the default and how to switch. */
|
|
2306
2439
|
function recordOurMesh(m, provenance) {
|
|
2307
2440
|
const cur = getCurrent();
|
|
2308
2441
|
const usableCurrent = cur && findMesh(cur) ? cur : undefined; // compute before recording m
|
|
@@ -2356,6 +2489,43 @@ async function waitReady(server, creds) {
|
|
|
2356
2489
|
}
|
|
2357
2490
|
return false;
|
|
2358
2491
|
}
|
|
2492
|
+
/** The broker's version, and whether `space`'s presence stream exists file-backed. Cotal creates
|
|
2493
|
+
* it in memory (#1356), so only a bucket an older cotal created is file-backed; an absent one
|
|
2494
|
+
* will be created in memory by `setupSpaceStreams`. */
|
|
2495
|
+
async function brokerVersion(server, space, creds) {
|
|
2496
|
+
const nc = await connect({ servers: server, ...standaloneConnectOpts({ creds, tls: false }) });
|
|
2497
|
+
try {
|
|
2498
|
+
requireBrokerFloor(nc);
|
|
2499
|
+
const version = nc.info?.version ?? "";
|
|
2500
|
+
let presenceFileBacked = false;
|
|
2501
|
+
try {
|
|
2502
|
+
const info = await (await jetstreamManager(nc)).streams.info(`KV_${presenceBucket(space)}`);
|
|
2503
|
+
presenceFileBacked = info.config.storage === StorageType.File;
|
|
2504
|
+
}
|
|
2505
|
+
catch (e) {
|
|
2506
|
+
const err = e;
|
|
2507
|
+
if (err.api_error?.err_code !== 10059 && !/stream not found/i.test(err.message ?? ""))
|
|
2508
|
+
throw e;
|
|
2509
|
+
}
|
|
2510
|
+
return { version, presenceFileBacked };
|
|
2511
|
+
}
|
|
2512
|
+
finally {
|
|
2513
|
+
await nc.drain();
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
// #1356: the presence bucket's file-backed latch is fixed in nats-server 2.14.5. Below that,
|
|
2517
|
+
// `cotal up` warns when the space's presence bucket is file-backed (never refuses: 2.14.0 meets the
|
|
2518
|
+
// SPEC §13.12 floor and is the bundled broker). A memory-backed bucket has no file store to latch.
|
|
2519
|
+
function belowPresenceSafeBroker(version) {
|
|
2520
|
+
const p = parseServerVersion(version);
|
|
2521
|
+
if (!p)
|
|
2522
|
+
return false;
|
|
2523
|
+
if (p.major !== 2)
|
|
2524
|
+
return p.major < 2;
|
|
2525
|
+
if (p.minor !== 14)
|
|
2526
|
+
return p.minor < 14;
|
|
2527
|
+
return p.patch < 5;
|
|
2528
|
+
}
|
|
2359
2529
|
/** Wildcard binds mean "every interface", so they are NOT an address a client can be told to dial:
|
|
2360
2530
|
* the probe/registry URL stays whatever it already is (loopback by default), which the wildcard
|
|
2361
2531
|
* bind necessarily covers. */
|
|
@@ -2463,6 +2633,23 @@ async function postStart(server, space, setup, seedFile) {
|
|
|
2463
2633
|
deliveryClass: setup ? "durable" : "live",
|
|
2464
2634
|
});
|
|
2465
2635
|
}
|
|
2636
|
+
/** A resume or restore skips `postStart` because the preserved store already holds every canonical
|
|
2637
|
+
* stream. A memory-backed bucket is the exception: the broker stop that preserved the store emptied
|
|
2638
|
+
* it, stream and all (#1356 made presence memory-backed). Recreate those, and only those, from the
|
|
2639
|
+
* same `ttlBuckets` list `setupSpaceStreams` creates them from, before any daemon opens them. */
|
|
2640
|
+
async function recreateMemoryBuckets(server, space, setup) {
|
|
2641
|
+
const nc = await connect({ servers: server, ...standaloneConnectOpts({ creds: setup?.creds, tls: false }) });
|
|
2642
|
+
try {
|
|
2643
|
+
requireBrokerFloor(nc);
|
|
2644
|
+
const kvm = new Kvm(nc);
|
|
2645
|
+
for (const [bucket, ttl, storage] of ttlBuckets(space))
|
|
2646
|
+
if (storage === StorageType.Memory)
|
|
2647
|
+
await kvm.create(bucket, { ttl, storage });
|
|
2648
|
+
}
|
|
2649
|
+
finally {
|
|
2650
|
+
await nc.drain();
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2466
2653
|
/** Load the declarative channels-config file to seed the registry. An explicit `--channels`
|
|
2467
2654
|
* path that's missing is a hard error; the default `.cotal/channels.json` is optional (absent
|
|
2468
2655
|
* ⇒ nothing to seed). */
|
|
@@ -2476,21 +2663,6 @@ function loadChannelsFile(explicit) {
|
|
|
2476
2663
|
}
|
|
2477
2664
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
2478
2665
|
}
|
|
2479
|
-
/** Ensure the space's trust material exists, render a server config, and mint a privileged
|
|
2480
|
-
* setup creds (used to pre-create streams once the server is up). The account signing key
|
|
2481
|
-
* in `.cotal/auth` is what `cotal mint` and the manager later use to issue per-agent creds.
|
|
2482
|
-
*
|
|
2483
|
-
* USER MODE (`user` set): additionally run the registered auth provider's `prepareServer` over the
|
|
2484
|
-
* SPACE-SCOPED state dir (`.cotal/auth/<space>/` — the multi-space-ready layout; nothing user-auth
|
|
2485
|
-
* lives flat) and preload its extra account(s) into the broker config. The inverse is fail-closed:
|
|
2486
|
-
* a space whose user-auth state exists MUST keep being started with --user-auth — regenerating the
|
|
2487
|
-
* config without the callout account would silently break every sentinel connect.
|
|
2488
|
-
*
|
|
2489
|
-
* `rotateSys` (`cotal up --rotate-sys`) is the class-3 renewal for an EXISTING space: rotate the
|
|
2490
|
-
* system account, re-mint `membership-observer.creds` + `connection-evictor.creds` against the
|
|
2491
|
-
* successor, and render the config from the ROTATED record so the broker this `up` starts is the one
|
|
2492
|
-
* that trusts them. It belongs here because this is the single site that both owns the trust record
|
|
2493
|
-
* and renders `server.conf`; anywhere else would publish creds the live broker cannot honor. */
|
|
2494
2666
|
/**
|
|
2495
2667
|
* Turn the `--tls-cert`/`--tls-key` pair into a validated {@link BrokerTransport}, or `plaintext`
|
|
2496
2668
|
* when neither was given.
|
|
@@ -2690,6 +2862,21 @@ function writeOpenBrokerConf(storeDir, opts) {
|
|
|
2690
2862
|
writeFileSync(confPath, openServerConfig({ port: opts.port, host: opts.host, storeDir, maxFileStore: opts.maxFileStore, transport: opts.transport }));
|
|
2691
2863
|
return confPath;
|
|
2692
2864
|
}
|
|
2865
|
+
/** Ensure the space's trust material exists, render a server config, and mint a privileged
|
|
2866
|
+
* setup creds (used to pre-create streams once the server is up). The account signing key
|
|
2867
|
+
* in `.cotal/auth` is what `cotal mint` and the manager later use to issue per-agent creds.
|
|
2868
|
+
*
|
|
2869
|
+
* USER MODE (`user` set): additionally run the registered auth provider's `prepareServer` over the
|
|
2870
|
+
* SPACE-SCOPED state dir (`.cotal/auth/<space>/` — the multi-space-ready layout; nothing user-auth
|
|
2871
|
+
* lives flat) and preload its extra account(s) into the broker config. The inverse is fail-closed:
|
|
2872
|
+
* a space whose user-auth state exists MUST keep being started with --user-auth — regenerating the
|
|
2873
|
+
* config without the callout account would silently break every sentinel connect.
|
|
2874
|
+
*
|
|
2875
|
+
* `rotateSys` (`cotal up --rotate-sys`) is the class-3 renewal for an EXISTING space: rotate the
|
|
2876
|
+
* system account, re-mint `membership-observer.creds` + `connection-evictor.creds` against the
|
|
2877
|
+
* successor, and render the config from the ROTATED record so the broker this `up` starts is the one
|
|
2878
|
+
* that trusts them. It belongs here because this is the single site that both owns the trust record
|
|
2879
|
+
* and renders `server.conf`; anywhere else would publish creds the live broker cannot honor. */
|
|
2693
2880
|
async function authSetup(storeDir, server, space, host = "127.0.0.1", user,
|
|
2694
2881
|
// NON-OPTIONAL, and it used to default to `{ kind: "plaintext" }`. That default was the hole: the
|
|
2695
2882
|
// manifest path called this with five arguments and silently got a cleartext listener while the
|
|
@@ -2868,23 +3055,6 @@ async function assertRootBrokerStopped(root) {
|
|
|
2868
3055
|
throw new Error(`--rotate-sys: mesh "${m.space}" for this root is still reachable at ${m.server} - stop it first (\`cotal down\`), then rotate`);
|
|
2869
3056
|
}
|
|
2870
3057
|
}
|
|
2871
|
-
/** Mint the two scoped creds the delivery daemon's membership feed loads (broker-sourced graph
|
|
2872
|
-
* membership), at the FRESH `cotal up` while the in-memory `$SYS` signing seed still exists:
|
|
2873
|
-
* - `membership-observer.creds` — SYSTEM-account CONNZ reader (the only window it can be minted: the
|
|
2874
|
-
* `$SYS` seed is never persisted).
|
|
2875
|
-
* - `membership-rw.creds` — DATA-account members-read + feed-write.
|
|
2876
|
-
* - `membership.json` — the DATA account id (the CONNZ/event subjects pin it; non-secret, but kept
|
|
2877
|
-
* 0600 alongside the creds).
|
|
2878
|
-
* All 0600. Best-effort: a failure logs and leaves the feed disabled (the graph degrades to traffic-
|
|
2879
|
-
* only, delivery is untouched). Runs only on a FRESH space (the `if (!auth)` branch); a normal down/up
|
|
2880
|
-
* keeps `.cotal/auth` + these creds and reuses them. A space provisioned before this feature has no
|
|
2881
|
-
* in-memory `$SYS` seed, so it gains membership only when its auth is regenerated (a fresh `.cotal/auth`)
|
|
2882
|
-
* — a documented migration property, not a silent no-op.
|
|
2883
|
-
* Coupling: `cotal clean all` deletes this identity-derived set (removeLocalState in clean.ts) —
|
|
2884
|
-
* a cred added here must be added to that removal list too. `membership-rw.creds` is a MIGRATED kind:
|
|
2885
|
-
* its write/read/delete all go through the {@link SecretStore} seam (here, the feed reader, and clean),
|
|
2886
|
-
* so the renewal owner can re-sign it into a hosted store. The observer / evictor / config stay on the
|
|
2887
|
-
* raw FS (static $SYS creds + non-secret config, not renewable kinds). */
|
|
2888
3058
|
/** Provision the DATA-account half of the membership bundle, on EVERY `up` rather than only a fresh
|
|
2889
3059
|
* space. Idempotent: it writes only what is absent and is silent when both are present.
|
|
2890
3060
|
*
|
|
@@ -2934,6 +3104,23 @@ async function healMembershipDataCreds(auth, root, space) {
|
|
|
2934
3104
|
console.error(c.dim(`• broker-sourced membership not repaired: ${e.message}`));
|
|
2935
3105
|
}
|
|
2936
3106
|
}
|
|
3107
|
+
/** Mint the two scoped creds the delivery daemon's membership feed loads (broker-sourced graph
|
|
3108
|
+
* membership), at the FRESH `cotal up` while the in-memory `$SYS` signing seed still exists:
|
|
3109
|
+
* - `membership-observer.creds` — SYSTEM-account CONNZ reader (the only window it can be minted: the
|
|
3110
|
+
* `$SYS` seed is never persisted).
|
|
3111
|
+
* - `membership-rw.creds` — DATA-account members-read + feed-write.
|
|
3112
|
+
* - `membership.json` — the DATA account id (the CONNZ/event subjects pin it; non-secret, but kept
|
|
3113
|
+
* 0600 alongside the creds).
|
|
3114
|
+
* All 0600. Best-effort: a failure logs and leaves the feed disabled (the graph degrades to traffic-
|
|
3115
|
+
* only, delivery is untouched). Runs only on a FRESH space (the `if (!auth)` branch); a normal down/up
|
|
3116
|
+
* keeps `.cotal/auth` + these creds and reuses them. A space provisioned before this feature has no
|
|
3117
|
+
* in-memory `$SYS` seed, so it gains membership only when its auth is regenerated (a fresh `.cotal/auth`)
|
|
3118
|
+
* — a documented migration property, not a silent no-op.
|
|
3119
|
+
* Coupling: `cotal clean all` deletes this identity-derived set (removeLocalState in clean.ts) —
|
|
3120
|
+
* a cred added here must be added to that removal list too. `membership-rw.creds` is a MIGRATED kind:
|
|
3121
|
+
* its write/read/delete all go through the {@link SecretStore} seam (here, the feed reader, and clean),
|
|
3122
|
+
* so the renewal owner can re-sign it into a hosted store. The observer / evictor / config stay on the
|
|
3123
|
+
* raw FS (static $SYS creds + non-secret config, not renewable kinds). */
|
|
2937
3124
|
async function provisionMembershipCreds(auth, root, space) {
|
|
2938
3125
|
try {
|
|
2939
3126
|
const observer = await mintMembershipObserverCreds(auth, newIdentity());
|