@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
@@ -0,0 +1,338 @@
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 { join } from "node:path";
5
+ import { BridgeDesired, BridgeFailureCode, BridgeFailureError, BridgeRuntime, BridgeUserError } from "../core";
6
+ import { BridgeMethod, type BridgePendingResult, type BridgeResponse } from "../protocol";
7
+ import { createServer, type ServerOptions } from "./bridgeServer";
8
+
9
+ const TOKEN = "test-token";
10
+
11
+ const unreachable = (what: string) => {
12
+ return () => {
13
+ throw new Error(`${what} is not part of this test`);
14
+ };
15
+ };
16
+
17
+ const baseOptions = (socket: string): ServerOptions => ({
18
+ socket,
19
+ token: TOKEN,
20
+ onStatus: () => ({
21
+ version: "0.0.0",
22
+ runtime: BridgeRuntime.Stopped,
23
+ desired: BridgeDesired.Stopped,
24
+ exitCode: null,
25
+ outOfMemory: false,
26
+ restarts: 0,
27
+ installError: null,
28
+ installHold: null,
29
+ }),
30
+ onManifest: unreachable("manifest"),
31
+ onStart: unreachable("start"),
32
+ onStop: unreachable("stop"),
33
+ onCommand: unreachable("command"),
34
+ onInvoke: unreachable("invoke"),
35
+ onEvents: unreachable("events"),
36
+ onSample: unreachable("sample"),
37
+ onQuiesce: unreachable("quiesce"),
38
+ onRelease: unreachable("release"),
39
+ onDescribe: unreachable("describe"),
40
+ onAnnounce: unreachable("announce"),
41
+ onPendingList: unreachable("pendingList"),
42
+ onPendingAdd: unreachable("pendingAdd"),
43
+ onLog: () => {
44
+ return;
45
+ },
46
+ });
47
+
48
+ const call = async (socket: string, frame: Record<string, unknown>): Promise<BridgeResponse> => {
49
+ return await new Promise<BridgeResponse>((resolve, reject) => {
50
+ void Bun.connect<{
51
+ buffer: string;
52
+ }>({
53
+ unix: socket,
54
+ socket: {
55
+ open(connection) {
56
+ connection.data = {
57
+ buffer: "",
58
+ };
59
+
60
+ connection.write(`${JSON.stringify(frame)}\n`);
61
+ },
62
+
63
+ data(connection, chunk) {
64
+ connection.data.buffer += chunk.toString();
65
+
66
+ const [line] = connection.data.buffer.split("\n");
67
+
68
+ if (!line) {
69
+ return;
70
+ }
71
+
72
+ connection.end();
73
+
74
+ resolve(JSON.parse(line) as BridgeResponse);
75
+ },
76
+
77
+ error(_connection, error) {
78
+ reject(error);
79
+ },
80
+ },
81
+ }).catch(reject);
82
+ });
83
+ };
84
+
85
+ describe("pending dispatch", () => {
86
+ let dir = "";
87
+ let socket = "";
88
+
89
+ beforeEach(async () => {
90
+ dir = await mkdtemp(join(tmpdir(), "bridge-server-"));
91
+ socket = join(dir, "bridge.sock");
92
+ });
93
+
94
+ afterEach(async () => {
95
+ await rm(dir, {
96
+ force: true,
97
+ recursive: true,
98
+ });
99
+ });
100
+
101
+ const listed: BridgePendingResult = {
102
+ supported: true,
103
+ staging: ".serverk-staging/pending-upload",
104
+ extensions: [
105
+ "jar",
106
+ ],
107
+ files: [],
108
+ rejected: null,
109
+ };
110
+
111
+ test("answers pendingList with what the driver lists", async () => {
112
+ const server = createServer({
113
+ ...baseOptions(socket),
114
+ onPendingList: async () => listed,
115
+ });
116
+
117
+ const response = await call(socket, {
118
+ id: 1,
119
+ token: TOKEN,
120
+ method: BridgeMethod.PendingList,
121
+ });
122
+
123
+ server.stop(true);
124
+
125
+ expect(response).toEqual({
126
+ id: 1,
127
+ ok: true,
128
+ result: listed,
129
+ });
130
+ });
131
+
132
+ test("answers pendingList with the unsupported sentinel when the driver declares no pending module", async () => {
133
+ const server = createServer({
134
+ ...baseOptions(socket),
135
+ onPendingList: async () => ({
136
+ supported: false,
137
+ staging: "",
138
+ extensions: [],
139
+ files: [],
140
+ rejected: null,
141
+ }),
142
+ });
143
+
144
+ const response = await call(socket, {
145
+ id: 2,
146
+ token: TOKEN,
147
+ method: BridgeMethod.PendingList,
148
+ });
149
+
150
+ server.stop(true);
151
+
152
+ expect(response).toEqual({
153
+ id: 2,
154
+ ok: true,
155
+ result: {
156
+ supported: false,
157
+ staging: "",
158
+ extensions: [],
159
+ files: [],
160
+ rejected: null,
161
+ },
162
+ });
163
+ });
164
+
165
+ test("passes the uploaded path to pendingAdd and answers with the refreshed list", async () => {
166
+ const received: string[] = [];
167
+
168
+ const server = createServer({
169
+ ...baseOptions(socket),
170
+ onPendingAdd: async (input) => {
171
+ received.push(input);
172
+
173
+ return listed;
174
+ },
175
+ });
176
+
177
+ const response = await call(socket, {
178
+ id: 3,
179
+ token: TOKEN,
180
+ method: BridgeMethod.PendingAdd,
181
+ input: ".serverk-staging/pending-upload/patch.jar",
182
+ });
183
+
184
+ server.stop(true);
185
+
186
+ expect(received).toEqual([
187
+ ".serverk-staging/pending-upload/patch.jar",
188
+ ]);
189
+ expect(response).toEqual({
190
+ id: 3,
191
+ ok: true,
192
+ result: listed,
193
+ });
194
+ });
195
+
196
+ test("refuses a pendingAdd carrying no path", async () => {
197
+ const server = createServer({
198
+ ...baseOptions(socket),
199
+ onPendingAdd: unreachable("pendingAdd"),
200
+ });
201
+
202
+ const response = await call(socket, {
203
+ id: 4,
204
+ token: TOKEN,
205
+ method: BridgeMethod.PendingAdd,
206
+ });
207
+
208
+ server.stop(true);
209
+
210
+ expect(response).toEqual({
211
+ id: 4,
212
+ ok: false,
213
+ error: "pendingAdd needs an input path",
214
+ });
215
+ });
216
+
217
+ test("answers pendingAdd with the rejection the driver reported beside the refreshed list", async () => {
218
+ const rejected: BridgePendingResult = {
219
+ ...listed,
220
+ rejected: {
221
+ expected: [
222
+ "patch.jar",
223
+ ],
224
+ },
225
+ };
226
+
227
+ const server = createServer({
228
+ ...baseOptions(socket),
229
+ onPendingAdd: async () => rejected,
230
+ });
231
+
232
+ const response = await call(socket, {
233
+ id: 6,
234
+ token: TOKEN,
235
+ method: BridgeMethod.PendingAdd,
236
+ input: ".serverk-staging/pending-upload/wrong.jar",
237
+ });
238
+
239
+ server.stop(true);
240
+
241
+ expect(response).toEqual({
242
+ id: 6,
243
+ ok: true,
244
+ result: rejected,
245
+ });
246
+ });
247
+
248
+ test("names an unknown method so an older agent can degrade", async () => {
249
+ const server = createServer(baseOptions(socket));
250
+
251
+ const response = await call(socket, {
252
+ id: 5,
253
+ token: TOKEN,
254
+ method: "pendingRemove",
255
+ });
256
+
257
+ server.stop(true);
258
+
259
+ expect(response).toEqual({
260
+ id: 5,
261
+ ok: false,
262
+ error: 'unknown method "pendingRemove"',
263
+ });
264
+ });
265
+ });
266
+
267
+ describe("error serialisation", () => {
268
+ let dir = "";
269
+ let socket = "";
270
+
271
+ beforeEach(async () => {
272
+ dir = await mkdtemp(join(tmpdir(), "bridge-server-"));
273
+ socket = join(dir, "bridge.sock");
274
+ });
275
+
276
+ afterEach(async () => {
277
+ await rm(dir, {
278
+ force: true,
279
+ recursive: true,
280
+ });
281
+ });
282
+
283
+ const thrown = async (error: unknown) => {
284
+ const server = createServer({
285
+ ...baseOptions(socket),
286
+ onPendingAdd: async () => {
287
+ throw error;
288
+ },
289
+ });
290
+
291
+ const response = await call(socket, {
292
+ id: 1,
293
+ token: TOKEN,
294
+ method: BridgeMethod.PendingAdd,
295
+ input: ".serverk-staging/pending-upload/patch.jar",
296
+ });
297
+
298
+ server.stop(true);
299
+
300
+ return response;
301
+ };
302
+
303
+ test("carries the localized text of a user error beside the message", async () => {
304
+ expect(
305
+ await thrown(
306
+ new BridgeUserError({
307
+ ar: "هذا مو الملف اللي ننتظره",
308
+ en: "this is not a file we are waiting for",
309
+ }),
310
+ ),
311
+ ).toEqual({
312
+ id: 1,
313
+ ok: false,
314
+ error: "this is not a file we are waiting for",
315
+ text: {
316
+ ar: "هذا مو الملف اللي ننتظره",
317
+ en: "this is not a file we are waiting for",
318
+ },
319
+ });
320
+ });
321
+
322
+ test("carries the failure code of a failure error and no text", async () => {
323
+ expect(await thrown(new BridgeFailureError(BridgeFailureCode.CatalogRestricted, "the author blocked it"))).toEqual({
324
+ id: 1,
325
+ ok: false,
326
+ error: "the author blocked it",
327
+ failure: BridgeFailureCode.CatalogRestricted,
328
+ });
329
+ });
330
+
331
+ test("carries the message alone for an ordinary error", async () => {
332
+ expect(await thrown(new Error("the file could not be read"))).toEqual({
333
+ id: 1,
334
+ ok: false,
335
+ error: "the file could not be read",
336
+ });
337
+ });
338
+ });
@@ -1,5 +1,5 @@
1
1
  import { timingSafeEqual } from "node:crypto";
