@serverkgg/bridge 0.0.9 → 0.0.10
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/cli/validate.ts +1 -0
- package/src/context/bridgeContext.ts +8 -1
- package/src/core/bridgeKind.ts +1 -0
- package/src/core/bridgeProbeError.ts +16 -0
- package/src/core/bridgeSetup.ts +20 -0
- package/src/core/bridgeValue.test.ts +29 -0
- package/src/core/bridgeValue.ts +12 -0
- package/src/core/index.ts +2 -0
- package/src/guides/blocks.ts +8 -0
- package/src/guides/markdown.test.ts +18 -0
- package/src/guides/markdown.ts +7 -1
- package/src/manifest/manifest.test.ts +49 -2
- package/src/manifest/presence.ts +9 -1
- package/src/modules/bridge.ts +20 -0
- package/src/modules/bridgeDriver.ts +2 -0
- package/src/modules/bridgeModule.ts +7 -0
- package/src/panel/bridgeDetail.ts +5 -5
- package/src/probes/a2s.test.ts +71 -0
- package/src/probes/a2s.ts +38 -11
- package/src/protocol/bridgeFrame.ts +5 -0
- package/src/protocol/bridgeLimits.ts +2 -0
- package/src/protocol/bridgeManifest.ts +2 -0
- package/src/protocol/bridgeSetup.ts +97 -0
- package/src/protocol/index.ts +1 -0
- package/src/runtime/bridgeConfigReader.ts +12 -8
- package/src/runtime/bridgeContext.ts +28 -2
- package/src/runtime/bridgeDriver.test.ts +272 -2
- package/src/runtime/bridgeDriver.ts +82 -1
- package/src/runtime/bridgeEvents.test.ts +96 -0
- package/src/runtime/bridgeEvents.ts +4 -1
- package/src/runtime/bridgeRegistry.test.ts +38 -1
- package/src/runtime/bridgeRegistry.ts +14 -1
- package/src/runtime/bridgeSecrets.test.ts +121 -0
- package/src/runtime/bridgeSecrets.ts +43 -0
- package/src/runtime/bridgeServer.test.ts +262 -2
- package/src/runtime/bridgeServer.ts +17 -0
- package/src/runtime/bridgeSetup.test.ts +189 -0
- package/src/runtime/bridgeSetup.ts +63 -0
- package/src/runtime/bridgeSignal.test.ts +114 -0
- package/src/runtime/bridgeSignal.ts +31 -0
- package/src/runtime/bridgeSupervisor.test.ts +111 -0
- package/src/runtime/bridgeSupervisor.ts +37 -5
- package/src/runtime/serve.ts +52 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/stripAnsi.ts +12 -0
- package/src/wire/panel.test.ts +93 -0
- package/src/wire/panel.ts +40 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { BridgeKind } from "../core";
|
|
6
|
+
import type { BridgeEventsModule } from "../modules";
|
|
7
|
+
import { BridgeEventName } from "../protocol";
|
|
8
|
+
import { createEventRing } from "./bridgeEvents";
|
|
9
|
+
import type { BridgeLeveledLogger } from "./bridgeLog";
|
|
10
|
+
import { createSupervisor } from "./bridgeSupervisor";
|
|
11
|
+
|
|
12
|
+
const RESET = "\u001B[m";
|
|
13
|
+
|
|
14
|
+
const PLAYER_JOINED = /^\[World\|(?<world>[^\]]+)\] Player '(?<player>[^']+)' joined world (?<target>\S+)$/;
|
|
15
|
+
|
|
16
|
+
const silent: BridgeLeveledLogger = Object.assign(() => {}, {
|
|
17
|
+
warn: () => {},
|
|
18
|
+
error: () => {},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const events: BridgeEventsModule = {
|
|
22
|
+
kind: BridgeKind.Events,
|
|
23
|
+
patterns: [
|
|
24
|
+
{
|
|
25
|
+
match: PLAYER_JOINED,
|
|
26
|
+
emit: BridgeEventName.PlayerJoined,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const emittedFor = async (line: string) => {
|
|
32
|
+
const dir = await mkdtemp(join(tmpdir(), "bridge-events-"));
|
|
33
|
+
const path = join(dir, "line.txt");
|
|
34
|
+
|
|
35
|
+
await Bun.write(path, `${line}\n`);
|
|
36
|
+
|
|
37
|
+
const supervisor = createSupervisor({
|
|
38
|
+
cwd: dir,
|
|
39
|
+
uid: process.getuid?.() ?? 0,
|
|
40
|
+
gid: process.getgid?.() ?? 0,
|
|
41
|
+
stopTimeoutSeconds: 1,
|
|
42
|
+
onLog: silent,
|
|
43
|
+
resolveCommand: async () => [
|
|
44
|
+
"bash",
|
|
45
|
+
"-c",
|
|
46
|
+
'cat "$0"; sleep 300',
|
|
47
|
+
path,
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const ring = createEventRing(events, supervisor);
|
|
52
|
+
|
|
53
|
+
await supervisor.start();
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
for (let waited = 0; waited < 5000; waited += 50) {
|
|
57
|
+
if (ring.drain(0).events.length > 0) {
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await Bun.sleep(50);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return ring.drain(0).events;
|
|
65
|
+
} finally {
|
|
66
|
+
await supervisor.stop();
|
|
67
|
+
await rm(dir, {
|
|
68
|
+
force: true,
|
|
69
|
+
recursive: true,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const joined = {
|
|
75
|
+
seq: 1,
|
|
76
|
+
event: BridgeEventName.PlayerJoined,
|
|
77
|
+
payload: {
|
|
78
|
+
world: "default",
|
|
79
|
+
player: "Meslzy",
|
|
80
|
+
target: "default",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
describe("matching a game line against the driver patterns", () => {
|
|
85
|
+
test("emits on the plain line", async () => {
|
|
86
|
+
expect(await emittedFor("[World|default] Player 'Meslzy' joined world default")).toEqual([
|
|
87
|
+
joined,
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("emits on the line hytale really prints, wrapped in ansi resets at both ends", async () => {
|
|
92
|
+
expect(await emittedFor(`${RESET}[World|default] Player 'Meslzy' joined world default${RESET}`)).toEqual([
|
|
93
|
+
joined,
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BridgeValues } from "../core";
|
|
2
2
|
import type { BridgeEventsModule } from "../modules";
|
|
3
3
|
import type { BridgeEvent } from "../protocol";
|
|
4
|
+
import { stripAnsi } from "../utils";
|
|
4
5
|
import type { BridgeSupervisor } from "./bridgeSupervisor";
|
|
5
6
|
|
|
6
7
|
const EVENT_BUFFER = 500;
|
|
@@ -26,12 +27,14 @@ export const createEventRing = (events: BridgeEventsModule | undefined, supervis
|
|
|
26
27
|
|
|
27
28
|
if (events) {
|
|
28
29
|
supervisor.subscribe((entry) => {
|
|
30
|
+
const line = stripAnsi(entry.line);
|
|
31
|
+
|
|
29
32
|
for (const pattern of events.patterns) {
|
|
30
33
|
if (pattern.stream && pattern.stream !== entry.stream) {
|
|
31
34
|
continue;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
const match =
|
|
37
|
+
const match = line.match(pattern.match);
|
|
35
38
|
|
|
36
39
|
if (!match) {
|
|
37
40
|
continue;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
-
import { BridgeAction, BridgeKind, type BridgeRow, type BridgeValues } from "../core";
|
|
3
|
+
import { BridgeAction, BridgeKind, type BridgeRow, BridgeSetupStepKind, type BridgeValues } from "../core";
|
|
4
4
|
import type { BridgeDriver } from "../modules";
|
|
5
5
|
import { type BridgeConfig, BridgeConfirm, BridgeEventName } from "../protocol";
|
|
6
6
|
import { createContext } from "./bridgeContext";
|
|
@@ -62,6 +62,9 @@ const registryFor = (driver: BridgeDriver, variables: Record<string, string> = {
|
|
|
62
62
|
onEmit(event, payload) {
|
|
63
63
|
ring.emit(event, payload);
|
|
64
64
|
},
|
|
65
|
+
onSetup() {
|
|
66
|
+
return;
|
|
67
|
+
},
|
|
65
68
|
});
|
|
66
69
|
|
|
67
70
|
return createRegistry(driver, context, supervisor, ring);
|
|
@@ -560,3 +563,37 @@ describe("detail modules", () => {
|
|
|
560
563
|
);
|
|
561
564
|
});
|
|
562
565
|
});
|
|
566
|
+
|
|
567
|
+
describe("the setup singleton", () => {
|
|
568
|
+
test("rides the manifest as its own block and never joins the panel modules", () => {
|
|
569
|
+
const steps = [
|
|
570
|
+
{
|
|
571
|
+
kind: BridgeSetupStepKind.Driver as const,
|
|
572
|
+
id: "sign-in",
|
|
573
|
+
title: {
|
|
574
|
+
ar: "تسجيل الدخول",
|
|
575
|
+
en: "Sign in",
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
];
|
|
579
|
+
|
|
580
|
+
const registry = registryFor({
|
|
581
|
+
...baseDriver(),
|
|
582
|
+
setup: {
|
|
583
|
+
kind: BridgeKind.Setup,
|
|
584
|
+
steps,
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
const manifest = registry.manifest();
|
|
589
|
+
|
|
590
|
+
expect(manifest.setup).toEqual({
|
|
591
|
+
steps,
|
|
592
|
+
});
|
|
593
|
+
expect(manifest.modules.some((module) => module.kind === BridgeKind.Setup)).toBe(false);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
test("leaves the manifest block empty for a driver that declares no setup", () => {
|
|
597
|
+
expect(registryFor(baseDriver()).manifest().setup).toBeNull();
|
|
598
|
+
});
|
|
599
|
+
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { BridgeContext } from "../context";
|
|
2
2
|
import { BridgeAction, BridgeKind, type BridgeRow, type BridgeValues, isReadAction } from "../core";
|
|
3
|
-
import type { BridgeDriver, BridgePanelModule, BridgeTerminalModule } from "../modules";
|
|
3
|
+
import type { BridgeDriver, BridgePanelModule, BridgeSetupModule, BridgeTerminalModule } from "../modules";
|
|
4
4
|
import {
|
|
5
5
|
BRIDGE_VERSION,
|
|
6
6
|
type BridgeManifest,
|
|
7
7
|
type BridgeModuleManifest,
|
|
8
|
+
type BridgeSetupManifest,
|
|
8
9
|
type BridgeTerminalManifest,
|
|
9
10
|
} from "../protocol";
|
|
10
11
|
import { actionsOf } from "./bridgeActions";
|
|
@@ -63,6 +64,16 @@ const terminalManifestOf = (module: BridgeTerminalModule | undefined): BridgeTer
|
|
|
63
64
|
};
|
|
64
65
|
};
|
|
65
66
|
|
|
67
|
+
const setupManifestOf = (module: BridgeSetupModule | undefined): BridgeSetupManifest | null => {
|
|
68
|
+
if (!module) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
steps: module.steps,
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
66
77
|
const withVariables = (context: BridgeContext, overlay: Record<string, string>): BridgeContext => {
|
|
67
78
|
return {
|
|
68
79
|
get server() {
|
|
@@ -88,6 +99,7 @@ const withVariables = (context: BridgeContext, overlay: Record<string, string>):
|
|
|
88
99
|
return context.exec(command, options);
|
|
89
100
|
},
|
|
90
101
|
logs: context.logs,
|
|
102
|
+
setup: context.setup,
|
|
91
103
|
emit(event, payload) {
|
|
92
104
|
context.emit(event, payload);
|
|
93
105
|
},
|
|
@@ -368,6 +380,7 @@ export const createRegistry = (
|
|
|
368
380
|
version: BRIDGE_VERSION,
|
|
369
381
|
panel: driver.panel ?? null,
|
|
370
382
|
terminal: terminalManifestOf(driver.terminal),
|
|
383
|
+
setup: setupManifestOf(driver.setup),
|
|
371
384
|
modules: entries,
|
|
372
385
|
events: ring.names(),
|
|
373
386
|
};
|
|
@@ -0,0 +1,121 @@
|
|
|
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 type { BridgeConfig } from "../protocol";
|
|
6
|
+
import { createBridgeSecrets } from "./bridgeSecrets";
|
|
7
|
+
|
|
8
|
+
const configWith = (secrets: Record<string, string>): BridgeConfig => ({
|
|
9
|
+
code: "wsvv8fakxm",
|
|
10
|
+
token: "token",
|
|
11
|
+
cwd: "/data",
|
|
12
|
+
run: "/run/bridge",
|
|
13
|
+
ports: {
|
|
14
|
+
game: 25_565,
|
|
15
|
+
},
|
|
16
|
+
memoryMb: 2048,
|
|
17
|
+
cpuPercent: 100,
|
|
18
|
+
gameUid: 1000,
|
|
19
|
+
gameGid: 1000,
|
|
20
|
+
variables: {
|
|
21
|
+
VERSION: "1.21",
|
|
22
|
+
},
|
|
23
|
+
secrets,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
let directory: string;
|
|
27
|
+
|
|
28
|
+
let path: string;
|
|
29
|
+
|
|
30
|
+
const write = async (config: BridgeConfig) => {
|
|
31
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
beforeEach(async () => {
|
|
35
|
+
directory = await mkdtemp(join(tmpdir(), "bridge-secrets-"));
|
|
36
|
+
path = join(directory, "config.json");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(async () => {
|
|
40
|
+
await rm(directory, {
|
|
41
|
+
force: true,
|
|
42
|
+
recursive: true,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("the bridge secrets reader", () => {
|
|
47
|
+
test("answers from the boot snapshot", async () => {
|
|
48
|
+
const config = configWith({
|
|
49
|
+
CURSEFORGE_API_KEY: "boot",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await write(config);
|
|
53
|
+
|
|
54
|
+
const secrets = createBridgeSecrets(path, config, 0);
|
|
55
|
+
|
|
56
|
+
expect(secrets.read("CURSEFORGE_API_KEY")).toBe("boot");
|
|
57
|
+
expect(secrets.read("MODRINTH_API_KEY")).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("sees a value the agent rewrote into the config", async () => {
|
|
61
|
+
const config = configWith({
|
|
62
|
+
CURSEFORGE_API_KEY: "boot",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await write(config);
|
|
66
|
+
|
|
67
|
+
const secrets = createBridgeSecrets(path, config, 0);
|
|
68
|
+
|
|
69
|
+
expect(secrets.read("CURSEFORGE_API_KEY")).toBe("boot");
|
|
70
|
+
|
|
71
|
+
await write(
|
|
72
|
+
configWith({
|
|
73
|
+
CURSEFORGE_API_KEY: "rotated",
|
|
74
|
+
MODRINTH_API_KEY: "added",
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect(secrets.read("CURSEFORGE_API_KEY")).toBe("rotated");
|
|
79
|
+
expect(secrets.read("MODRINTH_API_KEY")).toBe("added");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("keeps the last good values while the config is mid-write", async () => {
|
|
83
|
+
const config = configWith({
|
|
84
|
+
CURSEFORGE_API_KEY: "boot",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await write(config);
|
|
88
|
+
|
|
89
|
+
const secrets = createBridgeSecrets(path, config, 0);
|
|
90
|
+
|
|
91
|
+
expect(secrets.read("CURSEFORGE_API_KEY")).toBe("boot");
|
|
92
|
+
|
|
93
|
+
await writeFile(path, '{"code": "wsvv8fak');
|
|
94
|
+
|
|
95
|
+
expect(secrets.read("CURSEFORGE_API_KEY")).toBe("boot");
|
|
96
|
+
|
|
97
|
+
await rm(path);
|
|
98
|
+
|
|
99
|
+
expect(secrets.read("CURSEFORGE_API_KEY")).toBe("boot");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("re-reads the file at most once per refresh window", async () => {
|
|
103
|
+
const config = configWith({
|
|
104
|
+
CURSEFORGE_API_KEY: "boot",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await write(config);
|
|
108
|
+
|
|
109
|
+
const secrets = createBridgeSecrets(path, config, 60_000);
|
|
110
|
+
|
|
111
|
+
expect(secrets.read("CURSEFORGE_API_KEY")).toBe("boot");
|
|
112
|
+
|
|
113
|
+
await write(
|
|
114
|
+
configWith({
|
|
115
|
+
CURSEFORGE_API_KEY: "rotated",
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(secrets.read("CURSEFORGE_API_KEY")).toBe("boot");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import type { BridgeConfig } from "../protocol";
|
|
3
|
+
import { parseConfig } from "./bridgeConfigReader";
|
|
4
|
+
|
|
5
|
+
export const BRIDGE_SECRET_REFRESH_MS = 2000;
|
|
6
|
+
|
|
7
|
+
export interface BridgeSecrets {
|
|
8
|
+
read: (key: string) => string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const createBridgeSecrets = (
|
|
12
|
+
path: string,
|
|
13
|
+
config: BridgeConfig,
|
|
14
|
+
refreshMs: number = BRIDGE_SECRET_REFRESH_MS,
|
|
15
|
+
): BridgeSecrets => {
|
|
16
|
+
let values = config.secrets;
|
|
17
|
+
|
|
18
|
+
let checkedAt: number | null = null;
|
|
19
|
+
|
|
20
|
+
const refresh = () => {
|
|
21
|
+
const stamp = performance.now();
|
|
22
|
+
|
|
23
|
+
if (checkedAt !== null && stamp - checkedAt < refreshMs) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
checkedAt = stamp;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
values = parseConfig(readFileSync(path, "utf8")).secrets;
|
|
31
|
+
} catch {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
read(key) {
|
|
38
|
+
refresh();
|
|
39
|
+
|
|
40
|
+
return values[key] ?? null;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
};
|
|
@@ -2,9 +2,25 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
BridgeDesired,
|
|
7
|
+
BridgeFailureCode,
|
|
8
|
+
BridgeFailureError,
|
|
9
|
+
BridgeRuntime,
|
|
10
|
+
BridgeSetupPromptKind,
|
|
11
|
+
BridgeSetupStepState,
|
|
12
|
+
BridgeUserError,
|
|
13
|
+
} from "../core";
|
|
14
|
+
import {
|
|
15
|
+
BridgeMethod,
|
|
16
|
+
type BridgePendingResult,
|
|
17
|
+
type BridgeResponse,
|
|
18
|
+
type BridgeSetupResult,
|
|
19
|
+
type BridgeSetupRuntime,
|
|
20
|
+
type BridgeValues,
|
|
21
|
+
} from "../protocol";
|
|
7
22
|
import { createServer, type ServerOptions } from "./bridgeServer";
|
|
23
|
+
import { failedSetup, unsupportedSetup } from "./bridgeSetup";
|
|
8
24
|
|
|
9
25
|
const TOKEN = "test-token";
|
|
10
26
|
|
|
@@ -25,7 +41,9 @@ const baseOptions = (socket: string): ServerOptions => ({
|
|
|
25
41
|
outOfMemory: false,
|
|
26
42
|
restarts: 0,
|
|
27
43
|
installError: null,
|
|
44
|
+
installErrorText: null,
|
|
28
45
|
installHold: null,
|
|
46
|
+
setup: null,
|
|
29
47
|
}),
|
|
30
48
|
onManifest: unreachable("manifest"),
|
|
31
49
|
onStart: unreachable("start"),
|
|
@@ -40,6 +58,8 @@ const baseOptions = (socket: string): ServerOptions => ({
|
|
|
40
58
|
onAnnounce: unreachable("announce"),
|
|
41
59
|
onPendingList: unreachable("pendingList"),
|
|
42
60
|
onPendingAdd: unreachable("pendingAdd"),
|
|
61
|
+
onSetupStatus: unreachable("setupStatus"),
|
|
62
|
+
onSetupSubmit: unreachable("setupSubmit"),
|
|
43
63
|
onLog: () => {
|
|
44
64
|
return;
|
|
45
65
|
},
|
|
@@ -264,6 +284,246 @@ describe("pending dispatch", () => {
|
|
|
264
284
|
});
|
|
265
285
|
});
|
|
266
286
|
|
|
287
|
+
describe("setup dispatch", () => {
|
|
288
|
+
let dir = "";
|
|
289
|
+
let socket = "";
|
|
290
|
+
|
|
291
|
+
beforeEach(async () => {
|
|
292
|
+
dir = await mkdtemp(join(tmpdir(), "bridge-server-"));
|
|
293
|
+
socket = join(dir, "bridge.sock");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
afterEach(async () => {
|
|
297
|
+
await rm(dir, {
|
|
298
|
+
force: true,
|
|
299
|
+
recursive: true,
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const runtime: BridgeSetupRuntime = {
|
|
304
|
+
steps: [
|
|
305
|
+
{
|
|
306
|
+
id: "sign-in",
|
|
307
|
+
state: BridgeSetupStepState.Active,
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
prompt: {
|
|
311
|
+
kind: BridgeSetupPromptKind.Link,
|
|
312
|
+
message: {
|
|
313
|
+
ar: "سجل دخولك",
|
|
314
|
+
en: "sign in with your account",
|
|
315
|
+
},
|
|
316
|
+
url: "https://hytale.com/device",
|
|
317
|
+
code: "ABCD-EFGH",
|
|
318
|
+
expiresAt: null,
|
|
319
|
+
actions: [
|
|
320
|
+
{
|
|
321
|
+
id: "renew",
|
|
322
|
+
label: {
|
|
323
|
+
ar: "كود جديد",
|
|
324
|
+
en: "Get a new code",
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const reported: BridgeSetupResult = {
|
|
332
|
+
supported: true,
|
|
333
|
+
runtime,
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
test("answers setupStatus with what the driver reported", async () => {
|
|
337
|
+
const server = createServer({
|
|
338
|
+
...baseOptions(socket),
|
|
339
|
+
onSetupStatus: () => reported,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const response = await call(socket, {
|
|
343
|
+
id: 1,
|
|
344
|
+
token: TOKEN,
|
|
345
|
+
method: BridgeMethod.SetupStatus,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
server.stop(true);
|
|
349
|
+
|
|
350
|
+
expect(response).toEqual({
|
|
351
|
+
id: 1,
|
|
352
|
+
ok: true,
|
|
353
|
+
result: reported,
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("answers setupStatus with the unsupported sentinel when the driver declares no setup", async () => {
|
|
358
|
+
const server = createServer({
|
|
359
|
+
...baseOptions(socket),
|
|
360
|
+
onSetupStatus: () => unsupportedSetup(),
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const response = await call(socket, {
|
|
364
|
+
id: 2,
|
|
365
|
+
token: TOKEN,
|
|
366
|
+
method: BridgeMethod.SetupStatus,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
server.stop(true);
|
|
370
|
+
|
|
371
|
+
expect(response).toEqual({
|
|
372
|
+
id: 2,
|
|
373
|
+
ok: true,
|
|
374
|
+
result: {
|
|
375
|
+
supported: false,
|
|
376
|
+
runtime: null,
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("passes the step, action and args of a setupSubmit to the driver", async () => {
|
|
382
|
+
const received: {
|
|
383
|
+
step: string;
|
|
384
|
+
action: string;
|
|
385
|
+
args: BridgeValues;
|
|
386
|
+
}[] = [];
|
|
387
|
+
|
|
388
|
+
const server = createServer({
|
|
389
|
+
...baseOptions(socket),
|
|
390
|
+
onSetupSubmit: async (step, action, args) => {
|
|
391
|
+
received.push({
|
|
392
|
+
step,
|
|
393
|
+
action,
|
|
394
|
+
args,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
return reported;
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const response = await call(socket, {
|
|
402
|
+
id: 3,
|
|
403
|
+
token: TOKEN,
|
|
404
|
+
method: BridgeMethod.SetupSubmit,
|
|
405
|
+
module: "sign-in",
|
|
406
|
+
action: "renew",
|
|
407
|
+
args: {
|
|
408
|
+
force: true,
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
server.stop(true);
|
|
413
|
+
|
|
414
|
+
expect(received).toEqual([
|
|
415
|
+
{
|
|
416
|
+
step: "sign-in",
|
|
417
|
+
action: "renew",
|
|
418
|
+
args: {
|
|
419
|
+
force: true,
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
]);
|
|
423
|
+
expect(response).toEqual({
|
|
424
|
+
id: 3,
|
|
425
|
+
ok: true,
|
|
426
|
+
result: reported,
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("refuses a setupSubmit carrying no step id", async () => {
|
|
431
|
+
const server = createServer({
|
|
432
|
+
...baseOptions(socket),
|
|
433
|
+
onSetupSubmit: unreachable("setupSubmit"),
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const response = await call(socket, {
|
|
437
|
+
id: 4,
|
|
438
|
+
token: TOKEN,
|
|
439
|
+
method: BridgeMethod.SetupSubmit,
|
|
440
|
+
action: "renew",
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
server.stop(true);
|
|
444
|
+
|
|
445
|
+
expect(response).toEqual({
|
|
446
|
+
id: 4,
|
|
447
|
+
ok: false,
|
|
448
|
+
error: "setupSubmit needs a step id",
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("answers a rejected setupSubmit with a failed prompt instead of a failed call", async () => {
|
|
453
|
+
const server = createServer({
|
|
454
|
+
...baseOptions(socket),
|
|
455
|
+
onSetupSubmit: async () =>
|
|
456
|
+
failedSetup(runtime, {
|
|
457
|
+
ar: "الكود ما عاد ينفع",
|
|
458
|
+
en: "this code cannot be renewed any more",
|
|
459
|
+
}),
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const response = await call(socket, {
|
|
463
|
+
id: 5,
|
|
464
|
+
token: TOKEN,
|
|
465
|
+
method: BridgeMethod.SetupSubmit,
|
|
466
|
+
module: "sign-in",
|
|
467
|
+
action: "renew",
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
server.stop(true);
|
|
471
|
+
|
|
472
|
+
expect(response).toEqual({
|
|
473
|
+
id: 5,
|
|
474
|
+
ok: true,
|
|
475
|
+
result: {
|
|
476
|
+
supported: true,
|
|
477
|
+
runtime: {
|
|
478
|
+
steps: runtime.steps,
|
|
479
|
+
prompt: {
|
|
480
|
+
kind: BridgeSetupPromptKind.Failed,
|
|
481
|
+
message: {
|
|
482
|
+
ar: "الكود ما عاد ينفع",
|
|
483
|
+
en: "this code cannot be renewed any more",
|
|
484
|
+
},
|
|
485
|
+
actions: [
|
|
486
|
+
{
|
|
487
|
+
id: "renew",
|
|
488
|
+
label: {
|
|
489
|
+
ar: "كود جديد",
|
|
490
|
+
en: "Get a new code",
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
],
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("answers setupSubmit with the unsupported sentinel when the driver implements no submit", async () => {
|
|
501
|
+
const server = createServer({
|
|
502
|
+
...baseOptions(socket),
|
|
503
|
+
onSetupSubmit: async () => unsupportedSetup(),
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const response = await call(socket, {
|
|
507
|
+
id: 6,
|
|
508
|
+
token: TOKEN,
|
|
509
|
+
method: BridgeMethod.SetupSubmit,
|
|
510
|
+
module: "sign-in",
|
|
511
|
+
action: "renew",
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
server.stop(true);
|
|
515
|
+
|
|
516
|
+
expect(response).toEqual({
|
|
517
|
+
id: 6,
|
|
518
|
+
ok: true,
|
|
519
|
+
result: {
|
|
520
|
+
supported: false,
|
|
521
|
+
runtime: null,
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
267
527
|
describe("error serialisation", () => {
|
|
268
528
|
let dir = "";
|
|
269
529
|
let socket = "";
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type BridgeQuiesceResult,
|
|
13
13
|
type BridgeResponse,
|
|
14
14
|
type BridgeSampleResult,
|
|
15
|
+
type BridgeSetupResult,
|
|
15
16
|
type BridgeStatus,
|
|
16
17
|
} from "../protocol";
|
|
17
18
|
import { type BridgeLogger, errorText } from "./bridgeLog";
|
|
@@ -33,6 +34,8 @@ export interface ServerOptions {
|
|
|
33
34
|
onAnnounce: (message: string) => Promise<BridgeAnnounceResult>;
|
|
34
35
|
onPendingList: () => Promise<BridgePendingResult>;
|
|
35
36
|
onPendingAdd: (input: string) => Promise<BridgePendingResult>;
|
|
37
|
+
onSetupStatus: () => BridgeSetupResult;
|
|
38
|
+
onSetupSubmit: (step: string, action: string, args: BridgeValues) => Promise<BridgeSetupResult>;
|
|
36
39
|
onLog: BridgeLogger;
|
|
37
40
|
}
|
|
38
41
|
|
|
@@ -162,6 +165,20 @@ export const createServer = (options: ServerOptions) => {
|
|
|
162
165
|
return await options.onPendingAdd(input);
|
|
163
166
|
}
|
|
164
167
|
|
|
168
|
+
case BridgeMethod.SetupStatus: {
|
|
169
|
+
return options.onSetupStatus();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
case BridgeMethod.SetupSubmit: {
|
|
173
|
+
const step = asText(request.module);
|
|
174
|
+
|
|
175
|
+
if (!step) {
|
|
176
|
+
throw new Error("setupSubmit needs a step id");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return await options.onSetupSubmit(step, String(request.action ?? ""), asArgs(request.args));
|
|
180
|
+
}
|
|
181
|
+
|
|
165
182
|
default: {
|
|
166
183
|
throw new Error(`unknown method "${method}"`);
|
|
167
184
|
}
|