@serverkgg/bridge 0.0.21 → 0.1.2

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 (119) 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 +246 -0
  9. package/src/bot/botPanel.ts +129 -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 +283 -3
  50. package/src/cli/validate.ts +105 -7
  51. package/src/context/bridgeContext.ts +17 -1
  52. package/src/core/bridgeAction.ts +2 -0
  53. package/src/core/bridgeActivity.ts +9 -0
  54. package/src/core/bridgeAvailability.ts +10 -0
  55. package/src/core/bridgePort.test.ts +45 -0
  56. package/src/core/bridgePort.ts +26 -0
  57. package/src/core/bridgeUserError.test.ts +10 -0
  58. package/src/core/bridgeUserError.ts +6 -2
  59. package/src/core/describeDownloadFailure.test.ts +58 -0
  60. package/src/core/describeDownloadFailure.ts +69 -0
  61. package/src/core/index.ts +3 -0
  62. package/src/guides/compile.ts +2 -2
  63. package/src/manifest/index.ts +1 -0
  64. package/src/manifest/kind.ts +5 -0
  65. package/src/manifest/manifest.test.ts +630 -3
  66. package/src/manifest/manifest.ts +391 -87
  67. package/src/manifest/runtime.ts +2 -2
  68. package/src/manifest/source.ts +4 -4
  69. package/src/modules/bridge.ts +8 -1
  70. package/src/modules/bridgeLifecycleSubject.ts +8 -0
  71. package/src/modules/bridgeModule.ts +19 -1
  72. package/src/modules/bridgeStopSignal.ts +13 -0
  73. package/src/modules/index.ts +2 -0
  74. package/src/panel/bridgeDetail.ts +4 -1
  75. package/src/protocol/bridgeActivity.ts +54 -0
  76. package/src/protocol/bridgeConfig.ts +2 -0
  77. package/src/protocol/bridgeEvents.ts +30 -0
  78. package/src/protocol/bridgeFrame.ts +10 -1
  79. package/src/protocol/index.ts +1 -0
  80. package/src/runtime/bridgeActions.ts +19 -2
  81. package/src/runtime/bridgeActivity.test.ts +318 -0
  82. package/src/runtime/bridgeActivity.ts +268 -0
  83. package/src/runtime/bridgeConfigReader.test.ts +80 -0
  84. package/src/runtime/bridgeConfigReader.ts +20 -0
  85. package/src/runtime/bridgeContext.test.ts +247 -0
  86. package/src/runtime/bridgeContext.ts +43 -10
  87. package/src/runtime/bridgeDriver.test.ts +153 -1
  88. package/src/runtime/bridgeDriver.ts +64 -8
  89. package/src/runtime/bridgeFiles.test.ts +62 -0
  90. package/src/runtime/bridgeFiles.ts +12 -2
  91. package/src/runtime/bridgeLiveConfig.ts +18 -0
  92. package/src/runtime/bridgeLog.ts +10 -0
  93. package/src/runtime/bridgeRedaction.test.ts +410 -0
  94. package/src/runtime/bridgeRedaction.ts +214 -0
  95. package/src/runtime/bridgeRefusalError.ts +13 -0
  96. package/src/runtime/bridgeRegistry.test.ts +1056 -10
  97. package/src/runtime/bridgeRegistry.ts +464 -50
  98. package/src/runtime/bridgeScopedContext.ts +55 -0
  99. package/src/runtime/bridgeServer.test.ts +534 -4
  100. package/src/runtime/bridgeServer.ts +145 -38
  101. package/src/runtime/bridgeSetupHandlers.test.ts +156 -5
  102. package/src/runtime/bridgeSetupHandlers.ts +59 -14
  103. package/src/runtime/bridgeSupervisor.test.ts +670 -2
  104. package/src/runtime/bridgeSupervisor.ts +219 -67
  105. package/src/runtime/serve.ts +87 -11
  106. package/src/steam/steamcmd.test.ts +32 -1
  107. package/src/steam/steamcmd.ts +27 -4
  108. package/src/utils/changedSettingKeys.test.ts +65 -0
  109. package/src/utils/changedSettingKeys.ts +6 -0
  110. package/src/utils/describeExecFailure.test.ts +34 -0
  111. package/src/utils/describeExecFailure.ts +9 -0
  112. package/src/utils/index.ts +3 -0
  113. package/src/utils/sameSettingValue.test.ts +35 -0
  114. package/src/utils/sameSettingValue.ts +40 -0
  115. package/src/wire/activity.test.ts +95 -0
  116. package/src/wire/activity.ts +41 -0
  117. package/src/wire/index.ts +1 -0
  118. package/src/wire/panel.test.ts +67 -1
  119. package/src/wire/panel.ts +11 -0
