@serverkgg/bridge 0.1.3 → 0.1.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverkgg/bridge",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Serverk game bridge and driver runtime, wire protocol, manifest schema, and maintainer CLI",
5
5
  "homepage": "https://serverk.gg",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
package/rules/package.md CHANGED
@@ -37,6 +37,11 @@ One Serverk package (a game or a bot runtime) on `@serverkgg/bridge`, with no pi
37
37
  - A settings form targets `BridgeFormTarget.Settings` and module `settings`, with a one-sentence `help` and `restartHint: true`.
38
38
  - In a game, the roster and moderation tables go on `place: BridgePlace.Players`, metrics on `BridgePlace.Overview`.
39
39
  - A setup `Form` step points at an existing panel form by `tab`, `section` and field keys, never redeclaring a field; `Form` and `Open` steps state `required`. A game's setup ends with the `Open` step `invite` targeting `GuideOpenTab.Access`.
40
+ - A preview module that knows the change cannot install answers `blocked: true`, with the reason in `lines`. The panel then shows the lines with a close button only, and the platform refuses the change before it rebuilds anything.
41
+ - `BridgeConfirm.None` runs on the click: reads, downloads, start, stop and restart.
42
+ - `BridgeConfirm.Normal` asks once, for anything the player can come back from: a settings form, a variable flip, a reinstall, a version or type switch, a modpack or mod install, op and deop, kick, ban, whitelist, rotating a password.
43
+ - `BridgeConfirm.Strong` is only for an action with no way back, where no restore point is taken and nothing in the panel brings the state back: deleting a world, save, map or character data, resetting a dimension, a wipe. A protected action takes no restore point.
44
+ - A `Form` or `Catalog` section, a `Preview` answer and any section with `reinstall` are never `Strong`, because the platform takes a restore point first; `serverk-bridge validate` fails on such a section. The web shows `Strong` as a checkbox acknowledgement.
40
45
  - Emit event names from `BridgeEventName`, never strings, and list every event raised from code in `emits` (`patterns: []` when none match the log). A bot matches deployments with `DEPLOYMENT_EVENT_PATTERN`.
41
46
  - `install.run` runs on every boot: keep it idempotent with `readStamp` and `writeStamp` (`@serverkgg/bridge/install`), the stamp in `files.protected`. A game protects the default `.serverk-install.json`; a bot writes `.serverk/install.json` (`INSTALL_STAMP`, `shared/botRuntime.ts`) and protects `.serverk/**`.
42
47
 
@@ -8,7 +8,10 @@ import { GuideOpenTab } from "../guides";
8
8
  import { MANIFEST_SCHEMA_VERSION, PackageKind } from "../manifest";
9
9
  import type { BridgeDriver, BridgeSetupModule } from "../modules";
10
10
  import {
11
+ type BridgeCatalogSection,
12
+ BridgeConfirm,
11
13
  BridgeControl,
14
+ type BridgeDetailSection,
12
15
  type BridgeFormSection,
13
16
  BridgeFormTarget,
14
17
  BridgeLayout,
@@ -17,7 +20,13 @@ import {
17
20
  type BridgeTableSection,
18
21
  } from "../panel";
19
22
  import { RULES_FILE, readShippedRules, writeRules } from "./rules";
20
- import { availabilityReportLines, placementReportLines, runValidate, setupReportLines } from "./validate";
23
+ import {
24
+ availabilityReportLines,
25
+ confirmReportLines,
26
+ placementReportLines,
27
+ runValidate,
28
+ setupReportLines,
29
+ } from "./validate";
21
30
 
22
31
  const text = (value: string) => ({
23
32
  ar: value,
@@ -696,3 +705,120 @@ describe("availability report", () => {
696
705
  ).toEqual([]);
697
706
  });
698
707
  });
