@serverkgg/bridge 0.0.21 → 0.1.2

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 (119) hide show
  1. package/package.json +3 -1
  2. package/src/bot/botDeploy.test.ts +363 -0
  3. package/src/bot/botDeploy.ts +136 -0
  4. package/src/bot/botEnvironment.test.ts +189 -0
  5. package/src/bot/botEnvironment.ts +141 -0
  6. package/src/bot/botIdentity.test.ts +251 -0
  7. package/src/bot/botIdentity.ts +140 -0
  8. package/src/bot/botPanel.test.ts +246 -0
  9. package/src/bot/botPanel.ts +129 -0
  10. package/src/bot/botState.test.ts +45 -0
  11. package/src/bot/botState.ts +54 -0
  12. package/src/bot/botTerminal.test.ts +79 -0
  13. package/src/bot/botTerminal.ts +62 -0
  14. package/src/bot/botVariables.test.ts +193 -0
  15. package/src/bot/botVariables.ts +73 -0
  16. package/src/bot/detect/botFramework.ts +23 -0
  17. package/src/bot/detect/botFrameworks.ts +198 -0
  18. package/src/bot/detect/botInspectWarning.ts +15 -0
  19. package/src/bot/detect/botPackageManager.ts +6 -0
  20. package/src/bot/detect/botPlatform.ts +10 -0
  21. package/src/bot/detect/botRuntime.ts +15 -0
  22. package/src/bot/detect/botToken.test.ts +71 -0
  23. package/src/bot/detect/botToken.ts +126 -0
  24. package/src/bot/detect/detect.test.ts +543 -0
  25. package/src/bot/detect/detect.ts +842 -0
  26. package/src/bot/detect/detectResult.ts +40 -0
  27. package/src/bot/detect/detectTable.test.ts +72 -0
  28. package/src/bot/detect/detectTable.ts +166 -0
  29. package/src/bot/detect/envExample.test.ts +29 -0
  30. package/src/bot/detect/envExample.ts +48 -0
  31. package/src/bot/detect/envKey.ts +7 -0
  32. package/src/bot/detect/envScan.test.ts +39 -0
  33. package/src/bot/detect/envScan.ts +87 -0
  34. package/src/bot/detect/index.ts +17 -0
  35. package/src/bot/detect/inferBotPlatform.test.ts +60 -0
  36. package/src/bot/detect/inferBotPlatform.ts +28 -0
  37. package/src/bot/detect/keys.test.ts +28 -0
  38. package/src/bot/detect/keys.ts +64 -0
  39. package/src/bot/detect/sensitiveKey.test.ts +91 -0
  40. package/src/bot/detect/sensitiveKey.ts +44 -0
  41. package/src/bot/detect/serverkJson.test.ts +88 -0
  42. package/src/bot/detect/serverkJson.ts +174 -0
  43. package/src/bot/index.ts +8 -0
  44. package/src/cli/detectTable.test.ts +66 -0
  45. package/src/cli/detectTable.ts +43 -0
  46. package/src/cli/main.ts +12 -4
  47. package/src/cli/renderedFile.ts +35 -0
  48. package/src/cli/schema.ts +7 -26
  49. package/src/cli/validate.test.ts +283 -3
  50. package/src/cli/validate.ts +105 -7
  51. package/src/context/bridgeContext.ts +17 -1
  52. package/src/core/bridgeAction.ts +2 -0
  53. package/src/core/bridgeActivity.ts +9 -0
  54. package/src/core/bridgeAvailability.ts +10 -0
  55. package/src/core/bridgePort.test.ts +45 -0
  56. package/src/core/bridgePort.ts +26 -0
  57. package/src/core/bridgeUserError.test.ts +10 -0
  58. package/src/core/bridgeUserError.ts +6 -2
  59. package/src/core/describeDownloadFailure.test.ts +58 -0
  60. package/src/core/describeDownloadFailure.ts +69 -0
  61. package/src/core/index.ts +3 -0
  62. package/src/guides/compile.ts +2 -2
  63. package/src/manifest/index.ts +1 -0
  64. package/src/manifest/kind.ts +5 -0
  65. package/src/manifest/manifest.test.ts +630 -3
  66. package/src/manifest/manifest.ts +391 -87
  67. package/src/manifest/runtime.ts +2 -2
  68. package/src/manifest/source.ts +4 -4
  69. package/src/modules/bridge.ts +8 -1
  70. package/src/modules/bridgeLifecycleSubject.ts +8 -0
  71. package/src/modules/bridgeModule.ts +19 -1
  72. package/src/modules/bridgeStopSignal.ts +13 -0
  73. package/src/modules/index.ts +2 -0
  74. package/src/panel/bridgeDetail.ts +4 -1
  75. package/src/protocol/bridgeActivity.ts +54 -0
  76. package/src/protocol/bridgeConfig.ts +2 -0
  77. package/src/protocol/bridgeEvents.ts +30 -0
  78. package/src/protocol/bridgeFrame.ts +10 -1
  79. package/src/protocol/index.ts +1 -0
  80. package/src/runtime/bridgeActions.ts +19 -2
  81. package/src/runtime/bridgeActivity.test.ts +318 -0
  82. package/src/runtime/bridgeActivity.ts +268 -0
  83. package/src/runtime/bridgeConfigReader.test.ts +80 -0
  84. package/src/runtime/bridgeConfigReader.ts +20 -0
  85. package/src/runtime/bridgeContext.test.ts +247 -0
  86. package/src/runtime/bridgeContext.ts +43 -10
  87. package/src/runtime/bridgeDriver.test.ts +153 -1
  88. package/src/runtime/bridgeDriver.ts +64 -8
  89. package/src/runtime/bridgeFiles.test.ts +62 -0
  90. package/src/runtime/bridgeFiles.ts +12 -2
  91. package/src/runtime/bridgeLiveConfig.ts +18 -0
  92. package/src/runtime/bridgeLog.ts +10 -0
  93. package/src/runtime/bridgeRedaction.test.ts +410 -0
  94. package/src/runtime/bridgeRedaction.ts +214 -0
  95. package/src/runtime/bridgeRefusalError.ts +13 -0
  96. package/src/runtime/bridgeRegistry.test.ts +1056 -10
  97. package/src/runtime/bridgeRegistry.ts +464 -50
  98. package/src/runtime/bridgeScopedContext.ts +55 -0
  99. package/src/runtime/bridgeServer.test.ts +534 -4
  100. package/src/runtime/bridgeServer.ts +145 -38
  101. package/src/runtime/bridgeSetupHandlers.test.ts +156 -5
  102. package/src/runtime/bridgeSetupHandlers.ts +59 -14
  103. package/src/runtime/bridgeSupervisor.test.ts +670 -2
  104. package/src/runtime/bridgeSupervisor.ts +219 -67
  105. package/src/runtime/serve.ts +87 -11
  106. package/src/steam/steamcmd.test.ts +32 -1
  107. package/src/steam/steamcmd.ts +27 -4
  108. package/src/utils/changedSettingKeys.test.ts +65 -0
  109. package/src/utils/changedSettingKeys.ts +6 -0
  110. package/src/utils/describeExecFailure.test.ts +34 -0
  111. package/src/utils/describeExecFailure.ts +9 -0
  112. package/src/utils/index.ts +3 -0
  113. package/src/utils/sameSettingValue.test.ts +35 -0
  114. package/src/utils/sameSettingValue.ts +40 -0
  115. package/src/wire/activity.test.ts +95 -0
  116. package/src/wire/activity.ts +41 -0
  117. package/src/wire/index.ts +1 -0
  118. package/src/wire/panel.test.ts +67 -1
  119. package/src/wire/panel.ts +11 -0
