@serverkgg/bridge 0.0.8 → 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 +3 -3
- package/src/cli/validate.ts +2 -0
- package/src/context/bridgeContext.ts +17 -0
- package/src/context/index.ts +1 -0
- package/src/core/bridgeDownloadError.ts +10 -0
- package/src/core/bridgeHold.ts +12 -0
- package/src/core/bridgeKind.ts +3 -0
- package/src/core/bridgeProbeError.ts +16 -0
- package/src/core/bridgeSetup.ts +20 -0
- package/src/core/bridgeUserError.test.ts +23 -0
- package/src/core/bridgeUserError.ts +12 -0
- package/src/core/bridgeValue.test.ts +29 -0
- package/src/core/bridgeValue.ts +12 -0
- package/src/core/index.ts +5 -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/index.ts +1 -0
- package/src/manifest/manifest.test.ts +132 -0
- package/src/manifest/manifest.ts +21 -0
- package/src/manifest/presence.ts +56 -0
- package/src/manifest/primitives.ts +6 -0
- package/src/modules/bridge.ts +65 -2
- package/src/modules/bridgeDriver.ts +4 -0
- package/src/modules/bridgeModule.ts +32 -3
- package/src/panel/bridgeDetail.ts +49 -0
- package/src/panel/bridgeLayout.ts +1 -0
- package/src/panel/bridgePanel.ts +30 -0
- package/src/panel/bridgeUploadMode.ts +4 -0
- package/src/panel/index.ts +2 -0
- package/src/probes/a2s.test.ts +71 -0
- package/src/probes/a2s.ts +38 -11
- package/src/protocol/bridgeEgress.ts +8 -1
- package/src/protocol/bridgeEvents.ts +4 -0
- package/src/protocol/bridgeFrame.ts +33 -1
- package/src/protocol/bridgeLimits.ts +12 -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/bridgeActions.ts +7 -0
- package/src/runtime/bridgeConfigReader.ts +12 -8
- package/src/runtime/bridgeContext.ts +28 -2
- package/src/runtime/bridgeDriver.test.ts +748 -3
- package/src/runtime/bridgeDriver.ts +198 -4
- package/src/runtime/bridgeEgress.ts +3 -0
- package/src/runtime/bridgeEvents.test.ts +96 -0
- package/src/runtime/bridgeEvents.ts +11 -6
- package/src/runtime/bridgeFiles.test.ts +625 -15
- package/src/runtime/bridgeFiles.ts +152 -45
- package/src/runtime/bridgeInstall.test.ts +118 -0
- package/src/runtime/bridgeInstall.ts +55 -0
- package/src/runtime/bridgeNet.ts +32 -3
- package/src/runtime/bridgePending.test.ts +148 -0
- package/src/runtime/bridgePending.ts +43 -0
- package/src/runtime/bridgeRegistry.test.ts +172 -1
- package/src/runtime/bridgeRegistry.ts +40 -5
- package/src/runtime/bridgeSecrets.test.ts +121 -0
- package/src/runtime/bridgeSecrets.ts +43 -0
- package/src/runtime/bridgeServer.test.ts +598 -0
- package/src/runtime/bridgeServer.ts +42 -3
- 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 +55 -5
- package/src/runtime/bridgeZip.ts +179 -58
- package/src/runtime/serve.ts +115 -4
- package/src/utils/index.ts +1 -0
- package/src/utils/stripAnsi.ts +12 -0
- package/src/wire/panel.test.ts +259 -0
- package/src/wire/panel.ts +72 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { BRIDGE_PENDING_FILES_LIMIT, type BridgePendingFile } from "../protocol";
|
|
3
|
+
import type { BridgeLeveledLogger } from "./bridgeLog";
|
|
4
|
+
import { pendingResult, rejectedPending, unsupportedPending } from "./bridgePending";
|
|
5
|
+
|
|
6
|
+
const file = (id: string): BridgePendingFile => ({
|
|
7
|
+
id,
|
|
8
|
+
title: {
|
|
9
|
+
ar: id,
|
|
10
|
+
en: id,
|
|
11
|
+
},
|
|
12
|
+
fileName: `${id}.jar`,
|
|
13
|
+
sizeBytes: null,
|
|
14
|
+
digest: null,
|
|
15
|
+
sourceUrl: null,
|
|
16
|
+
note: null,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const files = (count: number) => {
|
|
20
|
+
return Array.from({
|
|
21
|
+
length: count,
|
|
22
|
+
}).map((_entry, index) => file(`file-${index}`));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const recorder = () => {
|
|
26
|
+
const warnings: string[] = [];
|
|
27
|
+
|
|
28
|
+
const onLog: BridgeLeveledLogger = Object.assign(
|
|
29
|
+
() => {
|
|
30
|
+
return;
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
warn: (message: string) => {
|
|
34
|
+
warnings.push(message);
|
|
35
|
+
},
|
|
36
|
+
error: () => {
|
|
37
|
+
return;
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
onLog,
|
|
44
|
+
warnings,
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
describe("unsupportedPending", () => {
|
|
49
|
+
test("answers the sentinel a driver without a pending module gets", () => {
|
|
50
|
+
expect(unsupportedPending()).toEqual({
|
|
51
|
+
supported: false,
|
|
52
|
+
staging: "",
|
|
53
|
+
extensions: [],
|
|
54
|
+
files: [],
|
|
55
|
+
rejected: null,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("pendingResult", () => {
|
|
61
|
+
test("carries the staging path, the extensions and the files the driver listed", () => {
|
|
62
|
+
const { onLog, warnings } = recorder();
|
|
63
|
+
|
|
64
|
+
expect(
|
|
65
|
+
pendingResult(
|
|
66
|
+
".serverk-staging/pending-upload",
|
|
67
|
+
[
|
|
68
|
+
"jar",
|
|
69
|
+
],
|
|
70
|
+
files(2),
|
|
71
|
+
onLog,
|
|
72
|
+
),
|
|
73
|
+
).toEqual({
|
|
74
|
+
supported: true,
|
|
75
|
+
staging: ".serverk-staging/pending-upload",
|
|
76
|
+
extensions: [
|
|
77
|
+
"jar",
|
|
78
|
+
],
|
|
79
|
+
files: files(2),
|
|
80
|
+
rejected: null,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(warnings).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("truncates a list longer than the platform takes and warns instead of throwing", () => {
|
|
87
|
+
const { onLog, warnings } = recorder();
|
|
88
|
+
|
|
89
|
+
const result = pendingResult(
|
|
90
|
+
".serverk-staging/pending-upload",
|
|
91
|
+
[
|
|
92
|
+
"jar",
|
|
93
|
+
],
|
|
94
|
+
files(BRIDGE_PENDING_FILES_LIMIT + 5),
|
|
95
|
+
onLog,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(result.files).toHaveLength(BRIDGE_PENDING_FILES_LIMIT);
|
|
99
|
+
expect(result.files.at(0)?.id).toBe("file-0");
|
|
100
|
+
expect(warnings).toEqual([
|
|
101
|
+
"pending list truncated",
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("rejectedPending", () => {
|
|
107
|
+
test("names every file still pending as what the upload was expected to be", () => {
|
|
108
|
+
const { onLog } = recorder();
|
|
109
|
+
|
|
110
|
+
const result = rejectedPending(
|
|
111
|
+
pendingResult(
|
|
112
|
+
".serverk-staging/pending-upload",
|
|
113
|
+
[
|
|
114
|
+
"jar",
|
|
115
|
+
],
|
|
116
|
+
files(2),
|
|
117
|
+
onLog,
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(result.rejected).toEqual({
|
|
122
|
+
expected: [
|
|
123
|
+
"file-0.jar",
|
|
124
|
+
"file-1.jar",
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
expect(result.files).toHaveLength(2);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("leaves an empty expectation when the driver has nothing pending left", () => {
|
|
131
|
+
const { onLog } = recorder();
|
|
132
|
+
|
|
133
|
+
expect(
|
|
134
|
+
rejectedPending(
|
|
135
|
+
pendingResult(
|
|
136
|
+
".serverk-staging/pending-upload",
|
|
137
|
+
[
|
|
138
|
+
"jar",
|
|
139
|
+
],
|
|
140
|
+
[],
|
|
141
|
+
onLog,
|
|
142
|
+
),
|
|
143
|
+
).rejected,
|
|
144
|
+
).toEqual({
|
|
145
|
+
expected: [],
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { BRIDGE_PENDING_FILES_LIMIT, type BridgePendingFile, type BridgePendingResult } from "../protocol";
|
|
2
|
+
import type { BridgeLeveledLogger } from "./bridgeLog";
|
|
3
|
+
|
|
4
|
+
export const unsupportedPending = (): BridgePendingResult => {
|
|
5
|
+
return {
|
|
6
|
+
supported: false,
|
|
7
|
+
staging: "",
|
|
8
|
+
extensions: [],
|
|
9
|
+
files: [],
|
|
10
|
+
rejected: null,
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const pendingResult = (
|
|
15
|
+
staging: string,
|
|
16
|
+
extensions: string[],
|
|
17
|
+
files: BridgePendingFile[],
|
|
18
|
+
onLog: BridgeLeveledLogger,
|
|
19
|
+
): BridgePendingResult => {
|
|
20
|
+
if (files.length > BRIDGE_PENDING_FILES_LIMIT) {
|
|
21
|
+
onLog.warn("pending list truncated", {
|
|
22
|
+
files: files.length,
|
|
23
|
+
limit: BRIDGE_PENDING_FILES_LIMIT,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
supported: true,
|
|
29
|
+
staging,
|
|
30
|
+
extensions,
|
|
31
|
+
files: files.slice(0, BRIDGE_PENDING_FILES_LIMIT),
|
|
32
|
+
rejected: null,
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const rejectedPending = (result: BridgePendingResult): BridgePendingResult => {
|
|
37
|
+
return {
|
|
38
|
+
...result,
|
|
39
|
+
rejected: {
|
|
40
|
+
expected: result.files.map((file) => file.fileName),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
};
|
|
@@ -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);
|
|
@@ -426,3 +429,171 @@ describe("preview modules", () => {
|
|
|
426
429
|
);
|
|
427
430
|
});
|
|
428
431
|
});
|
|
432
|
+
|
|
433
|
+
describe("detail modules", () => {
|
|
434
|
+
test("lists read and its own action ids in the manifest", () => {
|
|
435
|
+
const registry = registryFor({
|
|
436
|
+
...baseDriver(),
|
|
437
|
+
modules: {
|
|
438
|
+
modpackStatus: {
|
|
439
|
+
kind: BridgeKind.Detail,
|
|
440
|
+
read: async () => null,
|
|
441
|
+
actions: {
|
|
442
|
+
update: async () => null,
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
expect(registry.manifest().modules).toEqual([
|
|
449
|
+
{
|
|
450
|
+
id: "modpackStatus",
|
|
451
|
+
kind: BridgeKind.Detail,
|
|
452
|
+
requiresRunning: false,
|
|
453
|
+
refreshSeconds: null,
|
|
454
|
+
pageSize: null,
|
|
455
|
+
dependsOn: [],
|
|
456
|
+
actions: [
|
|
457
|
+
BridgeAction.Read,
|
|
458
|
+
"update",
|
|
459
|
+
],
|
|
460
|
+
},
|
|
461
|
+
]);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("returns what read answers", async () => {
|
|
465
|
+
const registry = registryFor({
|
|
466
|
+
...baseDriver(),
|
|
467
|
+
modules: {
|
|
468
|
+
modpackStatus: {
|
|
469
|
+
kind: BridgeKind.Detail,
|
|
470
|
+
read: async () => ({
|
|
471
|
+
id: "modrinth:abc",
|
|
472
|
+
title: "Better MC",
|
|
473
|
+
subtitle: "1.21.1 · NeoForge",
|
|
474
|
+
description: null,
|
|
475
|
+
image: null,
|
|
476
|
+
badges: [],
|
|
477
|
+
stats: [],
|
|
478
|
+
links: [],
|
|
479
|
+
stale: false,
|
|
480
|
+
actions: [],
|
|
481
|
+
}),
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
expect(await registry.invoke("modpackStatus", BridgeAction.Read)).toMatchObject({
|
|
487
|
+
id: "modrinth:abc",
|
|
488
|
+
title: "Better MC",
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("hands the argument bag to an action and returns its outcome", async () => {
|
|
493
|
+
const received: BridgeValues[] = [];
|
|
494
|
+
|
|
495
|
+
const registry = registryFor({
|
|
496
|
+
...baseDriver(),
|
|
497
|
+
modules: {
|
|
498
|
+
modpackStatus: {
|
|
499
|
+
kind: BridgeKind.Detail,
|
|
500
|
+
read: async () => null,
|
|
501
|
+
actions: {
|
|
502
|
+
update: async (_context, args) => {
|
|
503
|
+
received.push(args);
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
variables: {
|
|
507
|
+
MODPACK: "modrinth:abc:v2",
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
expect(
|
|
517
|
+
await registry.invoke("modpackStatus", "update", {
|
|
518
|
+
id: "modrinth:abc",
|
|
519
|
+
}),
|
|
520
|
+
).toEqual({
|
|
521
|
+
variables: {
|
|
522
|
+
MODPACK: "modrinth:abc:v2",
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
expect(received).toEqual([
|
|
527
|
+
{
|
|
528
|
+
id: "modrinth:abc",
|
|
529
|
+
},
|
|
530
|
+
]);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("refuses an action named after a read", () => {
|
|
534
|
+
expect(() =>
|
|
535
|
+
registryFor({
|
|
536
|
+
...baseDriver(),
|
|
537
|
+
modules: {
|
|
538
|
+
modpackStatus: {
|
|
539
|
+
kind: BridgeKind.Detail,
|
|
540
|
+
read: async () => null,
|
|
541
|
+
actions: {
|
|
542
|
+
read: async () => null,
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
}),
|
|
547
|
+
).toThrow('module "modpackStatus" declares action "read", which is reserved for reads');
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
test("refuses an action the module does not answer", async () => {
|
|
551
|
+
const registry = registryFor({
|
|
552
|
+
...baseDriver(),
|
|
553
|
+
modules: {
|
|
554
|
+
modpackStatus: {
|
|
555
|
+
kind: BridgeKind.Detail,
|
|
556
|
+
read: async () => null,
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
await expect(registry.invoke("modpackStatus", "update")).rejects.toThrow(
|
|
562
|
+
'module "modpackStatus" does not support "update"',
|
|
563
|
+
);
|
|
564
|
+
});
|
|
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";
|
|
@@ -18,7 +19,11 @@ interface CacheEntry {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
const requiresRunning = (module: BridgePanelModule) => {
|
|
21
|
-
if (
|
|
22
|
+
if (
|
|
23
|
+
module.kind === BridgeKind.Actions
|
|
24
|
+
|| module.kind === BridgeKind.Collection
|
|
25
|
+
|| module.kind === BridgeKind.Detail
|
|
26
|
+
) {
|
|
22
27
|
return module.requiresRunning ?? false;
|
|
23
28
|
}
|
|
24
29
|
|
|
@@ -26,7 +31,11 @@ const requiresRunning = (module: BridgePanelModule) => {
|
|
|
26
31
|
};
|
|
27
32
|
|
|
28
33
|
const refreshOf = (module: BridgePanelModule) => {
|
|
29
|
-
|
|
34
|
+
if (module.kind === BridgeKind.Collection || module.kind === BridgeKind.Detail) {
|
|
35
|
+
return module.refreshSeconds ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return null;
|
|
30
39
|
};
|
|
31
40
|
|
|
32
41
|
const dependsOf = (module: BridgePanelModule) => {
|
|
@@ -55,6 +64,16 @@ const terminalManifestOf = (module: BridgeTerminalModule | undefined): BridgeTer
|
|
|
55
64
|
};
|
|
56
65
|
};
|
|
57
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
|
+
|
|
58
77
|
const withVariables = (context: BridgeContext, overlay: Record<string, string>): BridgeContext => {
|
|
59
78
|
return {
|
|
60
79
|
get server() {
|
|
@@ -80,6 +99,7 @@ const withVariables = (context: BridgeContext, overlay: Record<string, string>):
|
|
|
80
99
|
return context.exec(command, options);
|
|
81
100
|
},
|
|
82
101
|
logs: context.logs,
|
|
102
|
+
setup: context.setup,
|
|
83
103
|
emit(event, payload) {
|
|
84
104
|
context.emit(event, payload);
|
|
85
105
|
},
|
|
@@ -138,11 +158,11 @@ export const createRegistry = (
|
|
|
138
158
|
const modules = driver.modules ?? {};
|
|
139
159
|
|
|
140
160
|
for (const [id, module] of Object.entries(modules)) {
|
|
141
|
-
if (module.kind !== BridgeKind.Actions) {
|
|
161
|
+
if (module.kind !== BridgeKind.Actions && module.kind !== BridgeKind.Detail) {
|
|
142
162
|
continue;
|
|
143
163
|
}
|
|
144
164
|
|
|
145
|
-
for (const action of Object.keys(module.actions)) {
|
|
165
|
+
for (const action of Object.keys(module.actions ?? {})) {
|
|
146
166
|
if (isReadAction(action)) {
|
|
147
167
|
throw new BridgeDriverError(
|
|
148
168
|
`module "${id}" declares action "${action}", which is reserved for reads — pick another id`,
|
|
@@ -290,6 +310,20 @@ export const createRegistry = (
|
|
|
290
310
|
break;
|
|
291
311
|
}
|
|
292
312
|
|
|
313
|
+
case BridgeKind.Detail: {
|
|
314
|
+
if (action === BridgeAction.Read) {
|
|
315
|
+
return await module.read(context);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const detailAction = module.actions?.[action];
|
|
319
|
+
|
|
320
|
+
if (detailAction) {
|
|
321
|
+
return await detailAction(context, args);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
|
|
293
327
|
case BridgeKind.Catalog: {
|
|
294
328
|
if (action === BridgeAction.Search) {
|
|
295
329
|
return await module.search(context, {
|
|
@@ -346,6 +380,7 @@ export const createRegistry = (
|
|
|
346
380
|
version: BRIDGE_VERSION,
|
|
347
381
|
panel: driver.panel ?? null,
|
|
348
382
|
terminal: terminalManifestOf(driver.terminal),
|
|
383
|
+
setup: setupManifestOf(driver.setup),
|
|
349
384
|
modules: entries,
|
|
350
385
|
events: ring.names(),
|
|
351
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
|
+
};
|