borgmcp-server 0.1.7 → 0.1.9

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.
Files changed (70) hide show
  1. package/README.md +61 -46
  2. package/SECURITY.md +12 -1
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/dist/bootstrap.d.ts +8 -1
  5. package/dist/bootstrap.js +39 -6
  6. package/dist/bootstrap.js.map +1 -1
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +176 -1
  9. package/dist/cli.js.map +1 -1
  10. package/dist/coordination-api.js +10 -1
  11. package/dist/coordination-api.js.map +1 -1
  12. package/dist/credentials.d.ts +5 -1
  13. package/dist/credentials.js +35 -3
  14. package/dist/credentials.js.map +1 -1
  15. package/dist/debug-log.d.ts +2 -2
  16. package/dist/debug-log.js +2 -2
  17. package/dist/debug-log.js.map +1 -1
  18. package/dist/https-server.d.ts +5 -1
  19. package/dist/https-server.js +93 -27
  20. package/dist/https-server.js.map +1 -1
  21. package/dist/index.d.ts +14 -1
  22. package/dist/index.js +8 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/main.js +1 -0
  25. package/dist/main.js.map +1 -1
  26. package/dist/managed-service.d.ts +20 -0
  27. package/dist/managed-service.js +80 -0
  28. package/dist/managed-service.js.map +1 -0
  29. package/dist/migrations.js +23 -0
  30. package/dist/migrations.js.map +1 -1
  31. package/dist/portable-credential-store.d.ts +16 -0
  32. package/dist/portable-credential-store.js +305 -0
  33. package/dist/portable-credential-store.js.map +1 -0
  34. package/dist/registry-artifact.d.ts +11 -0
  35. package/dist/registry-artifact.js +99 -0
  36. package/dist/registry-artifact.js.map +1 -0
  37. package/dist/runtime-identity.d.ts +19 -0
  38. package/dist/runtime-identity.js +61 -0
  39. package/dist/runtime-identity.js.map +1 -0
  40. package/dist/runtime-lifecycle.d.ts +56 -0
  41. package/dist/runtime-lifecycle.js +486 -0
  42. package/dist/runtime-lifecycle.js.map +1 -0
  43. package/dist/runtime-operator.d.ts +24 -0
  44. package/dist/runtime-operator.js +95 -0
  45. package/dist/runtime-operator.js.map +1 -0
  46. package/dist/service.d.ts +53 -5
  47. package/dist/service.js +326 -15
  48. package/dist/service.js.map +1 -1
  49. package/dist/store.d.ts +16 -7
  50. package/dist/store.js +93 -83
  51. package/dist/store.js.map +1 -1
  52. package/npm-shrinkwrap.json +6 -6
  53. package/package.json +2 -2
  54. package/src/bootstrap.ts +46 -5
  55. package/src/cli.ts +171 -1
  56. package/src/coordination-api.ts +12 -0
  57. package/src/credentials.ts +36 -5
  58. package/src/debug-log.ts +4 -3
  59. package/src/https-server.ts +124 -27
  60. package/src/index.ts +46 -1
  61. package/src/main.ts +1 -0
  62. package/src/managed-service.ts +107 -0
  63. package/src/migrations.ts +23 -0
  64. package/src/portable-credential-store.ts +306 -0
  65. package/src/registry-artifact.ts +111 -0
  66. package/src/runtime-identity.ts +82 -0
  67. package/src/runtime-lifecycle.ts +567 -0
  68. package/src/runtime-operator.ts +124 -0
  69. package/src/service.ts +401 -20
  70. package/src/store.ts +100 -83
package/dist/service.d.ts CHANGED
@@ -3,19 +3,49 @@ import { type CubeInvitationResult } from "./credentials.js";
3
3
  import { type HttpsServerOptions, type RunningServer } from "./https-server.js";
4
4
  import { openStore, type LivenessStore, type StorageLimits } from "./store.js";
5
5
  import type { CubeAccess } from "./store.js";
6
+ import { type RuntimeBuildIdentity } from "./runtime-identity.js";
7
+ import { type RuntimeUpdateResult } from "./runtime-operator.js";
6
8
  export interface ServerService {
7
9
  readonly start: (args: readonly string[]) => Promise<void>;
8
- readonly setup?: (options: SetupOptions) => Promise<BootstrapResult>;
10
+ readonly setup?: (options: SetupOptions) => Promise<ServerSetupResult>;
11
+ readonly status?: () => Promise<ServerRuntimeStatus>;
12
+ readonly update?: () => Promise<RuntimeUpdateResult>;
9
13
  readonly rotateClient?: (clientId: string) => Promise<string>;
10
14
  readonly revokeClient?: (clientId: string) => Promise<void>;
11
15
  readonly grantClient?: (clientId: string, cubeId: string, access: CubeAccess) => Promise<void>;
12
16
  readonly ungrantClient?: (clientId: string, cubeId: string) => Promise<void>;
13
17
  readonly createClientInvitation?: (recoveryCredential: string, cubeSelector?: string, access?: CubeAccess) => Promise<string | CubeInvitationResult>;
14
18
  readonly replaceOwnerInvitation?: (recoveryCredential: string) => Promise<string>;
19
+ readonly invite?: () => Promise<string>;
15
20
  }
