@serverkgg/bridge 0.0.10 → 0.0.11

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.
@@ -0,0 +1,303 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { tmpdir } from "node:os";
3
+ import type { BridgeContext } from "../context";
4
+ import { BridgeKind, BridgeSetupPromptKind, BridgeSetupStepKind, BridgeSetupStepState, BridgeUserError } from "../core";
5
+ import type { BridgeDriver, BridgeSetupModule } from "../modules";
6
+ import type { BridgeConfig, BridgeSetupRuntime } from "../protocol";
7
+ import { createContext } from "./bridgeContext";
8
+ import type { BridgeLeveledLogger } from "./bridgeLog";
9
+ import { log } from "./bridgeLog";
10
+ import { createSetupHandlers } from "./bridgeSetupHandlers";
11
+ import { createSupervisor } from "./bridgeSupervisor";
12
+
13
+ const text = (value: string) => ({
14
+ ar: value,
15
+ en: value,
16
+ });
17
+
18
+ const config = (): BridgeConfig => ({
19
+ code: "abcdef",
20
+ token: "token",
21
+ cwd: `${tmpdir()}/serverk-setup-handlers-test`,
22
+ run: `${tmpdir()}/serverk-setup-handlers-test/run`,
23
+ ports: {},
24
+ memoryMb: 1024,
25
+ cpuPercent: 100,
26
+ gameUid: 1000,
27
+ gameGid: 1000,
28
+ variables: {},
29
+ secrets: {},
30
+ });
31
+
32
+ const contextOf = (): BridgeContext => {
33
+ const settings = config();
34
+
35
+ const supervisor = createSupervisor({
36
+ cwd: settings.cwd,
37
+ uid: settings.gameUid,
38
+ gid: settings.gameGid,
39
+ stopTimeoutSeconds: 1,
40
+ onLog: log,
41
+ resolveCommand: async () => [
42
+ "run",
43
+ ],
44
+ });
45
+
46
+ return createContext(settings, supervisor, {
47
+ onLog: log,
48
+ onEmit() {
49
+ return;
50
+ },
51
+ onSetup() {
52
+ return;
53
+ },
54
+ });
55
+ };
56
+
57
+ const recorder = () => {
58
+ const warnings: {
59
+ message: string;
60
+ fields?: Record<string, unknown>;
61
+ }[] = [];
62
+
63
+ const logger: BridgeLeveledLogger = Object.assign(
64
+ () => {
65
+ return;
66
+ },
67
+ {
68
+ warn: (message: string, fields?: Record<string, unknown>) => {
69
+ warnings.push({
70
+ message,
71
+ fields,
72
+ });
73
+ },
74
+ error: () => {
75
+ return;
76
+ },
77
+ },
78
+ );
79
+
80
+ return {
81
+ logger,
82
+ warnings,
83
+ };
84
+ };
85
+
86
+ const driverWith = (setup?: BridgeSetupModule): BridgeDriver => ({
87
+ install: {
88
+ kind: BridgeKind.Install,
89
+ run: async () => {
90
+ return;
91
+ },
92
+ },
93
+ lifecycle: {
94
+ kind: BridgeKind.Lifecycle,
95
+ command: async () => [
96
+ "run",
97
+ ],
98
+ },
99
+ setup,
100
+ });
101
+
102
+ const signIn = (): BridgeSetupRuntime => ({
103
+ steps: [
104
+ {
105
+ id: "sign-in",
106
+ state: BridgeSetupStepState.Active,
107
+ },
108
+ ],
109
+ prompt: {
110
+ kind: BridgeSetupPromptKind.Link,
111
+ message: text("sign in with your account"),
112
+ url: "https://hytale.com/device",
113
+ code: "ABCD-EFGH",
114
+ expiresAt: null,
115
+ actions: [
116
+ {
117
+ id: "renew",
118
+ label: text("Get a new code"),
119
+ },
120
+ ],
121
+ },
122
+ });
123
+
124
+ const setupWith = (submit?: BridgeSetupModule["submit"]): BridgeSetupModule => ({
125
+ kind: BridgeKind.Setup,
126
+ steps: [
127
+ {
128
+ kind: BridgeSetupStepKind.Driver,
129
+ id: "sign-in",
130
+ title: text("Sign in"),
131
+ },
132
+ ],
133
+ submit,
134
+ });
135
+
136
+ describe("the setup handlers", () => {
137
+ test("answer unsupported for a driver that declares no setup", async () => {
138
+ const { logger } = recorder();
139
+ const handlers = createSetupHandlers(driverWith(), logger);
140
+
141
+ expect(handlers.onSetupStatus()).toEqual({
142
+ supported: false,
143
+ runtime: null,
144
+ });
145
+
146
+ expect(await handlers.onSetupSubmit(contextOf(), "sign-in", "renew", {})).toEqual({
147
+ supported: false,
148
+ runtime: null,
149
+ });
150
+ });
151
+
152
+ test("keep nothing for a driver that declares no setup, whatever it reports", () => {
153
+ const { logger, warnings } = recorder();
154
+ const handlers = createSetupHandlers(driverWith(), logger);
155
+
156
+ handlers.onSetup(signIn());
157
+
158
+ expect(handlers.current()).toBeNull();
159
+ expect(warnings).toEqual([]);
160
+ });
161
+
162
+ test("answer the normalized runtime the driver reported", () => {
163
+ const { logger, warnings } = recorder();
164
+ const handlers = createSetupHandlers(driverWith(setupWith()), logger);
165
+
166
+ handlers.onSetup({
167
+ steps: [
168
+ {
169
+ id: "sign-in",
170
+ state: BridgeSetupStepState.Active,
171
+ },
172
+ {
173
+ id: "invent",
174
+ state: BridgeSetupStepState.Pending,
175
+ },
176
+ ],
177
+ prompt: null,
178
+ });
179
+
180
+ expect(handlers.onSetupStatus()).toEqual({
181
+ supported: true,
182
+ runtime: {
183
+ steps: [
184
+ {
185
+ id: "sign-in",
186
+ state: BridgeSetupStepState.Active,
187
+ },
188
+ ],
189
+ prompt: null,
190
+ },
191
+ });
192
+ expect(handlers.current()).toEqual(handlers.onSetupStatus().runtime);
193
+ expect(warnings).toEqual([
194
+ {
195
+ message: "setup runtime normalized",
196
+ fields: {
197
+ reason: "unknown-step",
198
+ },
199
+ },
200
+ ]);
201
+ });
202
+
203
+ test("answer unsupported for a setup that exports no submit", async () => {
204
+ const { logger } = recorder();
205
+ const handlers = createSetupHandlers(driverWith(setupWith()), logger);
206
+
207
+ expect(await handlers.onSetupSubmit(contextOf(), "sign-in", "renew", {})).toEqual({
208
+ supported: false,
209
+ runtime: null,
210
+ });
211
+ });
212
+
213
+ test("turn a user error into a failed prompt that keeps the previous actions", async () => {
214
+ const { logger, warnings } = recorder();
215
+
216
+ const handlers = createSetupHandlers(
217
+ driverWith(
218
+ setupWith(async () => {
219
+ throw new BridgeUserError(text("this code cannot be renewed any more"));
220
+ }),
221
+ ),
222
+ logger,
223
+ );
224
+
225
+ handlers.onSetup(signIn());
226
+
227
+ expect(await handlers.onSetupSubmit(contextOf(), "sign-in", "renew", {})).toEqual({
228
+ supported: true,
229
+ runtime: {
230
+ steps: [
231
+ {
232
+ id: "sign-in",
233
+ state: BridgeSetupStepState.Active,
234
+ },
235
+ ],
236
+ prompt: {
237
+ kind: BridgeSetupPromptKind.Failed,
238
+ message: text("this code cannot be renewed any more"),
239
+ actions: [
240
+ {
241
+ id: "renew",
242
+ label: text("Get a new code"),
243
+ },
244
+ ],
245
+ },
246
+ },
247
+ });
248
+ expect(warnings.at(-1)?.message).toBe("setup submit rejected");
249
+ });
250
+
251
+ test("answer the runtime the driver reported while submitting", async () => {
252
+ const { logger } = recorder();
253
+
254
+ const handlers = createSetupHandlers(
255
+ driverWith(
256
+ setupWith(async () => {
257
+ handlers.onSetup({
258
+ steps: [
259
+ {
260
+ id: "sign-in",
261
+ state: BridgeSetupStepState.Done,
262
+ },
263
+ ],
264
+ prompt: null,
265
+ });
266
+ }),
267
+ ),
268
+ logger,
269
+ );
270
+
271
+ handlers.onSetup(signIn());
272
+
273
+ expect(await handlers.onSetupSubmit(contextOf(), "sign-in", "renew", {})).toEqual({
274
+ supported: true,
275
+ runtime: {
276
+ steps: [
277
+ {
278
+ id: "sign-in",
279
+ state: BridgeSetupStepState.Done,
280
+ },
281
+ ],
282
+ prompt: null,
283
+ },
284
+ });
285
+ });
286
+
287
+ test("let an error the driver cannot explain out", async () => {
288
+ const { logger } = recorder();
289
+
290
+ const handlers = createSetupHandlers(
291
+ driverWith(
292
+ setupWith(async () => {
293
+ throw new Error("the device endpoint answered 503");
294
+ }),
295
+ ),
296
+ logger,
297
+ );
298
+
299
+ await expect(handlers.onSetupSubmit(contextOf(), "sign-in", "renew", {})).rejects.toThrow(
300
+ "the device endpoint answered 503",
301
+ );
302
+ });
303
+ });
@@ -0,0 +1,71 @@
1
+ import type { BridgeContext } from "../context";
2
+ import { BridgeUserError, type BridgeValues } from "../core";
3
+ import type { BridgeDriver } from "../modules";
4
+ import type { BridgeSetupResult, BridgeSetupRuntime } from "../protocol";
5
+ import type { BridgeLeveledLogger } from "./bridgeLog";
6
+ import { failedSetup, normalizeSetupRuntime, unsupportedSetup } from "./bridgeSetup";
7
+
8
+ export interface BridgeSetupHandlers {
9
+ onSetup(runtime: BridgeSetupRuntime | null): void;
10
+ onSetupStatus(): BridgeSetupResult;
11
+ onSetupSubmit(context: BridgeContext, step: string, action: string, args: BridgeValues): Promise<BridgeSetupResult>;
12
+ current(): BridgeSetupRuntime | null;
13
+ }
14
+
15
+ export const createSetupHandlers = (driver: BridgeDriver, log: BridgeLeveledLogger): BridgeSetupHandlers => {
16
+ const declared: ReadonlySet<string> = new Set((driver.setup?.steps ?? []).map((step) => step.id));
17
+
18
+ let current: BridgeSetupRuntime | null = null;
19
+
20
+ return {
21
+ onSetup(runtime) {
22
+ current = driver.setup
23
+ ? normalizeSetupRuntime(runtime, {
24
+ declared,
25
+ warn: log.warn,
26
+ })
27
+ : null;
28
+ },
29
+ current() {
30
+ return current;
31
+ },
32
+ onSetupStatus() {
33
+ if (!driver.setup) {
34
+ return unsupportedSetup();
35
+ }
36
+
37
+ return {
38
+ supported: true,
39
+ runtime: current,
40
+ };
41
+ },
42
+ async onSetupSubmit(context, step, action, args) {
43
+ const module = driver.setup;
44
+
45
+ if (!module?.submit) {
46
+ return unsupportedSetup();
47
+ }
48
+
49
+ try {
50
+ await module.submit(context, step, action, args);
51
+ } catch (error) {
52
+ if (!(error instanceof BridgeUserError)) {
53
+ throw error;
54
+ }
55
+
56
+ log.warn("setup submit rejected", {
57
+ step,
58
+ action,
59
+ error: error.message,
60
+ });
61
+
62
+ return failedSetup(current, error.text);
63
+ }
64
+
65
+ return {
66
+ supported: true,
67
+ runtime: current,
68
+ };
69
+ },
70
+ };
71
+ };
@@ -7,8 +7,6 @@ import {
7
7
  type BridgeConfig,
8
8
  type BridgePendingResult,
9
9
  type BridgeSampleResult,
10
- type BridgeSetupResult,
11
- type BridgeSetupRuntime,
12
10
  type BridgeStatus,
13
11
  } from "../protocol";