708
+
709
+ describe("the confirm report", () => {
710
+ const driverWithSections = (sections: BridgeTab["sections"]): BridgeDriver => ({
711
+ install: {
712
+ kind: BridgeKind.Install,
713
+ run: async () => {
714
+ return;
715
+ },
716
+ },
717
+ lifecycle: {
718
+ kind: BridgeKind.Lifecycle,
719
+ command: async () => [
720
+ "run",
721
+ ],
722
+ },
723
+ panel: {
724
+ tabs: [
725
+ {
726
+ id: "settings",
727
+ title: text("Settings"),
728
+ sections,
729
+ },
730
+ ],
731
+ },
732
+ });
733
+
734
+ const versionForm = (confirm?: BridgeConfirm): BridgeFormSection => ({
735
+ layout: BridgeLayout.Form,
736
+ id: "version",
737
+ target: BridgeFormTarget.Variables,
738
+ reinstall: true,
739
+ confirm,
740
+ fields: [
741
+ {
742
+ key: "VERSION",
743
+ control: BridgeControl.Text,
744
+ label: text("Version"),
745
+ },
746
+ ],
747
+ });
748
+
749
+ const modpacks = (confirm?: BridgeConfirm): BridgeCatalogSection => ({
750
+ layout: BridgeLayout.Catalog,
751
+ id: "modpacks",
752
+ module: "modpacks",
753
+ confirm,
754
+ });
755
+
756
+ const modpack = (confirm?: BridgeConfirm, reinstall?: boolean): BridgeDetailSection => ({
757
+ layout: BridgeLayout.Detail,
758
+ id: "modpack",
759
+ module: "modpack",
760
+ variable: "MODPACK",
761
+ reinstall,
762
+ confirm,
763
+ });
764
+
765
+ const worlds = (): BridgeTableSection => ({
766
+ layout: BridgeLayout.Table,
767
+ id: "worlds",
768
+ module: "worlds",
769
+ columns: [
770
+ {
771
+ key: "name",
772
+ label: text("Name"),
773
+ },
774
+ ],
775
+ actions: [
776
+ {
777
+ id: "delete",
778
+ label: text("Delete"),
779
+ confirm: BridgeConfirm.Strong,
780
+ },
781
+ ],
782
+ });
783
+
784
+ it("fails a form or a catalog that asks for a strong confirmation", () => {
785
+ expect(
786
+ confirmReportLines(
787
+ driverWithSections([
788
+ versionForm(BridgeConfirm.Strong),
789
+ modpacks(BridgeConfirm.Strong),
790
+ ]),
791
+ ),
792
+ ).toEqual([
793
+ ' error: tab "settings" section "version" is a form with `confirm: strong` — the platform takes a restore point before applying it, so declare `normal`; `strong` is only for what nothing brings back',
794
+ ' error: tab "settings" section "modpacks" is a catalog with `confirm: strong` — the platform takes a restore point before applying it, so declare `normal`; `strong` is only for what nothing brings back',
795
+ ]);
796
+ });
797
+
798
+ it("fails any section that reinstalls behind a strong confirmation", () => {
799
+ expect(
800
+ confirmReportLines(
801
+ driverWithSections([
802
+ modpack(BridgeConfirm.Strong, true),
803
+ ]),
804
+ ),
805
+ ).toEqual([
806
+ ' error: tab "settings" section "modpack" reinstalls with `confirm: strong` — the platform takes a restore point before every reinstall, so declare `normal`; `strong` is only for what nothing brings back',
807
+ ]);
808
+ });
809
+
810
+ it("leaves a strong action that deletes data and every normal confirmation alone", () => {
811
+ expect(
812
+ confirmReportLines(
813
+ driverWithSections([
814
+ versionForm(BridgeConfirm.Normal),
815
+ versionForm(),
816
+ modpacks(BridgeConfirm.Normal),
817
+ modpack(BridgeConfirm.Normal, true),
818
+ modpack(BridgeConfirm.Strong),
819
+ worlds(),
820
+ ]),
821
+ ),
822
+ ).toEqual([]);
823
+ });
824
+ });
@@ -21,6 +21,7 @@ import {
21
21
  } from "../manifest";
22
22
  import type { BridgeDriver, BridgeSetupModule } from "../modules";
23
23
  import {
24
+ BridgeConfirm,
24
25
  BridgeLayout,
25
26
  BridgePlace,
26
27
  type BridgeSection,
@@ -313,6 +314,39 @@ const reportAvailability = (driver: BridgeDriver) => {
313
314
  }
314
315
  };
315
316
 
