@serverkgg/bridge 0.0.7 → 0.0.9

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.
Files changed (52) hide show
  1. package/package.json +4 -3
  2. package/src/cli/validate.ts +1 -0
  3. package/src/context/bridgeContext.ts +10 -0
  4. package/src/context/index.ts +1 -0
  5. package/src/core/bridgeDownloadError.ts +10 -0
  6. package/src/core/bridgeHold.ts +12 -0
  7. package/src/core/bridgeKind.ts +2 -0
  8. package/src/core/bridgeUserError.test.ts +23 -0
  9. package/src/core/bridgeUserError.ts +12 -0
  10. package/src/core/index.ts +3 -0
  11. package/src/manifest/index.ts +1 -0
  12. package/src/manifest/manifest.test.ts +85 -0
  13. package/src/manifest/manifest.ts +21 -0
  14. package/src/manifest/presence.ts +48 -0
  15. package/src/manifest/primitives.ts +6 -0
  16. package/src/modules/bridge.ts +45 -2
  17. package/src/modules/bridgeDriver.ts +2 -0
  18. package/src/modules/bridgeModule.ts +25 -3
  19. package/src/panel/bridgeDetail.ts +49 -0
  20. package/src/panel/bridgeLayout.ts +1 -0
  21. package/src/panel/bridgePanel.ts +30 -0
  22. package/src/panel/bridgeUploadMode.ts +4 -0
  23. package/src/panel/index.ts +2 -0
  24. package/src/protocol/bridgeEgress.ts +8 -1
  25. package/src/protocol/bridgeEvents.ts +4 -0
  26. package/src/protocol/bridgeFrame.ts +28 -1
  27. package/src/protocol/bridgeLimits.ts +10 -0
  28. package/src/runtime/bridgeActions.ts +7 -0
  29. package/src/runtime/bridgeDriver.test.ts +477 -2
  30. package/src/runtime/bridgeDriver.ts +116 -3
  31. package/src/runtime/bridgeEgress.ts +3 -0
  32. package/src/runtime/bridgeEvents.ts +7 -5
  33. package/src/runtime/bridgeFiles.test.ts +625 -15
  34. package/src/runtime/bridgeFiles.ts +152 -45
  35. package/src/runtime/bridgeInstall.test.ts +118 -0
  36. package/src/runtime/bridgeInstall.ts +55 -0
  37. package/src/runtime/bridgeNet.ts +32 -3
  38. package/src/runtime/bridgePending.test.ts +148 -0
  39. package/src/runtime/bridgePending.ts +43 -0
  40. package/src/runtime/bridgePrivilege.test.ts +77 -0
  41. package/src/runtime/bridgePrivilege.ts +5 -3
  42. package/src/runtime/bridgeRegistry.test.ts +134 -0
  43. package/src/runtime/bridgeRegistry.ts +26 -4
  44. package/src/runtime/bridgeServer.test.ts +338 -0
  45. package/src/runtime/bridgeServer.ts +25 -3
  46. package/src/runtime/bridgeSupervisor.ts +18 -0
  47. package/src/runtime/bridgeZip.ts +179 -58
  48. package/src/runtime/serve.ts +64 -4
  49. package/src/wire/index.ts +2 -0
  50. package/src/wire/panel.test.ts +451 -0
  51. package/src/wire/panel.ts +295 -0
  52. package/src/wire/zEnvKey.ts +7 -0
@@ -6,12 +6,26 @@ const CENTRAL_SIGNATURE = 0x02_01_4b_50;
6
6
 
7
7
  const LOCAL_SIGNATURE = 0x04_03_4b_50;
8
8
 
9
+ const ZIP64_LOCATOR_SIGNATURE = 0x07_06_4b_50;
10
+
11
+ const ZIP64_EOCD_SIGNATURE = 0x06_06_4b_50;
12
+
9
13
  const EOCD_MIN_LENGTH = 22;
