@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.
- package/package.json +3 -1
- package/src/bot/botDeploy.test.ts +363 -0
- package/src/bot/botDeploy.ts +136 -0
- package/src/bot/botEnvironment.test.ts +189 -0
- package/src/bot/botEnvironment.ts +141 -0
- package/src/bot/botIdentity.test.ts +251 -0
- package/src/bot/botIdentity.ts +140 -0
- package/src/bot/botPanel.test.ts +246 -0
- package/src/bot/botPanel.ts +129 -0
- package/src/bot/botState.test.ts +45 -0
- package/src/bot/botState.ts +54 -0
- package/src/bot/botTerminal.test.ts +79 -0
- package/src/bot/botTerminal.ts +62 -0
- package/src/bot/botVariables.test.ts +193 -0
- package/src/bot/botVariables.ts +73 -0
- package/src/bot/detect/botFramework.ts +23 -0
- package/src/bot/detect/botFrameworks.ts +198 -0
- package/src/bot/detect/botInspectWarning.ts +15 -0
- package/src/bot/detect/botPackageManager.ts +6 -0
- package/src/bot/detect/botPlatform.ts +10 -0
- package/src/bot/detect/botRuntime.ts +15 -0
- package/src/bot/detect/botToken.test.ts +71 -0
- package/src/bot/detect/botToken.ts +126 -0
- package/src/bot/detect/detect.test.ts +543 -0
- package/src/bot/detect/detect.ts +842 -0
- package/src/bot/detect/detectResult.ts +40 -0
- package/src/bot/detect/detectTable.test.ts +72 -0
- package/src/bot/detect/detectTable.ts +166 -0
- package/src/bot/detect/envExample.test.ts +29 -0
- package/src/bot/detect/envExample.ts +48 -0
- package/src/bot/detect/envKey.ts +7 -0
- package/src/bot/detect/envScan.test.ts +39 -0
- package/src/bot/detect/envScan.ts +87 -0
- package/src/bot/detect/index.ts +17 -0
- package/src/bot/detect/inferBotPlatform.test.ts +60 -0
- package/src/bot/detect/inferBotPlatform.ts +28 -0
- package/src/bot/detect/keys.test.ts +28 -0
- package/src/bot/detect/keys.ts +64 -0
- package/src/bot/detect/sensitiveKey.test.ts +91 -0
- package/src/bot/detect/sensitiveKey.ts +44 -0
- package/src/bot/detect/serverkJson.test.ts +88 -0
- package/src/bot/detect/serverkJson.ts +174 -0
- package/src/bot/index.ts +8 -0
- package/src/cli/detectTable.test.ts +66 -0
- package/src/cli/detectTable.ts +43 -0
- package/src/cli/main.ts +12 -4
- package/src/cli/renderedFile.ts +35 -0
- package/src/cli/schema.ts +7 -26
- package/src/cli/validate.test.ts +283 -3
- package/src/cli/validate.ts +105 -7
- package/src/context/bridgeContext.ts +17 -1
- package/src/core/bridgeAction.ts +2 -0
- package/src/core/bridgeActivity.ts +9 -0
- package/src/core/bridgeAvailability.ts +10 -0
- package/src/core/bridgePort.test.ts +45 -0
- package/src/core/bridgePort.ts +26 -0
- package/src/core/bridgeUserError.test.ts +10 -0
- package/src/core/bridgeUserError.ts +6 -2
- package/src/core/describeDownloadFailure.test.ts +58 -0
- package/src/core/describeDownloadFailure.ts +69 -0
- package/src/core/index.ts +3 -0
- package/src/guides/compile.ts +2 -2
- package/src/manifest/index.ts +1 -0
- package/src/manifest/kind.ts +5 -0
- package/src/manifest/manifest.test.ts +630 -3
- package/src/manifest/manifest.ts +391 -87
- package/src/manifest/runtime.ts +2 -2
- package/src/manifest/source.ts +4 -4
- package/src/modules/bridge.ts +8 -1
- package/src/modules/bridgeLifecycleSubject.ts +8 -0
- package/src/modules/bridgeModule.ts +19 -1
- package/src/modules/bridgeStopSignal.ts +13 -0
- package/src/modules/index.ts +2 -0
- package/src/panel/bridgeDetail.ts +4 -1
- package/src/protocol/bridgeActivity.ts +54 -0
- package/src/protocol/bridgeConfig.ts +2 -0
- package/src/protocol/bridgeEvents.ts +30 -0
- package/src/protocol/bridgeFrame.ts +10 -1
- package/src/protocol/index.ts +1 -0
- package/src/runtime/bridgeActions.ts +19 -2
- package/src/runtime/bridgeActivity.test.ts +318 -0
- package/src/runtime/bridgeActivity.ts +268 -0
- package/src/runtime/bridgeConfigReader.test.ts +80 -0
- package/src/runtime/bridgeConfigReader.ts +20 -0
- package/src/runtime/bridgeContext.test.ts +247 -0
- package/src/runtime/bridgeContext.ts +43 -10
- package/src/runtime/bridgeDriver.test.ts +153 -1
- package/src/runtime/bridgeDriver.ts +64 -8
- package/src/runtime/bridgeFiles.test.ts +62 -0
- package/src/runtime/bridgeFiles.ts +12 -2
- package/src/runtime/bridgeLiveConfig.ts +18 -0
- package/src/runtime/bridgeLog.ts +10 -0
- package/src/runtime/bridgeRedaction.test.ts +410 -0
- package/src/runtime/bridgeRedaction.ts +214 -0
- package/src/runtime/bridgeRefusalError.ts +13 -0
- package/src/runtime/bridgeRegistry.test.ts +1056 -10
- package/src/runtime/bridgeRegistry.ts +464 -50
- package/src/runtime/bridgeScopedContext.ts +55 -0
- package/src/runtime/bridgeServer.test.ts +534 -4
- package/src/runtime/bridgeServer.ts +145 -38
- package/src/runtime/bridgeSetupHandlers.test.ts +156 -5
- package/src/runtime/bridgeSetupHandlers.ts +59 -14
- package/src/runtime/bridgeSupervisor.test.ts +670 -2
- package/src/runtime/bridgeSupervisor.ts +219 -67
- package/src/runtime/serve.ts +87 -11
- package/src/steam/steamcmd.test.ts +32 -1
- package/src/steam/steamcmd.ts +27 -4
- package/src/utils/changedSettingKeys.test.ts +65 -0
- package/src/utils/changedSettingKeys.ts +6 -0
- package/src/utils/describeExecFailure.test.ts +34 -0
- package/src/utils/describeExecFailure.ts +9 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/sameSettingValue.test.ts +35 -0
- package/src/utils/sameSettingValue.ts +40 -0
- package/src/wire/activity.test.ts +95 -0
- package/src/wire/activity.ts +41 -0
- package/src/wire/index.ts +1 -0
- package/src/wire/panel.test.ts +67 -1
- package/src/wire/panel.ts +11 -0
package/src/cli/validate.test.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/cli/validate.ts
CHANGED
|
@@ -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
|
|
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),
|
|
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
|
|
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:
|
|
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 ===
|
|
508
|
+
if (document.kind === PackageKind.Guide) {
|
|
411
509
|
validateGuidePackage(source, document);
|
|
412
510
|
|
|
413
511
|
return;
|
|
414
512
|
}
|
|
415
513
|
|
|
416
|
-
await
|
|
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
|
}
|
package/src/core/bridgeAction.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/core/bridgePort.ts
CHANGED
|
@@ -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;
|