@serverkgg/bridge 0.0.8 → 0.0.10

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 (72) hide show
  1. package/package.json +3 -3
  2. package/src/cli/validate.ts +2 -0
  3. package/src/context/bridgeContext.ts +17 -0
  4. package/src/context/index.ts +1 -0
  5. package/src/core/bridgeDownloadError.ts +10 -0
  6. package/src/core/bridgeHold.ts +12 -0
  7. package/src/core/bridgeKind.ts +3 -0
  8. package/src/core/bridgeProbeError.ts +16 -0
  9. package/src/core/bridgeSetup.ts +20 -0
  10. package/src/core/bridgeUserError.test.ts +23 -0
  11. package/src/core/bridgeUserError.ts +12 -0
  12. package/src/core/bridgeValue.test.ts +29 -0
  13. package/src/core/bridgeValue.ts +12 -0
  14. package/src/core/index.ts +5 -0
  15. package/src/guides/blocks.ts +8 -0
  16. package/src/guides/markdown.test.ts +18 -0
  17. package/src/guides/markdown.ts +7 -1
  18. package/src/manifest/index.ts +1 -0
  19. package/src/manifest/manifest.test.ts +132 -0
  20. package/src/manifest/manifest.ts +21 -0
  21. package/src/manifest/presence.ts +56 -0
  22. package/src/manifest/primitives.ts +6 -0
  23. package/src/modules/bridge.ts +65 -2
  24. package/src/modules/bridgeDriver.ts +4 -0
  25. package/src/modules/bridgeModule.ts +32 -3
  26. package/src/panel/bridgeDetail.ts +49 -0
  27. package/src/panel/bridgeLayout.ts +1 -0
  28. package/src/panel/bridgePanel.ts +30 -0
  29. package/src/panel/bridgeUploadMode.ts +4 -0
  30. package/src/panel/index.ts +2 -0
  31. package/src/probes/a2s.test.ts +71 -0
  32. package/src/probes/a2s.ts +38 -11
  33. package/src/protocol/bridgeEgress.ts +8 -1
  34. package/src/protocol/bridgeEvents.ts +4 -0
  35. package/src/protocol/bridgeFrame.ts +33 -1
  36. package/src/protocol/bridgeLimits.ts +12 -0
  37. package/src/protocol/bridgeManifest.ts +2 -0
  38. package/src/protocol/bridgeSetup.ts +97 -0
  39. package/src/protocol/index.ts +1 -0
  40. package/src/runtime/bridgeActions.ts +7 -0
  41. package/src/runtime/bridgeConfigReader.ts +12 -8
  42. package/src/runtime/bridgeContext.ts +28 -2
  43. package/src/runtime/bridgeDriver.test.ts +748 -3
  44. package/src/runtime/bridgeDriver.ts +198 -4
  45. package/src/runtime/bridgeEgress.ts +3 -0
  46. package/src/runtime/bridgeEvents.test.ts +96 -0
  47. package/src/runtime/bridgeEvents.ts +11 -6
  48. package/src/runtime/bridgeFiles.test.ts +625 -15
  49. package/src/runtime/bridgeFiles.ts +152 -45
  50. package/src/runtime/bridgeInstall.test.ts +118 -0
  51. package/src/runtime/bridgeInstall.ts +55 -0
  52. package/src/runtime/bridgeNet.ts +32 -3
  53. package/src/runtime/bridgePending.test.ts +148 -0
  54. package/src/runtime/bridgePending.ts +43 -0
  55. package/src/runtime/bridgeRegistry.test.ts +172 -1
  56. package/src/runtime/bridgeRegistry.ts +40 -5
  57. package/src/runtime/bridgeSecrets.test.ts +121 -0
  58. package/src/runtime/bridgeSecrets.ts +43 -0
  59. package/src/runtime/bridgeServer.test.ts +598 -0
  60. package/src/runtime/bridgeServer.ts +42 -3
  61. package/src/runtime/bridgeSetup.test.ts +189 -0
  62. package/src/runtime/bridgeSetup.ts +63 -0
  63. package/src/runtime/bridgeSignal.test.ts +114 -0
  64. package/src/runtime/bridgeSignal.ts +31 -0
  65. package/src/runtime/bridgeSupervisor.test.ts +111 -0
  66. package/src/runtime/bridgeSupervisor.ts +55 -5
  67. package/src/runtime/bridgeZip.ts +179 -58
  68. package/src/runtime/serve.ts +115 -4
  69. package/src/utils/index.ts +1 -0
  70. package/src/utils/stripAnsi.ts +12 -0
  71. package/src/wire/panel.test.ts +259 -0
  72. package/src/wire/panel.ts +72 -0
