@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.
- package/package.json +3 -1
- package/src/bot/botDeploy.test.ts +363 -0
- package/src/bot/botDeploy.ts +136 -0
- package/src/bot/botEnvironment.test.ts +189 -0
- package/src/bot/botEnvironment.ts +141 -0
- package/src/bot/botIdentity.test.ts +251 -0
- package/src/bot/botIdentity.ts +140 -0
- package/src/bot/botPanel.test.ts +216 -0
- package/src/bot/botPanel.ts +121 -0
- package/src/bot/botState.test.ts +45 -0
- package/src/bot/botState.ts +54 -0
- package/src/bot/botTerminal.test.ts +79 -0
- package/src/bot/botTerminal.ts +62 -0
- package/src/bot/botVariables.test.ts +193 -0
- package/src/bot/botVariables.ts +73 -0
- package/src/bot/detect/botFramework.ts +23 -0
- package/src/bot/detect/botFrameworks.ts +198 -0
- package/src/bot/detect/botInspectWarning.ts +15 -0
- package/src/bot/detect/botPackageManager.ts +6 -0
- package/src/bot/detect/botPlatform.ts +10 -0
- package/src/bot/detect/botRuntime.ts +15 -0
- package/src/bot/detect/botToken.test.ts +71 -0
- package/src/bot/detect/botToken.ts +126 -0
- package/src/bot/detect/detect.test.ts +543 -0
- package/src/bot/detect/detect.ts +842 -0
- package/src/bot/detect/detectResult.ts +40 -0
- package/src/bot/detect/detectTable.test.ts +72 -0
- package/src/bot/detect/detectTable.ts +166 -0
- package/src/bot/detect/envExample.test.ts +29 -0
- package/src/bot/detect/envExample.ts +48 -0
- package/src/bot/detect/envKey.ts +7 -0
- package/src/bot/detect/envScan.test.ts +39 -0
- package/src/bot/detect/envScan.ts +87 -0
- package/src/bot/detect/index.ts +17 -0
- package/src/bot/detect/inferBotPlatform.test.ts +60 -0
- package/src/bot/detect/inferBotPlatform.ts +28 -0
- package/src/bot/detect/keys.test.ts +28 -0
- package/src/bot/detect/keys.ts +64 -0
- package/src/bot/detect/sensitiveKey.test.ts +91 -0
- package/src/bot/detect/sensitiveKey.ts +44 -0
- package/src/bot/detect/serverkJson.test.ts +88 -0
- package/src/bot/detect/serverkJson.ts +174 -0
- package/src/bot/index.ts +8 -0
- package/src/cli/detectTable.test.ts +66 -0
- package/src/cli/detectTable.ts +43 -0
- package/src/cli/main.ts +12 -4
- package/src/cli/renderedFile.ts +35 -0
- package/src/cli/schema.ts +7 -26
- package/src/cli/validate.test.ts +210 -2
- package/src/cli/validate.ts +71 -7
- package/src/context/bridgeContext.ts +11 -0
- package/src/core/bridgePort.test.ts +45 -0
- package/src/core/bridgePort.ts +26 -0
- package/src/guides/compile.ts +2 -2
- package/src/manifest/index.ts +1 -0
- package/src/manifest/kind.ts +5 -0
- package/src/manifest/manifest.test.ts +630 -3
- package/src/manifest/manifest.ts +391 -87
- package/src/manifest/runtime.ts +2 -2
- package/src/manifest/source.ts +4 -4
- package/src/modules/bridgeLifecycleSubject.ts +8 -0
- package/src/modules/bridgeModule.ts +6 -0
- package/src/modules/bridgeStopSignal.ts +13 -0
- package/src/modules/index.ts +2 -0
- package/src/panel/bridgeDetail.ts +2 -0
- package/src/protocol/bridgeConfig.ts +2 -0
- package/src/runtime/bridgeConfigReader.test.ts +80 -0
- package/src/runtime/bridgeConfigReader.ts +20 -0
- package/src/runtime/bridgeContext.test.ts +241 -0
- package/src/runtime/bridgeContext.ts +35 -9
- package/src/runtime/bridgeDriver.test.ts +132 -1
- package/src/runtime/bridgeDriver.ts +54 -1
- package/src/runtime/bridgeFiles.test.ts +62 -0
- package/src/runtime/bridgeFiles.ts +12 -2
- package/src/runtime/bridgeLiveConfig.ts +18 -0
- package/src/runtime/bridgeRedaction.test.ts +410 -0
- package/src/runtime/bridgeRedaction.ts +214 -0
- package/src/runtime/bridgeRegistry.test.ts +3 -0
- package/src/runtime/bridgeRegistry.ts +15 -0
- package/src/runtime/bridgeServer.test.ts +130 -0
- package/src/runtime/bridgeServer.ts +24 -7
- package/src/runtime/bridgeSetupHandlers.test.ts +88 -0
- package/src/runtime/bridgeSetupHandlers.ts +16 -6
- package/src/runtime/bridgeSupervisor.test.ts +670 -2
- package/src/runtime/bridgeSupervisor.ts +219 -67
- 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
|
|
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
|
|
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
|
+
});
|