@@ -1,9 +1,11 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
1
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { renderDetectTable } from "../bot/detect";
5
6
  import { BridgeKind, BridgeSetupStepKind } from "../core";
6
7
  import { GuideOpenTab } from "../guides";
8
+ import { MANIFEST_SCHEMA_VERSION, PackageKind } from "../manifest";
7
9
  import type { BridgeDriver, BridgeSetupModule } from "../modules";
8
10
  import {
9
11
  BridgeControl,
@@ -14,7 +16,7 @@ import {
14
16
  type BridgeTab,
15
17
  type BridgeTableSection,
16
18
  } from "../panel";
17
- import { placementReportLines, runValidate, setupReportLines } from "./validate";
19
+ import { availabilityReportLines, placementReportLines, runValidate, setupReportLines } from "./validate";
18
20
 
19
21
  const text = (value: string) => ({
20
22
  ar: value,
@@ -394,3 +396,281 @@ describe("the placement report", () => {
394
396
  ).toEqual([]);
395
397
  });
396
398
  });
399
+
400
+ const webp = (width: number, height: number) => {
401
+ const bytes = new Uint8Array(30);
402
+ const view = new DataView(bytes.buffer);
403
+ const ascii = (offset: number, value: string) => {
404
+ for (let index = 0; index < value.length; index += 1) {
405
+ bytes[offset + index] = value.charCodeAt(index);
406
+ }
407
+ };
408
+
409
+ ascii(0, "RIFF");
410
+ view.setUint32(4, bytes.byteLength - 8, true);
411
+ ascii(8, "WEBP");
412
+ ascii(12, "VP8X");
413
+ view.setUint32(16, 10, true);
414
+ view.setUint16(24, (width - 1) & 0xff_ff, true);
415
+ bytes[26] = ((width - 1) >> 16) & 0xff;
416
+ view.setUint16(27, (height - 1) & 0xff_ff, true);
417
+ bytes[29] = ((height - 1) >> 16) & 0xff;
418
+
419
+ return bytes;
420
+ };
421
+
422
+ const BOT_DRIVER = `export const driver = {
423
+ install: {
424
+ kind: "install",
425
+ run: async () => undefined,
426
+ },
427
+ lifecycle: {
428
+ kind: "lifecycle",
429
+ command: async () => ["node", "/serverk/launcher.mjs"],
430
+ environment: async () => ({}),
431
+ },
432
+ };
433
+ `;
434
+
435
+ const botManifest = (extra: Record<string, unknown> = {}) => ({
436
+ schemaVersion: MANIFEST_SCHEMA_VERSION,
437
+ kind: PackageKind.Bot,
438
+ metadata: {
439
+ id: "bot-node",
440
+ name: text("Node.js Bot"),
441
+ description: text("Run your own bot"),
442
+ tagline: text("Your code, always on"),
443
+ logo: "assets/logo.webp",
444
+ banner: "assets/banner.webp",
445
+ },
446
+ resources: {
447
+ minMemoryMb: 512,
448
+ recommendedMemoryMb: 1024,
449
+ minDiskMb: 2048,
450
+ },
451
+ container: {
452
+ image: "serverkgg/bot-node",
453
+ ports: [],
454
+ },
455
+ ...extra,
456
+ });
457
+
458
+ const WEBHOOK_PORT = {
459
+ key: "webhook",
460
+ containerPort: 8080,
461
+ protocol: "tcp",
462
+ hostRange: {
463
+ default: 30_000,
464
+ min: 30_000,
465
+ max: 30_099,
466
+ },
467
+ };
468
+
469
+ describe("validating a bot package", () => {
470
+ let root = "";
471
+ let lines: string[] = [];
472
+ let restore = () => {};
473
+
474
+ const writeBot = (manifest: Record<string, unknown>, detectTable: string | null = renderDetectTable()) => {
475
+ mkdirSync(join(root, "assets"));
476
+ mkdirSync(join(root, "src"));
477
+ mkdirSync(join(root, "image"));
478
+
479
+ if (detectTable !== null) {
480
+ writeFileSync(join(root, "image/detect.json"), detectTable);
481
+ }
482
+
483
+ writeFileSync(
484
+ join(root, "package.json"),
485
+ JSON.stringify({
486
+ name: "@serverkgg/bot-node",
487
+ dependencies: {
488
+ "@serverkgg/bridge": "0.1.0",
489
+ },
490
+ }),
491
+ );
492
+ writeFileSync(join(root, "serverk.yml"), JSON.stringify(manifest));
493
+ writeFileSync(join(root, "assets/logo.webp"), webp(256, 256));
494
+ writeFileSync(join(root, "assets/banner.webp"), webp(1280, 720));
495
+ writeFileSync(join(root, "src/driver.ts"), BOT_DRIVER);
496
+ };
497
+
498
+ const validate = () => {
499
+ return runValidate([
500
+ "--dir",
501
+ root,
502
+ ]);
503
+ };
504
+
505
+ beforeEach(() => {
506
+ root = mkdtempSync(`${tmpdir()}/serverk-validate-bot-`);
507
+ lines = [];
508
+
509
+ const spy = spyOn(console, "log").mockImplementation((line: unknown) => {
510
+ lines.push(String(line));
511
+ });
512
+
513
+ restore = () => spy.mockRestore();
514
+ });
515
+
516
+ afterEach(() => {
517
+ restore();
518
+ rmSync(root, {
519
+ force: true,
520
+ recursive: true,
521
+ });
522
+ });
523
+
524
+ test("validates a bot with no ports and says which kind it read", async () => {
525
+ writeBot(botManifest());
526
+
527
+ await validate();
528
+
529
+ expect(lines).toContain("bot-node is valid");
530
+ expect(lines).toContain(
531
+ " manifest: BotPackage, 0 port(s), 0 conditional, 0 tcp+udp, 0 block(s), 0 secret(s), 0 guide(s)",
532
+ );
533
+ expect(lines).toContain(" environment: vault off");
534
+ expect(lines).toContain(" detect: image/detect.json matches this bridge");
535
+ expect(lines.some((line) => line.includes("warning"))).toBe(false);
536
+ });
537
+
538
+ test("says when a bot turns the environment vault on", async () => {
539
+ writeBot(
540
+ botManifest({
541
+ environment: {
542
+ vault: true,
543
+ },
544
+ }),
545
+ );
546
+
547
+ await validate();
548
+
549
+ expect(lines).toContain(" environment: vault on");
550
+ });
551
+
552
+ test("fails a bot whose detection table is missing", async () => {
553
+ writeBot(botManifest(), null);
554
+
555
+ await expect(validate()).rejects.toThrow(
556
+ 'image/detect.json is missing — run "serverk-bridge detect-table" and commit it',
557
+ );
558
+ });
559
+
560
+ test("fails a bot whose detection table is stale", async () => {
561
+ writeBot(botManifest(), "{}\n");
562
+
563
+ await expect(validate()).rejects.toThrow(
564
+ 'image/detect.json is out of date — run "serverk-bridge detect-table" and commit it',
565
+ );
566
+ });
567
+
568
+ test("warns about a port on a bot but still validates it", async () => {
569
+ writeBot(
570
+ botManifest({
571
+ container: {
572
+ image: "serverkgg/bot-node",
573
+ ports: [
574
+ WEBHOOK_PORT,
575
+ ],
576
+ },
577
+ }),
578
+ );
579
+
580
+ await validate();
581
+
582
+ expect(lines).toContain("bot-node is valid");
583
+ expect(lines).toContain(
584
+ " warning: a bot package declares 1 port(s); bots need none — remove them unless a webhook needs a public port",
585
+ );
586
+ });
587
+
588
+ test("fails a bot that declares platform secrets", async () => {
589
+ writeBot(
590
+ botManifest({
591
+ secrets: [
592
+ {
593
+ key: "API_KEY",
594
+ description: text("A key"),
595
+ },
596
+ ],
597
+ }),
598
+ );
599
+
600
+ await expect(validate()).rejects.toThrow(
601
+ "a bot package declares no platform secrets; per-bot values live in the environment vault",
602
+ );
603
+ });
604
+ });
605
+
606
+ describe("availability report", () => {
607
+ const driverWithModules = (modules: BridgeDriver["modules"]): BridgeDriver => ({
608
+ install: {
609
+ kind: BridgeKind.Install,
610
+ run: async () => {
611
+ return;
612
+ },
613
+ },
614
+ lifecycle: {
615
+ kind: BridgeKind.Lifecycle,
616
+ command: async () => [
617
+ "run",
618
+ ],
619
+ },
620
+ modules,
621
+ });
622
+
623
+ test("warns about a protected module that cannot say when its action is unavailable", () => {
624
+ expect(
625
+ availabilityReportLines(
626
+ driverWithModules({
627
+ worlds: {
628
+ kind: BridgeKind.Collection,
629
+ protectedActions: [
630
+ "delete",
631
+ ],
632
+ list: async () => [],
633
+ actions: {
634
+ delete: async () => {
635
+ return;
636
+ },
637
+ },
638
+ },
639
+ }),
640
+ ),
641
+ ).toEqual([
642
+ ' warning: module "worlds" protects delete but declares no `unavailable` — the panel offers the action everywhere and the platform stops the game before the driver can refuse it',
643
+ ]);
644
+ });
645
+
646
+ test("says nothing when the protected module declares the hook or protects nothing", () => {
647
+ expect(
648
+ availabilityReportLines(
649
+ driverWithModules({
650
+ worlds: {
651
+ kind: BridgeKind.Collection,
652
+ protectedActions: [
653
+ "delete",
654
+ ],
655
+ list: async () => [],
656
+ actions: {
657
+ delete: async () => {
658
+ return;
659
+ },
660
+ },
661
+ unavailable: async () => ({}),
662
+ },
663
+ players: {
664
+ kind: BridgeKind.Collection,
665
+ list: async () => [],
666
+ actions: {
667
+ kick: async () => {
668
+ return;
669
+ },
670
+ },
671
+ },
672
+ }),
673
+ ),
674
+ ).toEqual([]);
675
+ });
676
+ });
@@ -4,13 +4,18 @@ import { pathToFileURL } from "node:url";
4
4
  import { parseArgs } from "node:util";