14
12
  import { configPath, readConfig, socketPath, statePath } from "./bridgeConfigReader";
@@ -20,7 +18,8 @@ import { errorText, log } from "./bridgeLog";
20
18
  import { pendingResult, rejectedPending, unsupportedPending } from "./bridgePending";
21
19
  import { createRegistry } from "./bridgeRegistry";
22
20
  import { createServer } from "./bridgeServer";
23
- import { failedSetup, setupSummaryOf, unsupportedSetup } from "./bridgeSetup";
21
+ import { setupSummaryOf } from "./bridgeSetup";
22
+ import { createSetupHandlers } from "./bridgeSetupHandlers";
24
23
  import { readDesired, writeDesired } from "./bridgeState";
25
24
  import { createSupervisor } from "./bridgeSupervisor";
26
25
 
@@ -74,7 +73,7 @@ export const serve = async (driver: BridgeDriver) => {
74
73
 
75
74
  let installHold: BridgeInstallHold | null = null;
76
75
 
77
- let setupRuntime: BridgeSetupRuntime | null = null;
76
+ const setup = createSetupHandlers(driver, log);
78
77
 
79
78
  const supervisor = createSupervisor({
80
79
  cwd: config.cwd,
@@ -106,7 +105,7 @@ export const serve = async (driver: BridgeDriver) => {
106
105
  ring.emit(event, payload);
107
106
  },
108
107
  onSetup(runtime) {
109
- setupRuntime = runtime;
108
+ setup.onSetup(runtime);
110
109
  },
111
110
  });
112
111
 
@@ -125,7 +124,7 @@ export const serve = async (driver: BridgeDriver) => {
125
124
  installError,
126
125
  installErrorText,
127
126
  installHold,
128
- setup: setupSummaryOf(setupRuntime),
127
+ setup: setupSummaryOf(setup.current()),
129
128
  };
130
129
  };