@@ -0,0 +1,148 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { BRIDGE_PENDING_FILES_LIMIT, type BridgePendingFile } from "../protocol";
3
+ import type { BridgeLeveledLogger } from "./bridgeLog";
4
+ import { pendingResult, rejectedPending, unsupportedPending } from "./bridgePending";
5
+
6
+ const file = (id: string): BridgePendingFile => ({
7
+ id,
8
+ title: {
9
+ ar: id,
10
+ en: id,
11
+ },
12
+ fileName: `${id}.jar`,
13
+ sizeBytes: null,
14
+ digest: null,
15
+ sourceUrl: null,
16
+ note: null,
17
+ });
18
+
19
+ const files = (count: number) => {
20
+ return Array.from({
21
+ length: count,
22
+ }).map((_entry, index) => file(`file-${index}`));
23
+ };
24
+
25
+ const recorder = () => {
26
+ const warnings: string[] = [];
27
+
28
+ const onLog: BridgeLeveledLogger = Object.assign(
29
+ () => {
30
+ return;
31
+ },
32
+ {
33
+ warn: (message: string) => {
34
+ warnings.push(message);
35
+ },
36
+ error: () => {
37
+ return;
38
+ },
39
+ },
40
+ );
41
+
42
+ return {
43
+ onLog,
44
+ warnings,
45
+ };
46
+ };
47
+
48
+ describe("unsupportedPending", () => {
49
+ test("answers the sentinel a driver without a pending module gets", () => {
50
+ expect(unsupportedPending()).toEqual({
51
+ supported: false,
52
+ staging: "",
53
+ extensions: [],
54
+ files: [],
55
+ rejected: null,
56
+ });
57
+ });
58
+ });
59
+
60
+ describe("pendingResult", () => {
61
+ test("carries the staging path, the extensions and the files the driver listed", () => {
62
+ const { onLog, warnings } = recorder();
63
+
64
+ expect(
65
+ pendingResult(
66
+ ".serverk-staging/pending-upload",
67
+ [
68
+ "jar",
69
+ ],
70
+ files(2),
71
+ onLog,
72
+ ),
73
+ ).toEqual({
74
+ supported: true,
75
+ staging: ".serverk-staging/pending-upload",
76
+ extensions: [
77
+ "jar",
78
+ ],
79
+ files: files(2),
80
+ rejected: null,
81
+ });
82
+
83
+ expect(warnings).toEqual([]);
84
+ });
85
+
86
+ test("truncates a list longer than the platform takes and warns instead of throwing", () => {
87
+ const { onLog, warnings } = recorder();
88
+
89
+ const result = pendingResult(
90
+ ".serverk-staging/pending-upload",
91
+ [
92
+ "jar",
93
+ ],
94
+ files(BRIDGE_PENDING_FILES_LIMIT + 5),
95
+ onLog,
96
+ );
97
+
98
+ expect(result.files).toHaveLength(BRIDGE_PENDING_FILES_LIMIT);
99
+ expect(result.files.at(0)?.id).toBe("file-0");
100
+ expect(warnings).toEqual([
101
+ "pending list truncated",
102
+ ]);
103
+ });
104
+ });
105
+
106
+ describe("rejectedPending", () => {
107
+ test("names every file still pending as what the upload was expected to be", () => {
108
+ const { onLog } = recorder();
109
+
110
+ const result = rejectedPending(
111
+ pendingResult(
112
+ ".serverk-staging/pending-upload",
113
+ [
114
+ "jar",
115
+ ],
116
+ files(2),
117
+ onLog,
118
+ ),
119
+ );
120
+
121
+ expect(result.rejected).toEqual({
122
+ expected: [
123
+ "file-0.jar",
124
+ "file-1.jar",
125
+ ],
126
+ });
127
+ expect(result.files).toHaveLength(2);
128
+ });
129
+
130
+ test("leaves an empty expectation when the driver has nothing pending left", () => {
131
+ const { onLog } = recorder();
132
+
133
+ expect(
134
+ rejectedPending(
135
+ pendingResult(
136
+ ".serverk-staging/pending-upload",
137
+ [
138
+ "jar",
139
+ ],
140
+ [],
141
+ onLog,
142
+ ),
143
+ ).rejected,
144
+ ).toEqual({
145
+ expected: [],
146
+ });
147
+ });
148
+ });
@@ -0,0 +1,43 @@
1
+ import { BRIDGE_PENDING_FILES_LIMIT, type BridgePendingFile, type BridgePendingResult } from "../protocol";
2
+ import type { BridgeLeveledLogger } from "./bridgeLog";
3
+
4
+ export const unsupportedPending = (): BridgePendingResult => {
5
+ return {
6
+ supported: false,
7
+ staging: "",
8
+ extensions: [],
9
+ files: [],
10
+ rejected: null,
11
+ };
12
+ };
13
+
14
+ export const pendingResult = (
15
+ staging: string,
16
+ extensions: string[],
17
+ files: BridgePendingFile[],
18
+ onLog: BridgeLeveledLogger,
19
+ ): BridgePendingResult => {
20
+ if (files.length > BRIDGE_PENDING_FILES_LIMIT) {
21
+ onLog.warn("pending list truncated", {
22
+ files: files.length,
23
+ limit: BRIDGE_PENDING_FILES_LIMIT,
24
+ });
25
+ }
26
+
27
+ return {
28
+ supported: true,
29
+ staging,
30
+ extensions,
31
+ files: files.slice(0, BRIDGE_PENDING_FILES_LIMIT),
32
+ rejected: null,
33
+ };
34
+ };
35
+
36
+ export const rejectedPending = (result: BridgePendingResult): BridgePendingResult => {
37
+ return {
38
+ ...result,
39
+ rejected: {
40
+ expected: result.files.map((file) => file.fileName),
41
+ },
42
+ };
43
+ };
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { tmpdir } from "node:os";
3
- import { BridgeAction, BridgeKind, type BridgeRow, type BridgeValues } from "../core";
3
+ import { BridgeAction, BridgeKind, type BridgeRow, BridgeSetupStepKind, type BridgeValues } from "../core";
4
4
  import type { BridgeDriver } from "../modules";