2
- import { BridgeFailureError, BridgeSecretError, type BridgeValues } from "../core";
2
+ import { BridgeFailureError, BridgeSecretError, BridgeUserError, type BridgeValues } from "../core";
3
3
  import {
4
4
  BRIDGE_FRAME_LIMIT,
5
5
  type BridgeAnnounceResult,
@@ -8,6 +8,7 @@ import {
8
8
  type BridgeEventsResult,
9
9
  type BridgeManifest,
10
10
  BridgeMethod,
11
+ type BridgePendingResult,
11
12
  type BridgeQuiesceResult,
12
13
  type BridgeResponse,
13
14
  type BridgeSampleResult,
@@ -30,6 +31,8 @@ export interface ServerOptions {
30
31
  onRelease: () => Promise<void>;
31
32
  onDescribe: () => Promise<BridgeDescribeResult>;
32
33
  onAnnounce: (message: string) => Promise<BridgeAnnounceResult>;
34
+ onPendingList: () => Promise<BridgePendingResult>;
35
+ onPendingAdd: (input: string) => Promise<BridgePendingResult>;
33
36
  onLog: BridgeLogger;
34
37
  }
35
38
 
@@ -47,7 +50,7 @@ interface IncomingFrame {
47
50
  message?: unknown;
48
51
  }
49
52
 
50
- const asMessage = (value: unknown): string | null => {
53
+ const asText = (value: unknown): string | null => {
51
54
  return typeof value === "string" && value.length > 0 ? value : null;
52
55
  };
53
56
 
@@ -136,7 +139,7 @@ export const createServer = (options: ServerOptions) => {
136
139
  }
137
140
 
138
141
  case BridgeMethod.Announce: {
139
- const message = asMessage(request.message);
142
+ const message = asText(request.message);
140
143
 
141
144
  if (!message) {
142
145
  throw new Error("announce needs a message");
@@ -145,6 +148,20 @@ export const createServer = (options: ServerOptions) => {
145
148
  return await options.onAnnounce(message);
146
149
  }
147
150
 
151
+ case BridgeMethod.PendingList: {
152
+ return await options.onPendingList();
153
+ }
154
+
155
+ case BridgeMethod.PendingAdd: {
156
+ const input = asText(request.input);
157
+
158
+ if (!input) {
159
+ throw new Error("pendingAdd needs an input path");
160
+ }
161
+
162
+ return await options.onPendingAdd(input);
163
+ }
164
+
148
165
  default: {
149
166
  throw new Error(`unknown method "${method}"`);
150
167
  }
@@ -249,6 +266,11 @@ export const createServer = (options: ServerOptions) => {
249
266
  failure: error.code,
250
267
  }
251
268
  : {}),
269
+ ...(error instanceof BridgeUserError
270
+ ? {
271
+ text: error.text,
272
+ }
273
+ : {}),
252
274
  }),
253
275
  );
254
276
  });