10
14
 
11
15
  const EOCD_SEARCH_LIMIT = 65_557;
12
16
 
17
+ const ZIP64_LOCATOR_LENGTH = 20;
18
+
19
+ const ZIP64_EOCD_MIN_LENGTH = 56;
20
+
21
+ const ZIP64_EXTRA_ID = 0x00_01;
22
+
23
+ const CENTRAL_HEADER_LENGTH = 46;
24
+
13
25
  const LOCAL_HEADER_LENGTH = 30;
14
26
 
27
+ const SIZE_SENTINEL = 0xff_ff_ff_ff;
28
+
15
29
  const STORED = 0;
16
30
 
17
31
  const DEFLATED = 8;
@@ -20,7 +34,7 @@ const ENCRYPTED_FLAG = 0x1;
20
34
 
21
35
  export const ZIP_ENTRY_LIMIT = 2048;
22
36
 
23
- export const ZIP_TREE_ENTRY_LIMIT = 8192;
37
+ export const ZIP_TREE_ENTRY_LIMIT = 16_384;
24
38
 
25
39
  const MAX_ENTRY_BYTES = 128 * 1024 * 1024;
26
40
 
@@ -30,14 +44,34 @@ const UNIX_MODE_SYMLINK = 0xa0_00;
30
44
 
31
45
  export interface ZipEntry {
32
46
  name: string;
33
- data: Uint8Array;
34
47
  symlink: boolean;
48
+ sizeBytes: number;
49
+ compressedSize: number;
50
+ method: number;
51
+ localOffset: number;
35
52
  }
36
53
 
37
- const findEndOfCentralDirectory = (buffer: Buffer) => {
38
- const start = Math.max(0, buffer.length - EOCD_SEARCH_LIMIT);
54
+ export interface ZipArchive {
55
+ entries: ZipEntry[];
56
+ read(entry: ZipEntry): Promise<Uint8Array>;
57
+ }
58
+
59
+ const readRange = async (path: string, start: number, end: number) => {
60
+ return Buffer.from(await Bun.file(path).slice(start, end).arrayBuffer());
61
+ };
62
+
63
+ const readOffset = (buffer: Buffer, offset: number) => {
64
+ const value = buffer.readBigUInt64LE(offset);
65
+
66
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
67
+ throw new Error("archive declares an offset this runtime cannot address");
68
+ }
69
+
70
+ return Number(value);
71
+ };
39
72
 