131
130
 
@@ -320,42 +319,10 @@ export const serve = async (driver: BridgeDriver) => {
320
319
  return await pending();
321
320
  },
322
321
  onSetupStatus() {
323
- if (!driver.setup) {
324
- return unsupportedSetup();
325
- }
326
-
327
- return {
328
- supported: true,
329
- runtime: setupRuntime,
330
- };
322
+ return setup.onSetupStatus();
331
323
  },
332
- async onSetupSubmit(step, action, args): Promise<BridgeSetupResult> {
333
- const module = driver.setup;
334
-
335
- if (!module?.submit) {
336
- return unsupportedSetup();
337
- }
338
-
339
- try {
340
- await module.submit(context, step, action, args);
341
- } catch (error) {
342
- if (!(error instanceof BridgeUserError)) {
343
- throw error;
344
- }
345
-
346
- log.warn("setup submit rejected", {
347
- step,
348
- action,
349
- error: error.message,
350
- });
351
-
352
- return failedSetup(setupRuntime, error.text);
353
- }
354
-
355
- return {
356
- supported: true,
357
- runtime: setupRuntime,
358
- };
324
+ async onSetupSubmit(step, action, args) {
325
+ return await setup.onSetupSubmit(context, step, action, args);
359
326
  },
360
327
  });
