@serverkgg/bridge 0.0.21 → 0.1.1

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 (86) 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 +216 -0
  9. package/src/bot/botPanel.ts +121 -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 +210 -2
  50. package/src/cli/validate.ts +71 -7
  51. package/src/context/bridgeContext.ts +11 -0
  52. package/src/core/bridgePort.test.ts +45 -0
  53. package/src/core/bridgePort.ts +26 -0
  54. package/src/guides/compile.ts +2 -2
  55. package/src/manifest/index.ts +1 -0
  56. package/src/manifest/kind.ts +5 -0
  57. package/src/manifest/manifest.test.ts +630 -3
  58. package/src/manifest/manifest.ts +391 -87
  59. package/src/manifest/runtime.ts +2 -2
  60. package/src/manifest/source.ts +4 -4
  61. package/src/modules/bridgeLifecycleSubject.ts +8 -0
  62. package/src/modules/bridgeModule.ts +6 -0
  63. package/src/modules/bridgeStopSignal.ts +13 -0
  64. package/src/modules/index.ts +2 -0
  65. package/src/panel/bridgeDetail.ts +2 -0
  66. package/src/protocol/bridgeConfig.ts +2 -0
  67. package/src/runtime/bridgeConfigReader.test.ts +80 -0
  68. package/src/runtime/bridgeConfigReader.ts +20 -0
  69. package/src/runtime/bridgeContext.test.ts +241 -0
  70. package/src/runtime/bridgeContext.ts +35 -9
  71. package/src/runtime/bridgeDriver.test.ts +132 -1
  72. package/src/runtime/bridgeDriver.ts +54 -1
  73. package/src/runtime/bridgeFiles.test.ts +62 -0
  74. package/src/runtime/bridgeFiles.ts +12 -2
  75. package/src/runtime/bridgeLiveConfig.ts +18 -0
  76. package/src/runtime/bridgeRedaction.test.ts +410 -0
  77. package/src/runtime/bridgeRedaction.ts +214 -0
  78. package/src/runtime/bridgeRegistry.test.ts +3 -0
  79. package/src/runtime/bridgeRegistry.ts +15 -0
  80. package/src/runtime/bridgeServer.test.ts +130 -0
  81. package/src/runtime/bridgeServer.ts +24 -7
  82. package/src/runtime/bridgeSetupHandlers.test.ts +88 -0
  83. package/src/runtime/bridgeSetupHandlers.ts +16 -6
  84. package/src/runtime/bridgeSupervisor.test.ts +670 -2
  85. package/src/runtime/bridgeSupervisor.ts +219 -67
  86. package/src/runtime/serve.ts +47 -13
@@ -1,10 +1,11 @@
1
1
  import { z } from "zod";
2
- import { PortJoin, PortProtocol } from "../core";
2
+ import { PORT_BLOCK_MAX_COUNT, PortJoin, PortProtocol, socketProtocolsOf } from "../core";
3
3
  import { BridgeIcon } from "../panel/bridgeIcon";
4
4
  import { BRIDGE_OPTION_VALUE_LENGTH_LIMIT, BRIDGE_VISIBLE_VALUES_LIMIT } from "../protocol/bridgeLimits";
5
5
  import { GameCategory } from "./category";
6
6
  import { type GuideEntry, type GuideTag, guideEntrySchema, guideTagSchema } from "./guide";
7
7
  import { IMAGE_NAMESPACE } from "./image";
8
+ import { PackageKind } from "./kind";
8
9
  import { localizedText } from "./localized";
9
10
  import { type ManifestPresence, manifestPresenceSchema } from "./presence";
10
11
  import {
@@ -30,11 +31,7 @@ const manifestHostRangeSchema = z
30
31
  max: zHostPort,
31
32
  })
32
33
  .refine((range) => range.min <= range.max, "min must not be greater than max")
33
- .refine((range) => range.min <= range.default && range.default <= range.max, "default must fall inside min..max")
34
- .refine(
35
- (range) => range.max - range.min + 1 <= HOST_RANGE_MAX_SIZE,
36
- `must span at most ${HOST_RANGE_MAX_SIZE} ports`,
37
- );
34
+ .refine((range) => range.min <= range.default && range.default <= range.max, "default must fall inside min..max");
38
35
 
39
36
  export type ManifestHostRange = z.infer<typeof manifestHostRangeSchema>;
40
37
 