5
5
  import { z } from "zod";
6
6
  import { type ArticlePack, compileArticles } from "../blog";
7
+ import { DETECT_TABLE_FILE, renderDetectTable } from "../bot/detect";
8
+ import { BridgeKind, PortProtocol } from "../core";
7
9
  import { GuideOpenTab, type GuideOpenTarget, type GuidePack, readGuidePack, readGuidePackage } from "../guides";
8
10
  import {
9
11
  declaredFiles,
10
12
  type GuidePackage,
13
+ isBotManifest,
11
14
  MANIFEST_FILE,
12
- type Manifest,
15
+ type Package,
16
+ PackageKind,
13
17
  type PackageSource,
18
+ type RunnableManifest,
14
19
  readAssets,
15
20
  readPackageDocument,
16
21
  } from "../manifest";
@@ -31,7 +36,7 @@ const HELP = `serverk-bridge validate [options]
31
36
 
32
37
  options:
33
38
  --dir <path> package directory (default: current directory)
34
- --driver <path> driver module (default: <dir>/src/driver.ts), game packages only
39
+ --driver <path> driver module (default: <dir>/src/driver.ts), runnable packages only
35
40
  --help show this help`;
36
41
 
37
42
  const BRIDGE_PACKAGE = "@serverkgg/bridge";