@@ -18,6 +18,8 @@ const RESTART_LIMIT = 5;
18
18
 
19
19
  const GAME_OOM_SCORE_ADJ = 500;
20
20
 
21
+ const SIGKILL_EXIT_CODE = 128 + 9;
22
+
21
23
  export interface SupervisorLine {
22
24
  stream: BridgeStream;
23
25
  line: string;
@@ -113,6 +115,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
113
115
  let shouldRun = false;
114
116
  let breaker = false;
115
117
  let exitCode: number | null = null;
118
+ let outOfMemory = false;
116
119
  let restarts = 0;
117
120
  let queue: Promise<unknown> = Promise.resolve();
118
121
 
@@ -307,6 +310,8 @@ export const createSupervisor = (options: SupervisorOptions) => {
307
310
  exitCode = code;
308
311
 
309
312
  if (!shouldRun) {
313
+ outOfMemory = false;
314
+
310
315
  runtime = BridgeRuntime.Stopped;
311
316
 
312
317
  options.onLog("game stopped", {
@@ -316,6 +321,14 @@ export const createSupervisor = (options: SupervisorOptions) => {
316
321
  return;
317
322
  }
318
323
 
324
+ outOfMemory = code === SIGKILL_EXIT_CODE;
325
+
326
+ if (outOfMemory) {
327
+ options.onLog.error("the game was killed for running out of memory", {
328
+ code,
329
+ });
330
+ }
331
+
319
332
  restarts += 1;
320
333
  restartStamps.push(performance.now());
321
334
 
@@ -375,6 +388,10 @@ export const createSupervisor = (options: SupervisorOptions) => {
375
388
  return exitCode;
376
389
  },
377
390
 
391
+ get outOfMemory() {
392
+ return outOfMemory;
393
+ },
394
+
378
395
  get restarts() {
379
396
  return restarts;
380
397
  },
@@ -382,6 +399,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
382
399
  async start() {
383
400
  shouldRun = true;
384
401
  breaker = false;
402
+ outOfMemory = false;
385
403
  restartStamps.length = 0;
386
404
 
387
405
  if (child) {