@@ -100,39 +97,55 @@ const manifestPortSchema = z.object({
100
97
  description:
101
98
  "Allocates this port at the named port's host port plus the offset, for games that derive one port from another. The named port must use the same protocol.",
102
99
  }),
100
+ count: z.int().min(1).max(PORT_BLOCK_MAX_COUNT).default(1).meta({
101
+ description:
102
+ "Reserves this many contiguous host ports, bound one to one from containerPort upward. A block is infrastructure, never a player address: it takes no name, hint, join, srv or follows.",
103
+ }),
103
104
  });
104
105
 
105
106
  export type ManifestPort = z.infer<typeof manifestPortSchema>;
106
107
 
107
- const manifestContainerSchema = z
108
- .object({
109
- image: zImageReference.meta({
110
- description: `Image repository, always "${IMAGE_NAMESPACE}/" followed by metadata.id.`,
111
- }),
112
- ports: z.array(manifestPortSchema).min(1).max(8),
113
- runtime: manifestRuntimeSchema.prefault({}).meta({
114
- description: "How the container is built and how long the platform waits for it.",
115
- }),
116
- })
117
- .check((ctx) => {
118
- if (ctx.issues.length > 0) {
119
- return;
120
- }
108
+ const MANIFEST_PORTS_LIMIT = 8;
121
109
 
122
- const { runtime } = ctx.value;
110
+ const containerSchemaWith = <Ports extends z.ZodType<ManifestPort[]>>(ports: Ports) => {
111
+ return z
112
+ .object({
113
+ image: zImageReference.meta({
114
+ description: `Image repository, always "${IMAGE_NAMESPACE}/" followed by metadata.id.`,
115
+ }),
116
+ ports,
117
+ runtime: manifestRuntimeSchema.prefault({}).meta({
118
+ description: "How the container is built and how long the platform waits for it.",
119
+ }),
120
+ })
121
+ .check((ctx) => {
122
+ if (ctx.issues.length > 0) {
123
+ return;
124
+ }
123
125
 
124
- if (runtime.platform === ManifestPlatform.Wine && runtime.shmMb < WINE_MIN_SHM_MB) {
125
- ctx.issues.push({
126
- code: "custom",
127
- message: `must be at least ${WINE_MIN_SHM_MB} when platform is "${ManifestPlatform.Wine}"`,
128
- input: runtime.shmMb,
129
- path: [
130
- "runtime",
131
- "shmMb",
132
- ],
133
- });
134
- }
135
- });
126
+ const { runtime } = ctx.value;
127
+
128
+ if (runtime.platform === ManifestPlatform.Wine && runtime.shmMb < WINE_MIN_SHM_MB) {
129
+ ctx.issues.push({
130
+ code: "custom",
131
+ message: `must be at least ${WINE_MIN_SHM_MB} when platform is "${ManifestPlatform.Wine}"`,
132
+ input: runtime.shmMb,
133
+ path: [
134
+ "runtime",
135
+ "shmMb",
136
+ ],
137
+ });
138
+ }
139
+ });
140
+ };
141
+
142
+ const gameContainerSchema = containerSchemaWith(z.array(manifestPortSchema).min(1).max(MANIFEST_PORTS_LIMIT));
143
+
144
+ const botContainerSchema = containerSchemaWith(
145
+ z.array(manifestPortSchema).max(MANIFEST_PORTS_LIMIT).default([]).meta({
146
+ description: "A bot listens on nothing. Declare a port only when a webhook needs a public address.",
147
+ }),
148
+ );
136
149
 
