@serverkgg/bridge 0.0.21 → 0.1.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.
Files changed (86) hide show
  1. package/package.json +3 -1
  2. package/src/bot/botDeploy.test.ts +363 -0
  3. package/src/bot/botDeploy.ts +136 -0
  4. package/src/bot/botEnvironment.test.ts +189 -0
  5. package/src/bot/botEnvironment.ts +141 -0
  6. package/src/bot/botIdentity.test.ts +251 -0
  7. package/src/bot/botIdentity.ts +140 -0
  8. package/src/bot/botPanel.test.ts +216 -0
  9. package/src/bot/botPanel.ts +121 -0
  10. package/src/bot/botState.test.ts +45 -0
  11. package/src/bot/botState.ts +54 -0
  12. package/src/bot/botTerminal.test.ts +79 -0
  13. package/src/bot/botTerminal.ts +62 -0
  14. package/src/bot/botVariables.test.ts +193 -0
  15. package/src/bot/botVariables.ts +73 -0
  16. package/src/bot/detect/botFramework.ts +23 -0
  17. package/src/bot/detect/botFrameworks.ts +198 -0
  18. package/src/bot/detect/botInspectWarning.ts +15 -0
  19. package/src/bot/detect/botPackageManager.ts +6 -0
  20. package/src/bot/detect/botPlatform.ts +10 -0
  21. package/src/bot/detect/botRuntime.ts +15 -0
  22. package/src/bot/detect/botToken.test.ts +71 -0
  23. package/src/bot/detect/botToken.ts +126 -0
  24. package/src/bot/detect/detect.test.ts +543 -0
  25. package/src/bot/detect/detect.ts +842 -0
  26. package/src/bot/detect/detectResult.ts +40 -0
  27. package/src/bot/detect/detectTable.test.ts +72 -0
  28. package/src/bot/detect/detectTable.ts +166 -0
  29. package/src/bot/detect/envExample.test.ts +29 -0
  30. package/src/bot/detect/envExample.ts +48 -0
  31. package/src/bot/detect/envKey.ts +7 -0
  32. package/src/bot/detect/envScan.test.ts +39 -0
  33. package/src/bot/detect/envScan.ts +87 -0
  34. package/src/bot/detect/index.ts +17 -0
  35. package/src/bot/detect/inferBotPlatform.test.ts +60 -0
  36. package/src/bot/detect/inferBotPlatform.ts +28 -0
  37. package/src/bot/detect/keys.test.ts +28 -0
  38. package/src/bot/detect/keys.ts +64 -0
  39. package/src/bot/detect/sensitiveKey.test.ts +91 -0
  40. package/src/bot/detect/sensitiveKey.ts +44 -0
  41. package/src/bot/detect/serverkJson.test.ts +88 -0
  42. package/src/bot/detect/serverkJson.ts +174 -0
  43. package/src/bot/index.ts +8 -0
  44. package/src/cli/detectTable.test.ts +66 -0
  45. package/src/cli/detectTable.ts +43 -0
  46. package/src/cli/main.ts +12 -4
  47. package/src/cli/renderedFile.ts +35 -0
  48. package/src/cli/schema.ts +7 -26
  49. package/src/cli/validate.test.ts +210 -2
  50. package/src/cli/validate.ts +71 -7
  51. package/src/context/bridgeContext.ts +11 -0
  52. package/src/core/bridgePort.test.ts +45 -0
  53. package/src/core/bridgePort.ts +26 -0
  54. package/src/guides/compile.ts +2 -2
  55. package/src/manifest/index.ts +1 -0
  56. package/src/manifest/kind.ts +5 -0
  57. package/src/manifest/manifest.test.ts +630 -3
  58. package/src/manifest/manifest.ts +391 -87
  59. package/src/manifest/runtime.ts +2 -2
  60. package/src/manifest/source.ts +4 -4
  61. package/src/modules/bridgeLifecycleSubject.ts +8 -0
  62. package/src/modules/bridgeModule.ts +6 -0
  63. package/src/modules/bridgeStopSignal.ts +13 -0
  64. package/src/modules/index.ts +2 -0
  65. package/src/panel/bridgeDetail.ts +2 -0
  66. package/src/protocol/bridgeConfig.ts +2 -0
  67. package/src/runtime/bridgeConfigReader.test.ts +80 -0
  68. package/src/runtime/bridgeConfigReader.ts +20 -0
  69. package/src/runtime/bridgeContext.test.ts +241 -0
  70. package/src/runtime/bridgeContext.ts +35 -9
  71. package/src/runtime/bridgeDriver.test.ts +132 -1
  72. package/src/runtime/bridgeDriver.ts +54 -1
  73. package/src/runtime/bridgeFiles.test.ts +62 -0
  74. package/src/runtime/bridgeFiles.ts +12 -2
  75. package/src/runtime/bridgeLiveConfig.ts +18 -0
  76. package/src/runtime/bridgeRedaction.test.ts +410 -0
  77. package/src/runtime/bridgeRedaction.ts +214 -0
  78. package/src/runtime/bridgeRegistry.test.ts +3 -0
  79. package/src/runtime/bridgeRegistry.ts +15 -0
  80. package/src/runtime/bridgeServer.test.ts +130 -0
  81. package/src/runtime/bridgeServer.ts +24 -7
  82. package/src/runtime/bridgeSetupHandlers.test.ts +88 -0
  83. package/src/runtime/bridgeSetupHandlers.ts +16 -6
  84. package/src/runtime/bridgeSupervisor.test.ts +670 -2
  85. package/src/runtime/bridgeSupervisor.ts +219 -67
  86. package/src/runtime/serve.ts +47 -13
