@serverkgg/bridge 0.1.1 → 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.
- package/package.json +1 -1
- package/src/bot/botPanel.test.ts +30 -0
- package/src/bot/botPanel.ts +9 -1
- package/src/cli/validate.test.ts +73 -1
- package/src/cli/validate.ts +35 -1
- package/src/context/bridgeContext.ts +6 -1
- package/src/core/bridgeAction.ts +2 -0
- package/src/core/bridgeActivity.ts +9 -0
- package/src/core/bridgeAvailability.ts +10 -0
- package/src/core/bridgeUserError.test.ts +10 -0
- package/src/core/bridgeUserError.ts +6 -2
- package/src/core/describeDownloadFailure.test.ts +58 -0
- package/src/core/describeDownloadFailure.ts +69 -0
- package/src/core/index.ts +3 -0
- package/src/modules/bridge.ts +8 -1
- package/src/modules/bridgeModule.ts +13 -1
- package/src/panel/bridgeDetail.ts +2 -1
- package/src/protocol/bridgeActivity.ts +54 -0
- package/src/protocol/bridgeEvents.ts +30 -0
- package/src/protocol/bridgeFrame.ts +10 -1
- package/src/protocol/index.ts +1 -0
- package/src/runtime/bridgeActions.ts +19 -2
- package/src/runtime/bridgeActivity.test.ts +318 -0
- package/src/runtime/bridgeActivity.ts +268 -0
- package/src/runtime/bridgeContext.test.ts +6 -0
- package/src/runtime/bridgeContext.ts +8 -1
- package/src/runtime/bridgeDriver.test.ts +21 -0
- package/src/runtime/bridgeDriver.ts +10 -7
- package/src/runtime/bridgeLog.ts +10 -0
- package/src/runtime/bridgeRefusalError.ts +13 -0
- package/src/runtime/bridgeRegistry.test.ts +1053 -10
- package/src/runtime/bridgeRegistry.ts +464 -65
- package/src/runtime/bridgeScopedContext.ts +55 -0
- package/src/runtime/bridgeServer.test.ts +404 -4
- package/src/runtime/bridgeServer.ts +125 -35
- package/src/runtime/bridgeSetupHandlers.test.ts +69 -6
- package/src/runtime/bridgeSetupHandlers.ts +45 -10
- package/src/runtime/serve.ts +45 -3
- package/src/steam/steamcmd.test.ts +32 -1
- package/src/steam/steamcmd.ts +27 -4
- package/src/utils/changedSettingKeys.test.ts +65 -0
- package/src/utils/changedSettingKeys.ts +6 -0
- package/src/utils/describeExecFailure.test.ts +34 -0
- package/src/utils/describeExecFailure.ts +9 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/sameSettingValue.test.ts +35 -0
- package/src/utils/sameSettingValue.ts +40 -0
- package/src/wire/activity.test.ts +95 -0
- package/src/wire/activity.ts +41 -0
- package/src/wire/index.ts +1 -0
- package/src/wire/panel.test.ts +67 -1
- package/src/wire/panel.ts +11 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { timingSafeEqual } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import type { Socket } from "bun";
|
|
3
|
+
import {
|
|
4
|
+
type BridgeActivity,
|
|
5
|
+
BridgeFailureError,
|
|
6
|
+
BridgeSecretError,
|
|
7
|
+
BridgeUserError,
|
|
8
|
+
type BridgeValues,
|
|
9
|
+
} from "../core";
|
|
3
10
|
import {
|
|
4
11
|
BRIDGE_FRAME_LIMIT,
|
|
5
12
|
type BridgeAnnounceResult,
|
|
@@ -15,8 +22,10 @@ import {
|
|
|
15
22
|
type BridgeSetupResult,
|
|
16
23
|
type BridgeStatus,
|
|
17
24
|
} from "../protocol";
|
|
18
|
-
import
|
|
25
|
+
import type { BridgeActivityAnswer } from "./bridgeActivity";
|
|
26
|
+
import { type BridgeLogger, errorDetail } from "./bridgeLog";
|
|
19
27
|
import { redactLocalized } from "./bridgeRedaction";
|
|
28
|
+
import { BridgeRefusalError } from "./bridgeRefusalError";
|
|
20
29
|
|
|
21
30
|
export interface ServerOptions {
|
|
22
31
|
socket: string;
|
|
@@ -26,7 +35,12 @@ export interface ServerOptions {
|
|
|
26
35
|
onStart: () => Promise<void>;
|
|
27
36
|
onStop: () => Promise<void>;
|
|
28
37
|
onCommand: (input: string, expect: string | undefined, timeoutMs: number | undefined) => Promise<BridgeCommandResult>;
|
|
29
|
-
onInvoke: (
|
|
38
|
+
onInvoke: (
|
|
39
|
+
module: string,
|
|
40
|
+
action: string,
|
|
41
|
+
args: BridgeValues,
|
|
42
|
+
recoveryOperationId?: string,
|
|
43
|
+
) => Promise<BridgeActivityAnswer<unknown>>;
|
|
30
44
|
onEvents: (cursor: number) => BridgeEventsResult;
|
|
31
45
|
onSample: () => Promise<BridgeSampleResult>;
|
|
32
46
|
onQuiesce: (supportsSnapshot: boolean, leaseId?: string) => Promise<BridgeQuiesceResult>;
|
|
@@ -36,11 +50,17 @@ export interface ServerOptions {
|
|
|
36
50
|
onPendingList: () => Promise<BridgePendingResult>;
|
|
37
51
|
onPendingAdd: (input: string) => Promise<BridgePendingResult>;
|
|
38
52
|
onSetupStatus: () => BridgeSetupResult;
|
|
39
|
-
onSetupSubmit: (step: string, action: string, args: BridgeValues) => Promise<BridgeSetupResult
|
|
53
|
+
onSetupSubmit: (step: string, action: string, args: BridgeValues) => Promise<BridgeActivityAnswer<BridgeSetupResult>>;
|
|
54
|
+
onUndelivered: (activity: BridgeActivity[]) => void;
|
|
40
55
|
onLog: BridgeLogger;
|
|
41
56
|
redact?: (text: string) => string;
|
|
42
57
|
}
|
|
43
58
|
|
|
59
|
+
interface SocketState {
|
|
60
|
+
buffer: string;
|
|
61
|
+
closed: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
44
64
|
interface IncomingFrame {
|
|
45
65
|
leaseId?: unknown;
|
|
46
66
|
supportsSnapshot?: unknown;
|
|
@@ -73,6 +93,23 @@ const encode = (response: BridgeResponse) => {
|
|
|
73
93
|
return `${JSON.stringify(response)}\n`;
|
|
74
94
|
};
|
|
75
95
|
|
|
96
|
+
interface PlainAnswer {
|
|
97
|
+
result: unknown;
|
|
98
|
+
activity: null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const deliver = (socket: Socket<SocketState>, frame: string): boolean => {
|
|
102
|
+
if (socket.data.closed) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
return socket.write(frame) === Buffer.byteLength(frame);
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
76
113
|
const asArgs = (value: unknown): BridgeValues => {
|
|
77
114
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
78
115
|
return {};
|
|
@@ -90,10 +127,10 @@ const asArgs = (value: unknown): BridgeValues => {
|
|
|
90
127
|
};
|
|
91
128
|
|
|
92
129
|
const logFailure = (onLog: BridgeLogger, method: string, error: unknown) => {
|
|
93
|
-
if (error instanceof BridgeUserError) {
|
|
130
|
+
if (error instanceof BridgeUserError || error instanceof BridgeRefusalError) {
|
|
94
131
|
onLog("bridge call refused", {
|
|
95
132
|
method,
|
|
96
|
-
error: error
|
|
133
|
+
error: errorDetail(error),
|
|
97
134
|
});
|
|
98
135
|
|
|
99
136
|
return;
|
|
@@ -101,7 +138,7 @@ const logFailure = (onLog: BridgeLogger, method: string, error: unknown) => {
|
|
|
101
138
|
|
|
102
139
|
onLog("bridge call failed", {
|
|
103
140
|
method,
|
|
104
|
-
error:
|
|
141
|
+
error: errorDetail(error),
|
|
105
142
|
stack: error instanceof Error ? (error.stack ?? null) : null,
|
|
106
143
|
});
|
|
107
144
|
};
|
|
@@ -137,15 +174,6 @@ export const createServer = (options: ServerOptions) => {
|
|
|
137
174
|
);
|
|
138
175
|
}
|
|
139
176
|
|
|
140
|
-
case BridgeMethod.Invoke: {
|
|
141
|
-
return await options.onInvoke(
|
|
142
|
-
String(request.module ?? ""),
|
|
143
|
-
String(request.action ?? ""),
|
|
144
|
-
asArgs(request.args),
|
|
145
|
-
asText(request.recoveryOperationId) ?? undefined,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
177
|
case BridgeMethod.Events: {
|
|
150
178
|
return options.onEvents(typeof request.cursor === "number" ? request.cursor : 0);
|
|
151
179
|
}
|
|
@@ -196,33 +224,68 @@ export const createServer = (options: ServerOptions) => {
|
|
|
196
224
|
return options.onSetupStatus();
|
|
197
225
|
}
|
|
198
226
|
|
|
199
|
-
|
|
200
|
-
|
|
227
|
+
default: {
|
|
228
|
+
throw new Error(`unknown method "${method}"`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
};
|
|
201
232
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
233
|
+
const answer = async (
|
|
234
|
+
method: string,
|
|
235
|
+
request: IncomingFrame,
|
|
236
|
+
): Promise<BridgeActivityAnswer<unknown> | PlainAnswer> => {
|
|
237
|
+
if (method === BridgeMethod.Invoke) {
|
|
238
|
+
return await options.onInvoke(
|
|
239
|
+
String(request.module ?? ""),
|
|
240
|
+
String(request.action ?? ""),
|
|
241
|
+
asArgs(request.args),
|
|
242
|
+
asText(request.recoveryOperationId) ?? undefined,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
205
245
|
|
|
206
|
-
|
|
207
|
-
|
|
246
|
+
if (method === BridgeMethod.SetupSubmit) {
|
|
247
|
+
const step = asText(request.module);
|
|
208
248
|
|
|
209
|
-
|
|
210
|
-
throw new Error(
|
|
249
|
+
if (!step) {
|
|
250
|
+
throw new Error("setupSubmit needs a step id");
|
|
211
251
|
}
|
|
252
|
+
|
|
253
|
+
return await options.onSetupSubmit(step, String(request.action ?? ""), asArgs(request.args));
|
|
212
254
|
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
result: await handle(method, request),
|
|
258
|
+
activity: null,
|
|
259
|
+
};
|
|
213
260
|
};
|
|
214
261
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
262
|
+
const undelivered = (method: string, activity: BridgeActivity[]) => {
|
|
263
|
+
if (activity.length === 0) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
options.onLog("answer not delivered, its activity goes to the events ring", {
|
|
268
|
+
method,
|
|
269
|
+
entries: activity.length,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
options.onUndelivered(activity);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
return Bun.listen<SocketState>({
|
|
218
276
|
unix: options.socket,
|
|
219
277
|
socket: {
|
|
220
278
|
open(socket) {
|
|
221
279
|
socket.data = {
|
|
222
280
|
buffer: "",
|
|
281
|
+
closed: false,
|
|
223
282
|
};
|
|
224
283
|
},
|
|
225
284
|
|
|
285
|
+
close(socket) {
|
|
286
|
+
socket.data.closed = true;
|
|
287
|
+
},
|
|
288
|
+
|
|
226
289
|
data(socket, chunk) {
|
|
227
290
|
socket.data.buffer += chunk.toString();
|
|
228
291
|
|
|
@@ -278,15 +341,32 @@ export const createServer = (options: ServerOptions) => {
|
|
|
278
341
|
continue;
|
|
279
342
|
}
|
|
280
343
|
|
|
281
|
-
void
|
|
282
|
-
.then((result) => {
|
|
283
|
-
|
|
284
|
-
|
|
344
|
+
void answer(String(request.method ?? ""), request)
|
|
345
|
+
.then(({ result, activity }) => {
|
|
346
|
+
const method = String(request.method ?? "");
|
|
347
|
+
|
|
348
|
+
let frame: string;
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
frame = encode({
|
|
285
352
|
id,
|
|
286
353
|
ok: true,
|
|
287
354
|
result,
|
|
288
|
-
|
|
289
|
-
|
|
355
|
+
...(activity === null
|
|
356
|
+
? {}
|
|
357
|
+
: {
|
|
358
|
+
activity,
|
|
359
|
+
}),
|
|
360
|
+
});
|
|
361
|
+
} catch (error) {
|
|
362
|
+
undelivered(method, activity ?? []);
|
|
363
|
+
|
|
364
|
+
throw error;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!deliver(socket, frame)) {
|
|
368
|
+
undelivered(method, activity ?? []);
|
|
369
|
+
}
|
|
290
370
|
})
|
|
291
371
|
.catch((error: unknown) => {
|
|
292
372
|
logFailure(options.onLog, String(request.method ?? ""), error);
|
|
@@ -297,7 +377,7 @@ export const createServer = (options: ServerOptions) => {
|
|
|
297
377
|
encode({
|
|
298
378
|
id,
|
|
299
379
|
ok: false,
|
|
300
|
-
error: redact(
|
|
380
|
+
error: redact(errorDetail(error)),
|
|
301
381
|
...(error instanceof BridgeSecretError
|
|
302
382
|
? {
|
|
303
383
|
secret: error.key,
|
|
@@ -313,6 +393,16 @@ export const createServer = (options: ServerOptions) => {
|
|
|
313
393
|
text: redactLocalized(error.text, redact),
|
|
314
394
|
}
|
|
315
395
|
: {}),
|
|
396
|
+
...(error instanceof BridgeRefusalError
|
|
397
|
+
? {
|
|
398
|
+
unchanged: true,
|
|
399
|
+
...(error.text
|
|
400
|
+
? {
|
|
401
|
+
text: redactLocalized(error.text, redact),
|
|
402
|
+
}
|
|
403
|
+
: {}),
|
|
404
|
+
}
|
|
405
|
+
: {}),
|
|
316
406
|
}),
|
|
317
407
|
);
|
|
318
408
|
});
|
|
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import type { BridgeContext } from "../context";
|
|
4
4
|
import { BridgeKind, BridgeSetupPromptKind, BridgeSetupStepKind, BridgeSetupStepState, BridgeUserError } from "../core";
|
|
5
5
|
import type { BridgeDriver, BridgeSetupModule } from "../modules";
|
|
6
|
-
import type
|
|
6
|
+
import { type BridgeConfig, BridgeEventName, type BridgeSetupRuntime } from "../protocol";
|
|
7
7
|
import { createContext } from "./bridgeContext";
|
|
8
8
|
import type { BridgeLeveledLogger } from "./bridgeLog";
|
|
9
9
|
import { log } from "./bridgeLog";
|
|
@@ -51,6 +51,9 @@ const contextOf = (): BridgeContext => {
|
|
|
51
51
|
onSetup() {
|
|
52
52
|
return;
|
|
53
53
|
},
|
|
54
|
+
onActivity() {
|
|
55
|
+
return;
|
|
56
|
+
},
|
|
54
57
|
async onRestart() {
|
|
55
58
|
return;
|
|
56
59
|
},
|
|
@@ -146,7 +149,7 @@ describe("the setup handlers", () => {
|
|
|
146
149
|
runtime: null,
|
|
147
150
|
});
|
|
148
151
|
|
|
149
|
-
expect(await handlers.onSetupSubmit(contextOf(), "sign-in", "renew", {})).toEqual({
|
|
152
|
+
expect((await handlers.onSetupSubmit(contextOf(), "sign-in", "renew", {})).result).toEqual({
|
|
150
153
|
supported: false,
|
|
151
154
|
runtime: null,
|
|
152
155
|
});
|
|
@@ -207,7 +210,7 @@ describe("the setup handlers", () => {
|
|
|
207
210
|
const { logger } = recorder();
|
|
208
211
|
const handlers = createSetupHandlers(driverWith(setupWith()), logger);
|
|
209
212
|
|
|
210
|
-
expect(await handlers.onSetupSubmit(contextOf(), "sign-in", "renew", {})).toEqual({
|
|
213
|
+
expect((await handlers.onSetupSubmit(contextOf(), "sign-in", "renew", {})).result).toEqual({
|
|
211
214
|
supported: false,
|
|
212
215
|
runtime: null,
|
|
213
216
|
});
|
|
@@ -227,7 +230,7 @@ describe("the setup handlers", () => {
|
|
|
227
230
|
|
|
228
231
|
handlers.onSetup(signIn());
|
|
229
232
|
|
|
230
|
-
expect(await handlers.onSetupSubmit(contextOf(), "sign-in", "renew", {})).toEqual({
|
|
233
|
+
expect((await handlers.onSetupSubmit(contextOf(), "sign-in", "renew", {})).result).toEqual({
|
|
231
234
|
supported: true,
|
|
232
235
|
runtime: {
|
|
233
236
|
steps: [
|
|
@@ -251,6 +254,65 @@ describe("the setup handlers", () => {
|
|
|
251
254
|
expect(warnings.at(-1)?.message).toBe("setup submit rejected");
|
|
252
255
|
});
|
|
253
256
|
|
|
257
|
+
test("add one rejected entry built from the user error", async () => {
|
|
258
|
+
const { logger } = recorder();
|
|
259
|
+
|
|
260
|
+
const handlers = createSetupHandlers(
|
|
261
|
+
driverWith(
|
|
262
|
+
setupWith(async () => {
|
|
263
|
+
throw new BridgeUserError({
|
|
264
|
+
ar: "الكود ما عاد ينفع",
|
|
265
|
+
en: "this code cannot be renewed any more",
|
|
266
|
+
});
|
|
267
|
+
}),
|
|
268
|
+
),
|
|
269
|
+
logger,
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
handlers.onSetup(signIn());
|
|
273
|
+
|
|
274
|
+
const { activity } = await handlers.onSetupSubmit(contextOf(), "sign-in", "renew", {});
|
|
275
|
+
|
|
276
|
+
expect(activity).toEqual([
|
|
277
|
+
{
|
|
278
|
+
name: BridgeEventName.SetupRejected,
|
|
279
|
+
text: {
|
|
280
|
+
ar: "الكود ما عاد ينفع",
|
|
281
|
+
en: "this code cannot be renewed any more",
|
|
282
|
+
},
|
|
283
|
+
details: {
|
|
284
|
+
step: "sign-in",
|
|
285
|
+
action: "renew",
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
]);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("answer what the driver reported while submitting", async () => {
|
|
292
|
+
const { logger } = recorder();
|
|
293
|
+
|
|
294
|
+
const handlers = createSetupHandlers(
|
|
295
|
+
driverWith(
|
|
296
|
+
setupWith(async (context) => {
|
|
297
|
+
context.activity.report({
|
|
298
|
+
name: BridgeEventName.SetupCompleted,
|
|
299
|
+
text: text("signed in"),
|
|
300
|
+
});
|
|
301
|
+
}),
|
|
302
|
+
),
|
|
303
|
+
logger,
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const { activity } = await handlers.onSetupSubmit(contextOf(), "sign-in", "renew", {});
|
|
307
|
+
|
|
308
|
+
expect(activity).toEqual([
|
|
309
|
+
{
|
|
310
|
+
name: BridgeEventName.SetupCompleted,
|
|
311
|
+
text: text("signed in"),
|
|
312
|
+
},
|
|
313
|
+
]);
|
|
314
|
+
});
|
|
315
|
+
|
|
254
316
|
test("answer the runtime the driver reported while submitting", async () => {
|
|
255
317
|
const { logger } = recorder();
|
|
256
318
|
|
|
@@ -273,7 +335,7 @@ describe("the setup handlers", () => {
|
|
|
273
335
|
|
|
274
336
|
handlers.onSetup(signIn());
|
|
275
337
|
|
|
276
|
-
expect(await handlers.onSetupSubmit(contextOf(), "sign-in", "renew", {})).toEqual({
|
|
338
|
+
expect((await handlers.onSetupSubmit(contextOf(), "sign-in", "renew", {})).result).toEqual({
|
|
277
339
|
supported: true,
|
|
278
340
|
runtime: {
|
|
279
341
|
steps: [
|
|
@@ -381,11 +443,12 @@ describe("the setup handlers redaction", () => {
|
|
|
381
443
|
|
|
382
444
|
handlers.onSetup(signIn());
|
|
383
445
|
|
|
384
|
-
const result = await handlers.onSetupSubmit(contextOf(), "sign-in", "renew", {});
|
|
446
|
+
const { result, activity } = await handlers.onSetupSubmit(contextOf(), "sign-in", "renew", {});
|
|
385
447
|
|
|
386
448
|
expect(result.runtime?.prompt).toMatchObject({
|
|
387
449
|
kind: BridgeSetupPromptKind.Failed,
|
|
388
450
|
message: text("telegram refused ••••"),
|
|
389
451
|
});
|
|
452
|
+
expect(JSON.stringify(activity)).not.toContain(SECRET);
|
|
390
453
|
});
|
|
391
454
|
});
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import type { BridgeContext } from "../context";
|
|
2
2
|
import { BridgeUserError, type BridgeValues } from "../core";
|
|
3
3
|
import type { BridgeDriver } from "../modules";
|
|
4
|
-
import type
|
|
5
|
-
import type
|
|
6
|
-
import { type
|
|
4
|
+
import { BridgeEventName, type BridgeSetupResult, type BridgeSetupRuntime } from "../protocol";
|
|
5
|
+
import { type ActivityNormalizeOptions, type BridgeActivityAnswer, createActivityScope } from "./bridgeActivity";
|
|
6
|
+
import { type BridgeLeveledLogger, errorDetail } from "./bridgeLog";
|
|
7
|
+
import { type BridgeRedact, redactLocalized, redactSetupRuntime, secretFieldKeys } from "./bridgeRedaction";
|
|
8
|
+
import { scopedContext } from "./bridgeScopedContext";
|
|
7
9
|
import { failedSetup, normalizeSetupRuntime, unsupportedSetup } from "./bridgeSetup";
|
|
8
10
|
|
|
9
11
|
export interface BridgeSetupHandlers {
|
|
10
12
|
onSetup(runtime: BridgeSetupRuntime | null): void;
|
|
11
13
|
onSetupStatus(): BridgeSetupResult;
|
|
12
|
-
onSetupSubmit(
|
|
14
|
+
onSetupSubmit(
|
|
15
|
+
context: BridgeContext,
|
|
16
|
+
step: string,
|
|
17
|
+
action: string,
|
|
18
|
+
args: BridgeValues,
|
|
19
|
+
): Promise<BridgeActivityAnswer<BridgeSetupResult>>;
|
|
13
20
|
current(): BridgeSetupRuntime | null;
|
|
14
21
|
}
|
|
15
22
|
|
|
@@ -22,6 +29,12 @@ export const createSetupHandlers = (
|
|
|
22
29
|
): BridgeSetupHandlers => {
|
|
23
30
|
const declared: ReadonlySet<string> = new Set((driver.setup?.steps ?? []).map((step) => step.id));
|
|
24
31
|
|
|
32
|
+
const activity: ActivityNormalizeOptions = {
|
|
33
|
+
secretKeys: new Set(secretFieldKeys(driver)),
|
|
34
|
+
redact,
|
|
35
|
+
warn: log.warn,
|
|
36
|
+
};
|
|
37
|
+
|
|
25
38
|
let current: BridgeSetupRuntime | null = null;
|
|
26
39
|
|
|
27
40
|
return {
|
|
@@ -53,28 +66,50 @@ export const createSetupHandlers = (
|
|
|
53
66
|
const module = driver.setup;
|
|
54
67
|
|
|
55
68
|
if (!module?.submit) {
|
|
56
|
-
return
|
|
69
|
+
return {
|
|
70
|
+
result: unsupportedSetup(),
|
|
71
|
+
activity: [],
|
|
72
|
+
};
|
|
57
73
|
}
|
|
58
74
|
|
|
75
|
+
const scope = createActivityScope(context.activity, activity);
|
|
76
|
+
|
|
59
77
|
try {
|
|
60
|
-
await module.submit(context, step, action, args);
|
|
78
|
+
await module.submit(scopedContext(context, scope.reporter), step, action, args);
|
|
61
79
|
} catch (error) {
|
|
62
80
|
if (!(error instanceof BridgeUserError)) {
|
|
81
|
+
scope.drain();
|
|
82
|
+
|
|
63
83
|
throw error;
|
|
64
84
|
}
|
|
65
85
|
|
|
66
86
|
log.warn("setup submit rejected", {
|
|
67
87
|
step,
|
|
68
88
|
action,
|
|
69
|
-
error: error
|
|
89
|
+
error: errorDetail(error),
|
|
70
90
|
});
|
|
71
91
|
|
|
72
|
-
|
|
92
|
+
scope.reporter.report({
|
|
93
|
+
name: BridgeEventName.SetupRejected,
|
|
94
|
+
text: error.text,
|
|
95
|
+
details: {
|
|
96
|
+
step,
|
|
97
|
+
action,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
result: failedSetup(current, redactLocalized(error.text, redact)),
|
|
103
|
+
activity: scope.close(),
|
|
104
|
+
};
|
|
73
105
|
}
|
|
74
106
|
|
|
75
107
|
return {
|
|
76
|
-
|
|
77
|
-
|
|
108
|
+
result: {
|
|
109
|
+
supported: true,
|
|
110
|
+
runtime: current,
|
|
111
|
+
},
|
|
112
|
+
activity: scope.close(),
|
|
78
113
|
};
|
|
79
114
|
},
|
|
80
115
|
};
|
package/src/runtime/serve.ts
CHANGED
|
@@ -5,10 +5,12 @@ import {
|
|
|
5
5
|
BRIDGE_INSTALL_ERROR_LIMIT,
|
|
6
6
|
BRIDGE_VERSION,
|
|
7
7
|
type BridgeConfig,
|
|
8
|
+
BridgeEventName,
|
|
8
9
|
type BridgePendingResult,
|
|
9
10
|
type BridgeSampleResult,
|
|
10
11
|
type BridgeStatus,
|
|
11
12
|
} from "../protocol";
|
|
13
|
+
import { type ActivityNormalizeOptions, createActivityReporter } from "./bridgeActivity";
|
|
12
14
|
import { createBackupLease } from "./bridgeBackupLease";
|
|
13
15
|
import { configPath, readConfig, socketPath, statePath } from "./bridgeConfigReader";
|
|
14
16
|
import { createConsoleHandler } from "./bridgeConsoleHandler";
|
|
@@ -17,7 +19,7 @@ import { validateDriver } from "./bridgeDriver";
|
|
|
17
19
|
import { createEventRing } from "./bridgeEvents";
|
|
18
20
|
import { decideInstall, recountHold } from "./bridgeInstall";
|
|
19
21
|
import { createBridgeLiveConfig } from "./bridgeLiveConfig";
|
|
20
|
-
import { errorText, log as stdoutLog } from "./bridgeLog";
|
|
22
|
+
import { errorDetail, errorText, log as stdoutLog } from "./bridgeLog";
|
|
21
23
|
import { pendingResult, rejectedPending, unsupportedPending } from "./bridgePending";
|
|
22
24
|
import { createBridgeRedaction, redactLocalized, redactLogger, secretFieldKeys } from "./bridgeRedaction";
|
|
23
25
|
import { createRegistry } from "./bridgeRegistry";
|
|
@@ -37,6 +39,17 @@ const DEFAULT_STOP_TIMEOUT_SECONDS = 30;
|
|
|
37
39
|
|
|
38
40
|
const DEFAULT_SAMPLE_REFRESH_SECONDS = 30;
|
|
39
41
|
|
|
42
|
+
const INSTALL_FAILED_TEXT: Record<BridgeLifecycleSubject, LocalizedText> = {
|
|
43
|
+
[BridgeLifecycleSubject.Game]: {
|
|
44
|
+
ar: "ما قدرنا نثبّت اللعبة. جرّب مرة ثانية.",
|
|
45
|
+
en: "We could not install the game. Try again.",
|
|
46
|
+
},
|
|
47
|
+
[BridgeLifecycleSubject.Bot]: {
|
|
48
|
+
ar: "ما قدرنا نثبّت كود بوتك. جرّب مرة ثانية.",
|
|
49
|
+
en: "We could not install your bot's code. Try again.",
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
40
53
|
const prepare = async (config: BridgeConfig) => {
|
|
41
54
|
await mkdir(config.cwd, {
|
|
42
55
|
recursive: true,
|
|
@@ -130,6 +143,19 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
130
143
|
|
|
131
144
|
const ring = createEventRing(driver.events, supervisor);
|
|
132
145
|
|
|
146
|
+
const activity: ActivityNormalizeOptions = {
|
|
147
|
+
secretKeys: new Set(secretFieldKeys(driver)),
|
|
148
|
+
redact,
|
|
149
|
+
warn: log.warn,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const reporter = createActivityReporter({
|
|
153
|
+
...activity,
|
|
154
|
+
onEmit(event, payload) {
|
|
155
|
+
ring.emit(event, payload);
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
133
159
|
const context = createContext(
|
|
134
160
|
config,
|
|
135
161
|
supervisor,
|
|
@@ -141,6 +167,9 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
141
167
|
onSetup(runtime) {
|
|
142
168
|
setup.onSetup(runtime);
|
|
143
169
|
},
|
|
170
|
+
onActivity(entry) {
|
|
171
|
+
reporter.report(entry);
|
|
172
|
+
},
|
|
144
173
|
async onRestart() {
|
|
145
174
|
if (current !== BridgeDesired.Running) {
|
|
146
175
|
throw new Error(
|
|
@@ -154,7 +183,7 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
154
183
|
live,
|
|
155
184
|
);
|
|
156
185
|
|
|
157
|
-
const registry = createRegistry(driver, context, supervisor, ring);
|
|
186
|
+
const registry = createRegistry(driver, context, supervisor, ring, activity);
|
|
158
187
|
const backupLease = createBackupLease(async () => {
|
|
159
188
|
if (supervisor.running) {
|
|
160
189
|
await driver.backup?.release?.(context);
|
|
@@ -409,6 +438,11 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
409
438
|
onSetupStatus() {
|
|
410
439
|
return setup.onSetupStatus();
|
|
411
440
|
},
|
|
441
|
+
onUndelivered(activity) {
|
|
442
|
+
for (const entry of activity) {
|
|
443
|
+
reporter.report(entry);
|
|
444
|
+
}
|
|
445
|
+
},
|
|
412
446
|
async onSetupSubmit(step, action, args) {
|
|
413
447
|
return await setup.onSetupSubmit(context, step, action, args);
|
|
414
448
|
},
|
|
@@ -462,7 +496,7 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
462
496
|
|
|
463
497
|
log("install complete");
|
|
464
498
|
} catch (error) {
|
|
465
|
-
installError = redact(
|
|
499
|
+
installError = redact(errorDetail(error)).slice(0, BRIDGE_INSTALL_ERROR_LIMIT);
|
|
466
500
|
installErrorText = error instanceof BridgeUserError ? redactLocalized(error.text, redact) : null;
|
|
467
501
|
|
|
468
502
|
supervisor.setRuntime(BridgeRuntime.Crashed);
|
|
@@ -471,6 +505,14 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
471
505
|
error: installError,
|
|
472
506
|
});
|
|
473
507
|
|
|
508
|
+
context.activity.report({
|
|
509
|
+
name: BridgeEventName.InstallFailed,
|
|
510
|
+
text:
|
|
511
|
+
error instanceof BridgeUserError
|
|
512
|
+
? error.text
|
|
513
|
+
: INSTALL_FAILED_TEXT[driver.lifecycle.subject ?? BridgeLifecycleSubject.Game],
|
|
514
|
+
});
|
|
515
|
+
|
|
474
516
|
return;
|
|
475
517
|
}
|
|
476
518
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { steamcmdArgv, steamcmdScript, steamcmdUpdateArgs } from "./steamcmd";
|
|
2
|
+
import { steamcmdArgv, steamcmdFailureText, steamcmdScript, steamcmdUpdateArgs } from "./steamcmd";
|
|
3
3
|
import { SteamcmdLoginKind, steamcmdLoginArgs } from "./steamcmdLogin";
|
|
4
4
|
import { SteamcmdPlatform } from "./steamcmdPlatform";
|
|
5
5
|
|
|
@@ -155,3 +155,34 @@ describe("steamcmdUpdateArgs", () => {
|
|
|
155
155
|
]);
|
|
156
156
|
});
|
|
157
157
|
});
|
|
158
|
+
|
|
159
|
+
describe("steamcmdFailureText", () => {
|
|
160
|
+
test("says the download account does not own the app when steam answers no subscription", () => {
|
|
161
|
+
expect(
|
|
162
|
+
steamcmdFailureText(
|
|
163
|
+
{
|
|
164
|
+
stdout: "No subscription",
|
|
165
|
+
stderr: "",
|
|
166
|
+
code: 8,
|
|
167
|
+
},
|
|
168
|
+
"assetto",
|
|
169
|
+
).en,
|
|
170
|
+
).toBe("Steam refused to download assetto because the download account does not own it. Contact support.");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("says the download failed for any other exit", () => {
|
|
174
|
+
expect(
|
|
175
|
+
steamcmdFailureText(
|
|
176
|
+
{
|
|
177
|
+
stdout: "",
|
|
178
|
+
stderr: "",
|
|
179
|
+
code: 8,
|
|
180
|
+
},
|
|
181
|
+
"palworld",
|
|
182
|
+
),
|
|
183
|
+
).toEqual({
|
|
184
|
+
ar: "ما قدرنا ننزّل palworld من Steam. جرّب مرة ثانية.",
|
|
185
|
+
en: "We could not download palworld from Steam. Try again.",
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|