@serverkgg/bridge 0.0.2 → 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 +1 -1
- package/src/cli/link.test.ts +199 -0
- package/src/cli/link.ts +319 -0
- package/src/cli/main.ts +8 -0
- package/src/cli/validate.test.ts +89 -0
- package/src/cli/validate.ts +100 -0
- package/src/context/bridgeContext.ts +7 -1
- package/src/core/bridgeKind.ts +1 -0
- package/src/modules/bridge.ts +6 -1
- package/src/modules/bridgeModule.ts +17 -3
- package/src/panel/bridgeLayout.ts +1 -0
- package/src/panel/bridgePanel.ts +34 -2
- package/src/protocol/bridgeCatalog.ts +1 -0
- package/src/protocol/bridgeEvents.ts +20 -0
- package/src/protocol/bridgeFrame.ts +2 -2
- package/src/protocol/bridgeLimits.ts +41 -0
- package/src/protocol/index.ts +2 -0
- package/src/runtime/bridgeActions.ts +53 -0
- package/src/runtime/bridgeDriver.test.ts +450 -0
- package/src/runtime/bridgeDriver.ts +245 -6
- package/src/runtime/bridgeEvents.ts +4 -1
- package/src/runtime/bridgeFiles.test.ts +334 -0
- package/src/runtime/bridgeFiles.ts +72 -10
- package/src/runtime/bridgeRegistry.test.ts +291 -0
- package/src/runtime/bridgeRegistry.ts +45 -51
- package/src/runtime/bridgeServer.ts +5 -20
- package/src/runtime/bridgeZip.ts +14 -3
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { Dirent } from "node:fs";
|
|
2
2
|
import { chown, mkdir, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import type { BridgeDownloadOptions, BridgeFileEntry, BridgeFiles } from "../context";
|
|
3
|
+
import type { BridgeDownloadOptions, BridgeExtractOptions, BridgeFileEntry, BridgeFiles } from "../context";
|
|
4
4
|
import { compileGlob } from "../utils";
|
|
5
5
|
import { type BridgeEgress, BridgeEgressCallError, BridgeEgressUnavailableError } from "./bridgeEgress";
|
|
6
6
|
import type { BridgeLogger } from "./bridgeLog";
|
|
7
7
|
import { USER_AGENT } from "./bridgeNet";
|
|
8
8
|
import { BridgePathError, resolveWithin, toSegments } from "./bridgePath";
|
|
9
9
|
import { isPrivileged } from "./bridgePrivilege";
|
|
10
|
-
import { readZip } from "./bridgeZip";
|
|
10
|
+
import { readZip, ZIP_ENTRY_LIMIT, ZIP_TREE_ENTRY_LIMIT } from "./bridgeZip";
|
|
11
11
|
|
|
12
12
|
const READ_LIMIT = 4 * 1024 * 1024;
|
|
13
13
|
|
|
@@ -93,6 +93,32 @@ const directorySize = async (path: string, depth: number): Promise<number> => {
|
|
|
93
93
|
return total;
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
+
const selectPrefix = (select: string | undefined) => {
|
|
97
|
+
if (select === undefined) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const segments = toSegments(select);
|
|
102
|
+
|
|
103
|
+
return segments.length === 0 ? null : `${segments.join("/")}/`;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const selectedName = (name: string, prefix: string | null) => {
|
|
107
|
+
if (prefix === null) {
|
|
108
|
+
return name;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return name.startsWith(prefix) ? name.slice(prefix.length) : null;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const treeSegments = (name: string) => {
|
|
115
|
+
if (name.startsWith("/") || /^[A-Za-z]:/.test(name)) {
|
|
116
|
+
throw new BridgePathError("invalid", `archive entry "${name}" is an absolute path`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return toSegments(name);
|
|
120
|
+
};
|
|
121
|
+
|
|
96
122
|
const walk = async (directory: string, prefix: string, depth: number, out: BridgeFileEntry[]) => {
|
|
97
123
|
if (depth > WALK_DEPTH || out.length >= WALK_LIMIT) {
|
|
98
124
|
return;
|
|
@@ -403,7 +429,7 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
|
|
|
403
429
|
});
|
|
404
430
|
},
|
|
405
431
|
|
|
406
|
-
async extract(path, destination,
|
|
432
|
+
async extract(path, destination, extractOptions: BridgeExtractOptions = {}) {
|
|
407
433
|
const resolved = await resolveWithin(options.root, path);
|
|
408
434
|
|
|
409
435
|
if (!resolved.exists || resolved.isDirectory) {
|
|
@@ -414,16 +440,32 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
|
|
|
414
440
|
throw new BridgePathError("invalid", `archive "${path}" is larger than the extract limit`);
|
|
415
441
|
}
|
|
416
442
|
|
|
417
|
-
const
|
|
443
|
+
const tree = extractOptions.tree === true;
|
|
444
|
+
const allowed = extractOptions.extensions?.map((extension) => extension.toLowerCase());
|
|
445
|
+
const prefix = selectPrefix(extractOptions.select);
|
|
446
|
+
const base = toSegments(destination);
|
|
418
447
|
const target = await ensureDirectory(destination);
|
|
419
448
|
const buffer = Buffer.from(await Bun.file(resolved.target).arrayBuffer());
|
|
449
|
+
const directories = new Set<string>();
|
|
420
450
|
const written: string[] = [];
|
|
421
451
|
|
|
422
|
-
for (const entry of readZip(buffer)) {
|
|
423
|
-
const
|
|
452
|
+
for (const entry of readZip(buffer, tree ? ZIP_TREE_ENTRY_LIMIT : ZIP_ENTRY_LIMIT)) {
|
|
453
|
+
const selected = selectedName(entry.name, prefix);
|
|
454
|
+
|
|
455
|
+
if (selected === null) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const segments = tree
|
|
460
|
+
? treeSegments(selected)
|
|
461
|
+
: [
|
|
462
|
+
selected.split("/").at(-1) ?? "",
|
|
463
|
+
];
|
|
464
|
+
|
|
465
|
+
const name = segments.at(-1) ?? "";
|
|
424
466
|
const extension = name.split(".").at(-1)?.toLowerCase() ?? "";
|
|
425
467
|
|
|
426
|
-
if (name.length === 0 ||
|
|
468
|
+
if (name.length === 0 || segments.some((segment) => segment.startsWith("."))) {
|
|
427
469
|
continue;
|
|
428
470
|
}
|
|
429
471
|
|
|
@@ -431,10 +473,30 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
|
|
|
431
473
|
continue;
|
|
432
474
|
}
|
|
433
475
|
|
|
434
|
-
|
|
476
|
+
if (tree && entry.symlink) {
|
|
477
|
+
throw new BridgePathError("invalid", `archive entry "${entry.name}" is a symlink`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const parent = segments.slice(0, -1);
|
|
481
|
+
|
|
482
|
+
if (parent.length > 0 && !directories.has(parent.join("/"))) {
|
|
483
|
+
await ensureDirectory(
|
|
484
|
+
[
|
|
485
|
+
...base,
|
|
486
|
+
...parent,
|
|
487
|
+
].join("/"),
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
directories.add(parent.join("/"));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const relative = [
|
|
494
|
+
...base,
|
|
495
|
+
...segments,
|
|
496
|
+
].join("/");
|
|
435
497
|
|
|
436
|
-
await Bun.write(`${target}/${
|
|
437
|
-
await own(`${target}/${
|
|
498
|
+
await Bun.write(`${target}/${segments.join("/")}`, entry.data);
|
|
499
|
+
await own(`${target}/${segments.join("/")}`);
|
|
438
500
|
|
|
439
501
|
written.push(relative);
|
|
440
502
|
}
|
|
@@ -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,5 +1,5 @@
|
|
|
1
1
|
import type { BridgeContext } from "../context";
|
|
2
|
-
import { BridgeAction, BridgeKind, type BridgeRow, type BridgeValues } from "../core";
|
|
2
|
+
import { BridgeAction, BridgeKind, type BridgeRow, type BridgeValues, isReadAction } from "../core";
|
|
3
3
|
import type { BridgeDriver, BridgePanelModule, BridgeTerminalModule } from "../modules";
|
|
4
4
|
import {
|
|
5
5
|
BRIDGE_VERSION,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type BridgeModuleManifest,
|
|
8
8
|
type BridgeTerminalManifest,
|
|
9
9
|
} from "../protocol";
|
|
10
|
+
import { actionsOf } from "./bridgeActions";
|
|
10
11
|
import { BridgeDriverError } from "./bridgeDriver";
|
|
11
12
|
import type { BridgeEventRing } from "./bridgeEvents";
|
|
12
13
|
import type { BridgeSupervisor } from "./bridgeSupervisor";
|
|
@@ -16,55 +17,12 @@ interface CacheEntry {
|
|
|
16
17
|
expiresAt: number;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return [
|
|
23
|
-
BridgeAction.List,
|
|
24
|
-
];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
case BridgeKind.Settings: {
|
|
28
|
-
return [
|
|
29
|
-
BridgeAction.Read,
|
|
30
|
-
...(module.write
|
|
31
|
-
? [
|
|
32
|
-
BridgeAction.Write,
|
|
33
|
-
]
|
|
34
|
-
: []),
|
|
35
|
-
];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
case BridgeKind.Collection: {
|
|
39
|
-
return [
|
|
40
|
-
BridgeAction.List,
|
|
41
|
-
...(module.add
|
|
42
|
-
? [
|
|
43
|
-
BridgeAction.Add,
|
|
44
|
-
]
|
|
45
|
-
: []),
|
|
46
|
-
...Object.keys(module.actions ?? {}),
|
|
47
|
-
];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
case BridgeKind.Catalog: {
|
|
51
|
-
return [
|
|
52
|
-
BridgeAction.Install,
|
|
53
|
-
BridgeAction.Installed,
|
|
54
|
-
BridgeAction.Remove,
|
|
55
|
-
BridgeAction.Search,
|
|
56
|
-
...(module.toggle
|
|
57
|
-
? [
|
|
58
|
-
BridgeAction.Toggle,
|
|
59
|
-
]
|
|
60
|
-
: []),
|
|
61
|
-
];
|
|
62
|
-
}
|
|
20
|
+
const requiresRunning = (module: BridgePanelModule) => {
|
|
21
|
+
if (module.kind === BridgeKind.Actions || module.kind === BridgeKind.Collection) {
|
|
22
|
+
return module.requiresRunning ?? false;
|
|
63
23
|
}
|
|
64
|
-
};
|
|
65
24
|
|
|
66
|
-
|
|
67
|
-
return module.kind === BridgeKind.Collection ? (module.requiresRunning ?? false) : false;
|
|
25
|
+
return false;
|
|
68
26
|
};
|
|
69
27
|
|
|
70
28
|
const refreshOf = (module: BridgePanelModule) => {
|
|
@@ -151,6 +109,18 @@ const asOptional = (args: BridgeValues, key: string) => {
|
|
|
151
109
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
152
110
|
};
|
|
153
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
|
+
|
|
154
124
|
const asPage = (args: BridgeValues) => {
|
|
155
125
|
const value = args.page;
|
|
156
126
|
|
|
@@ -167,6 +137,20 @@ export const createRegistry = (
|
|
|
167
137
|
const inflight = new Map<string, Promise<unknown>>();
|
|
168
138
|
const modules = driver.modules ?? {};
|
|
169
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
|
+
|
|
170
154
|
const cached = async (key: string, ttlSeconds: number | undefined, run: () => Promise<unknown>) => {
|
|
171
155
|
if (!ttlSeconds) {
|
|
172
156
|
return await run();
|
|
@@ -207,6 +191,18 @@ export const createRegistry = (
|
|
|
207
191
|
|
|
208
192
|
const dispatch = async (module: BridgePanelModule, name: string, action: string, args: BridgeValues) => {
|
|
209
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
|
+
|
|
210
206
|
case BridgeKind.Options: {
|
|
211
207
|
if (action === BridgeAction.List) {
|
|
212
208
|
const overlay: Record<string, string> = {};
|
|
@@ -268,7 +264,7 @@ export const createRegistry = (
|
|
|
268
264
|
throw new BridgeDriverError(`module "${name}" has no row "${id}"`);
|
|
269
265
|
}
|
|
270
266
|
|
|
271
|
-
await rowAction(context, row as BridgeRow);
|
|
267
|
+
await rowAction(context, row as BridgeRow, withoutId(args));
|
|
272
268
|
|
|
273
269
|
return null;
|
|
274
270
|
}
|
|
@@ -352,5 +348,3 @@ export const createRegistry = (
|
|
|
352
348
|
},
|
|
353
349
|
};
|
|
354
350
|
};
|
|
355
|
-
|
|
356
|
-
export type BridgeRegistry = ReturnType<typeof createRegistry>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { BridgeFailureError, BridgeSecretError, type BridgeValues
|
|
2
|
+
import { BridgeFailureError, BridgeSecretError, type BridgeValues } from "../core";
|
|
3
3
|
import {
|
|
4
4
|
BRIDGE_FRAME_LIMIT,
|
|
5
5
|
type BridgeAnnounceResult,
|
|
@@ -29,7 +29,7 @@ export interface ServerOptions {
|
|
|
29
29
|
onQuiesce: () => Promise<BridgeQuiesceResult>;
|
|
30
30
|
onRelease: () => Promise<void>;
|
|
31
31
|
onDescribe: () => Promise<BridgeDescribeResult>;
|
|
32
|
-
onAnnounce: (message:
|
|
32
|
+
onAnnounce: (message: string) => Promise<BridgeAnnounceResult>;
|
|
33
33
|
onLog: BridgeLogger;
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -47,21 +47,8 @@ interface IncomingFrame {
|
|
|
47
47
|
message?: unknown;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
const asMessage = (value: unknown):
|
|
51
|
-
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const { ar, en } = value as Record<string, unknown>;
|
|
56
|
-
|
|
57
|
-
if (typeof ar !== "string" || typeof en !== "string") {
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
ar,
|
|
63
|
-
en,
|
|
64
|
-
};
|
|
50
|
+
const asMessage = (value: unknown): string | null => {
|
|
51
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
65
52
|
};
|
|
66
53
|
|
|
67
54
|
const matches = (left: string, right: string) => {
|
|
@@ -152,7 +139,7 @@ export const createServer = (options: ServerOptions) => {
|
|
|
152
139
|
const message = asMessage(request.message);
|
|
153
140
|
|
|
154
141
|
if (!message) {
|
|
155
|
-
throw new Error("announce needs a message
|
|
142
|
+
throw new Error("announce needs a message");
|
|
156
143
|
}
|
|
157
144
|
|
|
158
145
|
return await options.onAnnounce(message);
|
|
@@ -276,5 +263,3 @@ export const createServer = (options: ServerOptions) => {
|
|
|
276
263
|
},
|
|
277
264
|
});
|
|
278
265
|
};
|
|
279
|
-
|
|
280
|
-
export type BridgeSocketServer = ReturnType<typeof createServer>;
|
package/src/runtime/bridgeZip.ts
CHANGED
|
@@ -18,13 +18,20 @@ const DEFLATED = 8;
|
|
|
18
18
|
|
|
19
19
|
const ENCRYPTED_FLAG = 0x1;
|
|
20
20
|
|
|
21
|
-
const
|
|
21
|
+
export const ZIP_ENTRY_LIMIT = 2048;
|
|
22
|
+
|
|
23
|
+
export const ZIP_TREE_ENTRY_LIMIT = 8192;
|
|
22
24
|
|
|
23
25
|
const MAX_ENTRY_BYTES = 128 * 1024 * 1024;
|
|
24
26
|
|
|
27
|
+
const UNIX_MODE_FORMAT_MASK = 0xf0_00;
|
|
28
|
+
|
|
29
|
+
const UNIX_MODE_SYMLINK = 0xa0_00;
|
|
30
|
+
|
|
25
31
|
export interface ZipEntry {
|
|
26
32
|
name: string;
|
|
27
33
|
data: Uint8Array;
|
|
34
|
+
symlink: boolean;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
const findEndOfCentralDirectory = (buffer: Buffer) => {
|
|
@@ -39,7 +46,7 @@ const findEndOfCentralDirectory = (buffer: Buffer) => {
|
|
|
39
46
|
return -1;
|
|
40
47
|
};
|
|
41
48
|
|
|
42
|
-
export const readZip = (buffer: Buffer): ZipEntry[] => {
|
|
49
|
+
export const readZip = (buffer: Buffer, maxEntries: number = ZIP_ENTRY_LIMIT): ZipEntry[] => {
|
|
43
50
|
const eocd = findEndOfCentralDirectory(buffer);
|
|
44
51
|
|
|
45
52
|
if (eocd === -1) {
|
|
@@ -49,7 +56,7 @@ export const readZip = (buffer: Buffer): ZipEntry[] => {
|
|
|
49
56
|
const total = buffer.readUInt16LE(eocd + 10);
|
|
50
57
|
const directoryOffset = buffer.readUInt32LE(eocd + 16);
|
|
51
58
|
|
|
52
|
-
if (total >
|
|
59
|
+
if (total > maxEntries) {
|
|
53
60
|
throw new Error(`archive has too many entries (${total})`);
|
|
54
61
|
}
|
|
55
62
|
|
|
@@ -69,8 +76,10 @@ export const readZip = (buffer: Buffer): ZipEntry[] => {
|
|
|
69
76
|
const nameLength = buffer.readUInt16LE(cursor + 28);
|
|
70
77
|
const extraLength = buffer.readUInt16LE(cursor + 30);
|
|
71
78
|
const commentLength = buffer.readUInt16LE(cursor + 32);
|
|
79
|
+
const externalAttributes = buffer.readUInt32LE(cursor + 38);
|
|
72
80
|
const localOffset = buffer.readUInt32LE(cursor + 42);
|
|
73
81
|
const name = buffer.subarray(cursor + 46, cursor + 46 + nameLength).toString("utf8");
|
|
82
|
+
const symlink = ((externalAttributes >>> 16) & UNIX_MODE_FORMAT_MASK) === UNIX_MODE_SYMLINK;
|
|
74
83
|
|
|
75
84
|
cursor += 46 + nameLength + extraLength + commentLength;
|
|
76
85
|
|
|
@@ -95,6 +104,7 @@ export const readZip = (buffer: Buffer): ZipEntry[] => {
|
|
|
95
104
|
entries.push({
|
|
96
105
|
name,
|
|
97
106
|
data: compressed,
|
|
107
|
+
symlink,
|
|
98
108
|
});
|
|
99
109
|
|
|
100
110
|
continue;
|
|
@@ -115,6 +125,7 @@ export const readZip = (buffer: Buffer): ZipEntry[] => {
|
|
|
115
125
|
entries.push({
|
|
116
126
|
name,
|
|
117
127
|
data,
|
|
128
|
+
symlink,
|
|
118
129
|
});
|
|
119
130
|
}
|
|
120
131
|
|