@@ -564,6 +564,68 @@ describe("extract", () => {
564
564
  expect(await read("config/beta.json")).toBe("beta");
565
565
  });
566
566
 
567
+ test("keeps dotfiles in a tree when asked and never writes .git or .serverk", async () => {
568
+ const path = await archive([
569
+ {
570
+ name: "bot/.env.example",
571
+ content: "DISCORD_TOKEN=",
572
+ },
573
+ {
574
+ name: "bot/src/.config/settings.json",
575
+ content: "{}",
576
+ },
577
+ {
578
+ name: "bot/index.js",
579
+ content: "run()",
580
+ },
581
+ {
582
+ name: "bot/.git/config",
583
+ content: "[core]",
584
+ },
585
+ {
586
+ name: "bot/.serverk/deploy-requested",
587
+ content: "{}",
588
+ },
589
+ ]);
590
+
591
+ const written = await files.extract(path, "", {
592
+ tree: true,
593
+ dotfiles: true,
594
+ select: "bot/",
595
+ });
596
+
597
+ expect(written).toEqual([
598
+ ".env.example",
599
+ "src/.config/settings.json",
600
+ "index.js",
601
+ ]);
602
+
603
+ expect(await read(".env.example")).toBe("DISCORD_TOKEN=");
604
+ expect(await files.exists(".git/config")).toBe(false);
605
+ expect(await files.exists(".serverk/deploy-requested")).toBe(false);
606
+ });
607
+
608
+ test("still skips dotfiles in a tree unless asked", async () => {
609
+ const path = await archive([
610
+ {
611
+ name: ".env.example",
612
+ content: "DISCORD_TOKEN=",
613
+ },
614
+ {
615
+ name: "index.js",
616
+ content: "run()",
617
+ },
618
+ ]);
619
+
620
+ const written = await files.extract(path, "", {
621
+ tree: true,
622
+ });
623
+
624
+ expect(written).toEqual([
625
+ "index.js",
626
+ ]);
627
+ });
628
+
567
629
  test("takes a single named entry when the select carries no trailing slash", async () => {
568
630
  const path = await archive([
569
631
  {
@@ -152,6 +152,15 @@ const compileSelect = (select: string): ArchiveSelect => {
152
152
  return select.endsWith("/") ? prefixSelect(select) : entrySelect(select);
153
153
  };
154
154
 
155
+ const ALWAYS_SKIPPED_SEGMENTS: ReadonlySet<string> = new Set([
156
+ ".git",
157
+ ".serverk",
158
+ ]);
159
+
160
+ const hiddenEntry = (segments: string[], dotfiles: boolean) => {
161
+ return segments.some((segment) => ALWAYS_SKIPPED_SEGMENTS.has(segment) || (!dotfiles && segment.startsWith(".")));
162
+ };
163
+
155
164
  const compileSelects = (select: string | undefined, selects: string[] | undefined) => {
156
165
  const compiled: ArchiveSelect[] = [];
157
166
 
@@ -603,6 +612,7 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
603
612
  }
604
613
 
605
614
  const tree = extractOptions.tree === true;
615
+ const dotfiles = extractOptions.dotfiles === true;
606
616
  const allowed = extractOptions.extensions?.map((extension) => extension.toLowerCase());
607
617
  const selects = compileSelects(extractOptions.select, extractOptions.selects);
608
618
  const excluded = compileGlobs(extractOptions.exclude ?? []);
@@ -638,7 +648,7 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
638
648
  selected.split("/").at(-1) ?? "",
639
649
  ];
640
650
  const name = segments.at(-1) ?? "";
641
- if (!name || segments.some((segment) => segment.startsWith("."))) {
651
+ if (!name || hiddenEntry(segments, dotfiles)) {
642
652
  continue;
643
653
  }
644
654
  if (allowed !== undefined && !allowed.includes(name.split(".").at(-1)?.toLowerCase() ?? "")) {
@@ -677,7 +687,7 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
677
687
  const name = segments.at(-1) ?? "";
678
688
  const extension = name.split(".").at(-1)?.toLowerCase() ?? "";
679
689
 
680
- if (name.length === 0 || segments.some((segment) => segment.startsWith("."))) {
690
+ if (name.length === 0 || hiddenEntry(segments, dotfiles)) {
681
691
  continue;
682
692
  }
683
693
 
@@ -14,9 +14,13 @@ export const BRIDGE_CONFIG_REFRESH_MS = 2000;
14
14
  */
15
15
  export interface BridgeLiveConfig {
16
16
  variable: (key: string) => string | null;
17
+ variables: () => Readonly<Record<string, string>>;
17
18
  secret: (key: string) => string | null;
19
+ secrets: () => Readonly<Record<string, string>>;
18
20
  memoryMb: () => number;
19
21
  cpuPercent: () => number;
22
+ publicAddress: () => string | null;
23
+ reload: () => void;
20
24
  }
21
25
 
22
26
  export const createBridgeLiveConfig = (
@@ -50,14 +54,28 @@ export const createBridgeLiveConfig = (
50
54
  variable(key) {
51
55
  return refresh().variables[key] ?? null;
52
56
  },
57
+ variables() {
58
+ return refresh().variables;
59
+ },
53
60
  secret(key) {
54
61
  return refresh().secrets[key] ?? null;
55
62
  },
63
+ secrets() {
64
+ return refresh().secrets;
65
+ },
56
66
  memoryMb() {
57
67
  return refresh().memoryMb;
58
68
  },
59
69
  cpuPercent() {
60
70
  return refresh().cpuPercent;
61
71
  },
72
+ publicAddress() {
73
+ return refresh().publicAddress ?? null;
74
+ },
75
+ reload() {
76
+ checkedAt = null;
77
+
78
+ refresh();
79
+ },
62
80
  };
63
81
  };
@@ -0,0 +1,410 @@
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 { BridgeKind } from "../core";
6
+ import type { BridgeDriver } from "../modules";
7
+ import { BridgeControl, BridgeFormTarget, BridgeLayout, type BridgeTab } from "../panel";
8
+ import type { BridgeConfig } from "../protocol";
9
+ import { createBridgeLiveConfig } from "./bridgeLiveConfig";
10
+ import type { BridgeLeveledLogger } from "./bridgeLog";
11
+ import { createBridgeRedaction, REDACTED, redactLocalized, redactLogger, secretFieldKeys } from "./bridgeRedaction";
12
+
13
+ const text = (value: string) => ({
14
+ ar: value,
15
+ en: value,
16
+ });
17
+
18
+ const TOKEN = "123456789:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw";
19
+
20
+ const driverWith = (tabs: BridgeTab[]): BridgeDriver => ({
21
+ install: {
22
+ kind: BridgeKind.Install,
23
+ run: async () => undefined,
24
+ },
25
+ lifecycle: {
26
+ kind: BridgeKind.Lifecycle,
27
+ command: async () => [
28
+ "run",
29
+ ],
30
+ },
31
+ panel: {
32
+ tabs,
33
+ },
34
+ });
35
+
36
+ const secretsTab: BridgeTab = {
37
+ id: "bot",
38
+ title: text("Bot"),
39
+ sections: [
40
+ {
41
+ layout: BridgeLayout.Form,
42
+ id: "token",
43
+ target: BridgeFormTarget.Variables,
44
+ fields: [
45
+ {
46
+ key: "BOT_PLATFORM",
47
+ control: BridgeControl.Select,
48
+ label: text("Platform"),
49
+ },
50
+ {
51
+ key: "BOT_TOKEN",
52
+ control: BridgeControl.Secret,
53
+ label: text("Token"),
54
+ },
55
+ ],
56
+ },
57
+ {
58
+ layout: BridgeLayout.Form,
59
+ id: "source",
60
+ target: BridgeFormTarget.Variables,
61
+ fields: [
62
+ {
63
+ key: "GIT_TOKEN",
64
+ control: BridgeControl.Secret,
65
+ label: text("Git token"),
66
+ },
67
+ {
68
+ key: "BOT_TOKEN",
69
+ control: BridgeControl.Secret,
70
+ label: text("Token again"),
71
+ },
72
+ ],
73
+ },
74
+ ],
75
+ };
76
+
77
+ const sourceOf = (initial: Record<string, string>, secrets: Record<string, string> = {}) => {
78
+ let variables: Readonly<Record<string, string>> = initial;
79
+ let current: Readonly<Record<string, string>> = secrets;
80
+
81
+ return {
82
+ source: {
83
+ variables: () => variables,
84
+ secrets: () => current,
85
+ },
86
+ save(next: Record<string, string>) {
87
+ variables = next;
88
+ },
89
+ seal(next: Record<string, string>) {
90
+ current = next;
91
+ },
92
+ };
93
+ };
94
+
95
+ describe("secretFieldKeys", () => {
96
+ test("collects every secret form field once and nothing else", () => {
97
+ expect(
98
+ secretFieldKeys(
99
+ driverWith([
100
+ secretsTab,
101
+ ]),
102
+ ),
103
+ ).toEqual([
104
+ "BOT_TOKEN",
105
+ "GIT_TOKEN",
106
+ ]);
107
+ });
108
+
109
+ test("finds nothing in a driver with no secret fields", () => {
110
+ expect(secretFieldKeys(driverWith([]))).toEqual([]);
111
+ });
112
+ });
113
+
114
+ describe("createBridgeRedaction", () => {
115
+ test("is built for every driver and leaves text alone until a secret exists", () => {
116
+ const { source, seal } = sourceOf({});
117
+ const redact = createBridgeRedaction([], source);
118
+
119
+ expect(redact(`token ${TOKEN}`)).toBe(`token ${TOKEN}`);
120
+
121
+ seal({
122
+ DISCORD_TOKEN: TOKEN,
123
+ });
124
+
125
+ expect(redact(`token ${TOKEN}`)).toBe(`token ${REDACTED}`);
126
+ });
127
+
128
+ test("blanks the platform secrets the manifest declares, with the same length floor", () => {
129
+ const redact = createBridgeRedaction(
130
+ [],
131
+ sourceOf(
132
+ {},
133
+ {
134
+ CURSEFORGE_API_KEY: "cf-api-key-0123456789",
135
+ SHORT_KEY: "short",
136
+ },
137
+ ).source,
138
+ );
139
+
140
+ expect(redact("key cf-api-key-0123456789 and short")).toBe(`key ${REDACTED} and short`);
141
+ });
142
+
143
+ test("blanks secret fields and platform secrets together", () => {
144
+ const redact = createBridgeRedaction(
145
+ [
146
+ "BOT_TOKEN",
147
+ ],
148
+ sourceOf(
149
+ {
150
+ BOT_TOKEN: TOKEN,
151
+ },
152
+ {
153
+ CURSEFORGE_API_KEY: "cf-api-key-0123456789",
154
+ },
155
+ ).source,
156
+ );
157
+
158
+ expect(redact(`${TOKEN} cf-api-key-0123456789`)).toBe(`${REDACTED} ${REDACTED}`);
159
+ });
160
+
161
+ test("replaces every occurrence of a secret value", () => {
162
+ const redact = createBridgeRedaction(
163
+ [
164
+ "BOT_TOKEN",
165
+ ],
166
+ sourceOf({
167
+ BOT_TOKEN: TOKEN,
168
+ }).source,
169
+ );
170
+
171
+ expect(redact(`InvalidToken: ${TOKEN} was rejected (${TOKEN})`)).toBe(
172
+ `InvalidToken: ${REDACTED} was rejected (${REDACTED})`,
173
+ );
174
+ });
175
+
176
+ test("blanks every line of a multi-line secret the process prints one line at a time", () => {
177
+ const key = [
178
+ "-----BEGIN PRIVATE KEY-----",
179
+ "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7",
180
+ " q1w2e3r4t5y6u7i8o9p0 ",
181
+ "short",
182
+ "-----END PRIVATE KEY-----",
183
+ ].join("\r\n");
184
+ const redact = createBridgeRedaction(
185
+ [],
186
+ sourceOf(
187
+ {},
188
+ {
189
+ GOOGLE_PRIVATE_KEY: key,
190
+ },
191
+ ).source,
192
+ );
193
+
194
+ expect(redact("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7")).toBe(REDACTED);
195
+ expect(redact("line: q1w2e3r4t5y6u7i8o9p0")).toBe(`line: ${REDACTED}`);
196
+ expect(redact("-----BEGIN PRIVATE KEY-----")).toBe(REDACTED);
197
+ expect(redact("short stays")).toBe("short stays");
198
+ expect(redact(`whole ${key}`)).toBe(`whole ${REDACTED}`);
199
+ });
200
+
201
+ test("leaves a value under eight characters alone", () => {
202
+ const redact = createBridgeRedaction(
203
+ [
204
+ "RCON_PASSWORD",
205
+ ],
206
+ sourceOf({
207
+ RCON_PASSWORD: "hunter2",
208
+ }).source,
209
+ );
210
+
211
+ expect(redact("the password is hunter2")).toBe("the password is hunter2");
212
+ });
213
+
214
+ test("escapes values that look like patterns and prefers the longest overlapping value", () => {
215
+ const redact = createBridgeRedaction(
216
+ [
217
+ "SHORT",
218
+ "LONG",
219
+ ],
220
+ sourceOf({
221
+ SHORT: "a.b+c*d?",
222
+ LONG: "a.b+c*d?-and-more",
223
+ }).source,
224
+ );
225
+
226
+ expect(redact("x a.b+c*d?-and-more y a.b+c*d? z aXb+c*d?")).toBe(`x ${REDACTED} y ${REDACTED} z aXb+c*d?`);
227
+ });
228
+
229
+ test("follows a value saved after it was built and forgets the old one", () => {
230
+ const { source, save } = sourceOf({
231
+ BOT_TOKEN: "old-token-value",
232
+ });
233
+ const redact = createBridgeRedaction(
234
+ [
235
+ "BOT_TOKEN",
236
+ ],
237
+ source,
238
+ );
239
+
240
+ expect(redact("old-token-value")).toBe(REDACTED);
241
+
242
+ save({
243
+ BOT_TOKEN: "new-token-value",
244
+ });
245
+
246
+ expect(redact("old-token-value new-token-value")).toBe(`old-token-value ${REDACTED}`);
247
+ });
248
+ });
249
+
250
+ describe("redacting what the bridge reports", () => {
251
+ const redact = (value: string) => value.split(TOKEN).join(REDACTED);
252
+
253
+ test("covers both languages of a customer-facing message", () => {
254
+ expect(redactLocalized(text(`bad ${TOKEN}`), redact)).toEqual(text(`bad ${REDACTED}`));
255
+ });
256
+
257
+ test("covers the message and every string field of a log line", () => {
258
+ const lines: unknown[] = [];
259
+ const record = (level: string) => (message: string, fields?: Record<string, unknown>) => {
260
+ lines.push({
261
+ level,
262
+ message,
263
+ fields,
264
+ });
265
+ };
266
+ const logger: BridgeLeveledLogger = Object.assign(record("info"), {
267
+ warn: record("warn"),
268
+ error: record("error"),
269
+ });
270
+ const redacted = redactLogger(logger, redact);
271
+
272
+ redacted(`saw ${TOKEN}`);
273
+ redacted.warn("driver failed", {
274
+ error: `InvalidToken: ${TOKEN}`,
275
+ code: 401,
276
+ nested: {
277
+ list: [
278
+ TOKEN,
279
+ ],
280
+ },
281
+ driver: true,
282
+ });
283
+ redacted.error("plain");
284
+
285
+ expect(lines).toEqual([
286
+ {
287
+ level: "info",
288
+ message: `saw ${REDACTED}`,
289
+ fields: undefined,
290
+ },
291
+ {
292
+ level: "warn",
293
+ message: "driver failed",
294
+ fields: {
295
+ error: `InvalidToken: ${REDACTED}`,
296
+ code: 401,
297
+ nested: {
298
+ list: [
299
+ REDACTED,
300
+ ],
301
+ },
302
+ driver: true,
303
+ },
304
+ },
305
+ {
306
+ level: "error",
307
+ message: "plain",
308
+ fields: undefined,
309
+ },
310
+ ]);
311
+ });
312
+ });
313
+
314
+ describe("redaction over the live config", () => {
315
+ let directory = "";
316
+ let path = "";
317
+
318
+ const configWith = (variables: Record<string, string>): BridgeConfig => ({
319
+ code: "wsvv8fakxm",
320
+ token: "token",
321
+ cwd: "/data",
322
+ run: "/run/bridge",
323
+ ports: {},
324
+ memoryMb: 1024,
325
+ cpuPercent: 100,
326
+ gameUid: 1000,
327
+ gameGid: 1000,
328
+ variables,
329
+ secrets: {},
330
+ });
331
+
332
+ const write = async (config: BridgeConfig) => {
333
+ await writeFile(path, `${JSON.stringify(config)}\n`);
334
+ };
335
+
336
+ beforeEach(async () => {
337
+ directory = await mkdtemp(join(tmpdir(), "bridge-redaction-"));
338
+ path = join(directory, "config.json");
339
+ });
340
+
341
+ afterEach(async () => {
342
+ await rm(directory, {
343
+ force: true,
344
+ recursive: true,
345
+ });
346
+ });
347
+
348
+ test("reads the platform secrets from the config the agent wrote", async () => {
349
+ const boot = {
350
+ ...configWith({}),
351
+ secrets: {
352
+ CURSEFORGE_API_KEY: "cf-api-key-0123456789",
353
+ },
354
+ };
355
+
356
+ await write(boot);
357
+
358
+ const redact = createBridgeRedaction([], createBridgeLiveConfig(path, boot, 60_000));
359
+
360
+ expect(redact("cf-api-key-0123456789")).toBe(REDACTED);
361
+ });
362
+
363
+ test("starts as the identity with no secret anywhere and blanks a secret written after boot", async () => {
364
+ const boot = configWith({});
365
+
366
+ await write(boot);
367
+
368
+ const live = createBridgeLiveConfig(path, boot, 60_000);
369
+ const redact = createBridgeRedaction([], live);
370
+
371
+ expect(redact(`key ${TOKEN}`)).toBe(`key ${TOKEN}`);
372
+
373
+ await write({
374
+ ...configWith({}),
375
+ secrets: {
376
+ MY_API_KEY: TOKEN,
377
+ },
378
+ });
379
+
380
+ live.reload();
381
+
382
+ expect(redact(`key ${TOKEN}`)).toBe(`key ${REDACTED}`);
383
+ });
384
+
385
+ test("picks up a token the agent writes into the config after boot", async () => {
386
+ const boot = configWith({});
387
+
388
+ await write(boot);
389
+
390
+ const live = createBridgeLiveConfig(path, boot, 60_000);
391
+ const redact = createBridgeRedaction(
392
+ [
393
+ "BOT_TOKEN",
394
+ ],
395
+ live,
396
+ );
397
+
398
+ expect(redact(TOKEN)).toBe(TOKEN);
399
+
400
+ await write(
401
+ configWith({
402
+ BOT_TOKEN: TOKEN,
403
+ }),
404
+ );
405
+
406
+ live.reload();
407
+
408
+ expect(redact(TOKEN)).toBe(REDACTED);
409
+ });
410
+ });