137
150
  const manifestMetadataSchema = z.object({
138
151
  id: zSlug.meta({
@@ -146,6 +159,10 @@ const manifestMetadataSchema = z.object({
146
159
  banner: zAssetPath,
147
160
  });
148
161
 
162
+ const botMetadataSchema = manifestMetadataSchema.omit({
163
+ category: true,
164
+ });
165
+
149
166
  const manifestResourcesSchema = z
150
167
  .object({
151
168
  minMemoryMb: z.int().min(128).max(1_048_576),
@@ -168,6 +185,13 @@ const manifestBackupSchema = z.object({
168
185
  }),
169
186
  });
170
187
 
188
+ const manifestEnvironmentSchema = z.object({
189
+ vault: z.boolean().default(false).meta({
190
+ description:
191
+ "Whether the customer keeps their own environment variables for this package. The platform stores them, hides sensitive values after saving, and delivers them to the process.",
192
+ }),
193
+ });
194
+
171
195
  const manifestResetSchema = z.object({
172
196
  keep: z.array(zGlob).max(32).default([]).meta({
173
197
  description: "Paths that survive a server reset. Everything else is deleted.",
@@ -180,6 +204,8 @@ const manifestSecretSchema = z.object({
180
204
  required: z.boolean().default(true),
181
205
  });
182
206
 
207
+ type ManifestSecret = z.infer<typeof manifestSecretSchema>;
208
+
183
209
  const manifestFileNoteSchema = z.object({
184
210
  match: zGlob,
185
211
  label: localizedText(48),
@@ -202,6 +228,8 @@ const manifestFilesSchema = z.object({
202
228
  }),
203
229
  });
204
230
 
231
+ type ManifestFiles = z.infer<typeof manifestFilesSchema>;
232
+
205
233
  type CheckIssue = {
206
234
  code: "custom";
207
235
  message: string;
@@ -329,6 +357,28 @@ const followIssues = (ports: ManifestPort[]): CheckIssue[] => {
329
357
  return;
330
358
  }
331
359
 
360
+ if (port.protocol === PortProtocol.TcpUdp) {
361
+ issues.push({
362
+ code: "custom",
363
+ message: `port "${port.key}" carries ${PortProtocol.TcpUdp} and cannot follow another port`,
364
+ input: follows.key,
365
+ path,
366
+ });
367
+
368
+ return;
369
+ }
370
+
371
+ if (leader.count > 1) {
372
+ issues.push({
373
+ code: "custom",
374
+ message: `"${follows.key}" is a block of ${leader.count} ports — a port only follows a single port`,
375
+ input: follows.key,
376
+ path,
377
+ });
378
+
379
+ return;
380
+ }
381
+
332
382
  if (leader.protocol !== port.protocol) {
333
383
  issues.push({
334
384
  code: "custom",
@@ -353,6 +403,161 @@ const followIssues = (ports: ManifestPort[]): CheckIssue[] => {
353
403
  return issues;
354
404
  };
355
405
 
406
+ const sharedSocketProtocols = (a: ManifestPort, b: ManifestPort) => {
407
+ const other = socketProtocolsOf(b.protocol);
408
+
409
+ return socketProtocolsOf(a.protocol).filter((protocol) => other.includes(protocol));
410
+ };
411
+
412
+ const BLOCK_REFUSED_FIELDS = [
413
+ "name",
414
+ "hint",
415
+ "srv",
416
+ "follows",
417
+ ] as const;
418
+
419
+ const blockIssues = (port: ManifestPort, index: number): CheckIssue[] => {
420
+ const issues: CheckIssue[] = [];
421
+ const path = [
422
+ "container",
423
+ "ports",
424
+ index,
425
+ ];
426
+
427
+ for (const field of BLOCK_REFUSED_FIELDS) {
428
+ if (port[field] === undefined) {
429
+ continue;
430
+ }
431
+
432
+ issues.push({
433
+ code: "custom",
434
+ message: `a block of ${port.count} ports is never a player address and takes no ${field}`,
435
+ input: port[field],
436
+ path: [
437
+ ...path,
438
+ field,
439
+ ],
440
+ });
441
+ }
442
+
443
+ if (port.join !== PortJoin.Combined) {
444
+ issues.push({
445
+ code: "custom",
446
+ message: `a block of ${port.count} ports is never a player address and takes no join`,
447
+ input: port.join,
448
+ path: [
449
+ ...path,
450
+ "join",
451
+ ],
452
+ });
453
+ }
454
+
455
+ if (port.hostRange.max - port.hostRange.min + 1 < port.count) {
456
+ issues.push({
457
+ code: "custom",
458
+ message: `must span at least ${port.count} ports to hold the block`,
459
+ input: port.hostRange,
460
+ path: [
461
+ ...path,
462
+ "hostRange",
463
+ ],
464
+ });
465
+ } else if (port.hostRange.default + port.count - 1 > port.hostRange.max) {
466
+ issues.push({
467
+ code: "custom",
468
+ message: `default must leave room for all ${port.count} ports before max`,
469
+ input: port.hostRange,
470
+ path: [
471
+ ...path,
472
+ "hostRange",
473
+ "default",
474
+ ],
475
+ });
476
+ }
477
+
478
+ if (!port.mirror && port.containerPort + port.count - 1 > 65_535) {
479
+ issues.push({
480
+ code: "custom",
481
+ message: `a block of ${port.count} ports starting here runs past 65535`,
482
+ input: port.containerPort,
483
+ path: [
484
+ ...path,
485
+ "containerPort",
486
+ ],
487
+ });
488
+ }
489
+
490
+ return issues;
491
+ };
492
+
493
+ const spanIssues = (port: ManifestPort, index: number): CheckIssue[] => {
494
+ const slots = Math.ceil((port.hostRange.max - port.hostRange.min + 1) / port.count);
495
+
496
+ if (slots <= HOST_RANGE_MAX_SIZE) {
497
+ return [];
498
+ }
499
+
500
+ return [
501
+ {
502
+ code: "custom",
503
+ message:
504
+ port.count > 1
505
+ ? `must span at most ${HOST_RANGE_MAX_SIZE} slots of ${port.count} ports`
506
+ : `must span at most ${HOST_RANGE_MAX_SIZE} ports`,
507
+ input: port.hostRange,
508
+ path: [
509
+ "container",
510
+ "ports",
511
+ index,
512
+ "hostRange",
513
+ ],
514
+ },
515
+ ];
516
+ };
517
+
518
+ const portShapeIssues = (port: ManifestPort, index: number): CheckIssue[] => {
519
+ const issues = [
520
+ ...spanIssues(port, index),
521
+ ...(port.count > 1 ? blockIssues(port, index) : []),
522
+ ];
523
+
524
+ if (port.protocol === PortProtocol.TcpUdp && port.srv !== undefined) {
525
+ issues.push({
526
+ code: "custom",
527
+ message: `a ${PortProtocol.TcpUdp} port takes no srv — an SRV record names one protocol`,
528
+ input: port.srv,
529
+ path: [
530
+ "container",
531
+ "ports",
532
+ index,
533
+ "srv",
534
+ ],
535
+ });
536
+ }
537
+
538
+ return issues;
539
+ };
540
+
541
+ const socketBindingIssues = (ports: ManifestPort[]): CheckIssue[] => {
542
+ const bindings = ports.reduce((total, port) => total + socketProtocolsOf(port.protocol).length, 0);
543
+
544
+ if (bindings <= MANIFEST_PORTS_LIMIT) {
545
+ return [];
546
+ }
547
+
548
+ return [
549
+ {
550
+ code: "custom",
551
+ message: `expands to ${bindings} socket ports — a ${PortProtocol.TcpUdp} port counts twice and the total must stay at most ${MANIFEST_PORTS_LIMIT}`,
552
+ input: bindings,
553
+ path: [
554
+ "container",
555
+ "ports",
556
+ ],
557
+ },
558
+ ];
559
+ };
560
+
356
561
  const portIssues = (ports: ManifestPort[]): CheckIssue[] => {
357
562
  const issues = duplicateIssues(
358
563
  ports,
@@ -371,13 +576,19 @@ const portIssues = (ports: ManifestPort[]): CheckIssue[] => {
371
576
  const a = ports[index];
372
577
  const b = ports[other];
373
578
 
374
- if (!a || !b || a.protocol !== b.protocol || !hostRangesOverlap(a, b) || sameFollowGroup(a, b)) {
579
+ if (!a || !b || !hostRangesOverlap(a, b) || sameFollowGroup(a, b)) {
580
+ continue;
581
+ }
582
+
583
+ const shared = sharedSocketProtocols(a, b);
584
+
585
+ if (shared.length === 0) {
375
586
  continue;
376
587
  }
377
588
 
378
589
  issues.push({
379
590
  code: "custom",
380
- message: `host range overlaps port "${a.key}" — two ${a.protocol} ports cannot share host ports`,
591
+ message: `host range overlaps port "${a.key}" — two ${shared.join(" and ")} ports cannot share host ports`,
381
592
  input: b.hostRange,
382
593
  path: [
383
594
  "container",
@@ -391,6 +602,8 @@ const portIssues = (ports: ManifestPort[]): CheckIssue[] => {
391
602
 
392
603
  return [
393
604
  ...issues,
605
+ ...ports.flatMap(portShapeIssues),
606
+ ...socketBindingIssues(ports),
394
607
  ...followIssues(ports),
395
608
  ];
396
609
  };
@@ -489,13 +702,88 @@ const guideIssues = (guideTags: GuideTag[], guides: GuideEntry[]): CheckIssue[]
489
702
  return issues;
490
703
  };
491
704
 
492
- export const manifestSchema = z
705
+ interface PackageShape {
706
+ metadata: {
707
+ id: string;
708
+ };
709
+ container: {
710
+ image: string;
711
+ ports: ManifestPort[];
712
+ };
713
+ files: ManifestFiles;
714
+ guideTags: GuideTag[];
715
+ guides: GuideEntry[];
716
+ }
717
+
718
+ const imageIssues = (manifest: PackageShape): CheckIssue[] => {
719
+ const expectedImage = `${IMAGE_NAMESPACE}/${manifest.metadata.id}`;
720
+
721
+ if (manifest.container.image === expectedImage) {
722
+ return [];
723
+ }
724
+
725
+ return [
726
+ {
727
+ code: "custom",
728
+ message: `must be "${expectedImage}" to match metadata.id`,
729
+ input: manifest.container.image,
730
+ path: [
731
+ "container",
732
+ "image",
733
+ ],
734
+ },
735
+ ];
736
+ };
737
+
738
+ const packageIssues = (manifest: PackageShape): CheckIssue[] => {
739
+ return [
740
+ ...imageIssues(manifest),
741
+ ...portIssues(manifest.container.ports),
742
+ ...duplicateIssues(
743
+ manifest.files.protected,
744
+ (path) => path,
745
+ (index) => [
746
+ "files",
747
+ "protected",
748
+ index,
749
+ ],
750
+ (key) => `"${key}" is protected twice`,
751
+ ),
752
+ ...duplicateIssues(
753
+ manifest.files.notable,
754
+ (note) => note.match,
755
+ (index) => [
756
+ "files",
757
+ "notable",
758
+ index,
759
+ "match",
760
+ ],
761
+ (key) => `"${key}" already has a note`,
762
+ ),
763
+ ...guideIssues(manifest.guideTags, manifest.guides),
764
+ ];
765
+ };
766
+
767
+ const secretIssues = (secrets: ManifestSecret[]): CheckIssue[] => {
768
+ return duplicateIssues(
769
+ secrets,
770
+ (secret) => secret.key,
771
+ (index) => [
772
+ "secrets",
773
+ index,
774
+ "key",
775
+ ],
776
+ (key) => `secret "${key}" is declared twice`,
777
+ );
778
+ };
779
+
780
+ export const gameManifestSchema = z
493
781
  .object({
494
782
  schemaVersion: z.literal(MANIFEST_SCHEMA_VERSION),
495
- kind: z.literal("GamePackage"),
783
+ kind: z.literal(PackageKind.Game),
496
784
  metadata: manifestMetadataSchema,
497
785
  resources: manifestResourcesSchema,
498
- container: manifestContainerSchema,
786
+ container: gameContainerSchema,
499
787
  guideTags: z.array(guideTagSchema).max(16).default([]),
500
788
  guides: z.array(guideEntrySchema).max(64).default([]),
501
789
  backup: manifestBackupSchema.prefault({}),
@@ -510,64 +798,71 @@ export const manifestSchema = z
510
798
  }
511
799
 
512
800
  const manifest = ctx.value;
513
- const expectedImage = `${IMAGE_NAMESPACE}/${manifest.metadata.id}`;
514
-
515
- if (manifest.container.image !== expectedImage) {
516
- ctx.issues.push({
517
- code: "custom",
518
- message: `must be "${expectedImage}" to match metadata.id`,
519
- input: manifest.container.image,
520
- path: [
521
- "container",
522
- "image",
523
- ],
524
- });
525
- }
526
801
 
527
802
  ctx.issues.push(
528
- ...portIssues(manifest.container.ports),
529
- ...duplicateIssues(
530
- manifest.secrets,
531
- (secret) => secret.key,
532
- (index) => [
533
- "secrets",
534
- index,
535
- "key",
536
- ],
537
- (key) => `secret "${key}" is declared twice`,
538
- ),
539
- ...duplicateIssues(
540
- manifest.files.protected,
541
- (path) => path,
542
- (index) => [
543
- "files",
544
- "protected",
545
- index,
546
- ],
547
- (key) => `"${key}" is protected twice`,
548
- ),
549
- ...duplicateIssues(
550
- manifest.files.notable,
551
- (note) => note.match,
552
- (index) => [
553
- "files",
554
- "notable",
555
- index,
556
- "match",
557
- ],
558
- (key) => `"${key}" already has a note`,
559
- ),
560
- ...guideIssues(manifest.guideTags, manifest.guides),
803
+ ...packageIssues(manifest),
804
+ ...secretIssues(manifest.secrets),
561
805
  ...presenceIssues(manifest.presence),
562
806
  );
563
807
  });
564
808
 
565
- export type Manifest = z.infer<typeof manifestSchema>;
809
+ export type GameManifest = z.infer<typeof gameManifestSchema>;
810
+
811
+ export const manifestSchema = gameManifestSchema;
812
+
813
+ export type Manifest = GameManifest;
814
+
815
+ export const botManifestSchema = z
816
+ .object({
817
+ schemaVersion: z.literal(MANIFEST_SCHEMA_VERSION),
818
+ kind: z.literal(PackageKind.Bot),
819
+ metadata: botMetadataSchema,
820
+ resources: manifestResourcesSchema,
821
+ container: botContainerSchema,
822
+ guideTags: z.array(guideTagSchema).max(16).default([]),
823
+ guides: z.array(guideEntrySchema).max(64).default([]),
824
+ backup: manifestBackupSchema.prefault({}),
825
+ reset: manifestResetSchema.prefault({}),
826
+ files: manifestFilesSchema.prefault({}),
827
+ environment: manifestEnvironmentSchema.prefault({}),
828
+ secrets: z
829
+ .never({
830
+ error: "a bot package declares no platform secrets; per-bot values live in the environment vault",
831
+ })
832
+ .optional()
833
+ .meta({
834
+ description: "Not declared by a bot package. Per-bot values live in the environment vault.",
835
+ }),
836
+ presence: z
837
+ .never({
838
+ error: "a bot package has no player presence",
839
+ })
840
+ .optional()
841
+ .meta({
842
+ description: "Not declared by a bot package. A bot has no player presence.",
843
+ }),
844
+ })
845
+ .check((ctx) => {
846
+ if (ctx.issues.length > 0) {
847
+ return;
848
+ }
849
+
850
+ ctx.issues.push(...packageIssues(ctx.value));
851
+ });
852
+
853
+ export type BotManifest = z.infer<typeof botManifestSchema>;
854
+
855
+ export const runnablePackageSchema = z.discriminatedUnion("kind", [
856
+ gameManifestSchema,
857
+ botManifestSchema,
858
+ ]);
859
+
860
+ export type RunnableManifest = z.infer<typeof runnablePackageSchema>;
566
861
 
567
862
  export const guidePackageSchema = z
568
863
  .object({
569
864
  schemaVersion: z.literal(MANIFEST_SCHEMA_VERSION),
570
- kind: z.literal("GuidePackage"),
865
+ kind: z.literal(PackageKind.Guide),
571
866
  metadata: z.object({
572
867
  id: zSlug,
573
868
  }),
@@ -585,8 +880,17 @@ export const guidePackageSchema = z
585
880
  export type GuidePackage = z.infer<typeof guidePackageSchema>;
586
881
 
587
882
  export const packageSchema = z.discriminatedUnion("kind", [
588
- manifestSchema,
883
+ gameManifestSchema,
884
+ botManifestSchema,
589
885
  guidePackageSchema,
590
886
  ]);
591
887
 
592
888
  export type Package = z.infer<typeof packageSchema>;
889
+
890
+ export const isGameManifest = (document: Package): document is GameManifest => {
891
+ return document.kind === PackageKind.Game;
892
+ };
893
+
894
+ export const isBotManifest = (document: Package): document is BotManifest => {
895
+ return document.kind === PackageKind.Bot;
896
+ };
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import type { Manifest } from "./manifest";
2
+ import type { RunnableManifest } from "./manifest";
3
3
 
4
4
  export enum ManifestPlatform {
5
5
  Linux = "linux",
@@ -81,7 +81,7 @@ export type ManifestRuntime = z.infer<typeof manifestRuntimeSchema>;
81
81
 
82
82
  export const MANIFEST_RUNTIME_DEFAULTS: ManifestRuntime = manifestRuntimeSchema.parse({});
83
83
 
84
- export const manifestRuntimeOf = (manifest: Manifest): ManifestRuntime => {
84
+ export const manifestRuntimeOf = (manifest: Pick<RunnableManifest, "container">): ManifestRuntime => {
85
85
  return {
86
86
  ...MANIFEST_RUNTIME_DEFAULTS,
87
87
  ...manifest.container.runtime,
@@ -1,5 +1,5 @@
1
1
  import { verifyAsset } from "./assets";
2
- import { type Manifest, manifestSchema, type Package, packageSchema } from "./manifest";
2
+ import { type Package, packageSchema, type RunnableManifest, runnablePackageSchema } from "./manifest";
3
3
 
4
4
  export const MANIFEST_FILE = "serverk.yml";
5
5
 
@@ -39,14 +39,14 @@ const readAsset = (source: PackageSource, asset: string) => {
39
39
  };
40
40
  };
41
41
 
42
- export const readAssets = (source: PackageSource, manifest: Manifest) => {
42
+ export const readAssets = (source: PackageSource, manifest: RunnableManifest) => {
43
43
  return {
44
44
  logo: readAsset(source, manifest.metadata.logo),
45
45
  banner: readAsset(source, manifest.metadata.banner),
46
46
  };
47
47
  };
48
48
 
49
- export const declaredFiles = (manifest: Manifest, guideFiles: string[] = []) => {
49
+ export const declaredFiles = (manifest: RunnableManifest, guideFiles: string[] = []) => {
50
50
  return [
51
51
  MANIFEST_FILE,
52
52
  manifest.metadata.logo,
@@ -78,7 +78,7 @@ export const computeBundleDigest = (source: PackageSource, paths: string[]) => {
78
78
  };
79
79
 
80
80
  export const readPackage = (source: PackageSource) => {
81
- const manifest: Manifest = manifestSchema.parse(readYaml(source, MANIFEST_FILE));
81
+ const manifest: RunnableManifest = runnablePackageSchema.parse(readYaml(source, MANIFEST_FILE));
82
82
 
83
83
  return {
84
84
  manifest,
@@ -0,0 +1,8 @@
1
+ export enum BridgeLifecycleSubject {
2
+ Game = "game",
3
+ Bot = "bot",
4
+ }
5
+
6
+ export const isBridgeLifecycleSubject = (value: unknown): value is BridgeLifecycleSubject => {
7
+ return Object.values(BridgeLifecycleSubject).some((subject) => subject === value);
8
+ };
@@ -11,6 +11,8 @@ import type { BridgeBackupSnapshot, BridgeDescribeResult, BridgePendingFile } fr
11
11
  import type { BridgePreview } from "../protocol/bridgePreview";
12
12
  import type { BridgeSetupStep } from "../protocol/bridgeSetup";
13
13
  import type { BridgeTerminalCommand, BridgeTerminalRule } from "../terminal";
14
+ import type { BridgeLifecycleSubject } from "./bridgeLifecycleSubject";
15
+ import type { BridgeStopSignal } from "./bridgeStopSignal";
14
16
 
15
17
  export type BridgeDescription = Omit<BridgeDescribeResult, "supported">;
16
18
 
@@ -25,7 +27,11 @@ export interface BridgeLifecycleModule {
25
27
  ready?: RegExp;
26
28
  failed?: RegExp;
27
29
  stopTimeoutSeconds?: number;
30
+ readyTimeoutSeconds?: number;
31
+ stopSignal?: BridgeStopSignal;
32
+ subject?: BridgeLifecycleSubject;
28
33
  command(context: BridgeContext): Promise<string[]>;
34
+ environment?(context: BridgeContext): Promise<Record<string, string>>;
29
35
  stop?(context: BridgeContext): Promise<void>;
30
36
  onReady?(context: BridgeContext): Promise<void>;
31
37
  }
@@ -0,0 +1,13 @@
1
+ export enum BridgeStopSignal {
2
+ Term = "SIGTERM",
3
+ Interrupt = "SIGINT",
4
+ }
5
+
6
+ export const BRIDGE_STOP_SIGNALS: Record<BridgeStopSignal, NodeJS.Signals> = {
7
+ [BridgeStopSignal.Term]: "SIGTERM",
8
+ [BridgeStopSignal.Interrupt]: "SIGINT",
9
+ };
10
+
11
+ export const isBridgeStopSignal = (value: unknown): value is BridgeStopSignal => {
12
+ return typeof value === "string" && Object.hasOwn(BRIDGE_STOP_SIGNALS, value);
13
+ };
@@ -1,3 +1,5 @@
1
1
  export * from "./bridge";
2
2
  export * from "./bridgeDriver";
3
+ export * from "./bridgeLifecycleSubject";
3
4
  export * from "./bridgeModule";
5
+ export * from "./bridgeStopSignal";