@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.
- 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 +216 -0
- package/src/bot/botPanel.ts +121 -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 +210 -2
- package/src/cli/validate.ts +71 -7
- package/src/context/bridgeContext.ts +11 -0
- package/src/core/bridgePort.test.ts +45 -0
- package/src/core/bridgePort.ts +26 -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/bridgeLifecycleSubject.ts +8 -0
- package/src/modules/bridgeModule.ts +6 -0
- package/src/modules/bridgeStopSignal.ts +13 -0
- package/src/modules/index.ts +2 -0
- package/src/panel/bridgeDetail.ts +2 -0
- package/src/protocol/bridgeConfig.ts +2 -0
- package/src/runtime/bridgeConfigReader.test.ts +80 -0
- package/src/runtime/bridgeConfigReader.ts +20 -0
- package/src/runtime/bridgeContext.test.ts +241 -0
- package/src/runtime/bridgeContext.ts +35 -9
- package/src/runtime/bridgeDriver.test.ts +132 -1
- package/src/runtime/bridgeDriver.ts +54 -1
- 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/bridgeRedaction.test.ts +410 -0
- package/src/runtime/bridgeRedaction.ts +214 -0
- package/src/runtime/bridgeRegistry.test.ts +3 -0
- package/src/runtime/bridgeRegistry.ts +15 -0
- package/src/runtime/bridgeServer.test.ts +130 -0
- package/src/runtime/bridgeServer.ts +24 -7
- package/src/runtime/bridgeSetupHandlers.test.ts +88 -0
- package/src/runtime/bridgeSetupHandlers.ts +16 -6
- package/src/runtime/bridgeSupervisor.test.ts +670 -2
- package/src/runtime/bridgeSupervisor.ts +219 -67
- package/src/runtime/serve.ts +47 -13
package/src/manifest/manifest.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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 ||
|
|
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 ${
|
|
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
|
-
|
|
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(
|
|
783
|
+
kind: z.literal(PackageKind.Game),
|
|
496
784
|
metadata: manifestMetadataSchema,
|
|
497
785
|
resources: manifestResourcesSchema,
|
|
498
|
-
container:
|
|
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
|
-
...
|
|
529
|
-
...
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
+
};
|
package/src/manifest/runtime.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import type {
|
|
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:
|
|
84
|
+
export const manifestRuntimeOf = (manifest: Pick<RunnableManifest, "container">): ManifestRuntime => {
|
|
85
85
|
return {
|
|
86
86
|
...MANIFEST_RUNTIME_DEFAULTS,
|
|
87
87
|
...manifest.container.runtime,
|
package/src/manifest/source.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { verifyAsset } from "./assets";
|
|
2
|
-
import { type
|
|
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:
|
|
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:
|
|
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:
|
|
81
|
+
const manifest: RunnableManifest = runnablePackageSchema.parse(readYaml(source, MANIFEST_FILE));
|
|
82
82
|
|
|
83
83
|
return {
|
|
84
84
|
manifest,
|
|
@@ -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
|
+
};
|