@ccmsg/cli 0.9.1 → 0.10.1
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/package.json +1 -1
- package/src/auth/admin.ts +32 -6
- package/src/cli.ts +298 -81
- package/src/daemon/control.ts +61 -9
- package/src/daemon/registry.ts +370 -65
- package/src/daemon/supervise.ts +14 -2
- package/src/instance/ccmsg-config.d.ts +111 -0
- package/src/instance/config.ts +444 -126
- package/src/instance/identity.ts +11 -2
- package/src/instance/instance.ts +9 -2
- package/src/instance/paths.ts +19 -6
- package/src/mesh/mesh.ts +30 -2
package/src/instance/identity.ts
CHANGED
|
@@ -9,7 +9,16 @@ import type { InstanceId } from "@ccmsg/protocol";
|
|
|
9
9
|
* spelling is sized for. */
|
|
10
10
|
const ID_BYTES = 16;
|
|
11
11
|
|
|
12
|
-
const ID = /^[0-9a-f]{32}$/;
|
|
12
|
+
export const ID = /^[0-9a-f]{32}$/;
|
|
13
|
+
|
|
14
|
+
/** A fresh id of that width, for whatever is being named.
|
|
15
|
+
*
|
|
16
|
+
* Shared with the cluster ids rather than written again there: what an id has
|
|
17
|
+
* to be is unguessable-by-accident and the same width wherever it is read, and
|
|
18
|
+
* a second generator is a second answer to how wide that is. */
|
|
19
|
+
export function newId(): string {
|
|
20
|
+
return randomBytes(ID_BYTES).toString("hex");
|
|
21
|
+
}
|
|
13
22
|
|
|
14
23
|
/** This instance's identity, read from the state directory and generated there
|
|
15
24
|
* the first time it is asked for.
|
|
@@ -26,7 +35,7 @@ const ID = /^[0-9a-f]{32}$/;
|
|
|
26
35
|
export function instanceIdentity(file: string): InstanceId {
|
|
27
36
|
const held = read(file);
|
|
28
37
|
if (held !== undefined) return held;
|
|
29
|
-
const made =
|
|
38
|
+
const made = newId();
|
|
30
39
|
mkdirSync(dirname(file), { recursive: true });
|
|
31
40
|
writeFileSync(file, `${made}\n`);
|
|
32
41
|
return made;
|
package/src/instance/instance.ts
CHANGED
|
@@ -176,7 +176,7 @@ export async function start(options: StartOptions = {}): Promise<StartOutcome> {
|
|
|
176
176
|
try {
|
|
177
177
|
// 3. the config. A broken one ends the start rather than turning the
|
|
178
178
|
// setting it carried silently off (DV-Q9).
|
|
179
|
-
const config = loadConfig(paths.
|
|
179
|
+
const config = await loadConfig(paths.configDir, paths.configHome);
|
|
180
180
|
// What the config says of the gateway, resolved before anything is built
|
|
181
181
|
// from it: a webhook source whose secret cannot be read ends the start
|
|
182
182
|
// here, for the same reason a broken config does (DV-Q9).
|
|
@@ -675,7 +675,14 @@ export class Instance {
|
|
|
675
675
|
// address is what says the caller is local (DR-0001 §2.2).
|
|
676
676
|
handle: (frame, conn) => {
|
|
677
677
|
const admin = adminRequestOf(frame);
|
|
678
|
-
if (admin !== undefined)
|
|
678
|
+
if (admin !== undefined) {
|
|
679
|
+
return Promise.resolve(
|
|
680
|
+
handleAdmin(
|
|
681
|
+
{ auth: this.#auth, ...(this.#mesh === undefined ? {} : { mesh: this.#mesh }) },
|
|
682
|
+
admin,
|
|
683
|
+
),
|
|
684
|
+
);
|
|
685
|
+
}
|
|
679
686
|
return this.handle(frame, conn);
|
|
680
687
|
},
|
|
681
688
|
}),
|
package/src/instance/paths.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { basename, isAbsolute, join } from "node:path";
|
|
4
4
|
import { currentSession, HARNESS } from "../harness/index.ts";
|
|
5
|
+
import { CLUSTERS_DIR, CLUSTERS_FILE, CONFIG_FILE, INSTANCES_DIR } from "./config.ts";
|
|
5
6
|
|
|
6
7
|
/** Every path one instance uses, decided in one place (daemon-v2 §8.1).
|
|
7
8
|
*
|
|
@@ -14,10 +15,17 @@ export interface InstancePaths {
|
|
|
14
15
|
readonly configHome: string;
|
|
15
16
|
/** What distinguishes this instance's files from another instance's. */
|
|
16
17
|
readonly key: string;
|
|
17
|
-
/**
|
|
18
|
-
*
|
|
19
|
-
* from the config home the way the rest of these are. */
|
|
18
|
+
/** Where a person writes settings, shared by every instance on this host: it
|
|
19
|
+
* holds what every instance starts from and one file per instance, so it is
|
|
20
|
+
* not derived from the config home the way the rest of these are. */
|
|
21
|
+
readonly configDir: string;
|
|
22
|
+
/** The file every instance's settings start from. */
|
|
20
23
|
readonly configFile: string;
|
|
24
|
+
/** Where the file naming this config home lives, one per instance. */
|
|
25
|
+
readonly instancesDir: string;
|
|
26
|
+
/** Which clusters this host knows of, and where each one's file is. */
|
|
27
|
+
readonly clustersFile: string;
|
|
28
|
+
readonly clustersDir: string;
|
|
21
29
|
readonly stateDir: string;
|
|
22
30
|
/** The address clients connect to. A symlink to whichever `socketReal` is
|
|
23
31
|
* currently serving, so a client's path outlives the process behind it. */
|
|
@@ -116,7 +124,11 @@ export function resolvePathsFor(configHome: string, env: Env = process.env): Ins
|
|
|
116
124
|
return {
|
|
117
125
|
configHome,
|
|
118
126
|
key,
|
|
119
|
-
|
|
127
|
+
configDir,
|
|
128
|
+
configFile: join(configDir, CONFIG_FILE),
|
|
129
|
+
instancesDir: join(configDir, INSTANCES_DIR),
|
|
130
|
+
clustersFile: join(configDir, CLUSTERS_FILE),
|
|
131
|
+
clustersDir: join(configDir, CLUSTERS_DIR),
|
|
120
132
|
stateDir,
|
|
121
133
|
socketDir,
|
|
122
134
|
socket: join(socketDir, SOCKET_NAME),
|
|
@@ -169,9 +181,10 @@ export function resolveSupervisorSocket(env: Env = process.env): string {
|
|
|
169
181
|
|
|
170
182
|
export const SUPERVISOR_SOCKET = "supervise.sock";
|
|
171
183
|
|
|
172
|
-
/** The
|
|
184
|
+
/** The file every instance's settings start from, for a caller that has no
|
|
185
|
+
* instance to resolve. */
|
|
173
186
|
export function resolveConfigFile(env: Env = process.env): string {
|
|
174
|
-
return join(resolveConfigDir(env),
|
|
187
|
+
return join(resolveConfigDir(env), CONFIG_FILE);
|
|
175
188
|
}
|
|
176
189
|
|
|
177
190
|
/** A name for one config home that is readable and cannot collide.
|
package/src/mesh/mesh.ts
CHANGED
|
@@ -334,7 +334,35 @@ export class Mesh {
|
|
|
334
334
|
* is what found which entry that is. */
|
|
335
335
|
get peers(): Endpoint[] {
|
|
336
336
|
const self = this.self;
|
|
337
|
-
return this.deps.peers.filter((peer) => peer !== self);
|
|
337
|
+
return this.deps.peers.filter((peer) => peer !== self && !this.#forgotten.has(peer));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** The peers taken off this host's list while this instance was running.
|
|
341
|
+
*
|
|
342
|
+
* Config is read once (DV-Q8) and this does not change that: what a person
|
|
343
|
+
* writes goes on taking effect at the next start. What this holds is the one
|
|
344
|
+
* edit that cannot wait for one — an endpoint this host is no longer to be
|
|
345
|
+
* talking to, which would otherwise stay connected until somebody restarted
|
|
346
|
+
* the instance. */
|
|
347
|
+
readonly #forgotten = new Set<Endpoint>();
|
|
348
|
+
|
|
349
|
+
/** Stop being a peer of this endpoint: drop the link if there is one, stop
|
|
350
|
+
* dialling it, and refuse its greeting if it dials us.
|
|
351
|
+
*
|
|
352
|
+
* Answers whether anything was actually cut, so `ccmsg mesh remove` can say
|
|
353
|
+
* which instances were talking to it rather than that it asked them all. */
|
|
354
|
+
forget(peer: Endpoint): boolean {
|
|
355
|
+
this.#forgotten.add(peer);
|
|
356
|
+
const retry = this.#retries.get(peer);
|
|
357
|
+
if (retry !== undefined) {
|
|
358
|
+
clearTimeout(retry);
|
|
359
|
+
this.#retries.delete(peer);
|
|
360
|
+
}
|
|
361
|
+
const link = this.#links.get(peer);
|
|
362
|
+
if (link === undefined) return false;
|
|
363
|
+
link.conn.close();
|
|
364
|
+
this.#drop(peer, link.conn);
|
|
365
|
+
return true;
|
|
338
366
|
}
|
|
339
367
|
|
|
340
368
|
/** Where peers reach this instance, as the probe settled it (§7.1).
|
|
@@ -620,7 +648,7 @@ export class Mesh {
|
|
|
620
648
|
if (claim.ver !== MESH_VER) {
|
|
621
649
|
throw new OpError("invalid_args", `this instance speaks mesh handshake ${MESH_VER}`);
|
|
622
650
|
}
|
|
623
|
-
if (!this.deps.peers.includes(claim.iss)) {
|
|
651
|
+
if (!this.deps.peers.includes(claim.iss) || this.#forgotten.has(claim.iss)) {
|
|
624
652
|
throw new OpError("forbidden", `${claim.iss} is not a peer of this instance`);
|
|
625
653
|
}
|
|
626
654
|
if (claim.aud !== self) {
|