borgmcp-server 0.1.1 → 0.1.4

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 (48) hide show
  1. package/README.md +69 -22
  2. package/dist/cli.js +62 -11
  3. package/dist/cli.js.map +1 -1
  4. package/dist/coordination-api.d.ts +2 -1
  5. package/dist/coordination-api.js +232 -18
  6. package/dist/coordination-api.js.map +1 -1
  7. package/dist/credentials.d.ts +10 -2
  8. package/dist/credentials.js +44 -6
  9. package/dist/credentials.js.map +1 -1
  10. package/dist/debug-log.d.ts +77 -0
  11. package/dist/debug-log.js +125 -0
  12. package/dist/debug-log.js.map +1 -0
  13. package/dist/https-server.d.ts +3 -0
  14. package/dist/https-server.js +80 -4
  15. package/dist/https-server.js.map +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/migrations.d.ts +4 -0
  18. package/dist/migrations.js +71 -0
  19. package/dist/migrations.js.map +1 -1
  20. package/dist/operator-error.d.ts +2 -1
  21. package/dist/operator-error.js +23 -5
  22. package/dist/operator-error.js.map +1 -1
  23. package/dist/role-section.d.ts +14 -0
  24. package/dist/role-section.js +87 -0
  25. package/dist/role-section.js.map +1 -0
  26. package/dist/service.d.ts +10 -3
  27. package/dist/service.js +218 -15
  28. package/dist/service.js.map +1 -1
  29. package/dist/start-options.d.ts +5 -1
  30. package/dist/start-options.js +17 -3
  31. package/dist/start-options.js.map +1 -1
  32. package/dist/store.d.ts +65 -1
  33. package/dist/store.js +398 -26
  34. package/dist/store.js.map +1 -1
  35. package/npm-shrinkwrap.json +2 -2
  36. package/package.json +1 -1
  37. package/src/cli.ts +61 -11
  38. package/src/coordination-api.ts +236 -14
  39. package/src/credentials.ts +71 -5
  40. package/src/debug-log.ts +165 -0
  41. package/src/https-server.ts +75 -2
  42. package/src/index.ts +1 -1
  43. package/src/migrations.ts +74 -0
  44. package/src/operator-error.ts +40 -6
  45. package/src/role-section.ts +108 -0
  46. package/src/service.ts +239 -18
  47. package/src/start-options.ts +21 -4
  48. package/src/store.ts +462 -28
package/src/service.ts CHANGED
@@ -9,8 +9,14 @@ import {
9
9
  loadTlsPrivateKey,
10
10
  type BootstrapResult,
11
11
  } from "./bootstrap.js";
12
- import { CredentialAuthority, CredentialDigester } from "./credentials.js";
12
+ import {
13
+ CredentialAuthority,
14
+ CredentialDigester,
15
+ type CubeInvitationResult,
16
+ } from "./credentials.js";
13
17
  import { CoordinationApi } from "./coordination-api.js";
18
+ import { createDebugLogger, disabledDebugLogger } from "./debug-log.js";
19
+ import { MigrationCompatibilityError } from "./migrations.js";
14
20
  import { createEnrollmentExchange } from "./enrollment.js";