16
21
  export interface SetupOptions {
17
22
  readonly reinitialize: boolean;
18
23
  }
24
+ export type ServerSetupResult = (Omit<BootstrapResult, "recoveryCredential" | "initialInvitation"> & {
25
+ readonly artifact?: {
26
+ readonly version: string;
27
+ readonly integrity: string;
28
+ readonly sourceSha: string | null;
29
+ };
30
+ }) | {
31
+ readonly existing: true;
32
+ readonly artifact?: {
33
+ readonly version: string;
34
+ readonly integrity: string;
35
+ readonly sourceSha: string | null;
36
+ };
37
+ };
38
+ export interface ServerRuntimeStatus {
39
+ readonly status: "running" | "stopped";
40
+ readonly artifact: {
41
+ readonly version: string;
42
+ readonly integrity: string;
43
+ } | null;
44
+ readonly buildIdentity: string | null;
45
+ readonly endpoint: string | null;
46
+ readonly mode: "foreground" | "managed" | "stopped";
47
+ readonly dataIdentity: "available" | "unavailable";
48
+ }
19
49
  export interface ServerEnvironment {
20
50
  readonly BORG_SERVER_TLS_KEY_FILE?: string;
21
51
  readonly BORG_SERVER_TLS_CERT_FILE?: string;
@@ -25,13 +55,17 @@ export interface ServerEnvironment {
25
55
  readonly BORG_SERVER_MAX_ACTIVITY_ENTRIES_PER_CUBE?: string;
26
56
  readonly BORG_SERVER_MAX_DATABASE_BYTES?: string;
27
57
  readonly BORG_SERVER_MIN_FREE_DISK_BYTES?: string;
58
+ readonly BORG_SERVER_SOURCE_SHA?: string;
59
+ readonly BORG_SERVER_ARTIFACT_INTEGRITY?: string;
60
+ readonly BORG_SERVER_PROCESS_MODE?: "foreground" | "managed";
61
+ readonly BORG_SERVER_RUNTIME_DIR?: string;
28
62
  }
29
63
  interface ServiceDependencies {
30
64
  readonly environment: ServerEnvironment;
31
65
  readonly readFile: (path: string) => Promise<Buffer>;
32
66
  readonly readPrivateKey: (path: string) => Promise<Buffer>;
33
67
  readonly startServer: (options: HttpsServerOptions) => Promise<RunningServer>;
34
- readonly onStarted: (origin: string) => void;
68
+ readonly onStarted: (origin: string, identity: RuntimeBuildIdentity) => void;
35
69
  readonly waitForShutdown: (server: RunningServer, signal?: AbortSignal) => Promise<void>;
36
70
  readonly debugOutput?: (line: string) => void;
37
71
  readonly installShutdownHandlers?: () => {
@@ -44,6 +78,15 @@ interface ServiceDependencies {
44
78
  };
45
79
  readonly onStartupPhase?: (phase: "pre-lock" | "post-lock" | "pre-listen") => Promise<void>;
46
80
  }
81
+ export type RuntimeLockStatus = {
82
+ readonly running: false;
83
+ } | {
84
+ readonly running: true;
85
+ readonly pid: number;
86
+ readonly identity: RuntimeBuildIdentity | null;
87
+ readonly endpoint: string | null;
88
+ readonly mode: "foreground" | "managed";
89
+ };
47
90
  export interface NodeServerTestHooks {
48
91
  readonly onStartupPhase?: (phase: "pre-lock" | "post-lock" | "pre-listen") => Promise<void>;
49
92
  readonly onSignalObserved?: () => void;
@@ -56,13 +99,18 @@ export declare function assertLanCaKeyOffline(runtimeDataDirectory: string): Pro
56
99
  export declare function selectServerEnvironment(environment: NodeJS.ProcessEnv): ServerEnvironment;
57
100
  export declare function resolveStorageLimits(environment: ServerEnvironment): StorageLimits;
58
101
  export declare const nodeServerService: ServerService;
59
- export declare function setupNodeServerInstallation(setupDataDirectory: string, bindHost: string, options: SetupOptions): Promise<BootstrapResult>;
60
- export declare function createOfflineCredentialService(offlineDataDirectory: string): Pick<Required<ServerService>, "rotateClient" | "revokeClient" | "grantClient" | "ungrantClient" | "createClientInvitation" | "replaceOwnerInvitation">;
102
+ export declare function inspectNodeRuntime(runtimeDataDirectory: string, managedRuntimeDirectory: string): Promise<ServerRuntimeStatus>;
103
+ export declare function setupNodeServerInstallation(setupDataDirectory: string, bindHost: string, options: SetupOptions, credentialRoot?: string): Promise<BootstrapResult | {
104
+ readonly existing: true;
105
+ }>;
106
+ export declare function createOfflineCredentialService(offlineDataDirectory: string, credentialRoot?: string): Pick<Required<ServerService>, "rotateClient" | "revokeClient" | "grantClient" | "ungrantClient" | "createClientInvitation" | "replaceOwnerInvitation" | "invite">;
61
107
  interface RuntimeLock {
62
108
  readonly release: () => Promise<void>;
109
+ readonly updateOrigin?: (origin: string) => Promise<void>;
63
110
  }
64
111
  export declare function isFatalTeardownError(error: unknown): boolean;
65
- export declare function acquireRuntimeLock(runtimeDataDirectory: string, purpose?: "server" | "exclusive-admin"): Promise<RuntimeLock>;
112
+ export declare function acquireRuntimeLock(runtimeDataDirectory: string, purpose?: "server" | "exclusive-admin", identity?: RuntimeBuildIdentity, mode?: "foreground" | "managed"): Promise<RuntimeLock>;
113
+ export declare function inspectRuntimeLock(runtimeDataDirectory: string): Promise<RuntimeLockStatus>;
66
114
  export declare function acquireInvitationMintLock(runtimeDataDirectory: string): Promise<RuntimeLock>;
67
115
  export declare function installProcessShutdownHandlers(): {
68
116
  readonly signal: AbortSignal;
package/dist/service.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { execFile } from "node:child_process";
2
3
  import { lstat, open, readFile, unlink } from "node:fs/promises";
3
4
  import { homedir } from "node:os";
4
- import { join } from "node:path";
5
+ import { basename, join } from "node:path";
6
+ import { promisify } from "node:util";
5
7
  import { bootstrapServer, loadDigestKey, loadTlsPrivateKey, } from "./bootstrap.js";
6
8
  import { CredentialAuthority, CredentialDigester, } from "./credentials.js";
7
9
  import { CoordinationApi } from "./coordination-api.js";
@@ -10,9 +12,17 @@ import { MigrationCompatibilityError } from "./migrations.js";
10
12
  import { createEnrollmentExchange } from "./enrollment.js";
11
13
  import { DEFAULT_SERVICE_LIMITS, startHttpsServer, } from "./https-server.js";
12
14
  import { resolveBindOptions } from "./network-policy.js";
15
+ import { DEFAULT_PORT } from "./network-policy.js";
16
+ import { readPortableServerCredential, writePortableServerCredential, } from "./portable-credential-store.js";
13
17
  import { invitationCubeAmbiguousError, operatorErrors, } from "./operator-error.js";
14
18
  import { parseStartOptions } from "./start-options.js";
15
19
  import { DEFAULT_STORAGE_LIMITS, openStore, preparePrivateDataDirectory, InvitationCubeAmbiguousError, InvitationCubeNotFoundError, } from "./store.js";
20
+ import { fileURLToPath } from "node:url";
21
+ import { loadRuntimeBuildIdentity } from "./runtime-identity.js";
22
+ import { createRuntimeLifecycle, createUnixNpmArtifactUnpacker, inspectActiveRuntimeArtifact, } from "./runtime-lifecycle.js";
23
+ import { createRegistryArtifactSource } from "./registry-artifact.js";
24
+ import { createRuntimeOperator } from "./runtime-operator.js";
25
+ import { createManagedServiceDefinition } from "./managed-service.js";
16
26
  const guardedRuntimeFailures = new Set();
17
27
  let nodeServerTestHooks;
18
28
  export function installNodeServerTestHooks(hooks) {
@@ -70,8 +80,20 @@ export function createNodeServerService(dependencies) {
70
80
  throw operatorErrors.SERVER_FILES_MISSING;
71
81
  }
72
82
  let runtimeLock;
83
+ let runtimeIdentity;
73
84
  try {
74
- runtimeLock = dataDirectory === undefined ? undefined : await acquireRuntimeLock(dataDirectory, "server");
85
+ runtimeIdentity = await loadRuntimeBuildIdentity({
86
+ ...(dependencies.environment.BORG_SERVER_SOURCE_SHA === undefined
87
+ ? {}
88
+ : { sourceSha: dependencies.environment.BORG_SERVER_SOURCE_SHA }),
89
+ ...(dependencies.environment.BORG_SERVER_ARTIFACT_INTEGRITY === undefined
90
+ ? {}
91
+ : { artifactIntegrity: dependencies.environment.BORG_SERVER_ARTIFACT_INTEGRITY }),
92
+ artifactDescriptorPath: fileURLToPath(new URL("../../artifact.json", import.meta.url)),
93
+ });
94
+ runtimeLock = dataDirectory === undefined
95
+ ? undefined
96
+ : await acquireRuntimeLock(dataDirectory, "server", runtimeIdentity, dependencies.environment.BORG_SERVER_PROCESS_MODE ?? "foreground");
75
97
  await dependencies.onStartupPhase?.("post-lock");
76
98
  throwIfShutdown(shutdown?.signal);
77
99
  }
@@ -146,6 +168,7 @@ export function createNodeServerService(dependencies) {
146
168
  ? {}
147
169
  : { handleCoordination: (request) => coordinationApi.handle(request) }),
148
170
  debugLogger,
171
+ runtimeIdentity,
149
172
  });
150
173
  throwIfShutdown(shutdown?.signal);
151
174
  if (authRuntime !== undefined) {
@@ -172,7 +195,8 @@ export function createNodeServerService(dependencies) {
172
195
  let failure;
173
196
  try {
174
197
  throwIfShutdown(shutdown?.signal);
175
- dependencies.onStarted(running.origin);
198
+ await runtimeLock?.updateOrigin?.(running.origin);
199
+ dependencies.onStarted(running.origin, runtimeIdentity);
176
200
  debugLogger.emit({ event: "lifecycle", action: "listening" });
177
201
  await dependencies.waitForShutdown(running, shutdown?.signal);
178
202
  }
@@ -216,6 +240,13 @@ export function selectServerEnvironment(environment) {
216
240
  const maxActivityEntries = environment["BORG_SERVER_MAX_ACTIVITY_ENTRIES_PER_CUBE"];
217
241
  const maxDatabaseBytes = environment["BORG_SERVER_MAX_DATABASE_BYTES"];
218
242
  const minFreeDiskBytes = environment["BORG_SERVER_MIN_FREE_DISK_BYTES"];
243
+ const sourceSha = environment["BORG_SERVER_SOURCE_SHA"];
244
+ const artifactIntegrity = environment["BORG_SERVER_ARTIFACT_INTEGRITY"];
245
+ const processMode = environment["BORG_SERVER_PROCESS_MODE"];
246
+ const runtimeDirectory = environment["BORG_SERVER_RUNTIME_DIR"];
247
+ if (processMode !== undefined && processMode !== "foreground" && processMode !== "managed") {
248
+ throw new Error("BORG_SERVER_PROCESS_MODE is invalid.");
249
+ }
219
250
  return {
220
251
  ...(keyFile === undefined ? {} : { BORG_SERVER_TLS_KEY_FILE: keyFile }),
221
252
  ...(certificateFile === undefined
@@ -229,6 +260,10 @@ export function selectServerEnvironment(environment) {
229
260
  : { BORG_SERVER_MAX_ACTIVITY_ENTRIES_PER_CUBE: maxActivityEntries }),
230
261
  ...(maxDatabaseBytes === undefined ? {} : { BORG_SERVER_MAX_DATABASE_BYTES: maxDatabaseBytes }),
231
262
  ...(minFreeDiskBytes === undefined ? {} : { BORG_SERVER_MIN_FREE_DISK_BYTES: minFreeDiskBytes }),
263
+ ...(sourceSha === undefined ? {} : { BORG_SERVER_SOURCE_SHA: sourceSha }),
264
+ ...(artifactIntegrity === undefined ? {} : { BORG_SERVER_ARTIFACT_INTEGRITY: artifactIntegrity }),
265
+ ...(processMode === undefined ? {} : { BORG_SERVER_PROCESS_MODE: processMode }),
266
+ ...(runtimeDirectory === undefined ? {} : { BORG_SERVER_RUNTIME_DIR: runtimeDirectory }),
232
267
  };
233
268
  }
234
269
  export function resolveStorageLimits(environment) {
@@ -260,6 +295,9 @@ function storageOperatorErrorCode(name) {
260
295
  }
261
296
  const serverEnvironment = selectServerEnvironment(process.env);
262
297
  const dataDirectory = serverEnvironment.BORG_SERVER_DATA_DIR ?? join(homedir(), ".borg", "server");
298
+ const runtimeDirectory = serverEnvironment.BORG_SERVER_RUNTIME_DIR ?? join(homedir(), ".borg", "server-runtime");
299
+ const credentialFile = join(homedir(), ".borg", "credentials");
300
+ const nodeRuntimeOperator = createNodeRuntimeOperator(runtimeDirectory, dataDirectory);
263
301
  const startOnlyService = createNodeServerService({
264
302
  environment: { ...serverEnvironment, BORG_SERVER_DATA_DIR: dataDirectory },
265
303
  readFile,
@@ -268,8 +306,29 @@ const startOnlyService = createNodeServerService({
268
306
  const running = await startHttpsServer(options);
269
307
  return nodeServerTestHooks?.wrapRunningServer?.(running) ?? running;
270
308
  },
271
- onStarted: (origin) => {
272
- console.error(`Borg server listening on ${origin}`);
309
+ onStarted: (origin, identity) => {
310
+ if (process.stderr.isTTY !== true || serverEnvironment.BORG_SERVER_PROCESS_MODE === "managed") {
311
+ console.error(JSON.stringify({
312
+ status: "running",
313
+ artifact: `borgmcp-server@${identity.package_version}`,
314
+ artifact_integrity: identity.artifact_integrity,
315
+ build_identity: identity.source_sha,
316
+ endpoint: origin,
317
+ mode: serverEnvironment.BORG_SERVER_PROCESS_MODE ?? "foreground",
318
+ data_identity: "available",
319
+ }));
320
+ }
321
+ else {
322
+ console.error([
323
+ "Starting verified local server in the foreground.",
324
+ `Artifact: borgmcp-server@${identity.package_version} (${identity.artifact_integrity ?? "unavailable"})`,
325
+ `Build identity: ${identity.source_sha ?? "unavailable"}`,
326
+ `Endpoint: ${origin}`,
327
+ "Data and identity: preserved",
328
+ "Ctrl-C stops the foreground process.",
329
+ "Foreground mode does not manage persistence.",
330
+ ].join("\n"));
331
+ }
273
332
  nodeServerTestHooks?.onListening?.(origin);
274
333
  },
275
334
  onStartupPhase: (phase) => nodeServerTestHooks?.onStartupPhase?.(phase) ?? Promise.resolve(),
@@ -285,9 +344,127 @@ const startOnlyService = createNodeServerService({
285
344
  });
286
345
  export const nodeServerService = {
287
346
  start: startOnlyService.start,
288
- setup: (options) => setupNodeServerInstallation(dataDirectory, resolveSetupBindHost(serverEnvironment), options),
289
- ...createOfflineCredentialService(dataDirectory),
347
+ setup: async (options) => {
348
+ const bindHost = resolveSetupBindHost(serverEnvironment);
349
+ if ((await inspectRuntimeLock(dataDirectory)).running)
350
+ throw operatorErrors.RUNTIME_ACTIVE;
351
+ const artifact = await nodeRuntimeOperator.prepareLatest(30_000);
352
+ const result = await setupNodeServerInstallation(dataDirectory, bindHost, options, credentialFile);
353
+ if (!("existing" in result)) {
354
+ const { recoveryCredential: _recovery, initialInvitation: _invitation, ...publicResult } = result;
355
+ return {
356
+ ...publicResult,
357
+ artifact: {
358
+ version: artifact.version,
359
+ integrity: artifact.integrity,
360
+ sourceSha: artifact.sourceSha,
361
+ },
362
+ };
363
+ }
364
+ return {
365
+ ...result,
366
+ artifact: {
367
+ version: artifact.version,
368
+ integrity: artifact.integrity,
369
+ sourceSha: artifact.sourceSha,
370
+ },
371
+ };
372
+ },
373
+ status: () => inspectNodeRuntime(dataDirectory, runtimeDirectory),
374
+ update: () => nodeRuntimeOperator.updateLatest(30_000),
375
+ ...createOfflineCredentialService(dataDirectory, credentialFile),
290
376
  };
377
+ function createNodeRuntimeOperator(managedRuntimeDirectory, runtimeDataDirectory) {
378
+ const platform = process.platform === "darwin" ? "launchd" : "systemd";
379
+ const definition = createManagedServiceDefinition({
380
+ platform,
381
+ nodeExecutable: process.execPath,
382
+ runtimeRoot: managedRuntimeDirectory,
383
+ dataDirectory: runtimeDataDirectory,
384
+ definitionPath: platform === "launchd"
385
+ ? join(homedir(), "Library", "LaunchAgents", "ai.borgmcp.server.plist")
386
+ : join(homedir(), ".config", "systemd", "user", "ai.borgmcp.server.service"),
387
+ ...(platform === "launchd" ? { launchdDomain: `gui/${process.getuid?.() ?? 0}` } : {}),
388
+ });
389
+ const run = async (command, signal) => {
390
+ const [executable, ...args] = command;
391
+ await promisify(execFile)(executable, args, {
392
+ signal,
393
+ timeout: 20_000,
394
+ maxBuffer: 64 * 1024,
395
+ encoding: "utf8",
396
+ });
397
+ };
398
+ const lifecycle = createRuntimeLifecycle({
399
+ unpack: createUnixNpmArtifactUnpacker(),
400
+ restart: (signal) => run(definition.restart, signal),
401
+ stop: (signal) => run(definition.stop, signal),
402
+ probe: (signal) => waitForRuntimeIdentity(runtimeDataDirectory, signal),
403
+ });
404
+ return createRuntimeOperator({
405
+ runtimeRoot: managedRuntimeDirectory,
406
+ artifacts: createRegistryArtifactSource(),
407
+ lifecycle,
408
+ isRunning: async () => {
409
+ const status = await inspectRuntimeLock(runtimeDataDirectory);
410
+ if (status.running && status.mode !== "managed") {
411
+ throw new Error("Foreground runtime must be stopped before artifact activation.");
412
+ }
413
+ return status.running;
414
+ },
415
+ });
416
+ }
417
+ async function waitForRuntimeIdentity(runtimeDataDirectory, signal) {
418
+ while (!signal.aborted) {
419
+ try {
420
+ const status = await inspectRuntimeLock(runtimeDataDirectory);
421
+ if (status.running && status.identity !== null)
422
+ return status.identity;
423
+ }
424
+ catch (error) {
425
+ if (error !== operatorErrors.RUNTIME_LOCK_STALE)
426
+ throw error;
427
+ }
428
+ await new Promise((resolve) => {
429
+ const timer = setTimeout(resolve, 50);
430
+ signal.addEventListener("abort", () => {
431
+ clearTimeout(timer);
432
+ resolve();
433
+ }, { once: true });
434
+ });
435
+ }
436
+ throw new Error("Managed runtime identity probe was cancelled.");
437
+ }
438
+ export async function inspectNodeRuntime(runtimeDataDirectory, managedRuntimeDirectory) {
439
+ const [lock, activeArtifact, dataIdentity] = await Promise.all([
440
+ inspectRuntimeLock(runtimeDataDirectory),
441
+ inspectActiveRuntimeArtifact(managedRuntimeDirectory),
442
+ hasDataIdentity(runtimeDataDirectory),
443
+ ]);
444
+ const identity = lock.running ? lock.identity : null;
445
+ const artifact = identity?.artifact_integrity === null || identity === null
446
+ ? activeArtifact === null ? null : { version: activeArtifact.version, integrity: activeArtifact.integrity }
447
+ : { version: identity.package_version, integrity: identity.artifact_integrity };
448
+ return Object.freeze({
449
+ status: lock.running ? "running" : "stopped",
450
+ artifact,
451
+ buildIdentity: identity?.source_sha ?? null,
452
+ endpoint: lock.running ? lock.endpoint : null,
453
+ mode: lock.running ? lock.mode : "stopped",
454
+ dataIdentity,
455
+ });
456
+ }
457
+ async function hasDataIdentity(directory) {
458
+ try {
459
+ const metadata = await lstat(join(directory, "server.json"));
460
+ return metadata.isFile() && !metadata.isSymbolicLink() ? "available" : "unavailable";
461
+ }
462
+ catch (error) {
463
+ if (error.code === "ENOENT")
464
+ return "unavailable";
465
+ throw error;
466
+ }
467
+ }
291
468
  const managedInstallationFiles = Object.freeze([
292
469
  "borg.db",
293
470
  "borg.db-wal",
@@ -300,20 +477,34 @@ const managedInstallationFiles = Object.freeze([
300
477
  "server.crt",
301
478
  "server.json",
302
479
  ]);
303
- export async function setupNodeServerInstallation(setupDataDirectory, bindHost, options) {
480
+ export async function setupNodeServerInstallation(setupDataDirectory, bindHost, options, credentialRoot) {
304
481
  const directory = await preparePrivateDataDirectory(setupDataDirectory);
305
482
  const runtimeLock = await acquireRuntimeLock(directory);
306
483
  let invitationLock;
307
484
  try {
308
485
  invitationLock = await acquireInvitationMintLock(directory);
309
486
  const existing = await inspectManagedInstallation(directory);
310
- if (existing.length !== 0 && !options.reinitialize)
311
- throw operatorErrors.INSTALLATION_EXISTS;
487
+ if (existing.length !== 0 && !options.reinitialize) {
488
+ const names = new Set(existing.map((path) => basename(path)));
489
+ const complete = [
490
+ "borg.db",
491
+ "credential-digest.key",
492
+ "ca.crt",
493
+ "server.key",
494
+ "server.crt",
495
+ "server.json",
496
+ ].every((name) => names.has(name));
497
+ if (!complete)
498
+ throw operatorErrors.INSTALLATION_EXISTS;
499
+ return Object.freeze({ existing: true });
500
+ }
312
501
  if (options.reinitialize) {
313
502
  for (const path of existing)
314
503
  await unlink(path);
315
504
  }
316
- return await bootstrapServer(directory, bindHost);
505
+ return await bootstrapServer(directory, bindHost, () => new Date(), credentialRoot === undefined
506
+ ? async () => undefined
507
+ : (record) => writePortableServerCredential(credentialRoot, record));
317
508
  }
318
509
  finally {
319
510
  if (invitationLock === undefined)
@@ -349,7 +540,7 @@ function resolveSetupBindHost(environment) {
349
540
  lanConsent: true,
350
541
  }).host;
351
542
  }
352
- export function createOfflineCredentialService(offlineDataDirectory) {
543
+ export function createOfflineCredentialService(offlineDataDirectory, credentialRoot) {
353
544
  const withAuthority = async (operation) => {
354
545
  const runtimeLock = await acquireRuntimeLock(offlineDataDirectory);
355
546
  let invitationLock;
@@ -388,7 +579,7 @@ export function createOfflineCredentialService(offlineDataDirectory) {
388
579
  const digestKey = await loadDigestKey(join(offlineDataDirectory, "credential-digest.key"));
389
580
  digester = new CredentialDigester(digestKey);
390
581
  digestKey.fill(0);
391
- return operation(new CredentialAuthority(runtime.credentials, digester));
582
+ return await operation(new CredentialAuthority(runtime.credentials, digester));
392
583
  }
393
584
  catch (error) {
394
585
  if (error instanceof MigrationCompatibilityError)
@@ -447,6 +638,21 @@ export function createOfflineCredentialService(offlineDataDirectory) {
447
638
  throw operatorErrors.RECOVERY_INVALID;
448
639
  return invitation;
449
640
  }),
641
+ invite: () => withInvitationAuthority(async (authority) => {
642
+ if (credentialRoot === undefined)
643
+ throw new Error("Local owner credential store is unavailable.");
644
+ const config = JSON.parse((await readFile(join(offlineDataDirectory, "server.json"))).toString("utf8"));
645
+ if (typeof config.bind_host !== "string" || typeof config.ca_spki_sha256 !== "string") {
646
+ throw new Error("Server identity is invalid.");
647
+ }
648
+ const origin = `https://${config.bind_host === "::1" ? "[::1]" : config.bind_host}:${DEFAULT_PORT}`;
649
+ const trustIdentity = `spki-sha256:${config.ca_spki_sha256}`;
650
+ const record = await readPortableServerCredential(credentialRoot, origin, trustIdentity);
651
+ const invitation = authority.createInvitationForOwnerCredential(record.credential, 15 * 60_000);
652
+ if (invitation === null)
653
+ throw new Error("Local owner credential is invalid.");
654
+ return invitation;
655
+ }),
450
656
  };
451
657
  }
452
658
  const canonicalCubeUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
@@ -515,13 +721,20 @@ export function isFatalTeardownError(error) {
515
721
  function fatalTeardownError(primary, cleanup) {
516
722
  return new FatalTeardownError(fatalTeardownCapability, primary, cleanup);
517
723
  }
518
- export async function acquireRuntimeLock(runtimeDataDirectory, purpose = "exclusive-admin") {
724
+ export async function acquireRuntimeLock(runtimeDataDirectory, purpose = "exclusive-admin", identity, mode = "foreground") {
519
725
  const path = join(runtimeDataDirectory, "runtime.lock");
520
726
  const nonce = randomUUID();
521
727
  try {
522
728
  const handle = await open(path, "wx", 0o600);
729
+ const record = {
730
+ pid: process.pid,
731
+ nonce,
732
+ purpose,
733
+ mode,
734
+ ...(identity === undefined ? {} : { runtime_identity: identity }),
735
+ };
523
736
  try {
524
- await handle.writeFile(JSON.stringify({ pid: process.pid, nonce, purpose }));
737
+ await handle.writeFile(JSON.stringify(record));
525
738
  }
526
739
  catch (error) {
527
740
  await handle.close();
@@ -529,6 +742,15 @@ export async function acquireRuntimeLock(runtimeDataDirectory, purpose = "exclus
529
742
  throw error;
530
743
  }
531
744
  return {
745
+ updateOrigin: async (origin) => {
746
+ if (!/^https:\/\/(?:\[[0-9a-f:]+\]|[0-9.]+):[0-9]{1,5}$/u.test(origin)) {
747
+ throw new Error("Runtime endpoint is invalid.");
748
+ }
749
+ record.endpoint = origin;
750
+ await handle.truncate(0);
751
+ await handle.write(JSON.stringify(record), 0, "utf8");
752
+ await handle.sync();
753
+ },
532
754
  release: async () => {
533
755
  await handle.close();
534
756
  try {
@@ -565,6 +787,95 @@ export async function acquireRuntimeLock(runtimeDataDirectory, purpose = "exclus
565
787
  throw operatorErrors.RUNTIME_LOCK_STALE;
566
788
  }
567
789
  }
790
+ export async function inspectRuntimeLock(runtimeDataDirectory) {
791
+ const path = join(runtimeDataDirectory, "runtime.lock");
792
+ let metadata;
793
+ try {
794
+ metadata = await lstat(path);
795
+ }
796
+ catch (error) {
797
+ if (error.code === "ENOENT")
798
+ return Object.freeze({ running: false });
799
+ throw error;
800
+ }
801
+ if (!metadata.isFile() || metadata.isSymbolicLink() || (metadata.mode & 0o077) !== 0 ||
802
+ metadata.size > 8 * 1024) {
803
+ throw operatorErrors.RUNTIME_LOCK_UNSAFE;
804
+ }
805
+ try {
806
+ const value = JSON.parse(await readFile(path, "utf8"));
807
+ if (!Number.isSafeInteger(value.pid) || value.pid <= 0 || value.purpose !== "server" ||
808
+ (value.endpoint !== undefined &&
809
+ (typeof value.endpoint !== "string" || !isRuntimeEndpoint(value.endpoint))) ||
810
+ (value.mode !== "foreground" && value.mode !== "managed")) {
811
+ throw new Error();
812
+ }
813
+ const pid = value.pid;
814
+ if (!processIsAlive(pid))
815
+ throw operatorErrors.RUNTIME_LOCK_STALE;
816
+ return Object.freeze({
817
+ running: true,
818
+ pid,
819
+ identity: decodeRuntimeLockIdentity(value.runtime_identity),
820
+ endpoint: value.endpoint ?? null,
821
+ mode: value.mode,
822
+ });
823
+ }
824
+ catch (error) {
825
+ if (error === operatorErrors.RUNTIME_LOCK_STALE)
826
+ throw error;
827
+ throw operatorErrors.RUNTIME_LOCK_INVALID;
828
+ }
829
+ }
830
+ function isRuntimeEndpoint(value) {
831
+ if (value.length > 512)
832
+ return false;
833
+ try {
834
+ const endpoint = new URL(value);
835
+ if (endpoint.protocol !== "https:" || endpoint.username !== "" || endpoint.password !== "" ||
836
+ endpoint.pathname !== "/" || endpoint.search !== "" || endpoint.hash !== "")
837
+ return false;
838
+ const host = endpoint.hostname.startsWith("[") && endpoint.hostname.endsWith("]")
839
+ ? endpoint.hostname.slice(1, -1)
840
+ : endpoint.hostname;
841
+ resolveBindOptions({ host, port: Number(endpoint.port || "443"), lanConsent: true });
842
+ return true;
843
+ }
844
+ catch {
845
+ return false;
846
+ }
847
+ }
848
+ function decodeRuntimeLockIdentity(value) {
849
+ if (value === undefined)
850
+ return null;
851
+ if (typeof value !== "object" || value === null || Array.isArray(value))
852
+ throw new Error();
853
+ const identity = value;
854
+ const packageVersion = identity["package_version"];
855
+ const sourceSha = identity["source_sha"];
856
+ const artifactIntegrity = identity["artifact_integrity"];
857
+ const protocolVersion = identity["protocol_version"];
858
+ const startedAt = identity["started_at"];
859
+ if (typeof packageVersion !== "string" ||
860
+ !/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/u.test(packageVersion) ||
861
+ (sourceSha !== null &&
862
+ (typeof sourceSha !== "string" || !/^[0-9a-f]{40}$/u.test(sourceSha))) ||
863
+ (artifactIntegrity !== null &&
864
+ (typeof artifactIntegrity !== "string" ||
865
+ !/^sha512-[A-Za-z0-9+/]{86}==$/u.test(artifactIntegrity))) ||
866
+ typeof protocolVersion !== "string" || protocolVersion.length < 1 || protocolVersion.length > 32 ||
867
+ typeof startedAt !== "string" || startedAt.length > 64 ||
868
+ !Number.isFinite(Date.parse(startedAt)) || new Date(startedAt).toISOString() !== startedAt) {
869
+ throw new Error();
870
+ }
871
+ return Object.freeze({
872
+ package_version: packageVersion,
873
+ source_sha: sourceSha,
874
+ artifact_integrity: artifactIntegrity,
875
+ protocol_version: protocolVersion,
876
+ started_at: startedAt,
877
+ });
878
+ }
568
879
  export async function acquireInvitationMintLock(runtimeDataDirectory) {
569
880
  const path = join(runtimeDataDirectory, "invitation-mint.lock");
570
881
  const nonce = randomUUID();