@@ -274,6 +279,39 @@ const reportPlacement = (driver: BridgeDriver) => {
274
279
  }
275
280
  };
276
281
 
282
+ export const availabilityReportLines = (driver: BridgeDriver): string[] => {
283
+ const lines: string[] = [];
284
+
285
+ for (const [id, module] of Object.entries(driver.modules ?? {})) {
286
+ if (
287
+ module.kind !== BridgeKind.Actions
288
+ && module.kind !== BridgeKind.Catalog
289
+ && module.kind !== BridgeKind.Collection
290
+ && module.kind !== BridgeKind.Settings
291
+ ) {
292
+ continue;
293
+ }
294
+
295
+ const protectedActions = module.protectedActions ?? [];
296
+
297
+ if (protectedActions.length === 0 || module.unavailable) {
298
+ continue;
299
+ }
300
+
301
+ lines.push(
302
+ ` warning: module "${id}" protects ${protectedActions.join(", ")} but declares no \`unavailable\` — the panel offers the action everywhere and the platform stops the game before the driver can refuse it`,
303
+ );
304
+ }
305
+
306
+ return lines;
307
+ };
308
+
309
+ const reportAvailability = (driver: BridgeDriver) => {
310
+ for (const line of availabilityReportLines(driver)) {
311
+ console.log(line);
312
+ }
313
+ };
314
+
277
315
  const openTargetOf = (target: GuideOpenTarget) => {
278
316
  if (target.tab === GuideOpenTab.Panel) {
279
317
  return `panel tab "${target.tabId}"`;
@@ -317,7 +355,54 @@ export const setupReportLines = (setup: BridgeSetupModule | undefined): string[]
317
355
  ];
318
356
  };