5
5
  import { type BridgeConfig, BridgeConfirm, BridgeEventName } from "../protocol";
6
6
  import { createContext } from "./bridgeContext";
@@ -62,6 +62,9 @@ const registryFor = (driver: BridgeDriver, variables: Record<string, string> = {
62
62
  onEmit(event, payload) {
63
63
  ring.emit(event, payload);
64
64
  },
65
+ onSetup() {
66
+ return;
67
+ },
65
68
  });
66
69
 
67
70
  return createRegistry(driver, context, supervisor, ring);
@@ -426,3 +429,171 @@ describe("preview modules", () => {
426
429
  );
427
430
  });
428
431
  });
432
+
433
+ describe("detail modules", () => {
434
+ test("lists read and its own action ids in the manifest", () => {
435
+ const registry = registryFor({
436
+ ...baseDriver(),
437
+ modules: {
438
+ modpackStatus: {
439
+ kind: BridgeKind.Detail,
440
+ read: async () => null,
441
+ actions: {
442
+ update: async () => null,
443
+ },
444
+ },
445
+ },
446
+ });
447
+
448
+ expect(registry.manifest().modules).toEqual([
449
+ {
450
+ id: "modpackStatus",
451
+ kind: BridgeKind.Detail,
452
+ requiresRunning: false,
453
+ refreshSeconds: null,
454
+ pageSize: null,
455
+ dependsOn: [],
456
+ actions: [
457
+ BridgeAction.Read,
458
+ "update",
459
+ ],
460
+ },
461
+ ]);
462
+ });
463
+
464
+ test("returns what read answers", async () => {
465
+ const registry = registryFor({
466
+ ...baseDriver(),
467
+ modules: {
468
+ modpackStatus: {
469
+ kind: BridgeKind.Detail,
470
+ read: async () => ({
471
+ id: "modrinth:abc",
472
+ title: "Better MC",
473
+ subtitle: "1.21.1 · NeoForge",
474
+ description: null,
475
+ image: null,
476
+ badges: [],
477
+ stats: [],
478
+ links: [],
479
+ stale: false,
480
+ actions: [],
481
+ }),
482
+ },
483
+ },
484
+ });
485
+
486
+ expect(await registry.invoke("modpackStatus", BridgeAction.Read)).toMatchObject({
487
+ id: "modrinth:abc",
488
+ title: "Better MC",
489
+ });
490
+ });
491
+
492
+ test("hands the argument bag to an action and returns its outcome", async () => {
493
+ const received: BridgeValues[] = [];
494
+
495
+ const registry = registryFor({
496
+ ...baseDriver(),
497
+ modules: {
498
+ modpackStatus: {
499
+ kind: BridgeKind.Detail,
500
+ read: async () => null,
501
+ actions: {
502
+ update: async (_context, args) => {
503
+ received.push(args);
504
+
505
+ return {
506
+ variables: {
507
+ MODPACK: "modrinth:abc:v2",
508
+ },
509
+ };
510
+ },
511
+ },
512
+ },
513
+ },
514
+ });
515
+
516
+ expect(
517
+ await registry.invoke("modpackStatus", "update", {
518
+ id: "modrinth:abc",
519
+ }),
520
+ ).toEqual({
521
+ variables: {
522
+ MODPACK: "modrinth:abc:v2",
523
+ },
524
+ });
525
+
526
+ expect(received).toEqual([
527
+ {
528
+ id: "modrinth:abc",
529
+ },
530
+ ]);
531
+ });
532
+
533
+ test("refuses an action named after a read", () => {
534
+ expect(() =>
535
+ registryFor({
536
+ ...baseDriver(),
537
+ modules: {
538
+ modpackStatus: {
539
+ kind: BridgeKind.Detail,
540
+ read: async () => null,
541
+ actions: {
542
+ read: async () => null,
543
+ },
544
+ },
545
+ },
546
+ }),
547
+ ).toThrow('module "modpackStatus" declares action "read", which is reserved for reads');
548
+ });
549
+
550
+ test("refuses an action the module does not answer", async () => {
551
+ const registry = registryFor({
552
+ ...baseDriver(),
553
+ modules: {
554
+ modpackStatus: {
555
+ kind: BridgeKind.Detail,
556
+ read: async () => null,
557
+ },
558
+ },
559
+ });
560
+
561
+ await expect(registry.invoke("modpackStatus", "update")).rejects.toThrow(
562
+ 'module "modpackStatus" does not support "update"',
563
+ );
564
+ });
565
+ });
566
+
567
+ describe("the setup singleton", () => {
568
+ test("rides the manifest as its own block and never joins the panel modules", () => {
569
+ const steps = [
570
+ {
571
+ kind: BridgeSetupStepKind.Driver as const,
572
+ id: "sign-in",
573
+ title: {
574
+ ar: "تسجيل الدخول",
575
+ en: "Sign in",
576
+ },
577
+ },
578
+ ];
579
+
580
+ const registry = registryFor({
581
+ ...baseDriver(),
582
+ setup: {
583
+ kind: BridgeKind.Setup,
584
+ steps,
585
+ },
586
+ });
587
+
588
+ const manifest = registry.manifest();
589
+
590
+ expect(manifest.setup).toEqual({
591
+ steps,
592
+ });
593
+ expect(manifest.modules.some((module) => module.kind === BridgeKind.Setup)).toBe(false);
594
+ });
595
+
596
+ test("leaves the manifest block empty for a driver that declares no setup", () => {
597
+ expect(registryFor(baseDriver()).manifest().setup).toBeNull();
598
+ });
599
+ });
@@ -1,10 +1,11 @@
1
1
  import type { BridgeContext } from "../context";
