@serverkgg/bridge 0.0.1 → 0.0.3
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 +10 -3
- package/src/cli/link.test.ts +199 -0
- package/src/cli/link.ts +319 -0
- package/src/cli/main.ts +19 -1
- package/src/cli/schema.ts +62 -0
- package/src/cli/validate.test.ts +89 -0
- package/src/cli/validate.ts +218 -35
- package/src/context/bridgeContext.ts +18 -2
- package/src/core/bridgeAction.ts +13 -0
- package/src/core/bridgeFailure.ts +16 -0
- package/src/core/bridgeKind.ts +3 -0
- package/src/core/bridgeNetError.ts +10 -0
- package/src/core/index.ts +2 -0
- package/src/guides/blocks.ts +167 -0
- package/src/guides/compile.ts +193 -0
- package/src/guides/compiled.ts +28 -0
- package/src/guides/index.ts +6 -0
- package/src/guides/markdown.test.ts +374 -0
- package/src/guides/markdown.ts +769 -0
- package/src/guides/variables.ts +52 -0
- package/src/guides/version.ts +1 -0
- package/src/main.ts +2 -1
- package/src/manifest/assets.ts +4 -4
- package/src/manifest/guide.ts +23 -18
- package/src/manifest/index.ts +3 -2
- package/src/manifest/jsonSchema.ts +16 -0
- package/src/manifest/localized.ts +7 -0
- package/src/manifest/manifest.test.ts +366 -0
- package/src/manifest/manifest.ts +354 -120
- package/src/manifest/primitives.ts +63 -0
- package/src/manifest/source.ts +5 -4
- package/src/modules/bridge.ts +25 -4
- package/src/modules/bridgeDriver.ts +4 -0
- package/src/modules/bridgeModule.ts +36 -24
- package/src/panel/bridgeAccess.ts +3 -0
- package/src/panel/bridgeLayout.ts +1 -0
- package/src/panel/bridgePanel.ts +40 -2
- package/src/panel/index.ts +1 -0
- package/src/protocol/bridgeCatalog.ts +57 -0
- package/src/protocol/bridgeEgress.ts +53 -0
- package/src/protocol/bridgeEvents.ts +20 -0
- package/src/protocol/bridgeFrame.ts +16 -0
- package/src/protocol/bridgeLimits.ts +41 -0
- package/src/protocol/bridgeLine.ts +41 -6
- package/src/protocol/bridgeManifest.ts +15 -1
- package/src/protocol/bridgeVersion.ts +1 -1
- package/src/protocol/index.ts +5 -0
- package/src/runtime/bridgeActions.ts +53 -0
- package/src/runtime/bridgeContext.ts +29 -9
- package/src/runtime/bridgeDriver.test.ts +450 -0
- package/src/runtime/bridgeDriver.ts +246 -6
- package/src/runtime/bridgeEgress.ts +360 -0
- package/src/runtime/bridgeEvents.ts +4 -1
- package/src/runtime/bridgeExec.ts +15 -5
- package/src/runtime/bridgeFiles.test.ts +334 -0
- package/src/runtime/bridgeFiles.ts +130 -31
- package/src/runtime/bridgeLog.ts +26 -3
- package/src/runtime/bridgeNet.ts +75 -22
- package/src/runtime/bridgeRegistry.test.ts +291 -0
- package/src/runtime/bridgeRegistry.ts +91 -52
- package/src/runtime/bridgeServer.ts +35 -3
- package/src/runtime/bridgeSupervisor.ts +6 -6
- package/src/runtime/bridgeZip.ts +14 -3
- package/src/runtime/index.ts +0 -17
- package/src/runtime/serve.ts +44 -3
- package/src/terminal/bridgeTerminalArg.ts +11 -0
- package/src/terminal/bridgeTerminalCommand.ts +11 -0
- package/src/terminal/bridgeTerminalLevel.ts +6 -0
- package/src/terminal/bridgeTerminalRule.ts +8 -0
- package/src/terminal/index.ts +4 -0
- package/src/manifest/checks.ts +0 -50
package/src/runtime/bridgeNet.ts
CHANGED
|
@@ -1,50 +1,103 @@
|
|
|
1
1
|
import type { BridgeNet, BridgeRequestOptions } from "../context";
|
|
2
|
+
import { BridgeNetError } from "../core";
|
|
3
|
+
import { type BridgeEgress, BridgeEgressCallError, BridgeEgressUnavailableError } from "./bridgeEgress";
|
|
2
4
|
|
|
3
5
|
const REQUEST_TIMEOUT_MS = 15_000;
|
|
4
6
|
|
|
5
7
|
const RESPONSE_LIMIT = 8 * 1024 * 1024;
|
|
6
8
|
|
|
7
|
-
const USER_AGENT = "serverk-bridge/1.0 (serverk.gg)";
|
|
9
|
+
export const USER_AGENT = "serverk-bridge/1.0 (serverk.gg)";
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
readonly status: number | null;
|
|
11
|
+
const LINE_BREAK = /[\r\n]/;
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
export const safeHeaders = (headers: Record<string, string> = {}) => {
|
|
14
|
+
const checked: Record<string, string> = {};
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
17
|
+
if (LINE_BREAK.test(key) || LINE_BREAK.test(value)) {
|
|
18
|
+
throw new BridgeNetError(null, "a header contains a line break");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
checked[key] = value;
|
|
17
22
|
}
|
|
18
|
-
}
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const remote = new URL(url);
|
|
24
|
+
return checked;
|
|
25
|
+
};
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
const readCapped = async (response: Response, host: string) => {
|
|
28
|
+
if (!response.body) {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const decoder = new TextDecoder();
|
|
33
|
+
const parts: string[] = [];
|
|
34
|
+
|
|
35
|
+
let read = 0;
|
|
36
|
+
|
|
37
|
+
for await (const chunk of response.body) {
|
|
38
|
+
read += chunk.byteLength;
|
|
39
|
+
|
|
40
|
+
if (read > RESPONSE_LIMIT) {
|
|
41
|
+
throw new BridgeNetError(null, `${host} returned more than ${RESPONSE_LIMIT} bytes`);
|
|
26
42
|
}
|
|
27
43
|
|
|
28
|
-
|
|
44
|
+
parts.push(
|
|
45
|
+
decoder.decode(chunk, {
|
|
46
|
+
stream: true,
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
parts.push(decoder.decode());
|
|
52
|
+
|
|
53
|
+
return parts.join("");
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const createBridgeNet = (egress: BridgeEgress): BridgeNet => {
|
|
57
|
+
const direct = async (remote: URL, headers: Record<string, string>, timeoutMs: number) => {
|
|
58
|
+
const response = await fetch(remote, {
|
|
29
59
|
headers: {
|
|
60
|
+
...headers,
|
|
30
61
|
"user-agent": USER_AGENT,
|
|
31
|
-
...options.headers,
|
|
32
62
|
},
|
|
33
|
-
redirect: "
|
|
34
|
-
signal: AbortSignal.timeout(
|
|
63
|
+
redirect: "error",
|
|
64
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
35
65
|
});
|
|
36
66
|
|
|
37
67
|
if (!response.ok) {
|
|
38
|
-
throw new BridgeNetError(response.status, `${
|
|
68
|
+
throw new BridgeNetError(response.status, `${remote.host} answered ${response.status}`);
|
|
39
69
|
}
|
|
40
70
|
|
|
41
|
-
|
|
71
|
+
return await readCapped(response, remote.host);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const request = async (url: string, options: BridgeRequestOptions = {}) => {
|
|
75
|
+
const remote = new URL(url);
|
|
76
|
+
|
|
77
|
+
if (remote.protocol !== "https:") {
|
|
78
|
+
throw new BridgeNetError(null, `${remote.host} is not https`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const headers = safeHeaders(options.headers);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const result = await egress.fetch(url, {
|
|
85
|
+
...options,
|
|
86
|
+
headers,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return result.body;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error instanceof BridgeEgressCallError) {
|
|
92
|
+
throw new BridgeNetError(error.status, error.message);
|
|
93
|
+
}
|
|
42
94
|
|
|
43
|
-
|
|
44
|
-
|
|
95
|
+
if (!(error instanceof BridgeEgressUnavailableError)) {
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
45
98
|
}
|
|
46
99
|
|
|
47
|
-
return
|
|
100
|
+
return await direct(remote, headers, options.timeoutMs ?? REQUEST_TIMEOUT_MS);
|
|
48
101
|
};
|
|
49
102
|
|
|
50
103
|
return {
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { BridgeAction, BridgeKind, type BridgeRow, type BridgeValues } from "../core";
|
|
4
|
+
import type { BridgeDriver } from "../modules";
|
|
5
|
+
import { type BridgeConfig, BridgeEventName } from "../protocol";
|
|
6
|
+
import { createContext } from "./bridgeContext";
|
|
7
|
+
import { createEventRing } from "./bridgeEvents";
|
|
8
|
+
import { log } from "./bridgeLog";
|
|
9
|
+
import { createRegistry } from "./bridgeRegistry";
|
|
10
|
+
import { createSupervisor } from "./bridgeSupervisor";
|
|
11
|
+
|
|
12
|
+
const config = (): BridgeConfig => ({
|
|
13
|
+
code: "abcdef",
|
|
14
|
+
token: "token",
|
|
15
|
+
cwd: `${tmpdir()}/serverk-registry-test`,
|
|
16
|
+
run: `${tmpdir()}/serverk-registry-test/run`,
|
|
17
|
+
ports: {},
|
|
18
|
+
memoryMb: 1024,
|
|
19
|
+
cpuPercent: 100,
|
|
20
|
+
gameUid: 1000,
|
|
21
|
+
gameGid: 1000,
|
|
22
|
+
variables: {},
|
|
23
|
+
secrets: {},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const baseDriver = (): BridgeDriver => ({
|
|
27
|
+
install: {
|
|
28
|
+
kind: BridgeKind.Install,
|
|
29
|
+
run: async () => {
|
|
30
|
+
return;
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
lifecycle: {
|
|
34
|
+
kind: BridgeKind.Lifecycle,
|
|
35
|
+
command: async () => [
|
|
36
|
+
"run",
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const registryFor = (driver: BridgeDriver) => {
|
|
42
|
+
const settings = config();
|
|
43
|
+
|
|
44
|
+
const supervisor = createSupervisor({
|
|
45
|
+
cwd: settings.cwd,
|
|
46
|
+
uid: settings.gameUid,
|
|
47
|
+
gid: settings.gameGid,
|
|
48
|
+
stopTimeoutSeconds: 1,
|
|
49
|
+
onLog: log,
|
|
50
|
+
resolveCommand: async () => [
|
|
51
|
+
"run",
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const ring = createEventRing(driver.events, supervisor);
|
|
56
|
+
|
|
57
|
+
const context = createContext(settings, supervisor, {
|
|
58
|
+
onLog: log,
|
|
59
|
+
onEmit(event, payload) {
|
|
60
|
+
ring.emit(event, payload);
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return createRegistry(driver, context, supervisor, ring);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
describe("actions modules", () => {
|
|
68
|
+
test("lists its action ids in the manifest", () => {
|
|
69
|
+
const registry = registryFor({
|
|
70
|
+
...baseDriver(),
|
|
71
|
+
modules: {
|
|
72
|
+
gameplay: {
|
|
73
|
+
kind: BridgeKind.Actions,
|
|
74
|
+
requiresRunning: true,
|
|
75
|
+
actions: {
|
|
76
|
+
timeDay: async () => {
|
|
77
|
+
return;
|
|
78
|
+
},
|
|
79
|
+
weatherClear: async () => {
|
|
80
|
+
return;
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(registry.manifest().modules).toEqual([
|
|
88
|
+
{
|
|
89
|
+
id: "gameplay",
|
|
90
|
+
kind: BridgeKind.Actions,
|
|
91
|
+
requiresRunning: true,
|
|
92
|
+
refreshSeconds: null,
|
|
93
|
+
pageSize: null,
|
|
94
|
+
dependsOn: [],
|
|
95
|
+
actions: [
|
|
96
|
+
"timeDay",
|
|
97
|
+
"weatherClear",
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("hands the whole argument bag to the handler", async () => {
|
|
104
|
+
const received: BridgeValues[] = [];
|
|
105
|
+
|
|
106
|
+
const registry = registryFor({
|
|
107
|
+
...baseDriver(),
|
|
108
|
+
modules: {
|
|
109
|
+
broadcast: {
|
|
110
|
+
kind: BridgeKind.Actions,
|
|
111
|
+
actions: {
|
|
112
|
+
say: async (_context, args) => {
|
|
113
|
+
received.push(args);
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await registry.invoke("broadcast", "say", {
|
|
121
|
+
message: "hello",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(received).toEqual([
|
|
125
|
+
{
|
|
126
|
+
message: "hello",
|
|
127
|
+
},
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("refuses to run while the game is stopped", async () => {
|
|
132
|
+
const registry = registryFor({
|
|
133
|
+
...baseDriver(),
|
|
134
|
+
modules: {
|
|
135
|
+
broadcast: {
|
|
136
|
+
kind: BridgeKind.Actions,
|
|
137
|
+
requiresRunning: true,
|
|
138
|
+
actions: {
|
|
139
|
+
say: async () => {
|
|
140
|
+
return;
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await expect(
|
|
148
|
+
registry.invoke("broadcast", "say", {
|
|
149
|
+
message: "hello",
|
|
150
|
+
}),
|
|
151
|
+
).rejects.toThrow('module "broadcast" needs the game to be running');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("rejects a driver that reuses a read action id", () => {
|
|
155
|
+
expect(() =>
|
|
156
|
+
registryFor({
|
|
157
|
+
...baseDriver(),
|
|
158
|
+
modules: {
|
|
159
|
+
broadcast: {
|
|
160
|
+
kind: BridgeKind.Actions,
|
|
161
|
+
actions: {
|
|
162
|
+
list: async () => {
|
|
163
|
+
return;
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
).toThrow('module "broadcast" declares action "list", which is reserved for reads');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("manifest events", () => {
|
|
174
|
+
test("lists the log patterns and the names the driver raises itself", () => {
|
|
175
|
+
const registry = registryFor({
|
|
176
|
+
...baseDriver(),
|
|
177
|
+
events: {
|
|
178
|
+
kind: BridgeKind.Events,
|
|
179
|
+
patterns: [
|
|
180
|
+
{
|
|
181
|
+
match: /Fatal error/,
|
|
182
|
+
emit: BridgeEventName.ServerCrashed,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
match: /Fatal error again/,
|
|
186
|
+
emit: BridgeEventName.ServerCrashed,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
emits: [
|
|
190
|
+
BridgeEventName.PlayerJoined,
|
|
191
|
+
BridgeEventName.PlayerLeft,
|
|
192
|
+
BridgeEventName.ServerCrashed,
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(registry.manifest().events).toEqual([
|
|
198
|
+
BridgeEventName.ServerCrashed,
|
|
199
|
+
BridgeEventName.PlayerJoined,
|
|
200
|
+
BridgeEventName.PlayerLeft,
|
|
201
|
+
]);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("stays on the log patterns when the driver raises nothing itself", () => {
|
|
205
|
+
const registry = registryFor({
|
|
206
|
+
...baseDriver(),
|
|
207
|
+
events: {
|
|
208
|
+
kind: BridgeKind.Events,
|
|
209
|
+
patterns: [
|
|
210
|
+
{
|
|
211
|
+
match: /Done/,
|
|
212
|
+
emit: BridgeEventName.ServerStarted,
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(registry.manifest().events).toEqual([
|
|
219
|
+
BridgeEventName.ServerStarted,
|
|
220
|
+
]);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("collection row actions", () => {
|
|
225
|
+
test("passes the row and the arguments without the row id", async () => {
|
|
226
|
+
const seen: {
|
|
227
|
+
row: BridgeRow;
|
|
228
|
+
args: BridgeValues;
|
|
229
|
+
}[] = [];
|
|
230
|
+
|
|
231
|
+
const registry = registryFor({
|
|
232
|
+
...baseDriver(),
|
|
233
|
+
modules: {
|
|
234
|
+
players: {
|
|
235
|
+
kind: BridgeKind.Collection,
|
|
236
|
+
list: async () => [
|
|
237
|
+
{
|
|
238
|
+
id: "meslzy",
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
actions: {
|
|
242
|
+
xp: async (_context, row, args) => {
|
|
243
|
+
seen.push({
|
|
244
|
+
row,
|
|
245
|
+
args,
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await registry.invoke("players", "xp", {
|
|
254
|
+
id: "meslzy",
|
|
255
|
+
amount: 30,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
expect(seen).toEqual([
|
|
259
|
+
{
|
|
260
|
+
row: {
|
|
261
|
+
id: "meslzy",
|
|
262
|
+
},
|
|
263
|
+
args: {
|
|
264
|
+
amount: 30,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("keeps listing the built-in actions beside the row actions", () => {
|
|
271
|
+
const registry = registryFor({
|
|
272
|
+
...baseDriver(),
|
|
273
|
+
modules: {
|
|
274
|
+
players: {
|
|
275
|
+
kind: BridgeKind.Collection,
|
|
276
|
+
list: async () => [],
|
|
277
|
+
actions: {
|
|
278
|
+
kick: async () => {
|
|
279
|
+
return;
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(registry.manifest().modules[0]?.actions).toEqual([
|
|
287
|
+
BridgeAction.List,
|
|
288
|
+
"kick",
|
|
289
|
+
]);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { BridgeContext } from "../context";
|
|
2
|
-
import { BridgeAction, BridgeKind, type BridgeRow, type BridgeValues } from "../core";
|
|
3
|
-
import type { BridgeDriver, BridgePanelModule } from "../modules";
|
|
4
|
-
import {
|
|
2
|
+
import { BridgeAction, BridgeKind, type BridgeRow, type BridgeValues, isReadAction } from "../core";
|
|
3
|
+
import type { BridgeDriver, BridgePanelModule, BridgeTerminalModule } from "../modules";
|
|
4
|
+
import {
|
|
5
|
+
BRIDGE_VERSION,
|
|
6
|
+
type BridgeManifest,
|
|
7
|
+
type BridgeModuleManifest,
|
|
8
|
+
type BridgeTerminalManifest,
|
|
9
|
+
} from "../protocol";
|
|
10
|
+
import { actionsOf } from "./bridgeActions";
|
|
5
11
|
import { BridgeDriverError } from "./bridgeDriver";
|
|
6
12
|
import type { BridgeEventRing } from "./bridgeEvents";
|
|
7
13
|
import type { BridgeSupervisor } from "./bridgeSupervisor";
|
|
@@ -11,50 +17,12 @@ interface CacheEntry {
|
|
|
11
17
|
expiresAt: number;
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return [
|
|
18
|
-
BridgeAction.List,
|
|
19
|
-
];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
case BridgeKind.Settings: {
|
|
23
|
-
return [
|
|
24
|
-
BridgeAction.Read,
|
|
25
|
-
...(module.write
|
|
26
|
-
? [
|
|
27
|
-
BridgeAction.Write,
|
|
28
|
-
]
|
|
29
|
-
: []),
|
|
30
|
-
];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
case BridgeKind.Collection: {
|
|
34
|
-
return [
|
|
35
|
-
BridgeAction.List,
|
|
36
|
-
...(module.add
|
|
37
|
-
? [
|
|
38
|
-
BridgeAction.Add,
|
|
39
|
-
]
|
|
40
|
-
: []),
|
|
41
|
-
...Object.keys(module.actions ?? {}),
|
|
42
|
-
];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
case BridgeKind.Catalog: {
|
|
46
|
-
return [
|
|
47
|
-
BridgeAction.Install,
|
|
48
|
-
BridgeAction.Installed,
|
|
49
|
-
BridgeAction.Remove,
|
|
50
|
-
BridgeAction.Search,
|
|
51
|
-
];
|
|
52
|
-
}
|
|
20
|
+
const requiresRunning = (module: BridgePanelModule) => {
|
|
21
|
+
if (module.kind === BridgeKind.Actions || module.kind === BridgeKind.Collection) {
|
|
22
|
+
return module.requiresRunning ?? false;
|
|
53
23
|
}
|
|
54
|
-
};
|
|
55
24
|
|
|
56
|
-
|
|
57
|
-
return module.kind === BridgeKind.Collection ? (module.requiresRunning ?? false) : false;
|
|
25
|
+
return false;
|
|
58
26
|
};
|
|
59
27
|
|
|
60
28
|
const refreshOf = (module: BridgePanelModule) => {
|
|
@@ -69,6 +37,24 @@ const pageSizeOf = (module: BridgePanelModule) => {
|
|
|
69
37
|
return module.kind === BridgeKind.Catalog ? (module.pageSize ?? null) : null;
|
|
70
38
|
};
|
|
71
39
|
|
|
40
|
+
const terminalManifestOf = (module: BridgeTerminalModule | undefined): BridgeTerminalManifest | null => {
|
|
41
|
+
if (!module) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
commands: module.commands,
|
|
47
|
+
rules: (module.rules ?? []).map((rule) => {
|
|
48
|
+
return {
|
|
49
|
+
match: rule.match.source,
|
|
50
|
+
flags: rule.match.flags,
|
|
51
|
+
level: rule.level,
|
|
52
|
+
stream: rule.stream,
|
|
53
|
+
};
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
72
58
|
const withVariables = (context: BridgeContext, overlay: Record<string, string>): BridgeContext => {
|
|
73
59
|
return {
|
|
74
60
|
get server() {
|
|
@@ -97,9 +83,7 @@ const withVariables = (context: BridgeContext, overlay: Record<string, string>):
|
|
|
97
83
|
emit(event, payload) {
|
|
98
84
|
context.emit(event, payload);
|
|
99
85
|
},
|
|
100
|
-
log
|
|
101
|
-
context.log(message, fields);
|
|
102
|
-
},
|
|
86
|
+
log: context.log,
|
|
103
87
|
};
|
|
104
88
|
};
|
|
105
89
|
|
|
@@ -119,6 +103,24 @@ const asText = (args: BridgeValues, key: string) => {
|
|
|
119
103
|
return typeof value === "string" ? value : "";
|
|
120
104
|
};
|
|
121
105
|
|
|
106
|
+
const asOptional = (args: BridgeValues, key: string) => {
|
|
107
|
+
const value = args[key];
|
|
108
|
+
|
|
109
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const withoutId = (args: BridgeValues) => {
|
|
113
|
+
const rest: BridgeValues = {};
|
|
114
|
+
|
|
115
|
+
for (const [key, value] of Object.entries(args)) {
|
|
116
|
+
if (key !== "id") {
|
|
117
|
+
rest[key] = value;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return rest;
|
|
122
|
+
};
|
|
123
|
+
|
|
122
124
|
const asPage = (args: BridgeValues) => {
|
|
123
125
|
const value = args.page;
|
|
124
126
|
|
|
@@ -135,6 +137,20 @@ export const createRegistry = (
|
|
|
135
137
|
const inflight = new Map<string, Promise<unknown>>();
|
|
136
138
|
const modules = driver.modules ?? {};
|
|
137
139
|
|
|
140
|
+
for (const [id, module] of Object.entries(modules)) {
|
|
141
|
+
if (module.kind !== BridgeKind.Actions) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const action of Object.keys(module.actions)) {
|
|
146
|
+
if (isReadAction(action)) {
|
|
147
|
+
throw new BridgeDriverError(
|
|
148
|
+
`module "${id}" declares action "${action}", which is reserved for reads — pick another id`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
138
154
|
const cached = async (key: string, ttlSeconds: number | undefined, run: () => Promise<unknown>) => {
|
|
139
155
|
if (!ttlSeconds) {
|
|
140
156
|
return await run();
|
|
@@ -175,6 +191,18 @@ export const createRegistry = (
|
|
|
175
191
|
|
|
176
192
|
const dispatch = async (module: BridgePanelModule, name: string, action: string, args: BridgeValues) => {
|
|
177
193
|
switch (module.kind) {
|
|
194
|
+
case BridgeKind.Actions: {
|
|
195
|
+
const serverAction = module.actions[action];
|
|
196
|
+
|
|
197
|
+
if (serverAction) {
|
|
198
|
+
await serverAction(context, args);
|
|
199
|
+
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
|
|
178
206
|
case BridgeKind.Options: {
|
|
179
207
|
if (action === BridgeAction.List) {
|
|
180
208
|
const overlay: Record<string, string> = {};
|
|
@@ -236,7 +264,7 @@ export const createRegistry = (
|
|
|
236
264
|
throw new BridgeDriverError(`module "${name}" has no row "${id}"`);
|
|
237
265
|
}
|
|
238
266
|
|
|
239
|
-
await rowAction(context, row as BridgeRow);
|
|
267
|
+
await rowAction(context, row as BridgeRow, withoutId(args));
|
|
240
268
|
|
|
241
269
|
return null;
|
|
242
270
|
}
|
|
@@ -246,7 +274,13 @@ export const createRegistry = (
|
|
|
246
274
|
|
|
247
275
|
case BridgeKind.Catalog: {
|
|
248
276
|
if (action === BridgeAction.Search) {
|
|
249
|
-
return await module.search(context,
|
|
277
|
+
return await module.search(context, {
|
|
278
|
+
query: asText(args, "query"),
|
|
279
|
+
page: asPage(args),
|
|
280
|
+
provider: asOptional(args, "provider"),
|
|
281
|
+
category: asOptional(args, "category"),
|
|
282
|
+
sort: asOptional(args, "sort"),
|
|
283
|
+
});
|
|
250
284
|
}
|
|
251
285
|
|
|
252
286
|
if (action === BridgeAction.Installed) {
|
|
@@ -263,6 +297,12 @@ export const createRegistry = (
|
|
|
263
297
|
return null;
|
|
264
298
|
}
|
|
265
299
|
|
|
300
|
+
if (action === BridgeAction.Toggle && module.toggle) {
|
|
301
|
+
await module.toggle(context, asString(args, "id"), args.enabled !== false);
|
|
302
|
+
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
266
306
|
break;
|
|
267
307
|
}
|
|
268
308
|
}
|
|
@@ -287,6 +327,7 @@ export const createRegistry = (
|
|
|
287
327
|
return {
|
|
288
328
|
version: BRIDGE_VERSION,
|
|
289
329
|
panel: driver.panel ?? null,
|
|
330
|
+
terminal: terminalManifestOf(driver.terminal),
|
|
290
331
|
modules: entries,
|
|
291
332
|
events: ring.names(),
|
|
292
333
|
};
|
|
@@ -307,5 +348,3 @@ export const createRegistry = (
|
|
|
307
348
|
},
|
|
308
349
|
};
|
|
309
350
|
};
|
|
310
|
-
|
|
311
|
-
export type BridgeRegistry = ReturnType<typeof createRegistry>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { BridgeSecretError, type BridgeValues } from "../core";
|
|
2
|
+
import { BridgeFailureError, BridgeSecretError, type BridgeValues } from "../core";
|
|
3
3
|
import {
|
|
4
4
|
BRIDGE_FRAME_LIMIT,
|
|
5
|
+
type BridgeAnnounceResult,
|
|
5
6
|
type BridgeCommandResult,
|
|
7
|
+
type BridgeDescribeResult,
|
|
6
8
|
type BridgeEventsResult,
|
|
7
9
|
type BridgeManifest,
|
|
8
10
|
BridgeMethod,
|
|
@@ -26,6 +28,8 @@ export interface ServerOptions {
|
|
|
26
28
|
onSample: () => Promise<BridgeSampleResult>;
|
|
27
29
|
onQuiesce: () => Promise<BridgeQuiesceResult>;
|
|
28
30
|
onRelease: () => Promise<void>;
|
|
31
|
+
onDescribe: () => Promise<BridgeDescribeResult>;
|
|
32
|
+
onAnnounce: (message: string) => Promise<BridgeAnnounceResult>;
|
|
29
33
|
onLog: BridgeLogger;
|
|
30
34
|
}
|
|
31
35
|
|
|
@@ -40,8 +44,13 @@ interface IncomingFrame {
|
|
|
40
44
|
action?: unknown;
|
|
41
45
|
args?: unknown;
|
|
42
46
|
cursor?: unknown;
|
|
47
|
+
message?: unknown;
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
const asMessage = (value: unknown): string | null => {
|
|
51
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
52
|
+
};
|
|
53
|
+
|
|
45
54
|
const matches = (left: string, right: string) => {
|
|
46
55
|
const a = Buffer.from(left);
|
|
47
56
|
const b = Buffer.from(right);
|
|
@@ -122,6 +131,20 @@ export const createServer = (options: ServerOptions) => {
|
|
|
122
131
|
return null;
|
|
123
132
|
}
|
|
124
133
|
|
|
134
|
+
case BridgeMethod.Describe: {
|
|
135
|
+
return await options.onDescribe();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case BridgeMethod.Announce: {
|
|
139
|
+
const message = asMessage(request.message);
|
|
140
|
+
|
|
141
|
+
if (!message) {
|
|
142
|
+
throw new Error("announce needs a message");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return await options.onAnnounce(message);
|
|
146
|
+
}
|
|
147
|
+
|
|
125
148
|
default: {
|
|
126
149
|
throw new Error(`unknown method "${method}"`);
|
|
127
150
|
}
|
|
@@ -205,6 +228,12 @@ export const createServer = (options: ServerOptions) => {
|
|
|
205
228
|
);
|
|
206
229
|
})
|
|
207
230
|
.catch((error: unknown) => {
|
|
231
|
+
options.onLog("bridge call failed", {
|
|
232
|
+
method: String(request.method ?? ""),
|
|
233
|
+
error: errorText(error),
|
|
234
|
+
stack: error instanceof Error ? (error.stack ?? null) : null,
|
|
235
|
+
});
|
|
236
|
+
|
|
208
237
|
socket.write(
|
|
209
238
|
encode({
|
|
210
239
|
id,
|
|
@@ -215,6 +244,11 @@ export const createServer = (options: ServerOptions) => {
|
|
|
215
244
|
secret: error.key,
|
|
216
245
|
}
|
|
217
246
|
: {}),
|
|
247
|
+
...(error instanceof BridgeFailureError
|
|
248
|
+
? {
|
|
249
|
+
failure: error.code,
|
|
250
|
+
}
|
|
251
|
+
: {}),
|
|
218
252
|
}),
|
|
219
253
|
);
|
|
220
254
|
});
|
|
@@ -229,5 +263,3 @@ export const createServer = (options: ServerOptions) => {
|
|
|
229
263
|
},
|
|
230
264
|
});
|
|
231
265
|
};
|
|
232
|
-
|
|
233
|
-
export type BridgeSocketServer = ReturnType<typeof createServer>;
|