@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
|
@@ -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
|
+
};
|
|
@@ -95,12 +95,24 @@ const withVariables = (context: BridgeContext, overlay: Record<string, string>):
|
|
|
95
95
|
variable(key) {
|
|
96
96
|
return Object.hasOwn(overlay, key) ? (overlay[key] ?? null) : context.variable(key);
|
|
97
97
|
},
|
|
98
|
+
variables() {
|
|
99
|
+
return {
|
|
100
|
+
...context.variables(),
|
|
101
|
+
...overlay,
|
|
102
|
+
};
|
|
103
|
+
},
|
|
98
104
|
secret(key) {
|
|
99
105
|
return context.secret(key);
|
|
100
106
|
},
|
|
107
|
+
secrets() {
|
|
108
|
+
return context.secrets();
|
|
109
|
+
},
|
|
101
110
|
port(key) {
|
|
102
111
|
return context.port(key);
|
|
103
112
|
},
|
|
113
|
+
portBlock(key) {
|
|
114
|
+
return context.portBlock(key);
|
|
115
|
+
},
|
|
104
116
|
files: context.files,
|
|
105
117
|
codec: context.codec,
|
|
106
118
|
net: context.net,
|
|
@@ -117,6 +129,9 @@ const withVariables = (context: BridgeContext, overlay: Record<string, string>):
|
|
|
117
129
|
emit(event, payload) {
|
|
118
130
|
context.emit(event, payload);
|
|
119
131
|
},
|
|
132
|
+
restart() {
|
|
133
|
+
return context.restart();
|
|
134
|
+
},
|
|
120
135
|
log: context.log,
|
|
121
136
|
};
|
|
122
137
|
};
|
|
@@ -654,3 +654,133 @@ describe("backup ownership dispatch", () => {
|
|
|
654
654
|
}
|
|
655
655
|
});
|
|
656
656
|
});
|
|
657
|
+
|
|
658
|
+
describe("error replies", () => {
|
|
659
|
+
const SECRET = "tok-1234567890-secret";
|
|
660
|
+
|
|
661
|
+
let dir = "";
|
|
662
|
+
let socket = "";
|
|
663
|
+
|
|
664
|
+
beforeEach(async () => {
|
|
665
|
+
dir = await mkdtemp(join(tmpdir(), "bridge-server-"));
|
|
666
|
+
socket = join(dir, "bridge.sock");
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
afterEach(async () => {
|
|
670
|
+
await rm(dir, {
|
|
671
|
+
force: true,
|
|
672
|
+
recursive: true,
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
test("hide a secret value from the error and the customer text", async () => {
|
|
677
|
+
const server = createServer({
|
|
678
|
+
...baseOptions(socket),
|
|
679
|
+
redact: (value) => value.split(SECRET).join("••••"),
|
|
680
|
+
onInvoke: async () => {
|
|
681
|
+
throw new BridgeUserError({
|
|
682
|
+
ar: `رفض ${SECRET}`,
|
|
683
|
+
en: `refused ${SECRET}`,
|
|
684
|
+
});
|
|
685
|
+
},
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const response = await call(socket, {
|
|
689
|
+
id: 9,
|
|
690
|
+
token: TOKEN,
|
|
691
|
+
method: BridgeMethod.Invoke,
|
|
692
|
+
module: "botDeploy",
|
|
693
|
+
action: "redeploy",
|
|
694
|
+
args: {},
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
server.stop(true);
|
|
698
|
+
|
|
699
|
+
expect(response).toEqual({
|
|
700
|
+
id: 9,
|
|
701
|
+
ok: false,
|
|
702
|
+
error: "refused ••••",
|
|
703
|
+
text: {
|
|
704
|
+
ar: "رفض ••••",
|
|
705
|
+
en: "refused ••••",
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
describe("failure logging", () => {
|
|
712
|
+
let dir = "";
|
|
713
|
+
let socket = "";
|
|
714
|
+
|
|
715
|
+
beforeEach(async () => {
|
|
716
|
+
dir = await mkdtemp(join(tmpdir(), "bridge-server-"));
|
|
717
|
+
socket = join(dir, "bridge.sock");
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
afterEach(async () => {
|
|
721
|
+
await rm(dir, {
|
|
722
|
+
force: true,
|
|
723
|
+
recursive: true,
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
const logged = async (error: unknown) => {
|
|
728
|
+
const lines: {
|
|
729
|
+
message: string;
|
|
730
|
+
fields: Record<string, unknown>;
|
|
731
|
+
}[] = [];
|
|
732
|
+
|
|
733
|
+
const server = createServer({
|
|
734
|
+
...baseOptions(socket),
|
|
735
|
+
onLog: (message, fields = {}) => {
|
|
736
|
+
lines.push({
|
|
737
|
+
message,
|
|
738
|
+
fields,
|
|
739
|
+
});
|
|
740
|
+
},
|
|
741
|
+
onInvoke: async () => {
|
|
742
|
+
throw error;
|
|
743
|
+
},
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
await call(socket, {
|
|
747
|
+
id: 3,
|
|
748
|
+
token: TOKEN,
|
|
749
|
+
method: BridgeMethod.Invoke,
|
|
750
|
+
module: "botDeploy",
|
|
751
|
+
action: "redeploy",
|
|
752
|
+
args: {},
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
server.stop(true);
|
|
756
|
+
|
|
757
|
+
return lines;
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
test("logs a refused action as a refusal with its message and no stack", async () => {
|
|
761
|
+
expect(
|
|
762
|
+
await logged(
|
|
763
|
+
new BridgeUserError({
|
|
764
|
+
ar: "فيه نشر شغّال الحين",
|
|
765
|
+
en: "a deploy is already running",
|
|
766
|
+
}),
|
|
767
|
+
),
|
|
768
|
+
).toEqual([
|
|
769
|
+
{
|
|
770
|
+
message: "bridge call refused",
|
|
771
|
+
fields: {
|
|
772
|
+
method: BridgeMethod.Invoke,
|
|
773
|
+
error: "a deploy is already running",
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
]);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
test("logs an ordinary error as a failure with its stack", async () => {
|
|
780
|
+
const [line] = await logged(new Error("the marker could not be written"));
|
|
781
|
+
|
|
782
|
+
expect(line?.message).toBe("bridge call failed");
|
|
783
|
+
expect(line?.fields.error).toBe("the marker could not be written");
|
|
784
|
+
expect(String(line?.fields.stack)).toContain("the marker could not be written");
|
|
785
|
+
});
|
|
786
|
+
});
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type BridgeStatus,
|
|
17
17
|
} from "../protocol";
|
|
18
18
|
import { type BridgeLogger, errorText } from "./bridgeLog";
|
|
19
|
+
import { redactLocalized } from "./bridgeRedaction";
|
|
19
20
|
|
|
20
21
|
export interface ServerOptions {
|
|
21
22
|
socket: string;
|
|
@@ -37,6 +38,7 @@ export interface ServerOptions {
|
|
|
37
38
|
onSetupStatus: () => BridgeSetupResult;
|
|
38
39
|
onSetupSubmit: (step: string, action: string, args: BridgeValues) => Promise<BridgeSetupResult>;
|
|
39
40
|
onLog: BridgeLogger;
|
|
41
|
+
redact?: (text: string) => string;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
interface IncomingFrame {
|
|
@@ -87,6 +89,23 @@ const asArgs = (value: unknown): BridgeValues => {
|
|
|
87
89
|
return out;
|
|
88
90
|
};
|
|
89
91
|
|
|
92
|
+
const logFailure = (onLog: BridgeLogger, method: string, error: unknown) => {
|
|
93
|
+
if (error instanceof BridgeUserError) {
|
|
94
|
+
onLog("bridge call refused", {
|
|
95
|
+
method,
|
|
96
|
+
error: error.message,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
onLog("bridge call failed", {
|
|
103
|
+
method,
|
|
104
|
+
error: errorText(error),
|
|
105
|
+
stack: error instanceof Error ? (error.stack ?? null) : null,
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
|
|
90
109
|
export const createServer = (options: ServerOptions) => {
|
|
91
110
|
const handle = async (method: string, request: IncomingFrame): Promise<unknown> => {
|
|
92
111
|
switch (method) {
|
|
@@ -270,17 +289,15 @@ export const createServer = (options: ServerOptions) => {
|
|
|
270
289
|
);
|
|
271
290
|
})
|
|
272
291
|
.catch((error: unknown) => {
|
|
273
|
-
options.onLog(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
stack: error instanceof Error ? (error.stack ?? null) : null,
|
|
277
|
-
});
|
|
292
|
+
logFailure(options.onLog, String(request.method ?? ""), error);
|
|
293
|
+
|
|
294
|
+
const redact = options.redact ?? ((text: string) => text);
|
|
278
295
|
|
|
279
296
|
socket.write(
|
|
280
297
|
encode({
|
|
281
298
|
id,
|
|
282
299
|
ok: false,
|
|
283
|
-
error: errorText(error),
|
|
300
|
+
error: redact(errorText(error)),
|
|
284
301
|
...(error instanceof BridgeSecretError
|
|
285
302
|
? {
|
|
286
303
|
secret: error.key,
|
|
@@ -293,7 +310,7 @@ export const createServer = (options: ServerOptions) => {
|
|
|
293
310
|
: {}),
|
|
294
311
|
...(error instanceof BridgeUserError
|
|
295
312
|
? {
|
|
296
|
-
text: error.text,
|
|
313
|
+
text: redactLocalized(error.text, redact),
|
|
297
314
|
}
|
|
298
315
|
: {}),
|
|
299
316
|
}),
|
|
@@ -51,6 +51,9 @@ const contextOf = (): BridgeContext => {
|
|
|
51
51
|
onSetup() {
|
|
52
52
|
return;
|
|
53
53
|
},
|
|
54
|
+
async onRestart() {
|
|
55
|
+
return;
|
|
56
|
+
},
|
|
54
57
|
});
|
|
55
58
|
};
|
|
56
59
|
|
|
@@ -301,3 +304,88 @@ describe("the setup handlers", () => {
|
|
|
301
304
|
);
|
|
302
305
|
});
|
|
303
306
|
});
|
|
307
|
+
|
|
308
|
+
describe("the setup handlers redaction", () => {
|
|
309
|
+
const SECRET = "123456789:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw";
|
|
310
|
+
|
|
311
|
+
const redact = (value: string) => value.split(SECRET).join("••••");
|
|
312
|
+
|
|
313
|
+
test("blanks a secret value in the prompt a driver reports", () => {
|
|
314
|
+
const { logger } = recorder();
|
|
315
|
+
const handlers = createSetupHandlers(driverWith(setupWith()), logger, redact);
|
|
316
|
+
|
|
317
|
+
handlers.onSetup({
|
|
318
|
+
steps: [
|
|
319
|
+
{
|
|
320
|
+
id: "sign-in",
|
|
321
|
+
state: BridgeSetupStepState.Active,
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
prompt: {
|
|
325
|
+
kind: BridgeSetupPromptKind.Progress,
|
|
326
|
+
message: text(`checking ${SECRET}`),
|
|
327
|
+
percent: 50,
|
|
328
|
+
detail: `token ${SECRET}`,
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
expect(handlers.onSetupStatus().runtime?.prompt).toEqual({
|
|
333
|
+
kind: BridgeSetupPromptKind.Progress,
|
|
334
|
+
message: text("checking ••••"),
|
|
335
|
+
percent: 50,
|
|
336
|
+
detail: "token ••••",
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("blanks a secret value in every text of a link prompt", () => {
|
|
341
|
+
const { logger } = recorder();
|
|
342
|
+
const handlers = createSetupHandlers(driverWith(setupWith()), logger, redact);
|
|
343
|
+
const reported = signIn();
|
|
344
|
+
|
|
345
|
+
handlers.onSetup({
|
|
346
|
+
...reported,
|
|
347
|
+
prompt: {
|
|
348
|
+
kind: BridgeSetupPromptKind.Link,
|
|
349
|
+
message: text(`sign in ${SECRET}`),
|
|
350
|
+
url: "https://hytale.com/device",
|
|
351
|
+
code: "ABCD-EFGH",
|
|
352
|
+
expiresAt: null,
|
|
353
|
+
actions: [
|
|
354
|
+
{
|
|
355
|
+
id: "renew",
|
|
356
|
+
label: text(`renew ${SECRET}`),
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
expect(JSON.stringify(handlers.current())).not.toContain(SECRET);
|
|
363
|
+
expect(handlers.current()?.prompt).toMatchObject({
|
|
364
|
+
message: text("sign in ••••"),
|
|
365
|
+
url: "https://hytale.com/device",
|
|
366
|
+
code: "ABCD-EFGH",
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("blanks a secret value in the text of a rejected submit", async () => {
|
|
371
|
+
const { logger } = recorder();
|
|
372
|
+
const handlers = createSetupHandlers(
|
|
373
|
+
driverWith(
|
|
374
|
+
setupWith(async () => {
|
|
375
|
+
throw new BridgeUserError(text(`telegram refused ${SECRET}`));
|
|
376
|
+
}),
|
|
377
|
+
),
|
|
378
|
+
logger,
|
|
379
|
+
redact,
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
handlers.onSetup(signIn());
|
|
383
|
+
|
|
384
|
+
const result = await handlers.onSetupSubmit(contextOf(), "sign-in", "renew", {});
|
|
385
|
+
|
|
386
|
+
expect(result.runtime?.prompt).toMatchObject({
|
|
387
|
+
kind: BridgeSetupPromptKind.Failed,
|
|
388
|
+
message: text("telegram refused ••••"),
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
});
|
|
@@ -3,6 +3,7 @@ import { BridgeUserError, type BridgeValues } from "../core";
|
|
|
3
3
|
import type { BridgeDriver } from "../modules";
|
|
4
4
|
import type { BridgeSetupResult, BridgeSetupRuntime } from "../protocol";
|
|
5
5
|
import type { BridgeLeveledLogger } from "./bridgeLog";
|
|
6
|
+
import { type BridgeRedact, redactLocalized, redactSetupRuntime } from "./bridgeRedaction";
|
|
6
7
|
import { failedSetup, normalizeSetupRuntime, unsupportedSetup } from "./bridgeSetup";
|
|
7
8
|
|
|
8
9
|
export interface BridgeSetupHandlers {
|
|
@@ -12,7 +13,13 @@ export interface BridgeSetupHandlers {
|
|
|
12
13
|
current(): BridgeSetupRuntime | null;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
const unredacted: BridgeRedact = (text) => text;
|
|
17
|
+
|
|
18
|
+
export const createSetupHandlers = (
|
|
19
|
+
driver: BridgeDriver,
|
|
20
|
+
log: BridgeLeveledLogger,
|
|
21
|
+
redact: BridgeRedact = unredacted,
|
|
22
|
+
): BridgeSetupHandlers => {
|
|
16
23
|
const declared: ReadonlySet<string> = new Set((driver.setup?.steps ?? []).map((step) => step.id));
|
|
17
24
|
|
|
18
25
|
let current: BridgeSetupRuntime | null = null;
|
|
@@ -20,10 +27,13 @@ export const createSetupHandlers = (driver: BridgeDriver, log: BridgeLeveledLogg
|
|
|
20
27
|
return {
|
|
21
28
|
onSetup(runtime) {
|
|
22
29
|
current = driver.setup
|
|
23
|
-
?
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
? redactSetupRuntime(
|
|
31
|
+
normalizeSetupRuntime(runtime, {
|
|
32
|
+
declared,
|
|
33
|
+
warn: log.warn,
|
|
34
|
+
}),
|
|
35
|
+
redact,
|
|
36
|
+
)
|
|
27
37
|
: null;
|
|
28
38
|
},
|
|
29
39
|
current() {
|
|
@@ -59,7 +69,7 @@ export const createSetupHandlers = (driver: BridgeDriver, log: BridgeLeveledLogg
|
|
|
59
69
|
error: error.message,
|
|
60
70
|
});
|
|
61
71
|
|
|
62
|
-
return failedSetup(current, error.text);
|
|
72
|
+
return failedSetup(current, redactLocalized(error.text, redact));
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
return {
|