2
2
  import { BridgeAction, BridgeKind, type BridgeRow, type BridgeValues, isReadAction } from "../core";
3
- import type { BridgeDriver, BridgePanelModule, BridgeTerminalModule } from "../modules";
3
+ import type { BridgeDriver, BridgePanelModule, BridgeSetupModule, BridgeTerminalModule } from "../modules";
4
4
  import {
5
5
  BRIDGE_VERSION,
6
6
  type BridgeManifest,
7
7
  type BridgeModuleManifest,
8
+ type BridgeSetupManifest,
8
9
  type BridgeTerminalManifest,
9
10
  } from "../protocol";
10
11
  import { actionsOf } from "./bridgeActions";
@@ -18,7 +19,11 @@ interface CacheEntry {
18
19
  }
19
20
 
20
21
  const requiresRunning = (module: BridgePanelModule) => {
21
- if (module.kind === BridgeKind.Actions || module.kind === BridgeKind.Collection) {
22
+ if (
23
+ module.kind === BridgeKind.Actions
24
+ || module.kind === BridgeKind.Collection
25
+ || module.kind === BridgeKind.Detail
26
+ ) {
22
27
  return module.requiresRunning ?? false;
23
28
  }
24
29
 
@@ -26,7 +31,11 @@ const requiresRunning = (module: BridgePanelModule) => {
26
31
  };
27
32
 
28
33
  const refreshOf = (module: BridgePanelModule) => {
29
- return module.kind === BridgeKind.Collection ? (module.refreshSeconds ?? null) : null;
34
+ if (module.kind === BridgeKind.Collection || module.kind === BridgeKind.Detail) {
35
+ return module.refreshSeconds ?? null;
36
+ }
37
+
38
+ return null;
30
39
  };
31
40
 
32
41
  const dependsOf = (module: BridgePanelModule) => {
@@ -55,6 +64,16 @@ const terminalManifestOf = (module: BridgeTerminalModule | undefined): BridgeTer
55
64
  };
56
65
  };
57
66
 
67
+ const setupManifestOf = (module: BridgeSetupModule | undefined): BridgeSetupManifest | null => {
68
+ if (!module) {
69
+ return null;
70
+ }
71
+
72
+ return {
73
+ steps: module.steps,
74
+ };
75
+ };
76
+
58
77
  const withVariables = (context: BridgeContext, overlay: Record<string, string>): BridgeContext => {
59
78
  return {
60
79
  get server() {
@@ -80,6 +99,7 @@ const withVariables = (context: BridgeContext, overlay: Record<string, string>):
80
99
  return context.exec(command, options);
81
100
  },
82
101
  logs: context.logs,
102
+ setup: context.setup,
83
103
  emit(event, payload) {
84
104
  context.emit(event, payload);
85
105
  },
@@ -138,11 +158,11 @@ export const createRegistry = (
138
158
  const modules = driver.modules ?? {};
139
159
 
140
160
  for (const [id, module] of Object.entries(modules)) {
141
- if (module.kind !== BridgeKind.Actions) {
161
+ if (module.kind !== BridgeKind.Actions && module.kind !== BridgeKind.Detail) {
142
162
  continue;
143
163
  }
144
164
 
145
- for (const action of Object.keys(module.actions)) {
165
+ for (const action of Object.keys(module.actions ?? {})) {
146
166
  if (isReadAction(action)) {
147
167
  throw new BridgeDriverError(
148
168
  `module "${id}" declares action "${action}", which is reserved for reads — pick another id`,
@@ -290,6 +310,20 @@ export const createRegistry = (
290
310
  break;
291
311
  }
292
312
 
313
+ case BridgeKind.Detail: {
314
+ if (action === BridgeAction.Read) {
315
+ return await module.read(context);
316
+ }
317
+
318
+ const detailAction = module.actions?.[action];
319
+
320
+ if (detailAction) {
321
+ return await detailAction(context, args);
322
+ }
323
+
324
+ break;
325
+ }
326
+
293
327
  case BridgeKind.Catalog: {
294
328
  if (action === BridgeAction.Search) {
295
329
  return await module.search(context, {
@@ -346,6 +380,7 @@ export const createRegistry = (
346
380
  version: BRIDGE_VERSION,
347
381
  panel: driver.panel ?? null,
348
382
  terminal: terminalManifestOf(driver.terminal),
383
+ setup: setupManifestOf(driver.setup),
349
384
  modules: entries,
350
385
  events: ring.names(),
351
386
  };
@@ -0,0 +1,121 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import type { BridgeConfig } from "../protocol";
6
+ import { createBridgeSecrets } from "./bridgeSecrets";
7
+
8
+ const configWith = (secrets: Record<string, string>): BridgeConfig => ({
9
+ code: "wsvv8fakxm",
10
+ token: "token",
11
+ cwd: "/data",
12
+ run: "/run/bridge",
13
+ ports: {
14
+ game: 25_565,
15
+ },
16
+ memoryMb: 2048,
17
+ cpuPercent: 100,
18
+ gameUid: 1000,
19
+ gameGid: 1000,
20
+ variables: {
21
+ VERSION: "1.21",
22
+ },
23
+ secrets,
24
+ });
25
+
26
+ let directory: string;
27
+
28
+ let path: string;
29
+
30
+ const write = async (config: BridgeConfig) => {
31
+ await writeFile(path, `${JSON.stringify(config, null, 2)}\n`);
32
+ };
33
+
34
+ beforeEach(async () => {
35
+ directory = await mkdtemp(join(tmpdir(), "bridge-secrets-"));
36
+ path = join(directory, "config.json");
37
+ });
38
+
39
+ afterEach(async () => {
40
+ await rm(directory, {
41
+ force: true,
42
+ recursive: true,
43
+ });
44
+ });
45
+
46
+ describe("the bridge secrets reader", () => {
47
+ test("answers from the boot snapshot", async () => {
48
+ const config = configWith({
49
+ CURSEFORGE_API_KEY: "boot",
50
+ });
51
+
52
+ await write(config);
53
+
54
+ const secrets = createBridgeSecrets(path, config, 0);
55
+
56
+ expect(secrets.read("CURSEFORGE_API_KEY")).toBe("boot");
57
+ expect(secrets.read("MODRINTH_API_KEY")).toBeNull();
58
+ });
59
+
60
+ test("sees a value the agent rewrote into the config", async () => {
61
+ const config = configWith({
62
+ CURSEFORGE_API_KEY: "boot",
63
+ });
64
+
65
+ await write(config);
66
+
67
+ const secrets = createBridgeSecrets(path, config, 0);
68
+
69
+ expect(secrets.read("CURSEFORGE_API_KEY")).toBe("boot");
70
+
71
+ await write(
72
+ configWith({
73
+ CURSEFORGE_API_KEY: "rotated",
74
+ MODRINTH_API_KEY: "added",
75
+ }),
76
+ );
77
+
78
+ expect(secrets.read("CURSEFORGE_API_KEY")).toBe("rotated");
79
+ expect(secrets.read("MODRINTH_API_KEY")).toBe("added");
80
+ });
81
+
82
+ test("keeps the last good values while the config is mid-write", async () => {
83
+ const config = configWith({
84
+ CURSEFORGE_API_KEY: "boot",
85
+ });
86
+
87
+ await write(config);
88
+
89
+ const secrets = createBridgeSecrets(path, config, 0);
90
+
91
+ expect(secrets.read("CURSEFORGE_API_KEY")).toBe("boot");
92
+
93
+ await writeFile(path, '{"code": "wsvv8fak');
94
+
95
+ expect(secrets.read("CURSEFORGE_API_KEY")).toBe("boot");
96
+
97
+ await rm(path);
98
+
99
+ expect(secrets.read("CURSEFORGE_API_KEY")).toBe("boot");
100
+ });
101
+
102
+ test("re-reads the file at most once per refresh window", async () => {
103
+ const config = configWith({
104
+ CURSEFORGE_API_KEY: "boot",
105
+ });
106
+
107
+ await write(config);
108
+
109
+ const secrets = createBridgeSecrets(path, config, 60_000);
110
+
111
+ expect(secrets.read("CURSEFORGE_API_KEY")).toBe("boot");
112
+
113
+ await write(
114
+ configWith({
115
+ CURSEFORGE_API_KEY: "rotated",
116
+ }),
117
+ );
118
+
119
+ expect(secrets.read("CURSEFORGE_API_KEY")).toBe("boot");
120
+ });
121
+ });
@@ -0,0 +1,43 @@
1
+ import { readFileSync } from "node:fs";
2
+ import type { BridgeConfig } from "../protocol";
3
+ import { parseConfig } from "./bridgeConfigReader";
4
+
5
+ export const BRIDGE_SECRET_REFRESH_MS = 2000;
6
+
7
+ export interface BridgeSecrets {
8
+ read: (key: string) => string | null;
9
+ }
10
+
11
+ export const createBridgeSecrets = (
12
+ path: string,
13
+ config: BridgeConfig,
14
+ refreshMs: number = BRIDGE_SECRET_REFRESH_MS,
15
+ ): BridgeSecrets => {
16
+ let values = config.secrets;
17
+
18
+ let checkedAt: number | null = null;
19
+
20
+ const refresh = () => {
21
+ const stamp = performance.now();
22
+
23
+ if (checkedAt !== null && stamp - checkedAt < refreshMs) {
24
+ return;
25
+ }
26
+
27
+ checkedAt = stamp;
28
+
29
+ try {
30
+ values = parseConfig(readFileSync(path, "utf8")).secrets;
31
+ } catch {
32
+ return;
33
+ }
34
+ };
35
+
36
+ return {
37
+ read(key) {
38
+ refresh();
39
+
40
+ return values[key] ?? null;
41
+ },
42
+ };
43
+ };