15
21
  import {
16
22
  DEFAULT_SERVICE_LIMITS,
@@ -20,22 +26,41 @@ import {
20
26
  } from "./https-server.js";
21
27
  import { createPart2ProtocolInfo } from "./protocol-draft.js";
22
28
  import { resolveBindOptions } from "./network-policy.js";
23
- import { operatorErrors, type OperatorErrorCode } from "./operator-error.js";
29
+ import {
30
+ invitationCubeAmbiguousError,
31
+ operatorErrors,
32
+ type OperatorErrorCode,
33
+ } from "./operator-error.js";
24
34
  import { parseStartOptions } from "./start-options.js";
25
- import { DEFAULT_STORAGE_LIMITS, openStore, type StorageLimits } from "./store.js";
35
+ import {
36
+ DEFAULT_STORAGE_LIMITS,
37
+ openStore,
38
+ preparePrivateDataDirectory,
39
+ InvitationCubeAmbiguousError,
40
+ InvitationCubeNotFoundError,
41
+ type StorageLimits,
42
+ } from "./store.js";
26
43
  import type { CubeAccess } from "./store.js";
27
44
 
28
45
  export interface ServerService {
29
46
  readonly start: (args: readonly string[]) => Promise<void>;
30
- readonly setup?: () => Promise<BootstrapResult>;
47
+ readonly setup?: (options: SetupOptions) => Promise<BootstrapResult>;
31
48
  readonly rotateClient?: (clientId: string) => Promise<string>;
32
49
  readonly revokeClient?: (clientId: string) => Promise<void>;
33
50
  readonly grantClient?: (clientId: string, cubeId: string, access: CubeAccess) => Promise<void>;
34
51
  readonly ungrantClient?: (clientId: string, cubeId: string) => Promise<void>;
35
- readonly createClientInvitation?: (recoveryCredential: string) => Promise<string>;
52
+ readonly createClientInvitation?: (
53
+ recoveryCredential: string,
54
+ cubeSelector?: string,
55
+ access?: CubeAccess,
56
+ ) => Promise<string | CubeInvitationResult>;
36
57
  readonly replaceOwnerInvitation?: (recoveryCredential: string) => Promise<string>;
37
58
  }
38
59
 
60
+ export interface SetupOptions {
61
+ readonly reinitialize: boolean;
62
+ }
63
+
39
64
  export interface ServerEnvironment {
40
65
  readonly BORG_SERVER_TLS_KEY_FILE?: string;
41
66
  readonly BORG_SERVER_TLS_CERT_FILE?: string;
@@ -54,6 +79,7 @@ interface ServiceDependencies {
54
79
  readonly startServer: (options: HttpsServerOptions) => Promise<RunningServer>;
55
80
  readonly onStarted: (origin: string) => void;
56
81
  readonly waitForShutdown: (server: RunningServer, signal?: AbortSignal) => Promise<void>;
82
+ readonly debugOutput?: (line: string) => void;
57
83
  readonly installShutdownHandlers?: () => { readonly signal: AbortSignal; readonly dispose: () => void };
58
84
  readonly openStore?: typeof openStore;
59
85
  readonly onStartupPhase?: (
@@ -91,17 +117,27 @@ export function createNodeServerService(dependencies: ServiceDependencies): Serv
91
117
  return {
92
118
  async start(args): Promise<void> {
93
119
  const shutdown = dependencies.installShutdownHandlers?.();
94
- let bind: ReturnType<typeof parseStartOptions>;
120
+ let bind: ReturnType<typeof parseStartOptions>["bind"];
121
+ let debugLogger = disabledDebugLogger;
95
122
  let dataDirectory: string | undefined;
96
123
  let storageLimits: StorageLimits;
97
124
  try {
98
125
  throwIfShutdown(shutdown?.signal);
99
126
  await dependencies.onStartupPhase?.("pre-lock");
100
127
  throwIfShutdown(shutdown?.signal);
101
- bind = parseStartOptions(args);
102
- const bindMode = resolveBindOptions(bind).mode;
128
+ const parsed = parseStartOptions(args);
129
+ bind = parsed.bind;
130
+ debugLogger = createDebugLogger(parsed.logLevel === "debug" ? dependencies.debugOutput : undefined);
131
+ const resolvedBind = resolveBindOptions(bind);
132
+ const bindMode = resolvedBind.mode;
103
133
  dataDirectory = dependencies.environment.BORG_SERVER_DATA_DIR;
104
134
  storageLimits = resolveStorageLimits(dependencies.environment);
135
+ debugLogger.emit({
136
+ event: "startup",
137
+ bindMode,
138
+ port: resolvedBind.port,
139
+ dataDirectory: dataDirectory === undefined ? "tls_only" : "configured",
140
+ });
105
141
  if (bindMode === "lan" && dataDirectory !== undefined) {
106
142
  await assertLanCaKeyOffline(dataDirectory);
107
143
  throwIfShutdown(shutdown?.signal);
@@ -124,7 +160,7 @@ export function createNodeServerService(dependencies: ServiceDependencies): Serv
124
160
 
125
161
  let runtimeLock: Awaited<ReturnType<typeof acquireRuntimeLock>> | undefined;
126
162
  try {
127
- runtimeLock = dataDirectory === undefined ? undefined : await acquireRuntimeLock(dataDirectory);
163
+ runtimeLock = dataDirectory === undefined ? undefined : await acquireRuntimeLock(dataDirectory, "server");
128
164
  await dependencies.onStartupPhase?.("post-lock");
129
165
  throwIfShutdown(shutdown?.signal);
130
166
  } catch (error) {
@@ -169,8 +205,14 @@ export function createNodeServerService(dependencies: ServiceDependencies): Serv
169
205
  } finally {
170
206
  digestKey.fill(0);
171
207
  }
172
- authority = new CredentialAuthority(authRuntime.credentials, digester);
173
- coordinationApi = new CoordinationApi(authRuntime, authority);
208
+ authority = new CredentialAuthority(
209
+ authRuntime.credentials,
210
+ digester,
211
+ () => new Date(),
212
+ undefined,
213
+ debugLogger,
214
+ );
215
+ coordinationApi = new CoordinationApi(authRuntime, authority, debugLogger);
174
216
  }
175
217
  await dependencies.onStartupPhase?.("pre-listen");
176
218
  throwIfShutdown(shutdown?.signal);
@@ -197,6 +239,7 @@ export function createNodeServerService(dependencies: ServiceDependencies): Serv
197
239
  ...(coordinationApi === undefined
198
240
  ? {}
199
241
  : { handleCoordination: (request) => coordinationApi.handle(request) }),
242
+ debugLogger,
200
243
  });
201
244
  throwIfShutdown(shutdown?.signal);
202
245
  } catch (error) {
@@ -217,6 +260,7 @@ export function createNodeServerService(dependencies: ServiceDependencies): Serv
217
260
  try {
218
261
  throwIfShutdown(shutdown?.signal);
219
262
  dependencies.onStarted(running.origin);
263
+ debugLogger.emit({ event: "lifecycle", action: "listening" });
220
264
  await dependencies.waitForShutdown(running, shutdown?.signal);
221
265
  } catch (error) {
222
266
  failed = true;
@@ -224,6 +268,7 @@ export function createNodeServerService(dependencies: ServiceDependencies): Serv
224
268
  }
225
269
  try {
226
270
  await teardownRuntime({ running, authRuntime, digester, runtimeLock });
271
+ debugLogger.emit({ event: "lifecycle", action: "stopped" });
227
272
  } catch (cleanupError) {
228
273
  shutdown?.dispose();
229
274
  throw fatalTeardownError(failed ? failure : undefined, cleanupError);
@@ -329,13 +374,69 @@ const startOnlyService = createNodeServerService({
329
374
  return handlers;
330
375
  },
331
376
  waitForShutdown,
377
+ debugOutput: (line) => console.error(line),
332
378
  });
333
379
  export const nodeServerService: ServerService = {
334
380
  start: startOnlyService.start,
335
- setup: () => bootstrapServer(dataDirectory, resolveSetupBindHost(serverEnvironment)),
381
+ setup: (options) => setupNodeServerInstallation(
382
+ dataDirectory,
383
+ resolveSetupBindHost(serverEnvironment),
384
+ options,
385
+ ),
336
386
  ...createOfflineCredentialService(dataDirectory),
337
387
  };
338
388
 
389
+ const managedInstallationFiles = Object.freeze([
390
+ "borg.db",
391
+ "borg.db-wal",
392
+ "borg.db-shm",
393
+ "borg.db-journal",
394
+ "credential-digest.key",
395
+ "ca.key",
396
+ "ca.crt",
397
+ "server.key",
398
+ "server.crt",
399
+ "server.json",
400
+ ]);
401
+
402
+ export async function setupNodeServerInstallation(
403
+ setupDataDirectory: string,
404
+ bindHost: string,
405
+ options: SetupOptions,
406
+ ): Promise<BootstrapResult> {
407
+ const directory = await preparePrivateDataDirectory(setupDataDirectory);
408
+ const runtimeLock = await acquireRuntimeLock(directory);
409
+ let invitationLock: RuntimeLock | undefined;
410
+ try {
411
+ invitationLock = await acquireInvitationMintLock(directory);
412
+ const existing = await inspectManagedInstallation(directory);
413
+ if (existing.length !== 0 && !options.reinitialize) throw operatorErrors.INSTALLATION_EXISTS;
414
+ if (options.reinitialize) {
415
+ for (const path of existing) await unlink(path);
416
+ }
417
+ return await bootstrapServer(directory, bindHost);
418
+ } finally {
419
+ if (invitationLock === undefined) await runtimeLock.release();
420
+ else await invitationLock.release().finally(() => runtimeLock.release());
421
+ }
422
+ }
423
+
424
+ async function inspectManagedInstallation(directory: string): Promise<string[]> {
425
+ const existing: string[] = [];
426
+ for (const name of managedInstallationFiles) {
427
+ const path = join(directory, name);
428
+ try {
429
+ const metadata = await lstat(path);
430
+ if (metadata.isSymbolicLink()) throw operatorErrors.DATA_PATH_SYMLINK;
431
+ if (!metadata.isFile()) throw new Error("Managed installation paths must be regular files.");
432
+ existing.push(path);
433
+ } catch (error) {
434
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
435
+ }
436
+ }
437
+ return existing;
438
+ }
439
+
339
440
  function resolveSetupBindHost(environment: ServerEnvironment): string {
340
441
  return resolveBindOptions({
341
442
  ...(environment.BORG_SERVER_BIND_HOST === undefined
@@ -356,9 +457,11 @@ export function createOfflineCredentialService(
356
457
  runtime: Awaited<ReturnType<typeof openStore>>,
357
458
  ) => T): Promise<T> => {
358
459
  const runtimeLock = await acquireRuntimeLock(offlineDataDirectory);
460
+ let invitationLock: RuntimeLock | undefined;
359
461
  let runtime: Awaited<ReturnType<typeof openStore>> | undefined;
360
462
  let digester: CredentialDigester | undefined;
361
463
  try {
464
+ invitationLock = await acquireInvitationMintLock(offlineDataDirectory);
362
465
  runtime = await openStore({ path: join(offlineDataDirectory, "borg.db") });
363
466
  const digestKey = await loadDigestKey(join(offlineDataDirectory, "credential-digest.key"));
364
467
  digester = new CredentialDigester(digestKey);
@@ -367,7 +470,35 @@ export function createOfflineCredentialService(
367
470
  } finally {
368
471
  digester?.destroy();
369
472
  runtime?.close();
370
- await runtimeLock.release();
473
+ if (invitationLock === undefined) await runtimeLock.release();
474
+ else await invitationLock.release().finally(() => runtimeLock.release());
475
+ }
476
+ };
477
+ const withInvitationAuthority = async <T>(operation: (authority: CredentialAuthority) => T): Promise<T> => {
478
+ const invitationLock = await acquireInvitationMintLock(offlineDataDirectory);
479
+ let offlineRuntimeLock: RuntimeLock | undefined;
480
+ let runtime: Awaited<ReturnType<typeof openStore>> | undefined;
481
+ let digester: CredentialDigester | undefined;
482
+ try {
483
+ const runtimeState = await invitationRuntimeState(offlineDataDirectory);
484
+ if (runtimeState === "offline") offlineRuntimeLock = await acquireRuntimeLock(offlineDataDirectory);
485
+ runtime = await openStore({
486
+ path: join(offlineDataDirectory, "borg.db"),
487
+ migrationMode: runtimeState === "live" ? "require-current" : "apply",
488
+ });
489
+ const digestKey = await loadDigestKey(join(offlineDataDirectory, "credential-digest.key"));
490
+ digester = new CredentialDigester(digestKey);
491
+ digestKey.fill(0);
492
+ return operation(new CredentialAuthority(runtime.credentials, digester));
493
+ } catch (error) {
494
+ if (error instanceof MigrationCompatibilityError) throw operatorErrors.INVITATION_SCHEMA_MISMATCH;
495
+ if (isSqliteContention(error)) throw operatorErrors.INVITATION_CONTENTION;
496
+ throw error;
497
+ } finally {
498
+ digester?.destroy();
499
+ runtime?.close();
500
+ if (offlineRuntimeLock === undefined) await invitationLock.release();
501
+ else await offlineRuntimeLock.release().finally(() => invitationLock.release());
371
502
  }
372
503
  };
373
504
  return {
@@ -385,12 +516,24 @@ export function createOfflineCredentialService(
385
516
  throw operatorErrors.GRANT_NOT_FOUND;
386
517
  }
387
518
  }),
388
- createClientInvitation: (recoveryCredential) => withAuthority((authority) => {
389
- const invitation = authority.createInvitation(recoveryCredential, 15 * 60_000);
519
+ createClientInvitation: (recoveryCredential, cubeSelector, access) => withInvitationAuthority((authority) => {
520
+ if (cubeSelector === undefined && access !== undefined) {
521
+ throw operatorErrors.INVITATION_CUBE_SELECTOR_INVALID;
522
+ }
523
+ const selector = cubeSelector === undefined ? undefined : parseInvitationCubeSelector(cubeSelector);
524
+ const invitation = selector === undefined
525
+ ? authority.createInvitation(recoveryCredential, 15 * 60_000)
526
+ : authority.createCubeInvitation(recoveryCredential, selector, access ?? "write", 15 * 60_000);
390
527
  if (invitation === null) throw operatorErrors.RECOVERY_INVALID;
391
528
  return invitation;
529
+ }).catch((error: unknown) => {
530
+ if (error instanceof InvitationCubeNotFoundError) throw operatorErrors.INVITATION_CUBE_NOT_FOUND;
531
+ if (error instanceof InvitationCubeAmbiguousError) {
532
+ throw invitationCubeAmbiguousError(error.candidateIds);
533
+ }
534
+ throw error;
392
535
  }),
393
- replaceOwnerInvitation: (recoveryCredential) => withAuthority((authority) => {
536
+ replaceOwnerInvitation: (recoveryCredential) => withInvitationAuthority((authority) => {
394
537
  const invitation = authority.replaceOwnerInvitation(recoveryCredential, 15 * 60_000);
395
538
  if (invitation === null) throw operatorErrors.RECOVERY_INVALID;
396
539
  return invitation;
@@ -398,6 +541,19 @@ export function createOfflineCredentialService(
398
541
  };
399
542
  }
400
543
 
544
+ 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;
545
+ const uuidLikeCubeSelector = /^[0-9a-fA-F-]{32,36}$/u;
546
+
547
+ function parseInvitationCubeSelector(
548
+ value: string,
549
+ ): { readonly kind: "id" | "name"; readonly value: string } {
550
+ if (canonicalCubeUuid.test(value)) return { kind: "id", value };
551
+ if (value.length === 0 || value.length > 120 || uuidLikeCubeSelector.test(value)) {
552
+ throw operatorErrors.INVITATION_CUBE_SELECTOR_INVALID;
553
+ }
554
+ return { kind: "name", value };
555
+ }
556
+
401
557
  interface RuntimeLock {
402
558
  readonly release: () => Promise<void>;
403
559
  }
@@ -445,13 +601,16 @@ function fatalTeardownError(primary: unknown, cleanup: unknown): FatalTeardownEr
445
601
  return new FatalTeardownError(fatalTeardownCapability, primary, cleanup);
446
602
  }
447
603
 
448
- export async function acquireRuntimeLock(runtimeDataDirectory: string): Promise<RuntimeLock> {
604
+ export async function acquireRuntimeLock(
605
+ runtimeDataDirectory: string,
606
+ purpose: "server" | "exclusive-admin" = "exclusive-admin",
607
+ ): Promise<RuntimeLock> {
449
608
  const path = join(runtimeDataDirectory, "runtime.lock");
450
609
  const nonce = randomUUID();
451
610
  try {
452
611
  const handle = await open(path, "wx", 0o600);
453
612
  try {
454
- await handle.writeFile(JSON.stringify({ pid: process.pid, nonce }));
613
+ await handle.writeFile(JSON.stringify({ pid: process.pid, nonce, purpose }));
455
614
  } catch (error) {
456
615
  await handle.close();
457
616
  await unlink(path).catch(() => undefined);
@@ -487,6 +646,68 @@ export async function acquireRuntimeLock(runtimeDataDirectory: string): Promise<
487
646
  }
488
647
  }
489
648
 
649
+ export async function acquireInvitationMintLock(runtimeDataDirectory: string): Promise<RuntimeLock> {
650
+ const path = join(runtimeDataDirectory, "invitation-mint.lock");
651
+ const nonce = randomUUID();
652
+ try {
653
+ const handle = await open(path, "wx", 0o600);
654
+ try {
655
+ await handle.writeFile(JSON.stringify({ pid: process.pid, nonce }));
656
+ } catch (error) {
657
+ await handle.close();
658
+ await unlink(path).catch(() => undefined);
659
+ throw error;
660
+ }
661
+ return {
662
+ release: async () => {
663
+ await handle.close();
664
+ try {
665
+ const current = JSON.parse(await readFile(path, "utf8")) as { nonce?: unknown };
666
+ if (current.nonce === nonce) await unlink(path);
667
+ } catch (error) {
668
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
669
+ }
670
+ },
671
+ };
672
+ } catch (error) {
673
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
674
+ throw operatorErrors.INVITATION_BUSY;
675
+ }
676
+ }
677
+
678
+ async function invitationRuntimeState(runtimeDataDirectory: string): Promise<"live" | "offline"> {
679
+ const path = join(runtimeDataDirectory, "runtime.lock");
680
+ let metadata;
681
+ try {
682
+ metadata = await lstat(path);
683
+ } catch (error) {
684
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return "offline";
685
+ throw error;
686
+ }
687
+ if (!metadata.isFile() || metadata.isSymbolicLink() || (metadata.mode & 0o077) !== 0) {
688
+ throw operatorErrors.RUNTIME_LOCK_UNSAFE;
689
+ }
690
+ let pid: number;
691
+ let purpose: unknown;
692
+ try {
693
+ const value = JSON.parse(await readFile(path, "utf8")) as { pid?: unknown; purpose?: unknown };
694
+ if (!Number.isSafeInteger(value.pid) || (value.pid as number) <= 0) throw new Error();
695
+ pid = value.pid as number;
696
+ purpose = value.purpose;
697
+ } catch {
698
+ throw operatorErrors.RUNTIME_LOCK_INVALID;
699
+ }
700
+ if (!processIsAlive(pid)) throw operatorErrors.RUNTIME_LOCK_STALE;
701
+ if (purpose !== "server") throw operatorErrors.RUNTIME_ACTIVE;
702
+ return "live";
703
+ }
704
+
705
+ function isSqliteContention(error: unknown): boolean {
706
+ if (typeof error !== "object" || error === null) return false;
707
+ const value = error as { code?: unknown; errcode?: unknown };
708
+ return value.code === "ERR_SQLITE_ERROR" && (value.errcode === 5 || value.errcode === 6);
709
+ }
710
+
490
711
  function processIsAlive(pid: number): boolean {
491
712
  try {
492
713
  process.kill(pid, 0);
@@ -1,10 +1,16 @@
1
1
  import type { BindOptionsInput } from "./network-policy.js";
2
2
  import { operatorErrors, type OperatorErrorCode } from "./operator-error.js";
3
3
 
4
- export function parseStartOptions(args: readonly string[]): BindOptionsInput {
4
+ export interface ParsedStartOptions {
5
+ readonly bind: BindOptionsInput;
6
+ readonly logLevel?: "debug";
7
+ }
8
+
9
+ export function parseStartOptions(args: readonly string[]): ParsedStartOptions {
5
10
  let host: string | undefined;
6
11
  let port: number | undefined;
7
12
  let lanConsent = false;
13
+ let logLevel: "debug" | undefined;
8
14
 
9
15
  for (let index = 0; index < args.length; index += 1) {
10
16
  const argument = args[index];
@@ -27,13 +33,24 @@ export function parseStartOptions(args: readonly string[]): BindOptionsInput {
27
33
  index += 1;
28
34
  continue;
29
35
  }
36
+ if (argument === "--log-level") {
37
+ if (logLevel !== undefined) throw operatorErrors.START_LOG_LEVEL_DUPLICATE;
38
+ const value = requiredValue(args, index, "START_LOG_LEVEL_MISSING");
39
+ if (value !== "debug") throw operatorErrors.START_LOG_LEVEL_INVALID;
40
+ logLevel = value;
41
+ index += 1;
42
+ continue;
43
+ }
30
44
  throw operatorErrors.START_OPTION_UNKNOWN;
31
45
  }
32
46
 
33
47
  return {
34
- ...(host === undefined ? {} : { host }),
35
- ...(port === undefined ? {} : { port }),
36
- ...(lanConsent ? { lanConsent: true } : {}),
48
+ bind: {
49
+ ...(host === undefined ? {} : { host }),
50
+ ...(port === undefined ? {} : { port }),
51
+ ...(lanConsent ? { lanConsent: true } : {}),
52
+ },
53
+ ...(logLevel === undefined ? {} : { logLevel }),
37
54
  };
38
55
  }
39
56