@@ -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
+ });
@@ -0,0 +1,214 @@
1
+ import { BridgeSetupPromptKind, type LocalizedText } from "../core";
2
+ import type { BridgeDriver } from "../modules";
3
+ import { BridgeControl, BridgeLayout } from "../panel";
4
+ import type { BridgeSetupAction, BridgeSetupPrompt, BridgeSetupRuntime } from "../protocol";
5
+ import type { BridgeLeveledLogger, BridgeLogger } from "./bridgeLog";
6
+
7
+ export const REDACTED = "••••";
8
+
9
+ export const REDACTION_MIN_LENGTH = 8;
10
+
11
+ const REDACTION_DEPTH_LIMIT = 4;
12
+
13
+ const PATTERN_SPECIALS = /[.*+?^${}()|[\]\\]/g;
14
+
15
+ export type BridgeRedact = (text: string) => string;
16
+
17
+ export interface BridgeRedactionSource {
18
+ variables(): Readonly<Record<string, string>>;
19
+ secrets(): Readonly<Record<string, string>>;
20
+ }
21
+
22
+ export const secretFieldKeys = (driver: BridgeDriver): string[] => {
23
+ const keys = new Set<string>();
24
+
25
+ for (const tab of driver.panel?.tabs ?? []) {
26
+ for (const section of tab.sections) {
27
+ if (section.layout !== BridgeLayout.Form) {
28
+ continue;
29
+ }
30
+
31
+ for (const field of section.fields) {
32
+ if (field.control === BridgeControl.Secret) {
33
+ keys.add(field.key);
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ return [
40
+ ...keys,
41
+ ];
42
+ };
43
+
44
+ const LINE_BREAK = /\r\n|\r|\n/;
45
+
46
+ const redactableParts = (value: string): string[] => {
47
+ if (!LINE_BREAK.test(value)) {
48
+ return [
49
+ value,
50
+ ];
51
+ }
52
+
53
+ return [
54
+ value,
55
+ ...value.split(LINE_BREAK).map((line) => line.trim()),
56
+ ];
57
+ };
58
+
59
+ const matcherFor = (values: string[]) => {
60
+ if (values.length === 0) {
61
+ return null;
62
+ }
63
+
64
+ return new RegExp(values.map((value) => value.replace(PATTERN_SPECIALS, "\\$&")).join("|"), "g");
65
+ };
66
+
67
+ export const createBridgeRedaction = (keys: string[], source: BridgeRedactionSource): BridgeRedact => {
68
+ let seenVariables: Readonly<Record<string, string>> | null = null;
69
+ let seenSecrets: Readonly<Record<string, string>> | null = null;
70
+ let signature = "";
71
+ let matcher: RegExp | null = null;
72
+
73
+ const current = () => {
74
+ const variables = source.variables();
75
+ const secrets = source.secrets();
76
+
77
+ if (variables === seenVariables && secrets === seenSecrets) {
78
+ return matcher;
79
+ }
80
+
81
+ seenVariables = variables;
82
+ seenSecrets = secrets;
83
+
84
+ const values = [
85
+ ...new Set(
86
+ [
87
+ ...keys.map((key) => variables[key] ?? ""),
88
+ ...Object.values(secrets),
89
+ ].flatMap(redactableParts),
90
+ ),
91
+ ]
92
+ .filter((value) => value.length >= REDACTION_MIN_LENGTH)
93
+ .sort((a, b) => b.length - a.length);
94
+ const next = values.join("\0");
95
+
96
+ if (next !== signature) {
97
+ signature = next;
98
+ matcher = matcherFor(values);
99
+ }
100
+
101
+ return matcher;
102
+ };
103
+
104
+ return (text) => {
105
+ const pattern = current();
106
+
107
+ return pattern ? text.replace(pattern, REDACTED) : text;
108
+ };
109
+ };
110
+
111
+ export const redactLocalized = (text: LocalizedText, redact: BridgeRedact): LocalizedText => {
112
+ return {
113
+ ar: redact(text.ar),
114
+ en: redact(text.en),
115
+ };
116
+ };
117
+
118
+ const redactActions = (actions: BridgeSetupAction[], redact: BridgeRedact): BridgeSetupAction[] => {
119
+ return actions.map((action) => ({
120
+ id: action.id,
121
+ label: redactLocalized(action.label, redact),
122
+ }));
123
+ };
124
+
125
+ const redactPrompt = (prompt: BridgeSetupPrompt, redact: BridgeRedact): BridgeSetupPrompt => {
126
+ const message = redactLocalized(prompt.message, redact);
127
+
128
+ switch (prompt.kind) {
129
+ case BridgeSetupPromptKind.Wait: {
130
+ return {
131
+ ...prompt,
132
+ message,
133
+ };
134
+ }
135
+
136
+ case BridgeSetupPromptKind.Progress: {
137
+ return {
138
+ ...prompt,
139
+ message,
140
+ detail: prompt.detail === null ? null : redact(prompt.detail),
141
+ };
142
+ }
143
+
144
+ case BridgeSetupPromptKind.Link: {
145
+ return {
146
+ ...prompt,
147
+ message,
148
+ url: redact(prompt.url),
149
+ code: prompt.code === null ? null : redact(prompt.code),
150
+ actions: redactActions(prompt.actions, redact),
151
+ };
152
+ }
153
+
154
+ case BridgeSetupPromptKind.Action:
155
+ case BridgeSetupPromptKind.Failed: {
156
+ return {
157
+ ...prompt,
158
+ message,
159
+ actions: redactActions(prompt.actions, redact),
160
+ };
161
+ }
162
+ }
163
+ };
164
+
165
+ export const redactSetupRuntime = (
166
+ runtime: BridgeSetupRuntime | null,
167
+ redact: BridgeRedact,
168
+ ): BridgeSetupRuntime | null => {
169
+ if (!runtime) {
170
+ return null;
171
+ }
172
+
173
+ return {
174
+ steps: runtime.steps,
175
+ prompt: runtime.prompt ? redactPrompt(runtime.prompt, redact) : null,
176
+ };
177
+ };
178
+
179
+ const redactValue = (value: unknown, redact: BridgeRedact, depth: number): unknown => {
180
+ if (typeof value === "string") {
181
+ return redact(value);
182
+ }
183
+
184
+ if (depth >= REDACTION_DEPTH_LIMIT || value === null || typeof value !== "object") {
185
+ return value;
186
+ }
187
+
188
+ if (Array.isArray(value)) {
189
+ return value.map((entry) => redactValue(entry, redact, depth + 1));
190
+ }
191
+
192
+ return Object.fromEntries(
193
+ Object.entries(value).map(([key, entry]) => [
194
+ key,
195
+ redactValue(entry, redact, depth + 1),
196
+ ]),
197
+ );
198
+ };
199
+
200
+ const redactingWriter = (write: BridgeLogger, redact: BridgeRedact): BridgeLogger => {
201
+ return (message, fields) => {
202
+ write(
203
+ redact(message),
204
+ fields === undefined ? undefined : (redactValue(fields, redact, 0) as Record<string, unknown>),
205
+ );
206
+ };
207
+ };
208
+
209
+ export const redactLogger = (logger: BridgeLeveledLogger, redact: BridgeRedact): BridgeLeveledLogger => {
210
+ return Object.assign(redactingWriter(logger, redact), {
211
+ warn: redactingWriter(logger.warn, redact),
212
+ error: redactingWriter(logger.error, redact),
213
+ });
214
+ };
@@ -0,0 +1,13 @@
1
+ import type { LocalizedText } from "../core";
2
+ import { BridgeDriverError } from "./bridgeDriver";
3
+
4
+ export class BridgeRefusalError extends BridgeDriverError {
5
+ readonly text: LocalizedText | null;
6
+
7
+ constructor(message: string, text: LocalizedText | null = null) {
8
+ super(message);
9
+
10
+ this.name = "BridgeRefusalError";
11
+ this.text = text;
12
+ }
13
+ }