319
357
 
320
- const validateGamePackage = async (source: PackageSource, manifest: Manifest, dir: string, driverArg?: string) => {
358
+ export const botPortLines = (manifest: RunnableManifest): string[] => {
359
+ const ports = manifest.container.ports.length;
360
+
361
+ if (!isBotManifest(manifest) || ports === 0) {
362
+ return [];
363
+ }
364
+
365
+ return [
366
+ ` warning: a bot package declares ${ports} port(s); bots need none — remove them unless a webhook needs a public port`,
367
+ ];
368
+ };
369
+
370
+ export const botEnvironmentLines = (manifest: RunnableManifest): string[] => {
371
+ if (!isBotManifest(manifest)) {
372
+ return [];
373
+ }
374
+
375
+ return [
376
+ ` environment: vault ${manifest.environment.vault ? "on" : "off"}`,
377
+ ];
378
+ };
379
+
380
+ export const botDetectLines = (source: PackageSource, manifest: RunnableManifest): string[] => {
381
+ if (!isBotManifest(manifest)) {
382
+ return [];
383
+ }
384
+
385
+ const bytes = source.read(DETECT_TABLE_FILE);
386
+
387
+ if (bytes === null) {
388
+ throw new Error(`${DETECT_TABLE_FILE} is missing — run "serverk-bridge detect-table" and commit it`);
389
+ }
390
+
391
+ if (new TextDecoder().decode(bytes) !== renderDetectTable()) {
392
+ throw new Error(`${DETECT_TABLE_FILE} is out of date — run "serverk-bridge detect-table" and commit it`);
393
+ }
394
+
395
+ return [
396
+ ` detect: ${DETECT_TABLE_FILE} matches this bridge`,
397
+ ];
398
+ };
399
+
400
+ const validateRunnablePackage = async (
401
+ source: PackageSource,
402
+ manifest: RunnableManifest,
403
+ dir: string,
404
+ driverArg?: string,
405
+ ) => {
321
406
  const assets = readAssets(source, manifest);
322
407
  const guideFiles = manifest.guides.flatMap((guide) => [
323
408
  guide.bodyPath.ar,
@@ -331,6 +416,7 @@ const validateGamePackage = async (source: PackageSource, manifest: Manifest, di
331
416
  }
332
417
 
333
418
  const pack = readGuidePack(source, manifest);
419
+ const detectLines = botDetectLines(source, manifest);
334
420
  const driverPath = driverArg ? resolve(driverArg) : join(dir, "src/driver.ts");
335
421
 
336
422
  if (!existsSync(driverPath)) {
@@ -348,11 +434,22 @@ const validateGamePackage = async (source: PackageSource, manifest: Manifest, di
348
434
  validatePortActivation(manifest.container.ports, driver);
349
435
 
350
436
  const conditionalPorts = manifest.container.ports.filter((port) => port.activeWhen.length > 0).length;
437
+ const pairedPorts = manifest.container.ports.filter((port) => port.protocol === PortProtocol.TcpUdp).length;
438
+ const blockPorts = manifest.container.ports.filter((port) => port.count > 1).length;
351
439
 
352
440
  console.log(`${manifest.metadata.id} is valid`);
353
441
  console.log(
354
- ` manifest: ${manifest.container.ports.length} port(s), ${conditionalPorts} conditional, ${manifest.secrets.length} secret(s), ${manifest.guides.length} guide(s)`,
442
+ ` manifest: ${manifest.kind}, ${manifest.container.ports.length} port(s), ${conditionalPorts} conditional, ${pairedPorts} tcp+udp, ${blockPorts} block(s), ${(manifest.secrets ?? []).length} secret(s), ${manifest.guides.length} guide(s)`,
355
443
  );
444
+
445
+ for (const line of [
446
+ ...botPortLines(manifest),
447
+ ...botEnvironmentLines(manifest),
448
+ ...detectLines,
449
+ ]) {
450
+ console.log(line);
451
+ }
452
+
356
453
  console.log(
357
454
  ` assets: logo ${assets.logo.dimensions.width}x${assets.logo.dimensions.height}, banner ${assets.banner.dimensions.width}x${assets.banner.dimensions.height}`,
358
455
  );
@@ -368,6 +465,7 @@ const validateGamePackage = async (source: PackageSource, manifest: Manifest, di
368
465
  reportEvents(driver);
369
466
  reportActionTargets(driver);
370
467
  reportPlacement(driver);
468
+ reportAvailability(driver);
371
469
  };
372
470
 
373
471
  export const runValidate = async (args: string[]) => {
@@ -399,7 +497,7 @@ export const runValidate = async (args: string[]) => {
399
497
 
400
498
  assertRegistryBridge(source);
401
499
 
402
- let document: Manifest | GuidePackage;
500
+ let document: Package;
403
501
 
404
502
  try {
405
503
  document = readPackageDocument(source);
@@ -407,11 +505,11 @@ export const runValidate = async (args: string[]) => {
407
505
  return invalidManifest(error);
408
506
  }
409
507
 
410
- if (document.kind === "GuidePackage") {
508
+ if (document.kind === PackageKind.Guide) {
411
509
  validateGuidePackage(source, document);
412
510
 
413
511
  return;
414
512
  }
415
513
 
416
- await validateGamePackage(source, document, dir, values.driver);
514
+ await validateRunnablePackage(source, document, dir, values.driver);
417
515
  };
@@ -1,4 +1,4 @@
1
- import type { BridgeStream, BridgeValues } from "../core";
1
+ import type { BridgeActivity, BridgeStream, BridgeValues } from "../core";
2
2
  import type { BridgeHttpMethod, BridgeSetupRuntime } from "../protocol";
3
3
 
4
4
  export interface BridgeServer {
@@ -6,6 +6,12 @@ export interface BridgeServer {
6
6
  memoryMb: number;
7
7
  cpuPercent: number;
8
8
  running: boolean;
9
+ readonly publicAddress: string | null;
10
+ }
11
+
12
+ export interface BridgePortBlock {
13
+ first: number;
14
+ count: number;
9
15
  }
10
16
 
11
17
  export interface BridgeFileEntry {
@@ -34,6 +40,7 @@ export interface BridgeExtractOptions {
34
40
  select?: string;
35
41
  selects?: string[];
36
42
  exclude?: string[];
43
+ dotfiles?: boolean;
37
44
  }
38
45
 
39
46
  export interface BridgeListOptions {
@@ -182,6 +189,10 @@ export interface BridgeSetupReporter {
182
189
  clear(): void;
183
190
  }
184
191
 
192
+ export interface BridgeActivityReporter {
193
+ report(entry: BridgeActivity): void;
194
+ }
195
+
185
196
  export interface BridgeLog {
186
197
  (message: string, fields?: BridgeValues): void;
187
198
  warn(message: string, fields?: BridgeValues): void;
@@ -191,8 +202,11 @@ export interface BridgeLog {
191
202
  export interface BridgeContext {
192
203
  readonly server: BridgeServer;
193
204
  variable(key: string): string | null;
205
+ variables(): Readonly<Record<string, string>>;
194
206
  secret(key: string): string | null;
207
+ secrets(): Readonly<Record<string, string>>;
195
208
  port(key: string): number;
209
+ portBlock(key: string): BridgePortBlock;
196
210
  files: BridgeFiles;
197
211
  codec: BridgeCodecs;
198
212
  net: BridgeNet;
@@ -202,6 +216,8 @@ export interface BridgeContext {
202
216
  exec(command: string[], options?: BridgeExecOptions): Promise<BridgeExecResult>;
203
217
  logs: BridgeLogs;
204
218
  setup: BridgeSetupReporter;
219
+ activity: BridgeActivityReporter;
205
220
  emit(event: string, payload?: BridgeValues): void;
221
+ restart(): Promise<void>;
206
222
  log: BridgeLog;
207
223
  }
@@ -1,5 +1,6 @@
1
1
  export enum BridgeAction {
2
2
  Add = "add",
3
+ Availability = "availability",
3
4
  Install = "install",
4
5
  Installed = "installed",
5
6
  List = "list",
@@ -16,6 +17,7 @@ export enum BridgeAction {
16
17
  }
17
18
 
18
19
  export const BRIDGE_READ_ACTIONS: readonly string[] = [
20
+ BridgeAction.Availability,
19
21
  BridgeAction.Releases,
20
22
  BridgeAction.Installed,
21
23
  BridgeAction.List,
@@ -0,0 +1,9 @@
1
+ import type { BridgeEventName } from "../protocol/bridgeEvents";
2
+ import type { BridgeValues, LocalizedText } from "./bridgeValue";
3
+
4
+ export interface BridgeActivity {
5
+ name: BridgeEventName;
6
+ text: LocalizedText;
7
+ subject?: string;
8
+ details?: BridgeValues;
9
+ }
@@ -0,0 +1,10 @@
1
+ import type { LocalizedText } from "./bridgeValue";
2
+
3
+ export const BRIDGE_AVAILABILITY_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
4
+
5
+ export type BridgeUnavailable = Record<string, LocalizedText>;
6
+
7
+ export interface BridgeAvailability {
8
+ module: BridgeUnavailable;
9
+ subjects: Record<string, BridgeUnavailable>;
10
+ }
@@ -0,0 +1,45 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { PortProtocol, portCountOf, SOCKET_PROTOCOLS, socketProtocolsOf } from "./bridgePort";
3
+
4
+ describe("socketProtocolsOf", () => {
5
+ test("keeps a single socket protocol as it is", () => {
6
+ expect(socketProtocolsOf(PortProtocol.Tcp)).toEqual([
7
+ PortProtocol.Tcp,
8
+ ]);
9
+ expect(socketProtocolsOf(PortProtocol.Udp)).toEqual([
10
+ PortProtocol.Udp,
11
+ ]);
12
+ });
13
+
14
+ test("expands a pair into both socket protocols", () => {
15
+ expect(socketProtocolsOf(PortProtocol.TcpUdp)).toEqual([
16
+ PortProtocol.Tcp,
17
+ PortProtocol.Udp,
18
+ ]);
19
+ });
20
+
21
+ test("never hands out the shared list itself", () => {
22
+ const expanded = socketProtocolsOf(PortProtocol.TcpUdp);
23
+
24
+ expanded.pop();
25
+
26
+ expect(SOCKET_PROTOCOLS).toEqual([
27
+ PortProtocol.Tcp,
28
+ PortProtocol.Udp,
29
+ ]);
30
+ });
31
+ });
32
+
33
+ describe("portCountOf", () => {
34
+ test("reads a port stored before blocks existed as a single port", () => {
35
+ expect(portCountOf({})).toBe(1);
36
+ });
37
+
38
+ test("reads the count of a block", () => {
39
+ expect(
40
+ portCountOf({
41
+ count: 32,
42
+ }),
43
+ ).toBe(32);
44
+ });
45
+ });
@@ -1,8 +1,34 @@
1
1
  export enum PortProtocol {
2
2
  Tcp = "tcp",
3
3
  Udp = "udp",
4
+ TcpUdp = "tcp+udp",
4
5
  }
5
6
 
7
+ export const PORT_BLOCK_MAX_COUNT = 64;
8
+
9
+ export type SocketProtocol = PortProtocol.Tcp | PortProtocol.Udp;
10
+
11
+ export const SOCKET_PROTOCOLS = [
12
+ PortProtocol.Tcp,
13
+ PortProtocol.Udp,
14
+ ] as const satisfies readonly SocketProtocol[];
15
+
16
+ export const socketProtocolsOf = (protocol: PortProtocol): SocketProtocol[] => {
17
+ if (protocol === PortProtocol.TcpUdp) {
18
+ return [
19
+ ...SOCKET_PROTOCOLS,
20
+ ];
21
+ }
22
+
23
+ return [
24
+ protocol,
25
+ ];
26
+ };
27
+
28
+ export const portCountOf = (port: { count?: number | undefined }): number => {
29
+ return port.count ?? 1;
30
+ };
31
+
6
32
  export enum PortJoin {
7
33
  Combined = "combined",
8
34
  Separate = "separate",
@@ -17,6 +17,16 @@ describe("BridgeUserError", () => {
17
17
  expect(error.text).toEqual(text);
18
18
  });
19
19
 
20
+ test("keeps the cause a driver attaches for the staff log", () => {
21
+ const cause = new Error("cdn.modrinth.com answered 404");
22
+ const error = new BridgeUserError(text, {
23
+ cause,
24
+ });
25
+
26
+ expect(error.cause).toBe(cause);
27
+ expect(error.message).toBe(text.en);
28
+ });
29
+
20
30
  test("is the class a driver reaches through the published namespace", () => {
21
31
  expect(new Bridge.UserError(text)).toBeInstanceOf(BridgeUserError);
22
32
  });
@@ -1,10 +1,14 @@
1
1
  import type { LocalizedText } from "./bridgeValue";
2
2
 
3
+ export interface BridgeUserErrorOptions {
4
+ cause?: unknown;
5
+ }
6
+
3
7
  export class BridgeUserError extends Error {
4
8
  readonly text: LocalizedText;
5
9
 
6
- constructor(text: LocalizedText) {
7
- super(text.en);
10
+ constructor(text: LocalizedText, options: BridgeUserErrorOptions = {}) {
11
+ super(text.en, options);
8
12
 
9
13
  this.name = "BridgeUserError";
10
14
  this.text = text;