@serverkgg/bridge 0.0.8 → 0.0.10

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 (72) hide show
  1. package/package.json +3 -3
  2. package/src/cli/validate.ts +2 -0
  3. package/src/context/bridgeContext.ts +17 -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 +3 -0
  8. package/src/core/bridgeProbeError.ts +16 -0
  9. package/src/core/bridgeSetup.ts +20 -0
  10. package/src/core/bridgeUserError.test.ts +23 -0
  11. package/src/core/bridgeUserError.ts +12 -0
  12. package/src/core/bridgeValue.test.ts +29 -0
  13. package/src/core/bridgeValue.ts +12 -0
  14. package/src/core/index.ts +5 -0
  15. package/src/guides/blocks.ts +8 -0
  16. package/src/guides/markdown.test.ts +18 -0
  17. package/src/guides/markdown.ts +7 -1
  18. package/src/manifest/index.ts +1 -0
  19. package/src/manifest/manifest.test.ts +132 -0
  20. package/src/manifest/manifest.ts +21 -0
  21. package/src/manifest/presence.ts +56 -0
  22. package/src/manifest/primitives.ts +6 -0
  23. package/src/modules/bridge.ts +65 -2
  24. package/src/modules/bridgeDriver.ts +4 -0
  25. package/src/modules/bridgeModule.ts +32 -3
  26. package/src/panel/bridgeDetail.ts +49 -0
  27. package/src/panel/bridgeLayout.ts +1 -0
  28. package/src/panel/bridgePanel.ts +30 -0
  29. package/src/panel/bridgeUploadMode.ts +4 -0
  30. package/src/panel/index.ts +2 -0
  31. package/src/probes/a2s.test.ts +71 -0
  32. package/src/probes/a2s.ts +38 -11
  33. package/src/protocol/bridgeEgress.ts +8 -1
  34. package/src/protocol/bridgeEvents.ts +4 -0
  35. package/src/protocol/bridgeFrame.ts +33 -1
  36. package/src/protocol/bridgeLimits.ts +12 -0
  37. package/src/protocol/bridgeManifest.ts +2 -0
  38. package/src/protocol/bridgeSetup.ts +97 -0
  39. package/src/protocol/index.ts +1 -0
  40. package/src/runtime/bridgeActions.ts +7 -0
  41. package/src/runtime/bridgeConfigReader.ts +12 -8
  42. package/src/runtime/bridgeContext.ts +28 -2
  43. package/src/runtime/bridgeDriver.test.ts +748 -3
  44. package/src/runtime/bridgeDriver.ts +198 -4
  45. package/src/runtime/bridgeEgress.ts +3 -0
  46. package/src/runtime/bridgeEvents.test.ts +96 -0
  47. package/src/runtime/bridgeEvents.ts +11 -6
  48. package/src/runtime/bridgeFiles.test.ts +625 -15
  49. package/src/runtime/bridgeFiles.ts +152 -45
  50. package/src/runtime/bridgeInstall.test.ts +118 -0
  51. package/src/runtime/bridgeInstall.ts +55 -0
  52. package/src/runtime/bridgeNet.ts +32 -3
  53. package/src/runtime/bridgePending.test.ts +148 -0
  54. package/src/runtime/bridgePending.ts +43 -0
  55. package/src/runtime/bridgeRegistry.test.ts +172 -1
  56. package/src/runtime/bridgeRegistry.ts +40 -5
  57. package/src/runtime/bridgeSecrets.test.ts +121 -0
  58. package/src/runtime/bridgeSecrets.ts +43 -0
  59. package/src/runtime/bridgeServer.test.ts +598 -0
  60. package/src/runtime/bridgeServer.ts +42 -3
  61. package/src/runtime/bridgeSetup.test.ts +189 -0
  62. package/src/runtime/bridgeSetup.ts +63 -0
  63. package/src/runtime/bridgeSignal.test.ts +114 -0
  64. package/src/runtime/bridgeSignal.ts +31 -0
  65. package/src/runtime/bridgeSupervisor.test.ts +111 -0
  66. package/src/runtime/bridgeSupervisor.ts +55 -5
  67. package/src/runtime/bridgeZip.ts +179 -58
  68. package/src/runtime/serve.ts +115 -4
  69. package/src/utils/index.ts +1 -0
  70. package/src/utils/stripAnsi.ts +12 -0
  71. package/src/wire/panel.test.ts +259 -0
  72. package/src/wire/panel.ts +72 -0
