@serverkgg/bridge 0.0.1 → 0.0.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 +10 -3
- package/src/cli/main.ts +11 -1
- package/src/cli/schema.ts +62 -0
- package/src/cli/validate.ts +119 -36
- package/src/context/bridgeContext.ts +11 -1
- package/src/core/bridgeAction.ts +13 -0
- package/src/core/bridgeFailure.ts +16 -0
- package/src/core/bridgeKind.ts +2 -0
- package/src/core/bridgeNetError.ts +10 -0
- package/src/core/index.ts +2 -0
- package/src/guides/blocks.ts +167 -0
- package/src/guides/compile.ts +193 -0
- package/src/guides/compiled.ts +28 -0
- package/src/guides/index.ts +6 -0
- package/src/guides/markdown.test.ts +374 -0
- package/src/guides/markdown.ts +769 -0
- package/src/guides/variables.ts +52 -0
- package/src/guides/version.ts +1 -0
- package/src/main.ts +2 -1
- package/src/manifest/assets.ts +4 -4
- package/src/manifest/guide.ts +23 -18
- package/src/manifest/index.ts +3 -2
- package/src/manifest/jsonSchema.ts +16 -0
- package/src/manifest/localized.ts +7 -0
- package/src/manifest/manifest.test.ts +366 -0
- package/src/manifest/manifest.ts +354 -120
- package/src/manifest/primitives.ts +63 -0
- package/src/manifest/source.ts +5 -4
- package/src/modules/bridge.ts +19 -3
- package/src/modules/bridgeDriver.ts +4 -0
- package/src/modules/bridgeModule.ts +22 -24
- package/src/panel/bridgeAccess.ts +3 -0
- package/src/panel/bridgePanel.ts +6 -0
- package/src/panel/index.ts +1 -0
- package/src/protocol/bridgeCatalog.ts +56 -0
- package/src/protocol/bridgeEgress.ts +53 -0
- package/src/protocol/bridgeFrame.ts +17 -1
- package/src/protocol/bridgeLine.ts +41 -6
- package/src/protocol/bridgeManifest.ts +15 -1
- package/src/protocol/bridgeVersion.ts +1 -1
- package/src/protocol/index.ts +3 -0
- package/src/runtime/bridgeContext.ts +29 -9
- package/src/runtime/bridgeDriver.ts +1 -0
- package/src/runtime/bridgeEgress.ts +360 -0
- package/src/runtime/bridgeExec.ts +15 -5
- package/src/runtime/bridgeFiles.ts +58 -21
- package/src/runtime/bridgeLog.ts +26 -3
- package/src/runtime/bridgeNet.ts +75 -22
- package/src/runtime/bridgeRegistry.ts +51 -6
- package/src/runtime/bridgeServer.ts +48 -1
- package/src/runtime/bridgeSupervisor.ts +6 -6
- package/src/runtime/index.ts +0 -17
- package/src/runtime/serve.ts +44 -3
- package/src/terminal/bridgeTerminalArg.ts +11 -0
- package/src/terminal/bridgeTerminalCommand.ts +11 -0
- package/src/terminal/bridgeTerminalLevel.ts +6 -0
- package/src/terminal/bridgeTerminalRule.ts +8 -0
- package/src/terminal/index.ts +4 -0
- package/src/manifest/checks.ts +0 -50
package/src/manifest/manifest.ts
CHANGED
|
@@ -1,170 +1,404 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { PortProtocol } from "../core";
|
|
3
|
-
import {
|
|
3
|
+
import { BridgeIcon } from "../panel/bridgeIcon";
|
|
4
4
|
import { GameCategory } from "./category";
|
|
5
|
-
import { guideEntrySchema } from "./guide";
|
|
5
|
+
import { type GuideEntry, type GuideTag, guideEntrySchema, guideTagSchema } from "./guide";
|
|
6
6
|
import { IMAGE_NAMESPACE } from "./image";
|
|
7
|
-
import {
|
|
7
|
+
import { localizedText } from "./localized";
|
|
8
|
+
import {
|
|
9
|
+
HOST_RANGE_MAX_SIZE,
|
|
10
|
+
zAssetPath,
|
|
11
|
+
zEnvKey,
|
|
12
|
+
zGlob,
|
|
13
|
+
zHexColor,
|
|
14
|
+
zHostPort,
|
|
15
|
+
zImageReference,
|
|
16
|
+
zPort,
|
|
17
|
+
zSlug,
|
|
18
|
+
} from "./primitives";
|
|
8
19
|
|
|
9
|
-
export const
|
|
20
|
+
export const MANIFEST_SCHEMA_VERSION = "serverk.gg/v1";
|
|
10
21
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const zRelativePath = z
|
|
14
|
-
.string()
|
|
15
|
-
.min(1)
|
|
16
|
-
.max(256)
|
|
17
|
-
.regex(/^[a-zA-Z0-9][a-zA-Z0-9/_-]*\.[a-z0-9]+$/);
|
|
18
|
-
|
|
19
|
-
const zAssetPath = zRelativePath.refine((value) => {
|
|
20
|
-
return value.endsWith(`.${ASSET_EXTENSION}`);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
export const MANIFEST_HOST_PORT_MIN = 1024;
|
|
24
|
-
|
|
25
|
-
export const MANIFEST_HOST_PORT_SPAN = 1000;
|
|
26
|
-
|
|
27
|
-
export const manifestPortHostSchema = z
|
|
22
|
+
const manifestHostRangeSchema = z
|
|
28
23
|
.object({
|
|
29
|
-
default:
|
|
30
|
-
min:
|
|
31
|
-
max:
|
|
24
|
+
default: zHostPort,
|
|
25
|
+
min: zHostPort,
|
|
26
|
+
max: zHostPort,
|
|
32
27
|
})
|
|
33
|
-
.refine((
|
|
34
|
-
.refine((
|
|
35
|
-
.refine(
|
|
28
|
+
.refine((range) => range.min <= range.max, "min must not be greater than max")
|
|
29
|
+
.refine((range) => range.min <= range.default && range.default <= range.max, "default must fall inside min..max")
|
|
30
|
+
.refine(
|
|
31
|
+
(range) => range.max - range.min + 1 <= HOST_RANGE_MAX_SIZE,
|
|
32
|
+
`must span at most ${HOST_RANGE_MAX_SIZE} ports`,
|
|
33
|
+
);
|
|
36
34
|
|
|
37
|
-
export type
|
|
35
|
+
export type ManifestHostRange = z.infer<typeof manifestHostRangeSchema>;
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
const manifestPortSchema = z.object({
|
|
40
38
|
key: z
|
|
41
39
|
.string()
|
|
42
40
|
.min(1)
|
|
43
41
|
.max(32)
|
|
44
|
-
.regex(/^[a-z][a-z0-9-]
|
|
45
|
-
|
|
42
|
+
.regex(/^[a-z][a-z0-9-]*$/, "must be lowercase letters, digits and dashes, starting with a letter"),
|
|
43
|
+
containerPort: zPort,
|
|
46
44
|
protocol: z.enum(PortProtocol).default(PortProtocol.Tcp),
|
|
47
|
-
|
|
45
|
+
hostRange: manifestHostRangeSchema,
|
|
48
46
|
});
|
|
49
47
|
|
|
50
48
|
export type ManifestPort = z.infer<typeof manifestPortSchema>;
|
|
51
49
|
|
|
52
|
-
const
|
|
53
|
-
.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.regex(new RegExp(`^${IMAGE_NAMESPACE}/[a-z0-9][a-z0-9-]*$`));
|
|
57
|
-
|
|
58
|
-
export const manifestRuntimeSchema = z.object({
|
|
59
|
-
image: zImageReference,
|
|
50
|
+
const manifestContainerSchema = z.object({
|
|
51
|
+
image: zImageReference.meta({
|
|
52
|
+
description: `Image repository, always "${IMAGE_NAMESPACE}/" followed by metadata.id.`,
|
|
53
|
+
}),
|
|
60
54
|
ports: z.array(manifestPortSchema).min(1).max(8),
|
|
61
55
|
});
|
|
62
56
|
|
|
63
|
-
|
|
64
|
-
id:
|
|
65
|
-
.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
description: localizedTextSchema,
|
|
71
|
-
tagline: localizedTextSchema,
|
|
57
|
+
const manifestMetadataSchema = z.object({
|
|
58
|
+
id: zSlug.meta({
|
|
59
|
+
description: "Slug for the game. Also the catalogue URL segment and the image name.",
|
|
60
|
+
}),
|
|
61
|
+
name: localizedText(64),
|
|
62
|
+
description: localizedText(600),
|
|
63
|
+
tagline: localizedText(120),
|
|
72
64
|
category: z.enum(GameCategory),
|
|
73
65
|
logo: zAssetPath,
|
|
74
66
|
banner: zAssetPath,
|
|
75
67
|
});
|
|
76
68
|
|
|
77
|
-
|
|
69
|
+
const manifestResourcesSchema = z
|
|
78
70
|
.object({
|
|
79
71
|
minMemoryMb: z.int().min(128).max(1_048_576),
|
|
80
72
|
recommendedMemoryMb: z.int().min(128).max(1_048_576),
|
|
81
73
|
minDiskMb: z.int().min(512),
|
|
82
|
-
|
|
83
|
-
|
|
74
|
+
playersPerMemoryGb: z.int().min(1).max(1024).optional(),
|
|
75
|
+
maxRecommendedPlayers: z.int().min(1).max(100_000).optional(),
|
|
84
76
|
})
|
|
85
|
-
.refine(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.min(1)
|
|
90
|
-
.max(256)
|
|
91
|
-
.regex(/^[^/\\][^\\]*$/)
|
|
92
|
-
.refine((value) => !value.split("/").includes(".."));
|
|
93
|
-
|
|
94
|
-
export const manifestQuiesceSchema = z.object({
|
|
95
|
-
before: z.string().max(256).optional(),
|
|
96
|
-
flush: z.string().max(256).optional(),
|
|
97
|
-
after: z.string().max(256).optional(),
|
|
98
|
-
await: z.string().max(256).optional(),
|
|
99
|
-
settleSeconds: z.int().min(0).max(120).default(BACKUP_SETTLE_SECONDS_DEFAULT),
|
|
100
|
-
});
|
|
77
|
+
.refine(
|
|
78
|
+
(resources) => resources.recommendedMemoryMb >= resources.minMemoryMb,
|
|
79
|
+
"recommendedMemoryMb must not be below minMemoryMb",
|
|
80
|
+
);
|
|
101
81
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
82
|
+
const manifestBackupSchema = z.object({
|
|
83
|
+
only: z.array(zGlob).max(32).default([]).meta({
|
|
84
|
+
description: "When set, only these paths are archived. Empty means the whole game directory.",
|
|
85
|
+
}),
|
|
86
|
+
except: z.array(zGlob).max(64).default([]).meta({
|
|
87
|
+
description: "Paths excluded from the archive, applied after only.",
|
|
88
|
+
}),
|
|
106
89
|
});
|
|
107
90
|
|
|
108
|
-
|
|
109
|
-
keep: z.array(
|
|
91
|
+
const manifestResetSchema = z.object({
|
|
92
|
+
keep: z.array(zGlob).max(32).default([]).meta({
|
|
93
|
+
description: "Paths that survive a server reset. Everything else is deleted.",
|
|
94
|
+
}),
|
|
110
95
|
});
|
|
111
96
|
|
|
112
|
-
const
|
|
113
|
-
.string()
|
|
114
|
-
.min(2)
|
|
115
|
-
.max(32)
|
|
116
|
-
.regex(/^[A-Z][A-Z0-9_]*$/);
|
|
117
|
-
|
|
118
|
-
export const manifestSecretSchema = z.object({
|
|
97
|
+
const manifestSecretSchema = z.object({
|
|
119
98
|
key: zEnvKey,
|
|
120
|
-
description:
|
|
99
|
+
description: localizedText(300),
|
|
121
100
|
required: z.boolean().default(true),
|
|
122
101
|
});
|
|
123
102
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
.regex(/^[^/\\][^\\]*$/)
|
|
134
|
-
.refine((value) => !value.split("/").includes("..")),
|
|
135
|
-
)
|
|
136
|
-
.max(64)
|
|
137
|
-
.default([]),
|
|
103
|
+
const manifestFileNoteSchema = z.object({
|
|
104
|
+
match: zGlob,
|
|
105
|
+
label: localizedText(48),
|
|
106
|
+
note: localizedText(400),
|
|
107
|
+
icon: z.enum(BridgeIcon).optional(),
|
|
108
|
+
color: zHexColor.optional(),
|
|
109
|
+
restart: z.boolean().default(false).meta({
|
|
110
|
+
description: "Whether editing this path needs a server restart to take effect.",
|
|
111
|
+
}),
|
|
138
112
|
});
|
|
139
113
|
|
|
140
|
-
export
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
reset: manifestResetSchema.optional(),
|
|
149
|
-
secrets: z.array(manifestSecretSchema).max(16).default([]),
|
|
150
|
-
files: manifestFilesSchema.default({
|
|
151
|
-
hidden: [],
|
|
114
|
+
export type ManifestFileNote = z.infer<typeof manifestFileNoteSchema>;
|
|
115
|
+
|
|
116
|
+
const manifestFilesSchema = z.object({
|
|
117
|
+
protected: z.array(zGlob).max(64).default([]).meta({
|
|
118
|
+
description: "Paths the player cannot edit, rename or delete from the file manager.",
|
|
119
|
+
}),
|
|
120
|
+
notable: z.array(manifestFileNoteSchema).max(32).default([]).meta({
|
|
121
|
+
description: "Paths the file manager annotates with a label, note and icon.",
|
|
152
122
|
}),
|
|
153
123
|
});
|
|
154
124
|
|
|
125
|
+
type CheckIssue = {
|
|
126
|
+
code: "custom";
|
|
127
|
+
message: string;
|
|
128
|
+
input: unknown;
|
|
129
|
+
path: (string | number)[];
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const duplicates = <Item>(items: Item[], keyOf: (item: Item) => string) => {
|
|
133
|
+
const seen = new Set<string>();
|
|
134
|
+
const found: {
|
|
135
|
+
key: string;
|
|
136
|
+
index: number;
|
|
137
|
+
}[] = [];
|
|
138
|
+
|
|
139
|
+
items.forEach((item, index) => {
|
|
140
|
+
const key = keyOf(item);
|
|
141
|
+
|
|
142
|
+
if (seen.has(key)) {
|
|
143
|
+
found.push({
|
|
144
|
+
key,
|
|
145
|
+
index,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
seen.add(key);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return found;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const duplicateIssues = <Item>(
|
|
156
|
+
items: Item[],
|
|
157
|
+
keyOf: (item: Item) => string,
|
|
158
|
+
path: (index: number) => (string | number)[],
|
|
159
|
+
message: (key: string) => string,
|
|
160
|
+
): CheckIssue[] => {
|
|
161
|
+
return duplicates(items, keyOf).map(({ key, index }) => ({
|
|
162
|
+
code: "custom" as const,
|
|
163
|
+
message: message(key),
|
|
164
|
+
input: key,
|
|
165
|
+
path: path(index),
|
|
166
|
+
}));
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const hostRangesOverlap = (a: ManifestPort, b: ManifestPort) => {
|
|
170
|
+
return a.hostRange.min <= b.hostRange.max && b.hostRange.min <= a.hostRange.max;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const portIssues = (ports: ManifestPort[]): CheckIssue[] => {
|
|
174
|
+
const issues = duplicateIssues(
|
|
175
|
+
ports,
|
|
176
|
+
(port) => port.key,
|
|
177
|
+
(index) => [
|
|
178
|
+
"container",
|
|
179
|
+
"ports",
|
|
180
|
+
index,
|
|
181
|
+
"key",
|
|
182
|
+
],
|
|
183
|
+
(key) => `port "${key}" is declared twice`,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
for (let index = 0; index < ports.length; index += 1) {
|
|
187
|
+
for (let other = index + 1; other < ports.length; other += 1) {
|
|
188
|
+
const a = ports[index];
|
|
189
|
+
const b = ports[other];
|
|
190
|
+
|
|
191
|
+
if (!a || !b || a.protocol !== b.protocol || !hostRangesOverlap(a, b)) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
issues.push({
|
|
196
|
+
code: "custom",
|
|
197
|
+
message: `host range overlaps port "${a.key}" — two ${a.protocol} ports cannot share host ports`,
|
|
198
|
+
input: b.hostRange,
|
|
199
|
+
path: [
|
|
200
|
+
"container",
|
|
201
|
+
"ports",
|
|
202
|
+
other,
|
|
203
|
+
"hostRange",
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return issues;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const guideIssues = (guideTags: GuideTag[], guides: GuideEntry[]): CheckIssue[] => {
|
|
213
|
+
const issues = [
|
|
214
|
+
...duplicateIssues(
|
|
215
|
+
guideTags,
|
|
216
|
+
(tag) => tag.id,
|
|
217
|
+
(index) => [
|
|
218
|
+
"guideTags",
|
|
219
|
+
index,
|
|
220
|
+
"id",
|
|
221
|
+
],
|
|
222
|
+
(key) => `guide tag "${key}" is declared twice`,
|
|
223
|
+
),
|
|
224
|
+
...duplicateIssues(
|
|
225
|
+
guides,
|
|
226
|
+
(guide) => guide.id,
|
|
227
|
+
(index) => [
|
|
228
|
+
"guides",
|
|
229
|
+
index,
|
|
230
|
+
"id",
|
|
231
|
+
],
|
|
232
|
+
(key) => `guide "${key}" is declared twice`,
|
|
233
|
+
),
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
const declared = new Set(guideTags.map((tag) => tag.id));
|
|
237
|
+
const bodyPaths = new Set<string>();
|
|
238
|
+
|
|
239
|
+
guides.forEach((guide, index) => {
|
|
240
|
+
if (guide.bodyPath.ar === guide.bodyPath.en) {
|
|
241
|
+
issues.push({
|
|
242
|
+
code: "custom",
|
|
243
|
+
message: `guide "${guide.id}" reuses one body file for both locales`,
|
|
244
|
+
input: guide.bodyPath,
|
|
245
|
+
path: [
|
|
246
|
+
"guides",
|
|
247
|
+
index,
|
|
248
|
+
"bodyPath",
|
|
249
|
+
],
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
guide.tags.forEach((tag, tagIndex) => {
|
|
254
|
+
if (declared.has(tag)) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
issues.push({
|
|
259
|
+
code: "custom",
|
|
260
|
+
message: `tag "${tag}" is not declared in guideTags`,
|
|
261
|
+
input: tag,
|
|
262
|
+
path: [
|
|
263
|
+
"guides",
|
|
264
|
+
index,
|
|
265
|
+
"tags",
|
|
266
|
+
tagIndex,
|
|
267
|
+
],
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const ownPaths = new Set<string>();
|
|
272
|
+
|
|
273
|
+
for (const locale of [
|
|
274
|
+
"ar",
|
|
275
|
+
"en",
|
|
276
|
+
] as const) {
|
|
277
|
+
const path = guide.bodyPath[locale];
|
|
278
|
+
|
|
279
|
+
if (ownPaths.has(path)) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
ownPaths.add(path);
|
|
284
|
+
|
|
285
|
+
if (bodyPaths.has(path)) {
|
|
286
|
+
issues.push({
|
|
287
|
+
code: "custom",
|
|
288
|
+
message: `body file "${path}" is already used by another guide`,
|
|
289
|
+
input: path,
|
|
290
|
+
path: [
|
|
291
|
+
"guides",
|
|
292
|
+
index,
|
|
293
|
+
"bodyPath",
|
|
294
|
+
locale,
|
|
295
|
+
],
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
bodyPaths.add(path);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return issues;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
export const manifestSchema = z
|
|
307
|
+
.object({
|
|
308
|
+
schemaVersion: z.literal(MANIFEST_SCHEMA_VERSION),
|
|
309
|
+
kind: z.literal("GamePackage"),
|
|
310
|
+
metadata: manifestMetadataSchema,
|
|
311
|
+
resources: manifestResourcesSchema,
|
|
312
|
+
container: manifestContainerSchema,
|
|
313
|
+
guideTags: z.array(guideTagSchema).max(16).default([]),
|
|
314
|
+
guides: z.array(guideEntrySchema).max(64).default([]),
|
|
315
|
+
backup: manifestBackupSchema.prefault({}),
|
|
316
|
+
reset: manifestResetSchema.prefault({}),
|
|
317
|
+
secrets: z.array(manifestSecretSchema).max(16).default([]),
|
|
318
|
+
files: manifestFilesSchema.prefault({}),
|
|
319
|
+
})
|
|
320
|
+
.check((ctx) => {
|
|
321
|
+
if (ctx.issues.length > 0) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const manifest = ctx.value;
|
|
326
|
+
const expectedImage = `${IMAGE_NAMESPACE}/${manifest.metadata.id}`;
|
|
327
|
+
|
|
328
|
+
if (manifest.container.image !== expectedImage) {
|
|
329
|
+
ctx.issues.push({
|
|
330
|
+
code: "custom",
|
|
331
|
+
message: `must be "${expectedImage}" to match metadata.id`,
|
|
332
|
+
input: manifest.container.image,
|
|
333
|
+
path: [
|
|
334
|
+
"container",
|
|
335
|
+
"image",
|
|
336
|
+
],
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
ctx.issues.push(
|
|
341
|
+
...portIssues(manifest.container.ports),
|
|
342
|
+
...duplicateIssues(
|
|
343
|
+
manifest.secrets,
|
|
344
|
+
(secret) => secret.key,
|
|
345
|
+
(index) => [
|
|
346
|
+
"secrets",
|
|
347
|
+
index,
|
|
348
|
+
"key",
|
|
349
|
+
],
|
|
350
|
+
(key) => `secret "${key}" is declared twice`,
|
|
351
|
+
),
|
|
352
|
+
...duplicateIssues(
|
|
353
|
+
manifest.files.protected,
|
|
354
|
+
(path) => path,
|
|
355
|
+
(index) => [
|
|
356
|
+
"files",
|
|
357
|
+
"protected",
|
|
358
|
+
index,
|
|
359
|
+
],
|
|
360
|
+
(key) => `"${key}" is protected twice`,
|
|
361
|
+
),
|
|
362
|
+
...duplicateIssues(
|
|
363
|
+
manifest.files.notable,
|
|
364
|
+
(note) => note.match,
|
|
365
|
+
(index) => [
|
|
366
|
+
"files",
|
|
367
|
+
"notable",
|
|
368
|
+
index,
|
|
369
|
+
"match",
|
|
370
|
+
],
|
|
371
|
+
(key) => `"${key}" already has a note`,
|
|
372
|
+
),
|
|
373
|
+
...guideIssues(manifest.guideTags, manifest.guides),
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
|
|
155
377
|
export type Manifest = z.infer<typeof manifestSchema>;
|
|
156
378
|
|
|
157
|
-
export const guidePackageSchema = z
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
|
|
379
|
+
export const guidePackageSchema = z
|
|
380
|
+
.object({
|
|
381
|
+
schemaVersion: z.literal(MANIFEST_SCHEMA_VERSION),
|
|
382
|
+
kind: z.literal("GuidePackage"),
|
|
383
|
+
metadata: z.object({
|
|
384
|
+
id: zSlug,
|
|
385
|
+
}),
|
|
386
|
+
guideTags: z.array(guideTagSchema).max(16).default([]),
|
|
387
|
+
guides: z.array(guideEntrySchema).min(1).max(64),
|
|
388
|
+
})
|
|
389
|
+
.check((ctx) => {
|
|
390
|
+
if (ctx.issues.length > 0) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
ctx.issues.push(...guideIssues(ctx.value.guideTags, ctx.value.guides));
|
|
395
|
+
});
|
|
169
396
|
|
|
170
397
|
export type GuidePackage = z.infer<typeof guidePackageSchema>;
|
|
398
|
+
|
|
399
|
+
export const packageSchema = z.discriminatedUnion("kind", [
|
|
400
|
+
manifestSchema,
|
|
401
|
+
guidePackageSchema,
|
|
402
|
+
]);
|
|
403
|
+
|
|
404
|
+
export type Package = z.infer<typeof packageSchema>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { IMAGE_NAMESPACE } from "./image";
|
|
3
|
+
|
|
4
|
+
export const HOST_RANGE_MIN_PORT = 1024;
|
|
5
|
+
|
|
6
|
+
export const HOST_RANGE_MAX_SIZE = 1000;
|
|
7
|
+
|
|
8
|
+
export const zSlug = z
|
|
9
|
+
.string()
|
|
10
|
+
.min(1)
|
|
11
|
+
.max(64)
|
|
12
|
+
.regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase letters, digits and dashes, starting with a letter or digit");
|
|
13
|
+
|
|
14
|
+
export const zPort = z.int("must be a whole number").min(1, "must be at least 1").max(65_535, "must be at most 65535");
|
|
15
|
+
|
|
16
|
+
export const zHostPort = z
|
|
17
|
+
.int("must be a whole number")
|
|
18
|
+
.min(HOST_RANGE_MIN_PORT, `must be at least ${HOST_RANGE_MIN_PORT} — lower ports are reserved`)
|
|
19
|
+
.max(65_535, "must be at most 65535");
|
|
20
|
+
|
|
21
|
+
export const zHexColor = z.stringFormat("hexColor", /^#[0-9a-fA-F]{6}$/, "must be a six-digit hex colour like #4a8fd4");
|
|
22
|
+
|
|
23
|
+
export const zEnvKey = z
|
|
24
|
+
.string()
|
|
25
|
+
.min(2)
|
|
26
|
+
.max(32)
|
|
27
|
+
.regex(/^[A-Z][A-Z0-9_]*$/, "must be SCREAMING_SNAKE_CASE, starting with a letter");
|
|
28
|
+
|
|
29
|
+
export const zImageReference = z.templateLiteral([
|
|
30
|
+
`${IMAGE_NAMESPACE}/`,
|
|
31
|
+
zSlug,
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const RELATIVE_PATH_MAX = 256;
|
|
35
|
+
|
|
36
|
+
const withoutTraversal = (value: string) => {
|
|
37
|
+
return !value.split("/").includes("..");
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const zRelativePath = z
|
|
41
|
+
.string()
|
|
42
|
+
.min(1)
|
|
43
|
+
.max(RELATIVE_PATH_MAX)
|
|
44
|
+
.regex(/^[a-zA-Z0-9][a-zA-Z0-9/_-]*\.[a-z0-9]+$/, "must be a relative file path with an extension");
|
|
45
|
+
|
|
46
|
+
export const zAssetPath = z
|
|
47
|
+
.string()
|
|
48
|
+
.min(1)
|
|
49
|
+
.max(RELATIVE_PATH_MAX)
|
|
50
|
+
.regex(/^[a-zA-Z0-9][a-zA-Z0-9/_-]*\.webp$/, "must be a relative path to a .webp file");
|
|
51
|
+
|
|
52
|
+
export const zMarkdownPath = z
|
|
53
|
+
.string()
|
|
54
|
+
.min(1)
|
|
55
|
+
.max(RELATIVE_PATH_MAX)
|
|
56
|
+
.regex(/^[a-zA-Z0-9][a-zA-Z0-9/_-]*\.md$/, "must be a relative path to a .md file");
|
|
57
|
+
|
|
58
|
+
export const zGlob = z
|
|
59
|
+
.string()
|
|
60
|
+
.min(1)
|
|
61
|
+
.max(RELATIVE_PATH_MAX)
|
|
62
|
+
.regex(/^[^/\\][^\\]*$/, "must be a relative pattern using forward slashes")
|
|
63
|
+
.refine(withoutTraversal, 'must not contain ".."');
|
package/src/manifest/source.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { verifyAsset } from "./assets";
|
|
2
|
-
import {
|
|
3
|
-
import { type Manifest, manifestSchema } from "./manifest";
|
|
2
|
+
import { type Manifest, manifestSchema, type Package, packageSchema } from "./manifest";
|
|
4
3
|
|
|
5
4
|
export const MANIFEST_FILE = "serverk.yml";
|
|
6
5
|
|
|
@@ -81,9 +80,11 @@ export const computeBundleDigest = (source: PackageSource, paths: string[]) => {
|
|
|
81
80
|
export const readPackage = (source: PackageSource) => {
|
|
82
81
|
const manifest: Manifest = manifestSchema.parse(readYaml(source, MANIFEST_FILE));
|
|
83
82
|
|
|
84
|
-
checkManifest(manifest);
|
|
85
|
-
|
|
86
83
|
return {
|
|
87
84
|
manifest,
|
|
88
85
|
};
|
|
89
86
|
};
|
|
87
|
+
|
|
88
|
+
export const readPackageDocument = (source: PackageSource): Package => {
|
|
89
|
+
return packageSchema.parse(readYaml(source, MANIFEST_FILE));
|
|
90
|
+
};
|
package/src/modules/bridge.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import type { BridgeContext, BridgeExecResult, BridgeFileEntry, BridgeReply } from "../context";
|
|
2
2
|
import type { BridgeOption, BridgeRow, BridgeValue, BridgeValues, LocalizedText } from "../core";
|
|
3
3
|
import type { BridgeField, BridgePanel, BridgeSection, BridgeTab } from "../panel";
|
|
4
|
-
import type { BridgeDriver } from "./bridgeDriver";
|
|
5
4
|
import type {
|
|
6
|
-
BridgeBackupModule,
|
|
7
5
|
BridgeCatalogEntry,
|
|
6
|
+
BridgeCatalogFacet,
|
|
8
7
|
BridgeCatalogHit,
|
|
9
|
-
BridgeCatalogModule,
|
|
10
8
|
BridgeCatalogPage,
|
|
9
|
+
BridgeCatalogProvider,
|
|
10
|
+
BridgeCatalogQuery,
|
|
11
|
+
} from "../protocol/bridgeCatalog";
|
|
12
|
+
import type { BridgeTerminalArg, BridgeTerminalCommand, BridgeTerminalRule } from "../terminal";
|
|
13
|
+
import type { BridgeDriver } from "./bridgeDriver";
|
|
14
|
+
import type {
|
|
15
|
+
BridgeAnnounceModule,
|
|
16
|
+
BridgeBackupModule,
|
|
17
|
+
BridgeCatalogModule,
|
|
11
18
|
BridgeCollectionModule,
|
|
12
19
|
BridgeEventPattern,
|
|
13
20
|
BridgeEventsModule,
|
|
@@ -18,14 +25,19 @@ import type {
|
|
|
18
25
|
BridgeRowAction,
|
|
19
26
|
BridgeSample,
|
|
20
27
|
BridgeSettingsModule,
|
|
28
|
+
BridgeTerminalModule,
|
|
21
29
|
} from "./bridgeModule";
|
|
22
30
|
|
|
23
31
|
export namespace Bridge {
|
|
32
|
+
export type Announce = BridgeAnnounceModule;
|
|
24
33
|
export type Backup = BridgeBackupModule;
|
|
25
34
|
export type Catalog = BridgeCatalogModule;
|
|
26
35
|
export type CatalogEntry = BridgeCatalogEntry;
|
|
36
|
+
export type CatalogFacet = BridgeCatalogFacet;
|
|
27
37
|
export type CatalogHit = BridgeCatalogHit;
|
|
28
38
|
export type CatalogPage = BridgeCatalogPage;
|
|
39
|
+
export type CatalogProvider = BridgeCatalogProvider;
|
|
40
|
+
export type CatalogQuery = BridgeCatalogQuery;
|
|
29
41
|
export type Collection<Row extends BridgeRow = BridgeRow> = BridgeCollectionModule<Row>;
|
|
30
42
|
export type Context = BridgeContext;
|
|
31
43
|
export type Driver = BridgeDriver;
|
|
@@ -46,6 +58,10 @@ export namespace Bridge {
|
|
|
46
58
|
export type Sample = BridgeSample;
|
|
47
59
|
export type Section = BridgeSection;
|
|
48
60
|
export type Settings = BridgeSettingsModule;
|
|
61
|
+
export type Terminal = BridgeTerminalModule;
|
|
62
|
+
export type TerminalArg = BridgeTerminalArg;
|
|
63
|
+
export type TerminalCommand = BridgeTerminalCommand;
|
|
64
|
+
export type TerminalRule = BridgeTerminalRule;
|
|
49
65
|
export type Tab = BridgeTab;
|
|
50
66
|
export type Text = LocalizedText;
|
|
51
67
|
export type Value = BridgeValue;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { BridgePanel } from "../panel";
|
|
2
2
|
import type {
|
|
3
|
+
BridgeAnnounceModule,
|
|
3
4
|
BridgeBackupModule,
|
|
4
5
|
BridgeEventsModule,
|
|
5
6
|
BridgeInstallModule,
|
|
6
7
|
BridgeLifecycleModule,
|
|
7
8
|
BridgePanelModule,
|
|
8
9
|
BridgeQueryModule,
|
|
10
|
+
BridgeTerminalModule,
|
|
9
11
|
} from "./bridgeModule";
|
|
10
12
|
|
|
11
13
|
export interface BridgeDriver {
|
|
@@ -14,6 +16,8 @@ export interface BridgeDriver {
|
|
|
14
16
|
events?: BridgeEventsModule;
|
|
15
17
|
query?: BridgeQueryModule;
|
|
16
18
|
backup?: BridgeBackupModule;
|
|
19
|
+
announce?: BridgeAnnounceModule;
|
|
20
|
+
terminal?: BridgeTerminalModule;
|
|
17
21
|
panel?: BridgePanel;
|
|
18
22
|
modules?: Record<string, BridgePanelModule>;
|
|
19
23
|
}
|