@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
|
@@ -51,7 +51,10 @@ export const createEventRing = (events: BridgeEventsModule | undefined, supervis
|
|
|
51
51
|
return {
|
|
52
52
|
names() {
|
|
53
53
|
return [
|
|
54
|
-
...new Set(
|
|
54
|
+
...new Set([
|
|
55
|
+
...(events?.patterns ?? []).map((pattern) => pattern.emit),
|
|
56
|
+
...(events?.emits ?? []),
|
|
57
|
+
]),
|
|
55
58
|
];
|
|
56
59
|
},
|
|
57
60
|
|
|
@@ -8,10 +8,20 @@ const OUTPUT_LIMIT = 256 * 1024;
|
|
|
8
8
|
|
|
9
9
|
const EXIT_GRACE_MS = 3000;
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
PATH
|
|
11
|
+
const FALLBACK_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
12
|
+
|
|
13
|
+
const inheritedPath = () => {
|
|
14
|
+
const declared = process.env.PATH ?? "";
|
|
15
|
+
|
|
16
|
+
return declared.length > 0 ? declared : FALLBACK_PATH;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const baseEnvironment = () => {
|
|
20
|
+
return {
|
|
21
|
+
HOME: "/home/container",
|
|
22
|
+
LANG: "C.UTF-8",
|
|
23
|
+
PATH: inheritedPath(),
|
|
24
|
+
};
|
|
15
25
|
};
|
|
16
26
|
|
|
17
27
|
export class BridgeExecError extends Error {
|
|
@@ -92,7 +102,7 @@ export const createBridgeExec = (target: BridgeExecTarget) => {
|
|
|
92
102
|
const child = Bun.spawn(argv, {
|
|
93
103
|
cwd: target.cwd,
|
|
94
104
|
env: {
|
|
95
|
-
...
|
|
105
|
+
...baseEnvironment(),
|
|
96
106
|
HOME: target.cwd,
|
|
97
107
|
...environment,
|
|
98
108
|
},
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import type { BridgeFiles } from "../context";
|
|
5
|
+
import type { BridgeEgress } from "./bridgeEgress";
|
|
6
|
+
import { createBridgeFiles } from "./bridgeFiles";
|
|
7
|
+
|
|
8
|
+
const LOCAL_SIGNATURE = 0x04_03_4b_50;
|
|
9
|
+
|
|
10
|
+
const CENTRAL_SIGNATURE = 0x02_01_4b_50;
|
|
11
|
+
|
|
12
|
+
const EOCD_SIGNATURE = 0x06_05_4b_50;
|
|
13
|
+
|
|
14
|
+
const FILE_ATTRIBUTES = 0x81_a4_00_00;
|
|
15
|
+
|
|
16
|
+
const SYMLINK_ATTRIBUTES = 0xa1_ff_00_00;
|
|
17
|
+
|
|
18
|
+
interface ArchiveEntry {
|
|
19
|
+
name: string;
|
|
20
|
+
content: string;
|
|
21
|
+
symlink?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const buildZip = (entries: ArchiveEntry[]) => {
|
|
25
|
+
const locals: Buffer[] = [];
|
|
26
|
+
const centrals: Buffer[] = [];
|
|
27
|
+
|
|
28
|
+
let offset = 0;
|
|
29
|
+
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const name = Buffer.from(entry.name, "utf8");
|
|
32
|
+
const data = Buffer.from(entry.content, "utf8");
|
|
33
|
+
|
|
34
|
+
const local = Buffer.alloc(30);
|
|
35
|
+
|
|
36
|
+
local.writeUInt32LE(LOCAL_SIGNATURE, 0);
|
|
37
|
+
local.writeUInt16LE(20, 4);
|
|
38
|
+
local.writeUInt32LE(data.length, 18);
|
|
39
|
+
local.writeUInt32LE(data.length, 22);
|
|
40
|
+
local.writeUInt16LE(name.length, 26);
|
|
41
|
+
|
|
42
|
+
const central = Buffer.alloc(46);
|
|
43
|
+
|
|
44
|
+
central.writeUInt32LE(CENTRAL_SIGNATURE, 0);
|
|
45
|
+
central.writeUInt16LE(20, 6);
|
|
46
|
+
central.writeUInt32LE(data.length, 20);
|
|
47
|
+
central.writeUInt32LE(data.length, 24);
|
|
48
|
+
central.writeUInt16LE(name.length, 28);
|
|
49
|
+
central.writeUInt32LE(entry.symlink === true ? SYMLINK_ATTRIBUTES : FILE_ATTRIBUTES, 38);
|
|
50
|
+
central.writeUInt32LE(offset, 42);
|
|
51
|
+
|
|
52
|
+
locals.push(local, name, data);
|
|
53
|
+
centrals.push(central, name);
|
|
54
|
+
|
|
55
|
+
offset += 30 + name.length + data.length;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const body = Buffer.concat(locals);
|
|
59
|
+
const directory = Buffer.concat(centrals);
|
|
60
|
+
const end = Buffer.alloc(22);
|
|
61
|
+
|
|
62
|
+
end.writeUInt32LE(EOCD_SIGNATURE, 0);
|
|
63
|
+
end.writeUInt16LE(entries.length, 8);
|
|
64
|
+
end.writeUInt16LE(entries.length, 10);
|
|
65
|
+
end.writeUInt32LE(directory.length, 12);
|
|
66
|
+
end.writeUInt32LE(body.length, 16);
|
|
67
|
+
|
|
68
|
+
return Buffer.concat([
|
|
69
|
+
body,
|
|
70
|
+
directory,
|
|
71
|
+
end,
|
|
72
|
+
]);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const egress: BridgeEgress = {
|
|
76
|
+
fetch() {
|
|
77
|
+
throw new Error("the egress channel is not part of this test");
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
download() {
|
|
81
|
+
throw new Error("the egress channel is not part of this test");
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
describe("extract", () => {
|
|
86
|
+
let root = "";
|
|
87
|
+
let files: BridgeFiles;
|
|
88
|
+
|
|
89
|
+
const archive = async (entries: ArchiveEntry[]) => {
|
|
90
|
+
await Bun.write(`${root}/pack.zip`, buildZip(entries));
|
|
91
|
+
|
|
92
|
+
return "pack.zip";
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const read = async (path: string) => {
|
|
96
|
+
return await Bun.file(`${root}/${path}`).text();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
beforeEach(async () => {
|
|
100
|
+
root = await mkdtemp(`${tmpdir()}/serverk-bridge-`);
|
|
101
|
+
|
|
102
|
+
files = createBridgeFiles({
|
|
103
|
+
root,
|
|
104
|
+
uid: 0,
|
|
105
|
+
gid: 0,
|
|
106
|
+
egress,
|
|
107
|
+
onLog: () => undefined,
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
afterEach(async () => {
|
|
112
|
+
await rm(root, {
|
|
113
|
+
force: true,
|
|
114
|
+
recursive: true,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("flattens paths, skips dotfiles and filters extensions by default", async () => {
|
|
119
|
+
const path = await archive([
|
|
120
|
+
{
|
|
121
|
+
name: "overrides/mods/alpha.jar",
|
|
122
|
+
content: "alpha",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "overrides/config/beta.json",
|
|
126
|
+
content: "beta",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: ".hidden.jar",
|
|
130
|
+
content: "hidden",
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
const written = await files.extract(path, "mods", {
|
|
135
|
+
extensions: [
|
|
136
|
+
"jar",
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(written).toEqual([
|
|
141
|
+
"mods/alpha.jar",
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
expect(await read("mods/alpha.jar")).toBe("alpha");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("flattens paths with no options at all", async () => {
|
|
148
|
+
const path = await archive([
|
|
149
|
+
{
|
|
150
|
+
name: "nested/deep/alpha.jar",
|
|
151
|
+
content: "alpha",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "beta.txt",
|
|
155
|
+
content: "beta",
|
|
156
|
+
},
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
const written = await files.extract(path, "plugins");
|
|
160
|
+
|
|
161
|
+
expect(written).toEqual([
|
|
162
|
+
"plugins/alpha.jar",
|
|
163
|
+
"plugins/beta.txt",
|
|
164
|
+
]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("keeps relative paths and creates intermediate directories in tree mode", async () => {
|
|
168
|
+
const path = await archive([
|
|
169
|
+
{
|
|
170
|
+
name: "mods/alpha.jar",
|
|
171
|
+
content: "alpha",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "config/deep/beta.json",
|
|
175
|
+
content: "beta",
|
|
176
|
+
},
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
const written = await files.extract(path, "staging", {
|
|
180
|
+
tree: true,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(written).toEqual([
|
|
184
|
+
"staging/mods/alpha.jar",
|
|
185
|
+
"staging/config/deep/beta.json",
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
expect(await read("staging/mods/alpha.jar")).toBe("alpha");
|
|
189
|
+
expect(await read("staging/config/deep/beta.json")).toBe("beta");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("keeps the dotfile skip in tree mode", async () => {
|
|
193
|
+
const path = await archive([
|
|
194
|
+
{
|
|
195
|
+
name: "mods/alpha.jar",
|
|
196
|
+
content: "alpha",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: "mods/.secret.jar",
|
|
200
|
+
content: "secret",
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: ".git/config.json",
|
|
204
|
+
content: "git",
|
|
205
|
+
},
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
const written = await files.extract(path, "staging", {
|
|
209
|
+
tree: true,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(written).toEqual([
|
|
213
|
+
"staging/mods/alpha.jar",
|
|
214
|
+
]);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("rejects absolute entries in tree mode", async () => {
|
|
218
|
+
const path = await archive([
|
|
219
|
+
{
|
|
220
|
+
name: "/etc/passwd",
|
|
221
|
+
content: "root",
|
|
222
|
+
},
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
await expect(
|
|
226
|
+
files.extract(path, "staging", {
|
|
227
|
+
tree: true,
|
|
228
|
+
}),
|
|
229
|
+
).rejects.toThrow("absolute path");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("rejects escaping entries in tree mode", async () => {
|
|
233
|
+
const path = await archive([
|
|
234
|
+
{
|
|
235
|
+
name: "overrides/../../escape.json",
|
|
236
|
+
content: "escape",
|
|
237
|
+
},
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
await expect(
|
|
241
|
+
files.extract(path, "staging", {
|
|
242
|
+
tree: true,
|
|
243
|
+
}),
|
|
244
|
+
).rejects.toThrow("is not allowed");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("rejects symlink entries in tree mode", async () => {
|
|
248
|
+
const path = await archive([
|
|
249
|
+
{
|
|
250
|
+
name: "mods/link.jar",
|
|
251
|
+
content: "/etc/passwd",
|
|
252
|
+
symlink: true,
|
|
253
|
+
},
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
await expect(
|
|
257
|
+
files.extract(path, "staging", {
|
|
258
|
+
tree: true,
|
|
259
|
+
}),
|
|
260
|
+
).rejects.toThrow("symlink");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("extracts only the selected prefix and strips it", async () => {
|
|
264
|
+
const path = await archive([
|
|
265
|
+
{
|
|
266
|
+
name: "overrides/mods/alpha.jar",
|
|
267
|
+
content: "alpha",
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: "server-overrides/mods/beta.jar",
|
|
271
|
+
content: "beta",
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: "modrinth.index.json",
|
|
275
|
+
content: "{}",
|
|
276
|
+
},
|
|
277
|
+
]);
|
|
278
|
+
|
|
279
|
+
const written = await files.extract(path, "", {
|
|
280
|
+
select: "overrides",
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
expect(written).toEqual([
|
|
284
|
+
"alpha.jar",
|
|
285
|
+
]);
|
|
286
|
+
|
|
287
|
+
expect(await read("alpha.jar")).toBe("alpha");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("composes select with tree", async () => {
|
|
291
|
+
const path = await archive([
|
|
292
|
+
{
|
|
293
|
+
name: "overrides/mods/alpha.jar",
|
|
294
|
+
content: "alpha",
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
name: "overrides/config/deep/beta.json",
|
|
298
|
+
content: "beta",
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: "modrinth.index.json",
|
|
302
|
+
content: "{}",
|
|
303
|
+
},
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
const written = await files.extract(path, "", {
|
|
307
|
+
select: "overrides/",
|
|
308
|
+
tree: true,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(written).toEqual([
|
|
312
|
+
"mods/alpha.jar",
|
|
313
|
+
"config/deep/beta.json",
|
|
314
|
+
]);
|
|
315
|
+
|
|
316
|
+
expect(await read("config/deep/beta.json")).toBe("beta");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("fails when nothing matches", async () => {
|
|
320
|
+
const path = await archive([
|
|
321
|
+
{
|
|
322
|
+
name: "overrides/mods/alpha.jar",
|
|
323
|
+
content: "alpha",
|
|
324
|
+
},
|
|
325
|
+
]);
|
|
326
|
+
|
|
327
|
+
await expect(
|
|
328
|
+
files.extract(path, "staging", {
|
|
329
|
+
select: "server-overrides",
|
|
330
|
+
tree: true,
|
|
331
|
+
}),
|
|
332
|
+
).rejects.toThrow("contained no matching files");
|
|
333
|
+
});
|
|
334
|
+
});
|
|
@@ -1,11 +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
|
+
import { type BridgeEgress, BridgeEgressCallError, BridgeEgressUnavailableError } from "./bridgeEgress";
|
|
5
6
|
import type { BridgeLogger } from "./bridgeLog";
|
|
7
|
+
import { USER_AGENT } from "./bridgeNet";
|
|
6
8
|
import { BridgePathError, resolveWithin, toSegments } from "./bridgePath";
|
|
7
9
|
import { isPrivileged } from "./bridgePrivilege";
|
|
8
|
-
import { readZip } from "./bridgeZip";
|
|
10
|
+
import { readZip, ZIP_ENTRY_LIMIT, ZIP_TREE_ENTRY_LIMIT } from "./bridgeZip";
|
|
9
11
|
|
|
10
12
|
const READ_LIMIT = 4 * 1024 * 1024;
|
|
11
13
|
|
|
@@ -21,8 +23,6 @@ const WALK_LIMIT = 4096;
|
|
|
21
23
|
|
|
22
24
|
const MILESTONE_STEP = 25;
|
|
23
25
|
|
|
24
|
-
const USER_AGENT = "serverk-bridge/1.0 (serverk.gg)";
|
|
25
|
-
|
|
26
26
|
const DIGEST_LENGTHS: Record<string, number> = {
|
|
27
27
|
sha1: 40,
|
|
28
28
|
sha256: 64,
|
|
@@ -33,9 +33,15 @@ export interface BridgeFilesOptions {
|
|
|
33
33
|
root: string;
|
|
34
34
|
uid: number;
|
|
35
35
|
gid: number;
|
|
36
|
+
egress: BridgeEgress;
|
|
36
37
|
onLog: BridgeLogger;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
interface DownloadSource {
|
|
41
|
+
body: AsyncIterable<Uint8Array>;
|
|
42
|
+
totalBytes: number | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
const parseDigest = (digest: string) => {
|
|
40
46
|
const [algo, expected] = digest.split(":");
|
|
41
47
|
|
|
@@ -87,6 +93,32 @@ const directorySize = async (path: string, depth: number): Promise<number> => {
|
|
|
87
93
|
return total;
|
|
88
94
|
};
|
|
89
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
|
+
|
|
90
122
|
const walk = async (directory: string, prefix: string, depth: number, out: BridgeFileEntry[]) => {
|
|
91
123
|
if (depth > WALK_DEPTH || out.length >= WALK_LIMIT) {
|
|
92
124
|
return;
|
|
@@ -169,6 +201,49 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
|
|
|
169
201
|
await ensureDirectory(segments.slice(0, -1).join("/"));
|
|
170
202
|
};
|
|
171
203
|
|
|
204
|
+
const openSource = async (remote: URL, downloadOptions: BridgeDownloadOptions): Promise<DownloadSource> => {
|
|
205
|
+
if (downloadOptions.cache !== false) {
|
|
206
|
+
try {
|
|
207
|
+
const shared = await options.egress.download(remote.href, {
|
|
208
|
+
digest: downloadOptions.digest,
|
|
209
|
+
sizeBytes: downloadOptions.sizeBytes,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
body: shared.body,
|
|
214
|
+
totalBytes: shared.sizeBytes > 0 ? shared.sizeBytes : (downloadOptions.sizeBytes ?? null),
|
|
215
|
+
};
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error instanceof BridgeEgressCallError) {
|
|
218
|
+
throw new BridgePathError("missing", `${remote.host} answered ${error.status ?? error.message}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!(error instanceof BridgeEgressUnavailableError)) {
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const response = await fetch(remote, {
|
|
228
|
+
headers: {
|
|
229
|
+
"user-agent": USER_AGENT,
|
|
230
|
+
},
|
|
231
|
+
redirect: "error",
|
|
232
|
+
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (!response.ok || !response.body) {
|
|
236
|
+
throw new BridgePathError("missing", `${remote.host} answered ${response.status}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const declared = Number(response.headers.get("content-length"));
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
body: response.body,
|
|
243
|
+
totalBytes: Number.isSafeInteger(declared) && declared > 0 ? declared : (downloadOptions.sizeBytes ?? null),
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
|
|
172
247
|
return {
|
|
173
248
|
async read(path) {
|
|
174
249
|
const resolved = await resolveWithin(options.root, path);
|
|
@@ -262,7 +337,7 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
|
|
|
262
337
|
const remote = new URL(url);
|
|
263
338
|
|
|
264
339
|
if (remote.protocol !== "https:") {
|
|
265
|
-
throw new BridgePathError("invalid",
|
|
340
|
+
throw new BridgePathError("invalid", `${remote.host} is not https`);
|
|
266
341
|
}
|
|
267
342
|
|
|
268
343
|
const limit = downloadOptions.sizeBytes ?? DOWNLOAD_LIMIT;
|
|
@@ -271,26 +346,14 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
|
|
|
271
346
|
throw new BridgePathError("invalid", `download of ${limit} bytes exceeds the limit`);
|
|
272
347
|
}
|
|
273
348
|
|
|
274
|
-
const
|
|
275
|
-
headers: {
|
|
276
|
-
"user-agent": USER_AGENT,
|
|
277
|
-
},
|
|
278
|
-
redirect: "follow",
|
|
279
|
-
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
if (!response.ok || !response.body) {
|
|
283
|
-
throw new BridgePathError("missing", `${url} answered ${response.status}`);
|
|
284
|
-
}
|
|
349
|
+
const source = await openSource(remote, downloadOptions);
|
|
285
350
|
|
|
286
351
|
await ensureParent(path);
|
|
287
352
|
|
|
288
353
|
const resolved = await resolveWithin(options.root, path);
|
|
289
354
|
const staging = `${resolved.target}.download`;
|
|
290
355
|
|
|
291
|
-
const
|
|
292
|
-
const totalBytes =
|
|
293
|
-
Number.isSafeInteger(declared) && declared > 0 ? declared : (downloadOptions.sizeBytes ?? null);
|
|
356
|
+
const totalBytes = source.totalBytes;
|
|
294
357
|
|
|
295
358
|
const hasher = downloadOptions.digest ? new Bun.CryptoHasher(parseDigest(downloadOptions.digest).algo) : null;
|
|
296
359
|
const writer = Bun.file(staging).writer();
|
|
@@ -305,11 +368,11 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
|
|
|
305
368
|
let milestone = 0;
|
|
306
369
|
|
|
307
370
|
try {
|
|
308
|
-
for await (const chunk of
|
|
371
|
+
for await (const chunk of source.body) {
|
|
309
372
|
readBytes += chunk.byteLength;
|
|
310
373
|
|
|
311
374
|
if (readBytes > limit) {
|
|
312
|
-
throw new BridgePathError("invalid", `${
|
|
375
|
+
throw new BridgePathError("invalid", `${remote.host} returned more than ${limit} bytes`);
|
|
313
376
|
}
|
|
314
377
|
|
|
315
378
|
hasher?.update(chunk);
|
|
@@ -336,7 +399,7 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
|
|
|
336
399
|
if (downloadOptions.sizeBytes !== undefined && readBytes !== downloadOptions.sizeBytes) {
|
|
337
400
|
throw new BridgePathError(
|
|
338
401
|
"invalid",
|
|
339
|
-
`${
|
|
402
|
+
`${remote.host} returned ${readBytes} bytes, expected ${downloadOptions.sizeBytes}`,
|
|
340
403
|
);
|
|
341
404
|
}
|
|
342
405
|
|
|
@@ -344,7 +407,7 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
|
|
|
344
407
|
const { expected } = parseDigest(downloadOptions.digest);
|
|
345
408
|
|
|
346
409
|
if (hasher.digest("hex") !== expected) {
|
|
347
|
-
throw new BridgePathError("invalid", `${
|
|
410
|
+
throw new BridgePathError("invalid", `${remote.host} failed digest verification`);
|
|
348
411
|
}
|
|
349
412
|
}
|
|
350
413
|
} catch (error) {
|
|
@@ -366,7 +429,7 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
|
|
|
366
429
|
});
|
|
367
430
|
},
|
|
368
431
|
|
|
369
|
-
async extract(path, destination,
|
|
432
|
+
async extract(path, destination, extractOptions: BridgeExtractOptions = {}) {
|
|
370
433
|
const resolved = await resolveWithin(options.root, path);
|
|
371
434
|
|
|
372
435
|
if (!resolved.exists || resolved.isDirectory) {
|
|
@@ -377,16 +440,32 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
|
|
|
377
440
|
throw new BridgePathError("invalid", `archive "${path}" is larger than the extract limit`);
|
|
378
441
|
}
|
|
379
442
|
|
|
380
|
-
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);
|
|
381
447
|
const target = await ensureDirectory(destination);
|
|
382
448
|
const buffer = Buffer.from(await Bun.file(resolved.target).arrayBuffer());
|
|
449
|
+
const directories = new Set<string>();
|
|
383
450
|
const written: string[] = [];
|
|
384
451
|
|
|
385
|
-
for (const entry of readZip(buffer)) {
|
|
386
|
-
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) ?? "";
|
|
387
466
|
const extension = name.split(".").at(-1)?.toLowerCase() ?? "";
|
|
388
467
|
|
|
389
|
-
if (name.length === 0 ||
|
|
468
|
+
if (name.length === 0 || segments.some((segment) => segment.startsWith("."))) {
|
|
390
469
|
continue;
|
|
391
470
|
}
|
|
392
471
|
|
|
@@ -394,10 +473,30 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
|
|
|
394
473
|
continue;
|
|
395
474
|
}
|
|
396
475
|
|
|
397
|
-
|
|
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("/");
|
|
398
497
|
|
|
399
|
-
await Bun.write(`${target}/${
|
|
400
|
-
await own(`${target}/${
|
|
498
|
+
await Bun.write(`${target}/${segments.join("/")}`, entry.data);
|
|
499
|
+
await own(`${target}/${segments.join("/")}`);
|
|
401
500
|
|
|
402
501
|
written.push(relative);
|
|
403
502
|
}
|
package/src/runtime/bridgeLog.ts
CHANGED
|
@@ -1,15 +1,38 @@
|
|
|
1
|
+
import { BRIDGE_LOG_SOURCE } from "../protocol/bridgeLine";
|
|
2
|
+
import { BridgeTerminalLevel } from "../terminal";
|
|
3
|
+
|
|
1
4
|
export type BridgeLogger = (message: string, fields?: Record<string, unknown>) => void;
|
|
2
5
|
|
|
3
|
-
export
|
|
6
|
+
export interface BridgeLeveledLogger extends BridgeLogger {
|
|
7
|
+
warn: BridgeLogger;
|
|
8
|
+
error: BridgeLogger;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const write = (level: BridgeTerminalLevel, message: string, fields: Record<string, unknown>) => {
|
|
4
12
|
process.stdout.write(
|
|
5
13
|
`${JSON.stringify({
|
|
6
|
-
source: "bridge",
|
|
7
|
-
message,
|
|
8
14
|
...fields,
|
|
15
|
+
source: BRIDGE_LOG_SOURCE,
|
|
16
|
+
level,
|
|
17
|
+
message,
|
|
9
18
|
})}\n`,
|
|
10
19
|
);
|
|
11
20
|
};
|
|
12
21
|
|
|
22
|
+
export const log: BridgeLeveledLogger = Object.assign(
|
|
23
|
+
(message: string, fields: Record<string, unknown> = {}) => {
|
|
24
|
+
write(BridgeTerminalLevel.Info, message, fields);
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
warn: (message: string, fields: Record<string, unknown> = {}) => {
|
|
28
|
+
write(BridgeTerminalLevel.Warn, message, fields);
|
|
29
|
+
},
|
|
30
|
+
error: (message: string, fields: Record<string, unknown> = {}) => {
|
|
31
|
+
write(BridgeTerminalLevel.Error, message, fields);
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
|
|
13
36
|
export const errorText = (error: unknown) => {
|
|
14
37
|
return error instanceof Error ? error.message : String(error);
|
|
15
38
|
};
|