@@ -1,4 +1,11 @@
1
- import { BRIDGE_ID_PATTERN, BRIDGE_MODULE_ID_PATTERN, BridgeKind, type LocalizedText } from "../core";
1
+ import {
2
+ BRIDGE_ID_PATTERN,
3
+ BRIDGE_MODULE_ID_PATTERN,
4
+ BridgeKind,
5
+ BridgeSetupStepKind,
6
+ type LocalizedText,
7
+ } from "../core";
8
+ import { GuideOpenTab } from "../guides";
2
9
  import type { ManifestPort } from "../manifest";
3
10
  import type { BridgeDriver, BridgePanelModule } from "../modules";
4
11
  import {
@@ -8,6 +15,7 @@ import {
8
15
  type BridgeSection,
9
16
  type BridgeSectionAction,
10
17
  type BridgeTab,
18
+ BridgeUploadMode,
11
19
  type BridgeVisibleWhen,
12
20
  } from "../panel";
13
21
  import {
@@ -16,6 +24,7 @@ import {
16
24
  BRIDGE_EVENTS_LIMIT,
17
25
  BRIDGE_EXTENSION_LENGTH_LIMIT,
18
26
  BRIDGE_FIELD_OPTIONS_LIMIT,
27
+ BRIDGE_FORM_CLEARS_LIMIT,
19
28
  BRIDGE_FORM_FIELDS_LIMIT,
20
29
  BRIDGE_ID_LENGTH_LIMIT,
21
30
  BRIDGE_KEY_LENGTH_LIMIT,
@@ -26,6 +35,7 @@ import {
26
35
  BRIDGE_PLACEHOLDER_LENGTH_LIMIT,
27
36
  BRIDGE_ROW_ACTIONS_LIMIT,
28
37
  BRIDGE_SECTION_ACTIONS_LIMIT,
38
+ BRIDGE_SETUP_STEPS_LIMIT,
29
39
  BRIDGE_STAGING_LENGTH_LIMIT,
30
40
  BRIDGE_TAB_SECTIONS_LIMIT,
31
41
  BRIDGE_TABLE_COLUMNS_LIMIT,
@@ -35,13 +45,17 @@ import {
35
45
  } from "../protocol";
36
46
  import { actionsOf } from "./bridgeActions";
37
47
 
48
+ const UPLOAD_MODES = new Set<string>(Object.values(BridgeUploadMode));
49
+
38
50
  const RESERVED_MODULE_IDS = new Set([
39
51
  "announce",
40
52
  "backup",
41
53
  "events",
42
54
  "install",
43
55
  "lifecycle",
56
+ "pending",
44
57
  "query",
58
+ "setup",
45
59
  ]);
46
60
 
47
61
  export class BridgeDriverError extends Error {
@@ -259,6 +273,58 @@ const validateSection = (modules: Record<string, BridgePanelModule>, section: Br
259
273
  countIn(section.fields.length, BRIDGE_FORM_FIELDS_LIMIT, "fields", where);
260
274
  validateFields(modules, section.fields, where);
261
275
 
276
+ const clears = section.clears ?? [];
277
+
278
+ if (clears.length > 0 && section.target !== BridgeFormTarget.Variables) {
279
+ fail(`${where} clears variables but does not target variables`);
280
+ }
281
+
282
+ countIn(clears.length, BRIDGE_FORM_CLEARS_LIMIT, "cleared variables", where);
283
+
284
+ const cleared = new Set<string>();
285
+
286
+ for (const clear of clears) {
287
+ if (cleared.has(clear.variable)) {
288
+ fail(`${where} clears "${clear.variable}" more than once`);
289
+ }
290
+
291
+ cleared.add(clear.variable);
292
+
293
+ if (section.fields.some((field) => field.key === clear.variable)) {
294
+ fail(`${where} clears "${clear.variable}", which is one of its own fields`);
295
+ }
296
+
297
+ validateText(clear.warning, `clear "${clear.variable}" warning`, where);
298
+ }
299
+
300
+ return;
301
+ }
302
+
303
+ case BridgeLayout.Detail: {
304
+ const module = moduleOf(modules, section.module, where);
305
+
306
+ if (module.kind !== BridgeKind.Detail) {
307
+ fail(`${where} references "${section.module}", which is not a detail module`);
308
+
309
+ return;
310
+ }
311
+
312
+ countIn((section.actions ?? []).length, BRIDGE_SECTION_ACTIONS_LIMIT, "actions", where);
313
+ validateActions(modules, section.actions ?? [], where);
314
+
315
+ for (const action of section.actions ?? []) {
316
+ if (action.download) {
317
+ fail(`${where} declares action "${action.id}" as a download, which a detail cannot serve`);
318
+ }
319
+
320
+ if (!module.actions?.[action.id]) {
321
+ fail(`${where} declares action "${action.id}", which module "${section.module}" does not implement`);
322
+ }
323
+ }
324
+
325
+ validateText(section.confirmText, "confirm text", where);
326
+ validateText(section.related?.label, "related label", where);
327
+
262
328
  return;
263
329
  }
264
330
 
@@ -313,6 +379,10 @@ const validateSection = (modules: Record<string, BridgePanelModule>, section: Br
313
379
  for (const extension of section.upload.extensions) {
314
380
  lengthIn(extension, BRIDGE_EXTENSION_LENGTH_LIMIT, `upload extension "${extension}"`, where);
315
381
  }
382
+
383
+ if (section.upload.mode !== undefined && !UPLOAD_MODES.has(section.upload.mode)) {
384
+ fail(`${where} declares an unknown upload mode "${section.upload.mode}"`);
385
+ }
316
386
  }
317
387
  }
318
388
 
@@ -346,6 +416,107 @@ const validateSection = (modules: Record<string, BridgePanelModule>, section: Br
346
416
  }
347
417
  };
348
418
 
419
+ const validatePending = (driver: BridgeDriver) => {
420
+ const pending = driver.pending;
421
+
422
+ if (!pending) {
423
+ return;
424
+ }
425
+
426
+ const where = "the pending module";
427
+
428
+ if (pending.kind !== BridgeKind.Pending) {
429
+ fail(`${where} declares kind "${pending.kind}", which is not "${BridgeKind.Pending}"`);
430
+ }
431
+
432
+ if (pending.staging.length === 0) {
433
+ fail(`${where} declares no staging path`);
434
+ }
435
+
436
+ lengthIn(pending.staging, BRIDGE_STAGING_LENGTH_LIMIT, "staging path", where);
437
+
438
+ if (pending.extensions.length === 0) {
439
+ fail(`${where} declares no extensions`);
440
+ }
441
+
442
+ countIn(pending.extensions.length, BRIDGE_UPLOAD_EXTENSIONS_LIMIT, "extensions", where);
443
+
444
+ for (const extension of pending.extensions) {
445
+ lengthIn(extension, BRIDGE_EXTENSION_LENGTH_LIMIT, `extension "${extension}"`, where);
446
+ }
447
+ };
448
+
449
+ const formSectionOf = (driver: BridgeDriver, tabId: string, sectionId: string) => {
450
+ const tab = (driver.panel?.tabs ?? []).find((entry) => entry.id === tabId);
451
+ const section = (tab?.sections ?? []).find((entry) => entry.id === sectionId);
452
+
453
+ if (!section || section.layout !== BridgeLayout.Form) {
454
+ return null;
455
+ }
456
+
457
+ return section;
458
+ };
459
+
460
+ const validateSetup = (driver: BridgeDriver) => {
461
+ const setup = driver.setup;
462
+
463
+ if (!setup) {
464
+ return;
465
+ }
466
+
467
+ const where = "the setup module";
468
+
469
+ if (setup.kind !== BridgeKind.Setup) {
470
+ fail(`${where} declares kind "${setup.kind}", which is not "${BridgeKind.Setup}"`);
471
+ }
472
+
473
+ if (setup.steps.length === 0) {
474
+ fail(`${where} declares no steps`);
475
+ }
476
+
477
+ countIn(setup.steps.length, BRIDGE_SETUP_STEPS_LIMIT, "steps", where);
478
+
479
+ const ids = new Set<string>();
480
+
481
+ for (const step of setup.steps) {
482
+ if (ids.has(step.id)) {
483
+ fail(`${where} declares step "${step.id}" more than once`);
484
+ }
485
+
486
+ ids.add(step.id);
487
+
488
+ const at = `${where} step "${step.id}"`;
489
+
490
+ validateId(step.id, `${where} step`);
491
+ validateText(step.title, "title", at);
492
+ validateText(step.help, "help", at);
493
+
494
+ if (step.kind === BridgeSetupStepKind.Form) {
495
+ const section = formSectionOf(driver, step.tab, step.section);
496
+
497
+ if (!section) {
498
+ fail(`${at} names tab "${step.tab}" section "${step.section}", which is not a form section of this panel`);
499
+
500
+ continue;
501
+ }
502
+
503
+ for (const key of step.fields ?? []) {
504
+ if (!section.fields.some((field) => field.key === key)) {
505
+ fail(`${at} names field "${key}", which section "${step.section}" does not declare`);
506
+ }
507
+ }
508
+ }
509
+
510
+ if (step.kind === BridgeSetupStepKind.Open) {
511
+ const target = step.target;
512
+
513
+ if (target.tab === GuideOpenTab.Panel && !(driver.panel?.tabs ?? []).some((tab) => tab.id === target.tabId)) {
514
+ fail(`${at} opens panel tab "${target.tabId}", which the panel does not declare`);
515
+ }
516
+ }
517
+ }
518
+ };
519
+
349
520
  const validateTab = (modules: Record<string, BridgePanelModule>, tab: BridgeTab) => {
350
521
  const sections = new Set<string>();
351
522
 
@@ -382,6 +553,9 @@ export const validateDriver = (driver: BridgeDriver) => {
382
553
 
383
554
  countIn(emitted.size, BRIDGE_EVENTS_LIMIT, "event names", "the driver");
384
555
 
556
+ validatePending(driver);
557
+ validateSetup(driver);
558
+
385
559
  for (const [id, module] of Object.entries(modules)) {
386
560
  validateModuleId(id, "module");
387
561
 
@@ -401,7 +575,7 @@ export const validateDriver = (driver: BridgeDriver) => {
401
575
  }
402
576
  }
403
577
 
404
- if (module.kind === BridgeKind.Collection) {
578
+ if (module.kind === BridgeKind.Collection || module.kind === BridgeKind.Detail) {
405
579
  for (const action of Object.keys(module.actions ?? {})) {
406
580
  validateModuleId(action, `module "${id}" action`);
407
581
  }
@@ -429,6 +603,26 @@ export const validateDriver = (driver: BridgeDriver) => {
429
603
 
430
604
  validateTab(modules, tab);
431
605
  }
606
+
607
+ const declared = declaredVariables(driver);
608
+
609
+ for (const tab of driver.panel.tabs) {
610
+ for (const section of tab.sections) {
611
+ const where = `tab "${tab.id}" section "${section.id}"`;
612
+
613
+ if (section.layout === BridgeLayout.Detail && section.related && !tabs.has(section.related.tab)) {
614
+ fail(`${where} relates to tab "${section.related.tab}", which the panel does not declare`);
615
+ }
616
+
617
+ if (section.layout === BridgeLayout.Form) {
618
+ for (const clear of section.clears ?? []) {
619
+ if (!declared.has(clear.variable)) {
620
+ fail(`${where} clears "${clear.variable}", which no variables form, catalog or detail declares`);
621
+ }
622
+ }
623
+ }
624
+ }
625
+ }
432
626
  };
433
627
 
434
628
  const declaredVariables = (driver: BridgeDriver) => {
@@ -442,7 +636,7 @@ const declaredVariables = (driver: BridgeDriver) => {
442
636
  }
443
637
  }
444
638
 
445
- if (section.layout === BridgeLayout.Catalog && section.variable) {
639
+ if ((section.layout === BridgeLayout.Catalog || section.layout === BridgeLayout.Detail) && section.variable) {
446
640
  declared.add(section.variable);
447
641
  }
448
642
  }
@@ -461,7 +655,7 @@ export const validatePortActivation = (ports: ManifestPort[], driver: BridgeDriv
461
655
  }
462
656
 
463
657
  fail(
464
- `port "${port.key}" activeWhen[${index}] names "${rule.variable}", which no variables form or catalog declares`,
658
+ `port "${port.key}" activeWhen[${index}] names "${rule.variable}", which no variables form, catalog or detail declares`,
465
659
  );
466
660
  }
467
661
  }
@@ -7,6 +7,7 @@ import {
7
7
  BridgeEgressMethod,
8
8
  type BridgeEgressRequest,
9
9
  type BridgeEgressResponse,
10
+ type BridgeHttpMethod,
10
11
  } from "../protocol";
11
12
 
12
13
  const CONNECT_RETRY_MS = 10_000;
@@ -39,6 +40,8 @@ export class BridgeEgressCallError extends Error {
39
40
  }
40
41
 
41
42
  export interface BridgeEgressCallOptions {
43
+ httpMethod?: BridgeHttpMethod;
44
+ body?: string;
42
45
  headers?: Record<string, string>;
43
46
  timeoutMs?: number;
44
47
  cache?: boolean;
@@ -0,0 +1,96 @@
1
+ import { 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 { BridgeKind } from "../core";
6
+ import type { BridgeEventsModule } from "../modules";
7
+ import { BridgeEventName } from "../protocol";
8
+ import { createEventRing } from "./bridgeEvents";
9
+ import type { BridgeLeveledLogger } from "./bridgeLog";
10
+ import { createSupervisor } from "./bridgeSupervisor";
11
+
12
+ const RESET = "\u001B[m";
13
+
14
+ const PLAYER_JOINED = /^\[World\|(?<world>[^\]]+)\] Player '(?<player>[^']+)' joined world (?<target>\S+)$/;
15
+
16
+ const silent: BridgeLeveledLogger = Object.assign(() => {}, {
17
+ warn: () => {},
18
+ error: () => {},
19
+ });
20
+
21
+ const events: BridgeEventsModule = {
22
+ kind: BridgeKind.Events,
23
+ patterns: [
24
+ {
25
+ match: PLAYER_JOINED,
26
+ emit: BridgeEventName.PlayerJoined,
27
+ },
28
+ ],
29
+ };
30
+
31
+ const emittedFor = async (line: string) => {
32
+ const dir = await mkdtemp(join(tmpdir(), "bridge-events-"));
33
+ const path = join(dir, "line.txt");
34
+
35
+ await Bun.write(path, `${line}\n`);
36
+
37
+ const supervisor = createSupervisor({
38
+ cwd: dir,
39
+ uid: process.getuid?.() ?? 0,
40
+ gid: process.getgid?.() ?? 0,
41
+ stopTimeoutSeconds: 1,
42
+ onLog: silent,
43
+ resolveCommand: async () => [
44
+ "bash",
45
+ "-c",
46
+ 'cat "$0"; sleep 300',
47
+ path,
48
+ ],
49
+ });
50
+
51
+ const ring = createEventRing(events, supervisor);
52
+
53
+ await supervisor.start();
54
+
55
+ try {
56
+ for (let waited = 0; waited < 5000; waited += 50) {
57
+ if (ring.drain(0).events.length > 0) {
58
+ break;
59
+ }
60
+
61
+ await Bun.sleep(50);
62
+ }
63
+
64
+ return ring.drain(0).events;
65
+ } finally {
66
+ await supervisor.stop();
67
+ await rm(dir, {
68
+ force: true,
69
+ recursive: true,
70
+ });
71
+ }
72
+ };
73
+
74
+ const joined = {
75
+ seq: 1,
76
+ event: BridgeEventName.PlayerJoined,
77
+ payload: {
78
+ world: "default",
79
+ player: "Meslzy",
80
+ target: "default",
81
+ },
82
+ };
83
+
84
+ describe("matching a game line against the driver patterns", () => {
85
+ test("emits on the plain line", async () => {
86
+ expect(await emittedFor("[World|default] Player 'Meslzy' joined world default")).toEqual([
87
+ joined,
88
+ ]);
89
+ });
90
+
91
+ test("emits on the line hytale really prints, wrapped in ansi resets at both ends", async () => {
92
+ expect(await emittedFor(`${RESET}[World|default] Player 'Meslzy' joined world default${RESET}`)).toEqual([
93
+ joined,
94
+ ]);
95
+ });
96
+ });
@@ -1,6 +1,7 @@
1
1
  import type { BridgeValues } from "../core";
2
2
  import type { BridgeEventsModule } from "../modules";
3
3
  import type { BridgeEvent } from "../protocol";
4
+ import { stripAnsi } from "../utils";
4
5
  import type { BridgeSupervisor } from "./bridgeSupervisor";
5
6
 
6
7
  const EVENT_BUFFER = 500;
@@ -26,22 +27,26 @@ export const createEventRing = (events: BridgeEventsModule | undefined, supervis
26
27
 
27
28
  if (events) {
28
29
  supervisor.subscribe((entry) => {
30
+ const line = stripAnsi(entry.line);
31
+
29
32
  for (const pattern of events.patterns) {
30
33
  if (pattern.stream && pattern.stream !== entry.stream) {
31
34
  continue;
32
35
  }
33
36
 
34
- const match = entry.line.match(pattern.match);
37
+ const match = line.match(pattern.match);
35
38
 
36
39
  if (!match) {
37
40
  continue;
38
41
  }
39
42
 
40
- const groups = match.groups
41
- ? {
42
- ...match.groups,
43
- }
44
- : null;
43
+ const groups =
44
+ match.groups || pattern.payload
45
+ ? {
46
+ ...pattern.payload,
47
+ ...match.groups,
48
+ }
49
+ : null;
45
50
 
46
51
  push(pattern.emit, groups);
47
52
  }