317
+ const RESTORE_POINT_LAYOUTS: ReadonlySet<BridgeLayout> = new Set([
318
+ BridgeLayout.Form,
319
+ BridgeLayout.Catalog,
320
+ ]);
321
+
322
+ export const confirmReportLines = (driver: BridgeDriver): string[] => {
323
+ const lines: string[] = [];
324
+
325
+ for (const tab of driver.panel?.tabs ?? []) {
326
+ for (const section of tab.sections) {
327
+ if (!("confirm" in section) || section.confirm !== BridgeConfirm.Strong) {
328
+ continue;
329
+ }
330
+
331
+ if (RESTORE_POINT_LAYOUTS.has(section.layout)) {
332
+ lines.push(
333
+ ` error: tab "${tab.id}" section "${section.id}" is a ${section.layout} with \`confirm: strong\` — the platform takes a restore point before applying it, so declare \`normal\`; \`strong\` is only for what nothing brings back`,
334
+ );
335
+
336
+ continue;
337
+ }
338
+
339
+ if ("reinstall" in section && section.reinstall === true) {
340
+ lines.push(
341
+ ` error: tab "${tab.id}" section "${section.id}" reinstalls with \`confirm: strong\` — the platform takes a restore point before every reinstall, so declare \`normal\`; \`strong\` is only for what nothing brings back`,
342
+ );
343
+ }
344
+ }
345
+ }
346
+
347
+ return lines;
348
+ };
349
+
316
350
  const openTargetOf = (target: GuideOpenTarget) => {
317
351
  if (target.tab === GuideOpenTab.Panel) {
318
352
  return `panel tab "${target.tabId}"`;
@@ -434,6 +468,14 @@ const validateRunnablePackage = async (
434
468
  validateDriver(driver);
435
469
  validatePortActivation(manifest.container.ports, driver);
436
470
 
471
+ const confirmLines = confirmReportLines(driver);
472
+
473
+ if (confirmLines.length > 0) {
474
+ throw new Error(
475
+ `the panel asks for a strong confirmation where a restore point is taken:\n${confirmLines.join("\n")}`,
476
+ );
477
+ }
478
+
437
479
  const conditionalPorts = manifest.container.ports.filter((port) => port.activeWhen.length > 0).length;
438
480
  const pairedPorts = manifest.container.ports.filter((port) => port.protocol === PortProtocol.TcpUdp).length;
439
481
  const blockPorts = manifest.container.ports.filter((port) => port.count > 1).length;
@@ -5,4 +5,5 @@ export interface BridgePreview {
5
5
  variables?: Record<string, string>;
6
6
  confirm: BridgeConfirm;
7
7
  lines: LocalizedText[];
8
+ blocked?: boolean;
8
9
  }
@@ -249,7 +249,7 @@ export const rconAccessSections = (options: RconAccessSectionsOptions = {}): Bri
249
249
  ar: "غيّر كلمة المرور",
250
250
  en: "Rotate the password",
251
251
  },
252
- confirm: BridgeConfirm.Strong,
252
+ confirm: BridgeConfirm.Normal,
253
253
  confirmText: {
254
254
  ar: "ستتوقف كل أداة خارجية تستخدمها حتى تضيف إليها كلمة المرور الجديدة.",
255
255
  en: "Every external tool you use stops working until you paste the new password into it.",
@@ -550,6 +550,38 @@ describe("preview modules", () => {
550
550
  });
551
551
  });
552
552
 
553
+ it("passes a blocked preview through untouched", async () => {
554
+ const registry = registryFor({
555
+ ...baseDriver(),
556
+ modules: {
557
+ transition: {
558
+ kind: BridgeKind.Preview,
559
+ preview: async () => ({
560
+ confirm: BridgeConfirm.Strong,
561
+ blocked: true,
562
+ lines: [
563
+ {
564
+ ar: "لا يوجد بيلد مستقر لهذا الإصدار بعد",
565
+ en: "There is no stable build for this version yet",
566
+ },
567
+ ],
568
+ }),
569
+ },
570
+ },
571
+ });
572
+
573
+ expect((await registry.invoke("transition", BridgeAction.Preview)).result).toEqual({
574
+ confirm: BridgeConfirm.Strong,
575
+ blocked: true,
576
+ lines: [
577
+ {
578
+ ar: "لا يوجد بيلد مستقر لهذا الإصدار بعد",
579
+ en: "There is no stable build for this version yet",
580
+ },
581
+ ],
582
+ });
583
+ });
584
+
553
585
  it("refuses an action a preview module does not answer", async () => {
554
586
  const registry = registryFor({
555
587
  ...baseDriver(),