@serverkgg/bridge 0.0.6 → 0.0.8

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.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Serverk game bridge — driver runtime, wire protocol, manifest schema, and maintainer CLI",
5
5
  "homepage": "https://serverk.gg",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -21,11 +21,12 @@
21
21
  "./protocol": "./src/protocol/index.ts",
22
22
  "./manifest": "./src/manifest/index.ts",
23
23
  "./guides": "./src/guides/index.ts",
24
+ "./wire": "./src/wire/index.ts",
24
25
  "./package.json": "./package.json"
25
26
  },
26
27
  "scripts": {
27
28
  "tsc": "tsc",
28
- "test": "bun test"
29
+ "test": "bun test --parallel"
29
30
  },
30
31
  "dependencies": {
31
32
  "zod": "4.4.3"
@@ -97,9 +97,10 @@ export const createBridgeExec = (target: BridgeExecTarget) => {
97
97
  throw new BridgeExecError("the exec command is empty");
98
98
  }
99
99
 
100
- const { argv } = await unprivilegedArgv(command, target.uid, target.gid);
100
+ const { argv, credentials } = await unprivilegedArgv(command, target.uid, target.gid);
101
101
 
102
102
  const child = Bun.spawn(argv, {
103
+ ...credentials,
103
104
  cwd: target.cwd,
104
105
  env: {
105
106
  ...baseEnvironment(),
@@ -0,0 +1,77 @@
1
+ import { afterEach, describe, expect, it, spyOn } from "bun:test";
2
+ import { privilegePlan, readEffectiveUid, setprivArgv, spawnCredentials } from "./bridgePrivilege";
3
+
4
+ const COMMAND = [
5
+ "java",
6
+ "-jar",
7
+ "server.jar",
8
+ ];
9
+
10
+ describe("spawnCredentials", () => {
11
+ it("carries the uid and gid for the spawn options", () => {
12
+ expect(spawnCredentials(1000, 1000)).toEqual({
13
+ uid: 1000,
14
+ gid: 1000,
15
+ });
16
+ });
17
+ });
18
+
19
+ describe("setprivArgv", () => {
20
+ afterEach(() => {
21
+ spyOn(Bun, "which").mockRestore();
22
+ });
23
+
24
+ it("wraps the command with a full privilege drop", () => {
25
+ spyOn(Bun, "which").mockReturnValue("/usr/bin/setpriv");
26
+
27
+ expect(setprivArgv(COMMAND, 1000, 1000)).toEqual([
28
+ "setpriv",
29
+ "--reuid=1000",
30
+ "--regid=1000",
31
+ "--clear-groups",
32
+ "--",
33
+ ...COMMAND,
34
+ ]);
35
+ });
36
+
37
+ it("refuses to build a wrapper the image cannot run", () => {
38
+ spyOn(Bun, "which").mockReturnValue(null);
39
+
40
+ expect(() => setprivArgv(COMMAND, 1000, 1000)).toThrow("setpriv is missing");
41
+ });
42
+ });
43
+
44
+ describe("privilegePlan", () => {
45
+ afterEach(() => {
46
+ spyOn(Bun, "which").mockRestore();
47
+ });
48
+
49
+ it("passes credentials to the runtime when the spawn uid option is honoured", () => {
50
+ const plan = privilegePlan(true, COMMAND, 1000, 1000);
51
+
52
+ expect(plan.argv, "a native runtime must run the bare command").toEqual(COMMAND);
53
+ expect(plan.credentials, "the drop must ride the spawn options or the game runs as root").toEqual({
54
+ uid: 1000,
55
+ gid: 1000,
56
+ });
57
+ });
58
+
59
+ it("falls back to setpriv with no spawn credentials otherwise", () => {
60
+ spyOn(Bun, "which").mockReturnValue("/usr/bin/setpriv");
61
+
62
+ const plan = privilegePlan(false, COMMAND, 1000, 1000);
63
+
64
+ expect(plan.argv.at(0), "the wrapper must lead the argv").toBe("setpriv");
65
+ expect(plan.credentials, "credentials on a setpriv wrapper would fail the spawn").toEqual({});
66
+ });
67
+ });
68
+
69
+ describe("readEffectiveUid", () => {
70
+ it("reads this process's own effective uid", async () => {
71
+ expect(await readEffectiveUid(process.pid)).toBe(process.getuid?.() ?? 0);
72
+ });
73
+
74
+ it("answers null for a pid that does not exist", async () => {
75
+ expect(await readEffectiveUid(2 ** 22 - 1)).toBeNull();
76
+ });
77
+ });
@@ -39,14 +39,16 @@ export const settleUid = async (pid: number, expected: number) => {
39
39
  return observed;
40
40
  };
41
41
 
42
- const spawnWithUid = (command: string[], uid: number, gid: number) => {
43
- const credentials: object = {
42
+ export const spawnCredentials = (uid: number, gid: number): object => {
43
+ return {
44
44
  uid,
45
45
  gid,
46
46
  };
47
+ };
47
48
 
49
+ const spawnWithUid = (command: string[], uid: number, gid: number) => {
48
50
  return Bun.spawn(command, {
49
- ...credentials,
51
+ ...spawnCredentials(uid, gid),
50
52
  stderr: "ignore",
51
53
  stdin: "ignore",
52
54
  stdout: "ignore",
@@ -89,11 +91,13 @@ export const setprivArgv = (command: string[], uid: number, gid: number) => {
89
91
  ];
90
92
  };
91
93
 
92
- export const unprivilegedArgv = async (command: string[], uid: number, gid: number) => {
93
- const native = await probeNativeUid(uid, gid);
94
-
94
+ export const privilegePlan = (native: boolean, command: string[], uid: number, gid: number) => {
95
95
  return {
96
96
  argv: native ? command : setprivArgv(command, uid, gid),
97
- native,
97
+ credentials: native ? spawnCredentials(uid, gid) : {},
98
98
  };
99
99
  };
100
+
101
+ export const unprivilegedArgv = async (command: string[], uid: number, gid: number) => {
102
+ return privilegePlan(await probeNativeUid(uid, gid), command, uid, gid);
103
+ };
@@ -218,8 +218,9 @@ export const createSupervisor = (options: SupervisorOptions) => {
218
218
  ...rest,
219
219
  ];
220
220
 
221
- const { argv: spawnArgv } = await unprivilegedArgv(argv, options.uid, options.gid);
221
+ const { argv: spawnArgv, credentials } = await unprivilegedArgv(argv, options.uid, options.gid);
222
222
  const spawned = Bun.spawn(spawnArgv, {
223
+ ...credentials,
223
224
  cwd: options.cwd,
224
225
  stderr: "pipe",
225
226
  stdin: "pipe",
@@ -0,0 +1,2 @@
1
+ export * from "./panel";
2
+ export * from "./zEnvKey";
@@ -0,0 +1,285 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ BridgeAccess,
4
+ BridgeAction,
5
+ BridgeConfirm,
6
+ BridgeControl,
7
+ BridgeFormTarget,
8
+ BridgeKind,
9
+ BridgeLayout,
10
+ type BridgeManifest,
11
+ } from "@serverkgg/bridge/protocol";
12
+ import { bridgeManifestSchema } from "./panel";
13
+
14
+ const text = (value: string) => ({
15
+ ar: value,
16
+ en: value,
17
+ });
18
+
19
+ const manifest = (): BridgeManifest => ({
20
+ version: "1.0.0",
21
+ panel: {
22
+ tabs: [
23
+ {
24
+ id: "gameplay",
25
+ title: text("Gameplay"),
26
+ access: BridgeAccess.Invoke,
27
+ sections: [
28
+ {
29
+ layout: BridgeLayout.Actions,
30
+ id: "broadcast",
31
+ title: text("Broadcast"),
32
+ help: text("Send a message to everyone playing"),
33
+ module: "broadcast",
34
+ actions: [
35
+ {
36
+ id: "say",
37
+ label: text("Say"),
38
+ fields: [
39
+ {
40
+ key: "message",
41
+ control: BridgeControl.Text,
42
+ label: text("Message"),
43
+ maxLength: 200,
44
+ },
45
+ ],
46
+ },
47
+ {
48
+ id: "saveAll",
49
+ label: text("Save"),
50
+ confirm: BridgeConfirm.Normal,
51
+ },
52
+ ],
53
+ },
54
+ {
55
+ layout: BridgeLayout.Table,
56
+ id: "players",
57
+ module: "players",
58
+ columns: [
59
+ {
60
+ key: "name",
61
+ label: text("Name"),
62
+ },
63
+ ],
64
+ actions: [
65
+ {
66
+ id: "xp",
67
+ label: text("Give XP"),
68
+ fields: [
69
+ {
70
+ key: "amount",
71
+ control: BridgeControl.Number,
72
+ label: text("Amount"),
73
+ min: 1,
74
+ max: 1000,
75
+ },
76
+ {
77
+ key: "kind",
78
+ control: BridgeControl.Select,
79
+ label: text("Kind"),
80
+ options: [
81
+ {
82
+ value: "levels",
83
+ label: text("Levels"),
84
+ },
85
+ {
86
+ value: "points",
87
+ label: text("Points"),
88
+ },
89
+ ],
90
+ },
91
+ ],
92
+ },
93
+ ],
94
+ },
95
+ ],
96
+ },
97
+ ],
98
+ },
99
+ terminal: null,
100
+ modules: [
101
+ {
102
+ id: "broadcast",
103
+ kind: BridgeKind.Actions,
104
+ requiresRunning: true,
105
+ refreshSeconds: null,
106
+ pageSize: null,
107
+ dependsOn: [],
108
+ actions: [
109
+ "say",
110
+ "saveAll",
111
+ ],
112
+ },
113
+ {
114
+ id: "players",
115
+ kind: BridgeKind.Collection,
116
+ requiresRunning: true,
117
+ refreshSeconds: 10,
118
+ pageSize: null,
119
+ dependsOn: [],
120
+ actions: [
121
+ "list",
122
+ "xp",
123
+ ],
124
+ },
125
+ ],
126
+ events: [
127
+ "PlayerJoined",
128
+ ],
129
+ });
130
+
131
+ const previewManifest = (): BridgeManifest => ({
132
+ version: "1.0.0",
133
+ panel: {
134
+ tabs: [
135
+ {
136
+ id: "version",
137
+ title: text("Version"),
138
+ sections: [
139
+ {
140
+ layout: BridgeLayout.Form,
141
+ id: "version",
142
+ target: BridgeFormTarget.Variables,
143
+ preview: {
144
+ module: "transition",
145
+ },
146
+ reinstall: true,
147
+ confirm: BridgeConfirm.Strong,
148
+ fields: [
149
+ {
150
+ key: "MC_VERSION",
151
+ control: BridgeControl.Text,
152
+ label: text("Version"),
153
+ },
154
+ ],
155
+ },
156
+ ],
157
+ },
158
+ ],
159
+ },
160
+ terminal: null,
161
+ modules: [
162
+ {
163
+ id: "transition",
164
+ kind: BridgeKind.Preview,
165
+ requiresRunning: false,
166
+ refreshSeconds: null,
167
+ pageSize: null,
168
+ dependsOn: [],
169
+ actions: [
170
+ BridgeAction.Preview,
171
+ ],
172
+ },
173
+ ],
174
+ events: [],
175
+ });
176
+
177
+ const tabsOf = (count: number) => {
178
+ return Array.from({
179
+ length: count,
180
+ }).map((_entry, index) => ({
181
+ id: `tab-${index}`,
182
+ title: text(`Tab ${index}`),
183
+ sections: [
184
+ {
185
+ layout: BridgeLayout.Actions,
186
+ id: "broadcast",
187
+ module: "broadcast",
188
+ actions: [
189
+ {
190
+ id: "say",
191
+ label: text("Say"),
192
+ },
193
+ ],
194
+ },
195
+ ],
196
+ }));
197
+ };
198
+
199
+ describe("the panel mirror", () => {
200
+ test("carries an actions section and its fields through unchanged", () => {
201
+ const source = manifest();
202
+ const parsed = bridgeManifestSchema.parse(source);
203
+
204
+ expect(parsed).toEqual(source);
205
+ });
206
+
207
+ test("refuses an action carrying more fields than a prompt fits", () => {
208
+ const source = manifest();
209
+ const [tab] = source.panel?.tabs ?? [];
210
+ const [section] = tab?.sections ?? [];
211
+
212
+ if (section?.layout !== BridgeLayout.Actions) {
213
+ throw new Error("fixture is missing its actions section");
214
+ }
215
+
216
+ const [action] = section.actions;
217
+
218
+ if (!action) {
219
+ throw new Error("fixture is missing its action");
220
+ }
221
+
222
+ action.fields = Array.from({
223
+ length: 6,
224
+ }).map((_entry, index) => ({
225
+ key: `field${index}`,
226
+ control: BridgeControl.Text,
227
+ label: text("Field"),
228
+ }));
229
+
230
+ expect(bridgeManifestSchema.safeParse(source).success).toBe(false);
231
+ });
232
+
233
+ test("keeps a variables form's preview ref instead of stripping it", () => {
234
+ const source = previewManifest();
235
+ const parsed = bridgeManifestSchema.parse(source);
236
+ const [section] = parsed.panel?.tabs[0]?.sections ?? [];
237
+
238
+ if (section?.layout !== BridgeLayout.Form) {
239
+ throw new Error("fixture is missing its form section");
240
+ }
241
+
242
+ expect(section.preview).toEqual({
243
+ module: "transition",
244
+ });
245
+ expect(parsed).toEqual(source);
246
+ });
247
+
248
+ test("takes a preview module and the preview action it answers", () => {
249
+ const parsed = bridgeManifestSchema.parse(previewManifest());
250
+
251
+ expect(parsed.modules[0]).toEqual({
252
+ id: "transition",
253
+ kind: BridgeKind.Preview,
254
+ requiresRunning: false,
255
+ refreshSeconds: null,
256
+ pageSize: null,
257
+ dependsOn: [],
258
+ actions: [
259
+ BridgeAction.Preview,
260
+ ],
261
+ });
262
+ });
263
+
264
+ test("takes eight tabs and refuses nine", () => {
265
+ const source = manifest();
266
+
267
+ expect(
268
+ bridgeManifestSchema.safeParse({
269
+ ...source,
270
+ panel: {
271
+ tabs: tabsOf(8),
272
+ },
273
+ }).success,
274
+ ).toBe(true);
275
+
276
+ expect(
277
+ bridgeManifestSchema.safeParse({
278
+ ...source,
279
+ panel: {
280
+ tabs: tabsOf(9),
281
+ },
282
+ }).success,
283
+ ).toBe(false);
284
+ });
285
+ });
@@ -0,0 +1,263 @@
1
+ import {
2
+ BRIDGE_ID_PATTERN,
3
+ BRIDGE_MODULE_ID_PATTERN,
4
+ BridgeAccess,
5
+ BridgeConfirm,
6
+ BridgeControl,
7
+ BridgeFormTarget,
8
+ BridgeIcon,
9
+ BridgeKind,
10
+ BridgeLayout,
11
+ type BridgeManifest,
12
+ type BridgePanel,
13
+ type BridgeSection,
14
+ BridgeStream,
15
+ type BridgeTerminalArg,
16
+ type BridgeTerminalCommand,
17
+ BridgeTerminalLevel,
18
+ type BridgeTerminalManifest,
19
+ type BridgeTerminalRuleManifest,
20
+ } from "@serverkgg/bridge/protocol";
21
+ import { z } from "zod";
22
+ import { zEnvKey } from "./zEnvKey";
23
+
24
+ const PANEL_TEXT_LENGTH_LIMIT = 512;
25
+
26
+ const ACTION_FIELDS_LIMIT = 5;
27
+
28
+ const SECTION_ACTIONS_LIMIT = 10;
29
+
30
+ const zKey = z
31
+ .string()
32
+ .min(1)
33
+ .max(64)
34
+ .regex(/^[A-Za-z][A-Za-z0-9_-]*$/);
35
+
36
+ const zBridgeId = z.string().min(1).max(32).regex(BRIDGE_ID_PATTERN);
37
+
38
+ export const zBridgeModuleId = z.string().min(1).max(32).regex(BRIDGE_MODULE_ID_PATTERN);
39
+
40
+ export const bridgeValueSchema = z.union([
41
+ z.boolean(),
42
+ z.number(),
43
+ z.string().max(8192),
44
+ z.null(),
45
+ ]);
46
+
47
+ export type BridgeValueInput = z.infer<typeof bridgeValueSchema>;
48
+
49
+ export const bridgeValuesSchema = z.record(zKey, bridgeValueSchema);
50
+
51
+ export type BridgeValuesInput = z.infer<typeof bridgeValuesSchema>;
52
+
53
+ export const panelTextSchema = z.object({
54
+ ar: z.string().min(1).max(PANEL_TEXT_LENGTH_LIMIT),
55
+ en: z.string().min(1).max(PANEL_TEXT_LENGTH_LIMIT),
56
+ });
57
+
58
+ export const bridgeOptionSchema = z.object({
59
+ value: z.string().min(1).max(128),
60
+ label: panelTextSchema,
61
+ help: panelTextSchema.optional(),
62
+ latest: z.boolean().optional(),
63
+ });
64
+
65
+ export const bridgeVisibleWhenSchema = z
66
+ .object({
67
+ variable: zKey,
68
+ values: z.array(z.string().max(128)).min(1).max(16).optional(),
69
+ empty: z.boolean().optional(),
70
+ })
71
+ .refine((rule) => rule.empty !== undefined || rule.values !== undefined);
72
+
73
+ export const bridgeOptionsRefSchema = z.object({
74
+ module: zBridgeModuleId,
75
+ });
76
+
77
+ export const bridgePreviewRefSchema = z.object({
78
+ module: zBridgeModuleId,
79
+ });
80
+
81
+ export const bridgeFieldSchema = z.object({
82
+ key: zKey,
83
+ control: z.enum(BridgeControl),
84
+ label: panelTextSchema,
85
+ help: panelTextSchema.optional(),
86
+ warning: panelTextSchema.optional(),
87
+ placeholder: z.string().max(256).optional(),
88
+ min: z.number().optional(),
89
+ max: z.number().optional(),
90
+ step: z.number().optional(),
91
+ maxLength: z.int().positive().optional(),
92
+ options: z
93
+ .union([
94
+ z.array(bridgeOptionSchema).max(64),
95
+ bridgeOptionsRefSchema,
96
+ ])
97
+ .optional(),
98
+ visibleWhen: bridgeVisibleWhenSchema.optional(),
99
+ });
100
+
101
+ export const bridgeSectionActionSchema = z.object({
102
+ id: zKey,
103
+ label: panelTextSchema,
104
+ confirm: z.enum(BridgeConfirm).optional(),
105
+ confirmText: panelTextSchema.optional(),
106
+ download: z.boolean().optional(),
107
+ fields: z.array(bridgeFieldSchema).max(ACTION_FIELDS_LIMIT).optional(),
108
+ });
109
+
110
+ export const bridgeSectionAddSchema = z.object({
111
+ label: panelTextSchema,
112
+ placeholder: z.string().max(256).optional(),
113
+ });
114
+
115
+ export const bridgeSectionUploadSchema = z.object({
116
+ label: panelTextSchema,
117
+ extensions: z.array(z.string().min(1).max(16)).min(1).max(8),
118
+ staging: z.string().min(1).max(128),
119
+ });
120
+
121
+ export const bridgeActionsSectionSchema = z.object({
122
+ layout: z.literal(BridgeLayout.Actions),
123
+ id: zBridgeId,
124
+ title: panelTextSchema.optional(),
125
+ help: panelTextSchema.optional(),
126
+ module: zBridgeModuleId,
127
+ actions: z.array(bridgeSectionActionSchema).min(1).max(SECTION_ACTIONS_LIMIT),
128
+ visibleWhen: bridgeVisibleWhenSchema.optional(),
129
+ access: z.enum(BridgeAccess).optional(),
130
+ });
131
+
132
+ export const bridgeFormSectionSchema = z.object({
133
+ layout: z.literal(BridgeLayout.Form),
134
+ id: zBridgeId,
135
+ title: panelTextSchema.optional(),
136
+ target: z.enum(BridgeFormTarget),
137
+ module: zBridgeModuleId.optional(),
138
+ preview: bridgePreviewRefSchema.optional(),
139
+ reinstall: z.boolean().optional(),
140
+ restartHint: z.boolean().optional(),
141
+ confirm: z.enum(BridgeConfirm).optional(),
142
+ confirmText: panelTextSchema.optional(),
143
+ fields: z.array(bridgeFieldSchema).min(1).max(32),
144
+ visibleWhen: bridgeVisibleWhenSchema.optional(),
145
+ access: z.enum(BridgeAccess).optional(),
146
+ });
147
+
148
+ export const bridgeColumnSchema = z.object({
149
+ key: zKey,
150
+ label: panelTextSchema,
151
+ });
152
+
153
+ export const bridgeTableSectionSchema = z.object({
154
+ layout: z.literal(BridgeLayout.Table),
155
+ id: zBridgeId,
156
+ title: panelTextSchema.optional(),
157
+ module: zBridgeModuleId,
158
+ restartHint: z.boolean().optional(),
159
+ columns: z.array(bridgeColumnSchema).min(1).max(8),
160
+ add: bridgeSectionAddSchema.optional(),
161
+ upload: bridgeSectionUploadSchema.optional(),
162
+ actions: z.array(bridgeSectionActionSchema).max(8).optional(),
163
+ empty: panelTextSchema.optional(),
164
+ visibleWhen: bridgeVisibleWhenSchema.optional(),
165
+ access: z.enum(BridgeAccess).optional(),
166
+ });
167
+
168
+ export const bridgeCardsSectionSchema = z.object({
169
+ layout: z.literal(BridgeLayout.Cards),
170
+ id: zBridgeId,
171
+ title: panelTextSchema.optional(),
172
+ module: zBridgeModuleId,
173
+ titleKey: zKey,
174
+ subtitleKey: zKey.optional(),
175
+ actions: z.array(bridgeSectionActionSchema).max(8).optional(),
176
+ empty: panelTextSchema.optional(),
177
+ visibleWhen: bridgeVisibleWhenSchema.optional(),
178
+ access: z.enum(BridgeAccess).optional(),
179
+ });
180
+
181
+ export const bridgeCatalogSectionSchema = z.object({
182
+ layout: z.literal(BridgeLayout.Catalog),
183
+ id: zBridgeId,
184
+ title: panelTextSchema.optional(),
185
+ module: zBridgeModuleId,
186
+ restartHint: z.boolean().optional(),
187
+ reinstall: z.boolean().optional(),
188
+ variable: zEnvKey.optional(),
189
+ confirm: z.enum(BridgeConfirm).optional(),
190
+ confirmText: panelTextSchema.optional(),
191
+ empty: panelTextSchema.optional(),
192
+ visibleWhen: bridgeVisibleWhenSchema.optional(),
193
+ access: z.enum(BridgeAccess).optional(),
194
+ });
195
+
196
+ export const bridgeSectionSchema: z.ZodType<BridgeSection> = z.discriminatedUnion("layout", [
197
+ bridgeActionsSectionSchema,
198
+ bridgeCardsSectionSchema,
199
+ bridgeCatalogSectionSchema,
200
+ bridgeFormSectionSchema,
201
+ bridgeTableSectionSchema,
202
+ ]);
203
+
204
+ export const bridgeTabSchema = z.object({
205
+ id: zBridgeId,
206
+ title: panelTextSchema,
207
+ icon: z.enum(BridgeIcon).optional(),
208
+ sections: z.array(bridgeSectionSchema).min(1).max(8),
209
+ access: z.enum(BridgeAccess).optional(),
210
+ });
211
+
212
+ export const bridgePanelSchema: z.ZodType<BridgePanel> = z.object({
213
+ tabs: z.array(bridgeTabSchema).min(1).max(8),
214
+ });
215
+
216
+ export const bridgeTerminalArgSchema: z.ZodType<BridgeTerminalArg> = z.object({
217
+ key: zKey,
218
+ label: panelTextSchema.optional(),
219
+ required: z.boolean().optional(),
220
+ variadic: z.boolean().optional(),
221
+ values: z.array(z.string().min(1).max(64)).max(64).optional(),
222
+ module: zBridgeModuleId.optional(),
223
+ column: zKey.optional(),
224
+ });
225
+
226
+ export const bridgeTerminalCommandSchema: z.ZodType<BridgeTerminalCommand> = z.object({
227
+ name: z.string().min(1).max(64),
228
+ summary: panelTextSchema,
229
+ syntax: z.string().max(160).optional(),
230
+ args: z.array(bridgeTerminalArgSchema).max(8).optional(),
231
+ danger: z.boolean().optional(),
232
+ requiresRunning: z.boolean().optional(),
233
+ });
234
+
235
+ export const bridgeTerminalRuleSchema: z.ZodType<BridgeTerminalRuleManifest> = z.object({
236
+ match: z.string().min(1).max(512),
237
+ flags: z.string().max(8),
238
+ level: z.enum(BridgeTerminalLevel),
239
+ stream: z.enum(BridgeStream).optional(),
240
+ });
241
+
242
+ export const bridgeTerminalManifestSchema: z.ZodType<BridgeTerminalManifest> = z.object({
243
+ commands: z.array(bridgeTerminalCommandSchema).max(128),
244
+ rules: z.array(bridgeTerminalRuleSchema).max(64),
245
+ });
246
+
247
+ export const bridgeModuleManifestSchema = z.object({
248
+ id: zBridgeModuleId,
249
+ kind: z.enum(BridgeKind),
250
+ requiresRunning: z.boolean(),
251
+ refreshSeconds: z.int().positive().nullable(),
252
+ pageSize: z.int().positive().nullable().default(null),
253
+ dependsOn: z.array(zKey).max(8),
254
+ actions: z.array(z.string().min(1).max(64)).max(16),
255
+ });
256
+
257
+ export const bridgeManifestSchema: z.ZodType<BridgeManifest> = z.object({
258
+ version: z.string(),
259
+ panel: bridgePanelSchema.nullable(),
260
+ terminal: bridgeTerminalManifestSchema.nullable().default(null),
261
+ modules: z.array(bridgeModuleManifestSchema).max(32),
262
+ events: z.array(z.string().min(1).max(64)).max(32),
263
+ });
@@ -0,0 +1,7 @@
1
+ import { z } from "zod";
2
+
3
+ export const zEnvKey = z
4
+ .string()
5
+ .min(2)
6
+ .max(32)
7
+ .regex(/^[A-Z][A-Z0-9_]*$/);