361
328
 
@@ -5,6 +5,7 @@ import {
5
5
  BridgeAction,
6
6
  BridgeConfirm,
7
7
  BridgeControl,
8
+ type BridgeField,
8
9
  BridgeFormTarget,
9
10
  BridgeKind,
10
11
  BridgeLayout,
@@ -457,6 +458,67 @@ describe("the upload mirror", () => {
457
458
  });
458
459
  });
459
460
 
461
+ const fieldManifest = (field: BridgeField): BridgeManifest => ({
462
+ version: "1.0.0",
463
+ panel: {
464
+ tabs: [
465
+ {
466
+ id: "license",
467
+ title: text("Licence"),
468
+ sections: [
469
+ {
470
+ layout: BridgeLayout.Form,
471
+ id: "license",
472
+ target: BridgeFormTarget.Variables,
473
+ fields: [
474
+ field,
475
+ ],
476
+ },
477
+ ],
478
+ },
479
+ ],
480
+ },
481
+ terminal: null,
482
+ setup: null,
483
+ modules: [],
484
+ events: [],
485
+ });
486
+
487
+ describe("secret fields", () => {
488
+ test("carries a secret control with its pattern and hint through unchanged", () => {
489
+ const source = fieldManifest({
490
+ key: "LICENSE_KEY",
491
+ control: BridgeControl.Secret,
492
+ label: text("Licence key"),
493
+ pattern: "^cfxk_\\w{1,60}_\\w{1,20}$",
494
+ patternHint: text("A licence key starts with cfxk_"),
495
+ });
496
+
497
+ expect(bridgeManifestSchema.parse(source)).toEqual(source);
498
+ });
499
+
500
+ test("refuses a control the platform does not know", () => {
501
+ const source = fieldManifest({
502
+ key: "LICENSE_KEY",
503
+ control: "password" as BridgeControl,
504
+ label: text("Licence key"),
505
+ });
506
+
507
+ expect(bridgeManifestSchema.safeParse(source).success).toBe(false);
508
+ });
509
+
510
+ test("refuses a pattern longer than the platform takes", () => {
511
+ const source = fieldManifest({
512
+ key: "LICENSE_KEY",
513
+ control: BridgeControl.Secret,
514
+ label: text("Licence key"),
515
+ pattern: `^${"a".repeat(256)}$`,
516
+ });
517
+
518
+ expect(bridgeManifestSchema.safeParse(source).success).toBe(false);
519
+ });
520
+ });
521
+
460
522
  const setupManifest = (setup: BridgeSetupManifest | null): BridgeManifest => ({
461
523
  version: "1.0.0",
462
524
  panel: null,
package/src/wire/panel.ts CHANGED
@@ -2,6 +2,7 @@ import { guideOpenTargetSchema } from "@serverkgg/bridge/guides";
2
2
  import {
3
3
  BRIDGE_ID_PATTERN,
4
4
  BRIDGE_MODULE_ID_PATTERN,
5
+ BRIDGE_PATTERN_LENGTH_LIMIT,
5
6
  BridgeAccess,
6
7
  BridgeConfirm,
7
8
  BridgeControl,
@@ -100,6 +101,8 @@ export const bridgeFieldSchema = z.object({
100
101
  max: z.number().optional(),
101
102
  step: z.number().optional(),
102
103
  maxLength: z.int().positive().optional(),
104
+ pattern: z.string().max(BRIDGE_PATTERN_LENGTH_LIMIT).optional(),
105
+ patternHint: panelTextSchema.optional(),
103
106
  options: z
104
107
  .union([
105
108
  z.array(bridgeOptionSchema).max(64),