@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.
@@ -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 = randomBytes(ID_BYTES).toString("hex");
38
+ const made = newId();
30
39
  mkdirSync(dirname(file), { recursive: true });
31
40
  writeFileSync(file, `${made}\n`);
32
41
  return made;
@@ -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.configFile, paths.configHome);
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) return Promise.resolve(handleAdmin(this.#auth, admin));
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
  }),
@@ -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
- /** The one file a person edits, shared by every instance on this host: it
18
- * carries the defaults and the list of config homes, so it is not derived
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
- configFile: join(configDir, "config.json"),
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 shared config file, for a caller that has no instance to resolve. */
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), "config.json");
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) {