40
- for (let offset = buffer.length - EOCD_MIN_LENGTH; offset >= start; offset--) {
73
+ const findEndOfCentralDirectory = (buffer: Buffer) => {
74
+ for (let offset = buffer.length - EOCD_MIN_LENGTH; offset >= 0; offset--) {
41
75
  if (buffer.readUInt32LE(offset) === EOCD_SIGNATURE) {
42
76
  return offset;
43
77
  }
@@ -46,88 +80,175 @@ const findEndOfCentralDirectory = (buffer: Buffer) => {
46
80
  return -1;
47
81
  };
48
82
 
49
- export const readZip = (buffer: Buffer, maxEntries: number = ZIP_ENTRY_LIMIT): ZipEntry[] => {
50
- const eocd = findEndOfCentralDirectory(buffer);
83
+ interface CentralDirectory {
84
+ total: number;
85
+ offset: number;
86
+ sizeBytes: number;
87
+ }
51
88
 
52
- if (eocd === -1) {
53
- throw new Error("archive has no zip central directory");
89
+ const readZip64Directory = async (path: string, tail: Buffer, eocd: number): Promise<CentralDirectory | null> => {
90
+ const locator = eocd - ZIP64_LOCATOR_LENGTH;
91
+
92
+ if (locator < 0 || tail.readUInt32LE(locator) !== ZIP64_LOCATOR_SIGNATURE) {
93
+ return null;
54
94
  }
55
95
 
56
- const total = buffer.readUInt16LE(eocd + 10);
57
- const directoryOffset = buffer.readUInt32LE(eocd + 16);
96
+ const record = readOffset(tail, locator + 8);
97
+ const buffer = await readRange(path, record, record + ZIP64_EOCD_MIN_LENGTH);
58
98
 
59
- if (total > maxEntries) {
60
- throw new Error(`archive has too many entries (${total})`);
99
+ if (buffer.length < ZIP64_EOCD_MIN_LENGTH || buffer.readUInt32LE(0) !== ZIP64_EOCD_SIGNATURE) {
100
+ throw new Error("archive has a corrupt zip64 central directory record");
61
101
  }
62
102
 
63
- const entries: ZipEntry[] = [];
103
+ return {
104
+ total: readOffset(buffer, 32),
105
+ sizeBytes: readOffset(buffer, 40),
106
+ offset: readOffset(buffer, 48),
107
+ };
108
+ };
64
109
 
65
- let cursor = directoryOffset;
110
+ interface Zip64Extended {
111
+ sizeBytes: number | null;
112
+ compressedSize: number | null;
113
+ localOffset: number | null;
114
+ }
66
115
 
67
- for (let index = 0; index < total; index++) {
68
- if (cursor + 46 > buffer.length || buffer.readUInt32LE(cursor) !== CENTRAL_SIGNATURE) {
69
- throw new Error("archive central directory is corrupt");
70
- }
116
+ const readZip64Extra = (extra: Buffer, sizeBytes: number, compressedSize: number, localOffset: number) => {
117
+ const extended: Zip64Extended = {
118
+ sizeBytes: null,
119
+ compressedSize: null,
120
+ localOffset: null,
121
+ };
71
122
 
72
- const flags = buffer.readUInt16LE(cursor + 8);
73
- const method = buffer.readUInt16LE(cursor + 10);
74
- const compressedSize = buffer.readUInt32LE(cursor + 20);
75
- const uncompressedSize = buffer.readUInt32LE(cursor + 24);
76
- const nameLength = buffer.readUInt16LE(cursor + 28);
77
- const extraLength = buffer.readUInt16LE(cursor + 30);
78
- const commentLength = buffer.readUInt16LE(cursor + 32);
79
- const externalAttributes = buffer.readUInt32LE(cursor + 38);
80
- const localOffset = buffer.readUInt32LE(cursor + 42);
81
- const name = buffer.subarray(cursor + 46, cursor + 46 + nameLength).toString("utf8");
82
- const symlink = ((externalAttributes >>> 16) & UNIX_MODE_FORMAT_MASK) === UNIX_MODE_SYMLINK;
123
+ let cursor = 0;
83
124
 
84
- cursor += 46 + nameLength + extraLength + commentLength;
125
+ while (cursor + 4 <= extra.length) {
126
+ const id = extra.readUInt16LE(cursor);
127
+ const length = extra.readUInt16LE(cursor + 2);
128
+ const body = extra.subarray(cursor + 4, cursor + 4 + length);
85
129
 
86
- if (name.endsWith("/") || (flags & ENCRYPTED_FLAG) !== 0) {
130
+ cursor += 4 + length;
131
+
132
+ if (id !== ZIP64_EXTRA_ID) {
87
133
  continue;
88
134
  }
89
135
 
90
- if (uncompressedSize > MAX_ENTRY_BYTES) {
91
- throw new Error(`archive entry "${name}" is too large`);
136
+ let field = 0;
137
+
138
+ if (sizeBytes === SIZE_SENTINEL && field + 8 <= body.length) {
139
+ extended.sizeBytes = readOffset(body, field);
140
+ field += 8;
141
+ }
142
+
143
+ if (compressedSize === SIZE_SENTINEL && field + 8 <= body.length) {
144
+ extended.compressedSize = readOffset(body, field);
145
+ field += 8;
92
146
  }
93
147
 
94
- if (localOffset + LOCAL_HEADER_LENGTH > buffer.length || buffer.readUInt32LE(localOffset) !== LOCAL_SIGNATURE) {
95
- throw new Error(`archive entry "${name}" has a corrupt local header`);
148
+ if (localOffset === SIZE_SENTINEL && field + 8 <= body.length) {
149
+ extended.localOffset = readOffset(body, field);
96
150
  }
97
151
 
98
- const localNameLength = buffer.readUInt16LE(localOffset + 26);
99
- const localExtraLength = buffer.readUInt16LE(localOffset + 28);
100
- const dataOffset = localOffset + LOCAL_HEADER_LENGTH + localNameLength + localExtraLength;
101
- const compressed = buffer.subarray(dataOffset, dataOffset + compressedSize);
152
+ return extended;
153
+ }
102
154
 
103
- if (method === STORED) {
104
- entries.push({
105
- name,
106
- data: compressed,
107
- symlink,
108
- });
155
+ return extended;
156
+ };
109
157
 
110
- continue;
111
- }
158
+ export const openZip = async (path: string, maxEntries: number = ZIP_ENTRY_LIMIT): Promise<ZipArchive> => {
159
+ const size = Bun.file(path).size;
160
+ const tailStart = Math.max(0, size - (EOCD_SEARCH_LIMIT + ZIP64_LOCATOR_LENGTH));
161
+ const tail = await readRange(path, tailStart, size);
162
+ const eocd = tail.length < EOCD_MIN_LENGTH ? -1 : findEndOfCentralDirectory(tail);
163
+
164
+ if (eocd === -1) {
165
+ throw new Error("archive has no zip central directory");
166
+ }
167
+
168
+ const directory = (await readZip64Directory(path, tail, eocd)) ?? {
169
+ total: tail.readUInt16LE(eocd + 10),
170
+ sizeBytes: tail.readUInt32LE(eocd + 12),
171
+ offset: tail.readUInt32LE(eocd + 16),
172
+ };
173
+
174
+ if (directory.total > maxEntries) {
175
+ throw new Error(`archive has too many entries (${directory.total})`);
176
+ }
177
+
178
+ const central = await readRange(path, directory.offset, directory.offset + directory.sizeBytes);
179
+ const entries: ZipEntry[] = [];
112
180
 
113
- if (method !== DEFLATED) {
114
- throw new Error(`archive entry "${name}" uses unsupported compression ${method}`);
181
+ let cursor = 0;
182
+
183
+ for (let index = 0; index < directory.total; index++) {
184
+ if (cursor + CENTRAL_HEADER_LENGTH > central.length || central.readUInt32LE(cursor) !== CENTRAL_SIGNATURE) {
185
+ throw new Error("archive central directory is corrupt");
115
186
  }
116
187
 
117
- const data = inflateRawSync(compressed, {
118
- maxOutputLength: MAX_ENTRY_BYTES,
119
- });
188
+ const flags = central.readUInt16LE(cursor + 8);
189
+ const method = central.readUInt16LE(cursor + 10);
190
+ const compressedSize = central.readUInt32LE(cursor + 20);
191
+ const sizeBytes = central.readUInt32LE(cursor + 24);
192
+ const nameLength = central.readUInt16LE(cursor + 28);
193
+ const extraLength = central.readUInt16LE(cursor + 30);
194
+ const commentLength = central.readUInt16LE(cursor + 32);
195
+ const externalAttributes = central.readUInt32LE(cursor + 38);
196
+ const localOffset = central.readUInt32LE(cursor + 42);
197
+ const nameStart = cursor + CENTRAL_HEADER_LENGTH;
198
+ const name = central.subarray(nameStart, nameStart + nameLength).toString("utf8");
199
+ const extra = central.subarray(nameStart + nameLength, nameStart + nameLength + extraLength);
200
+ const extended = readZip64Extra(extra, sizeBytes, compressedSize, localOffset);
201
+
202
+ cursor = nameStart + nameLength + extraLength + commentLength;
120
203
 
121
- if (data.length !== uncompressedSize) {
122
- throw new Error(`archive entry "${name}" did not inflate to its declared size`);
204
+ if (name.endsWith("/") || (flags & ENCRYPTED_FLAG) !== 0) {
205
+ continue;
123
206
  }
124
207
 
125
208
  entries.push({
126
209
  name,
127
- data,
128
- symlink,
210
+ symlink: ((externalAttributes >>> 16) & UNIX_MODE_FORMAT_MASK) === UNIX_MODE_SYMLINK,
211
+ sizeBytes: extended.sizeBytes ?? sizeBytes,
212
+ compressedSize: extended.compressedSize ?? compressedSize,
213
+ method,
214
+ localOffset: extended.localOffset ?? localOffset,
129
215
  });
130
216
  }
131
217
 
132
- return entries;
218
+ return {
219
+ entries,
220
+
221
+ async read(entry) {
222
+ if (entry.sizeBytes > MAX_ENTRY_BYTES || entry.compressedSize > MAX_ENTRY_BYTES) {
223
+ throw new Error(`archive entry "${entry.name}" is too large`);
224
+ }
225
+
226
+ const header = await readRange(path, entry.localOffset, entry.localOffset + LOCAL_HEADER_LENGTH);
227
+
228
+ if (header.length < LOCAL_HEADER_LENGTH || header.readUInt32LE(0) !== LOCAL_SIGNATURE) {
229
+ throw new Error(`archive entry "${entry.name}" has a corrupt local header`);
230
+ }
231
+
232
+ const dataOffset = entry.localOffset + LOCAL_HEADER_LENGTH + header.readUInt16LE(26) + header.readUInt16LE(28);
233
+ const compressed = await readRange(path, dataOffset, dataOffset + entry.compressedSize);
234
+
235
+ if (entry.method === STORED) {
236
+ return compressed;
237
+ }
238
+
239
+ if (entry.method !== DEFLATED) {
240
+ throw new Error(`archive entry "${entry.name}" uses unsupported compression ${entry.method}`);
241
+ }
242
+
243
+ const data = inflateRawSync(compressed, {
244
+ maxOutputLength: MAX_ENTRY_BYTES,
245
+ });
246
+
247
+ if (data.length !== entry.sizeBytes) {
248
+ throw new Error(`archive entry "${entry.name}" did not inflate to its declared size`);
249
+ }
250
+
251
+ return data;
252
+ },
253
+ };
133
254
  };
@@ -1,10 +1,11 @@
1
1
  import { chmod, chown, mkdir, rm } from "node:fs/promises";
2
- import { BridgeDesired, BridgeRuntime } from "../core";
2
+ import { BridgeDesired, type BridgeInstallHold, BridgeRuntime, BridgeUserError } from "../core";
3
3
  import type { BridgeDriver } from "../modules";
4
4
  import {
5
5
  BRIDGE_INSTALL_ERROR_LIMIT,
6
6
  BRIDGE_VERSION,
7
7
  type BridgeConfig,
8
+ type BridgePendingResult,
8
9
  type BridgeSampleResult,
9
10
  type BridgeStatus,
10
11
  } from "../protocol";
@@ -12,7 +13,9 @@ import { configPath, readConfig, socketPath, statePath } from "./bridgeConfigRea
12
13
  import { createContext } from "./bridgeContext";
13
14
  import { validateDriver } from "./bridgeDriver";
14
15
  import { createEventRing } from "./bridgeEvents";
16
+ import { decideInstall, recountHold } from "./bridgeInstall";
15
17
  import { errorText, log } from "./bridgeLog";
18
+ import { pendingResult, rejectedPending, unsupportedPending } from "./bridgePending";
16
19
  import { createRegistry } from "./bridgeRegistry";
17
20
  import { createServer } from "./bridgeServer";
18
21
  import { readDesired, writeDesired } from "./bridgeState";
@@ -64,6 +67,8 @@ export const serve = async (driver: BridgeDriver) => {
64
67
 
65
68
  let installError: string | null = null;
66
69
 
70
+ let installHold: BridgeInstallHold | null = null;
71
+
67
72
  const supervisor = createSupervisor({
68
73
  cwd: config.cwd,
69
74
  uid: config.gameUid,
@@ -105,8 +110,10 @@ export const serve = async (driver: BridgeDriver) => {
105
110
  runtime: supervisor.runtime,
106
111
  desired: current,
107
112
  exitCode: supervisor.exitCode,
113
+ outOfMemory: supervisor.outOfMemory,
108
114
  restarts: supervisor.restarts,
109
115
  installError,
116
+ installHold,
110
117
  };
111
118
  };
112
119
 
@@ -152,6 +159,20 @@ export const serve = async (driver: BridgeDriver) => {
152
159
  return value;
153
160
  };
154
161
 
162
+ const pending = async (): Promise<BridgePendingResult> => {
163
+ const module = driver.pending;
164
+
165
+ if (!module) {
166
+ return unsupportedPending();
167
+ }
168
+
169
+ const files = await module.list(context);
170
+
171
+ installHold = recountHold(installHold, files.length);
172
+
173
+ return pendingResult(module.staging, module.extensions, files, log);
174
+ };
175
+
155
176
  const server = createServer({
156
177
  socket: socketPath(config),
157
178
  token: config.token,
@@ -260,6 +281,32 @@ export const serve = async (driver: BridgeDriver) => {
260
281
  delivered: true,
261
282
  };
262
283
  },
284
+ async onPendingList() {
285
+ return await pending();
286
+ },
287
+ async onPendingAdd(input) {
288
+ const module = driver.pending;
289
+
290
+ if (!module) {
291
+ return unsupportedPending();
292
+ }
293
+
294
+ try {
295
+ await module.add(context, input);
296
+ } catch (error) {
297
+ if (!(error instanceof BridgeUserError)) {
298
+ throw error;
299
+ }
300
+
301
+ log.warn("pending upload rejected", {
302
+ error: error.message,
303
+ });
304
+
305
+ return rejectedPending(await pending());
306
+ }
307
+
308
+ return await pending();
309
+ },
263
310
  });
264
311
 
265
312
  await chmod(socketPath(config), SOCKET_MODE);
@@ -303,8 +350,10 @@ export const serve = async (driver: BridgeDriver) => {
303
350
 
304
351
  log("installing");
305
352
 
353
+ let decision = decideInstall(undefined, current);
354
+
306
355
  try {
307
- await driver.install.run(context);
356
+ decision = decideInstall(await driver.install.run(context), current);
308
357
 
309
358
  log("install complete");
310
359
  } catch (error) {
@@ -319,9 +368,20 @@ export const serve = async (driver: BridgeDriver) => {
319
368
  return;
320
369
  }
321
370
 
322
- supervisor.setRuntime(current === BridgeDesired.Running ? BridgeRuntime.Starting : BridgeRuntime.Stopped);
371
+ installHold = decision.hold;
372
+
373
+ supervisor.setRuntime(decision.runtime);
374
+
375
+ if (installHold) {
376
+ log("install held", {
377
+ reason: installHold.reason,
378
+ count: installHold.count,
379
+ });
380
+
381
+ return;
382
+ }
323
383
 
324
- if (current === BridgeDesired.Running) {
384
+ if (decision.start) {
325
385
  try {
326
386
  await supervisor.start();
327
387
  } catch (error) {
@@ -0,0 +1,2 @@
1
+ export * from "./panel";
2
+ export * from "./zEnvKey";