@serverkgg/bridge 0.0.8 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/package.json +3 -3
  2. package/src/cli/validate.ts +2 -0
  3. package/src/context/bridgeContext.ts +17 -0
  4. package/src/context/index.ts +1 -0
  5. package/src/core/bridgeDownloadError.ts +10 -0
  6. package/src/core/bridgeHold.ts +12 -0
  7. package/src/core/bridgeKind.ts +3 -0
  8. package/src/core/bridgeProbeError.ts +16 -0
  9. package/src/core/bridgeSetup.ts +20 -0
  10. package/src/core/bridgeUserError.test.ts +23 -0
  11. package/src/core/bridgeUserError.ts +12 -0
  12. package/src/core/bridgeValue.test.ts +29 -0
  13. package/src/core/bridgeValue.ts +12 -0
  14. package/src/core/index.ts +5 -0
  15. package/src/guides/blocks.ts +8 -0
  16. package/src/guides/markdown.test.ts +18 -0
  17. package/src/guides/markdown.ts +7 -1
  18. package/src/manifest/index.ts +1 -0
  19. package/src/manifest/manifest.test.ts +132 -0
  20. package/src/manifest/manifest.ts +21 -0
  21. package/src/manifest/presence.ts +56 -0
  22. package/src/manifest/primitives.ts +6 -0
  23. package/src/modules/bridge.ts +65 -2
  24. package/src/modules/bridgeDriver.ts +4 -0
  25. package/src/modules/bridgeModule.ts +32 -3
  26. package/src/panel/bridgeDetail.ts +49 -0
  27. package/src/panel/bridgeLayout.ts +1 -0
  28. package/src/panel/bridgePanel.ts +30 -0
  29. package/src/panel/bridgeUploadMode.ts +4 -0
  30. package/src/panel/index.ts +2 -0
  31. package/src/probes/a2s.test.ts +71 -0
  32. package/src/probes/a2s.ts +38 -11
  33. package/src/protocol/bridgeEgress.ts +8 -1
  34. package/src/protocol/bridgeEvents.ts +4 -0
  35. package/src/protocol/bridgeFrame.ts +33 -1
  36. package/src/protocol/bridgeLimits.ts +12 -0
  37. package/src/protocol/bridgeManifest.ts +2 -0
  38. package/src/protocol/bridgeSetup.ts +97 -0
  39. package/src/protocol/index.ts +1 -0
  40. package/src/runtime/bridgeActions.ts +7 -0
  41. package/src/runtime/bridgeConfigReader.ts +12 -8
  42. package/src/runtime/bridgeContext.ts +28 -2
  43. package/src/runtime/bridgeDriver.test.ts +748 -3
  44. package/src/runtime/bridgeDriver.ts +198 -4
  45. package/src/runtime/bridgeEgress.ts +3 -0
  46. package/src/runtime/bridgeEvents.test.ts +96 -0
  47. package/src/runtime/bridgeEvents.ts +11 -6
  48. package/src/runtime/bridgeFiles.test.ts +625 -15
  49. package/src/runtime/bridgeFiles.ts +152 -45
  50. package/src/runtime/bridgeInstall.test.ts +118 -0
  51. package/src/runtime/bridgeInstall.ts +55 -0
  52. package/src/runtime/bridgeNet.ts +32 -3
  53. package/src/runtime/bridgePending.test.ts +148 -0
  54. package/src/runtime/bridgePending.ts +43 -0
  55. package/src/runtime/bridgeRegistry.test.ts +172 -1
  56. package/src/runtime/bridgeRegistry.ts +40 -5
  57. package/src/runtime/bridgeSecrets.test.ts +121 -0
  58. package/src/runtime/bridgeSecrets.ts +43 -0
  59. package/src/runtime/bridgeServer.test.ts +598 -0
  60. package/src/runtime/bridgeServer.ts +42 -3
  61. package/src/runtime/bridgeSetup.test.ts +189 -0
  62. package/src/runtime/bridgeSetup.ts +63 -0
  63. package/src/runtime/bridgeSignal.test.ts +114 -0
  64. package/src/runtime/bridgeSignal.ts +31 -0
  65. package/src/runtime/bridgeSupervisor.test.ts +111 -0
  66. package/src/runtime/bridgeSupervisor.ts +55 -5
  67. package/src/runtime/bridgeZip.ts +179 -58
  68. package/src/runtime/serve.ts +115 -4
  69. package/src/utils/index.ts +1 -0
  70. package/src/utils/stripAnsi.ts +12 -0
  71. package/src/wire/panel.test.ts +259 -0
  72. package/src/wire/panel.ts +72 -0
@@ -0,0 +1,598 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ BridgeDesired,
7
+ BridgeFailureCode,
8
+ BridgeFailureError,
9
+ BridgeRuntime,
10
+ BridgeSetupPromptKind,
11
+ BridgeSetupStepState,
12
+ BridgeUserError,
13
+ } from "../core";
14
+ import {
15
+ BridgeMethod,
16
+ type BridgePendingResult,
17
+ type BridgeResponse,
18
+ type BridgeSetupResult,
19
+ type BridgeSetupRuntime,
20
+ type BridgeValues,
21
+ } from "../protocol";
22
+ import { createServer, type ServerOptions } from "./bridgeServer";
23
+ import { failedSetup, unsupportedSetup } from "./bridgeSetup";
24
+
25
+ const TOKEN = "test-token";
26
+
27
+ const unreachable = (what: string) => {
28
+ return () => {
29
+ throw new Error(`${what} is not part of this test`);
30
+ };
31
+ };
32
+
33
+ const baseOptions = (socket: string): ServerOptions => ({
34
+ socket,
35
+ token: TOKEN,
36
+ onStatus: () => ({
37
+ version: "0.0.0",
38
+ runtime: BridgeRuntime.Stopped,
39
+ desired: BridgeDesired.Stopped,
40
+ exitCode: null,
41
+ outOfMemory: false,
42
+ restarts: 0,
43
+ installError: null,
44
+ installErrorText: null,
45
+ installHold: null,
46
+ setup: null,
47
+ }),
48
+ onManifest: unreachable("manifest"),
49
+ onStart: unreachable("start"),
50
+ onStop: unreachable("stop"),
51
+ onCommand: unreachable("command"),
52
+ onInvoke: unreachable("invoke"),
53
+ onEvents: unreachable("events"),
54
+ onSample: unreachable("sample"),
55
+ onQuiesce: unreachable("quiesce"),
56
+ onRelease: unreachable("release"),
57
+ onDescribe: unreachable("describe"),
58
+ onAnnounce: unreachable("announce"),
59
+ onPendingList: unreachable("pendingList"),
60
+ onPendingAdd: unreachable("pendingAdd"),
61
+ onSetupStatus: unreachable("setupStatus"),
62
+ onSetupSubmit: unreachable("setupSubmit"),
63
+ onLog: () => {
64
+ return;
65
+ },
66
+ });
67
+
68
+ const call = async (socket: string, frame: Record<string, unknown>): Promise<BridgeResponse> => {
69
+ return await new Promise<BridgeResponse>((resolve, reject) => {
70
+ void Bun.connect<{
71
+ buffer: string;
72
+ }>({
73
+ unix: socket,
74
+ socket: {
75
+ open(connection) {
76
+ connection.data = {
77
+ buffer: "",
78
+ };
79
+
80
+ connection.write(`${JSON.stringify(frame)}\n`);
81
+ },
82
+
83
+ data(connection, chunk) {
84
+ connection.data.buffer += chunk.toString();
85
+
86
+ const [line] = connection.data.buffer.split("\n");
87
+
88
+ if (!line) {
89
+ return;
90
+ }
91
+
92
+ connection.end();
93
+
94
+ resolve(JSON.parse(line) as BridgeResponse);
95
+ },
96
+
97
+ error(_connection, error) {
98
+ reject(error);
99
+ },
100
+ },
101
+ }).catch(reject);
102
+ });
103
+ };
104
+
105
+ describe("pending dispatch", () => {
106
+ let dir = "";
107
+ let socket = "";
108
+
109
+ beforeEach(async () => {
110
+ dir = await mkdtemp(join(tmpdir(), "bridge-server-"));
111
+ socket = join(dir, "bridge.sock");
112
+ });
113
+
114
+ afterEach(async () => {
115
+ await rm(dir, {
116
+ force: true,
117
+ recursive: true,
118
+ });
119
+ });
120
+
121
+ const listed: BridgePendingResult = {
122
+ supported: true,
123
+ staging: ".serverk-staging/pending-upload",
124
+ extensions: [
125
+ "jar",
126
+ ],
127
+ files: [],
128
+ rejected: null,
129
+ };
130
+
131
+ test("answers pendingList with what the driver lists", async () => {
132
+ const server = createServer({
133
+ ...baseOptions(socket),
134
+ onPendingList: async () => listed,
135
+ });
136
+
137
+ const response = await call(socket, {
138
+ id: 1,
139
+ token: TOKEN,
140
+ method: BridgeMethod.PendingList,
141
+ });
142
+
143
+ server.stop(true);
144
+
145
+ expect(response).toEqual({
146
+ id: 1,
147
+ ok: true,
148
+ result: listed,
149
+ });
150
+ });
151
+
152
+ test("answers pendingList with the unsupported sentinel when the driver declares no pending module", async () => {
153
+ const server = createServer({
154
+ ...baseOptions(socket),
155
+ onPendingList: async () => ({
156
+ supported: false,
157
+ staging: "",
158
+ extensions: [],
159
+ files: [],
160
+ rejected: null,
161
+ }),
162
+ });
163
+
164
+ const response = await call(socket, {
165
+ id: 2,
166
+ token: TOKEN,
167
+ method: BridgeMethod.PendingList,
168
+ });
169
+
170
+ server.stop(true);
171
+
172
+ expect(response).toEqual({
173
+ id: 2,
174
+ ok: true,
175
+ result: {
176
+ supported: false,
177
+ staging: "",
178
+ extensions: [],
179
+ files: [],
180
+ rejected: null,
181
+ },
182
+ });
183
+ });
184
+
185
+ test("passes the uploaded path to pendingAdd and answers with the refreshed list", async () => {
186
+ const received: string[] = [];
187
+
188
+ const server = createServer({
189
+ ...baseOptions(socket),
190
+ onPendingAdd: async (input) => {
191
+ received.push(input);
192
+
193
+ return listed;
194
+ },
195
+ });
196
+
197
+ const response = await call(socket, {
198
+ id: 3,
199
+ token: TOKEN,
200
+ method: BridgeMethod.PendingAdd,
201
+ input: ".serverk-staging/pending-upload/patch.jar",
202
+ });
203
+
204
+ server.stop(true);
205
+
206
+ expect(received).toEqual([
207
+ ".serverk-staging/pending-upload/patch.jar",
208
+ ]);
209
+ expect(response).toEqual({
210
+ id: 3,
211
+ ok: true,
212
+ result: listed,
213
+ });
214
+ });
215
+
216
+ test("refuses a pendingAdd carrying no path", async () => {
217
+ const server = createServer({
218
+ ...baseOptions(socket),
219
+ onPendingAdd: unreachable("pendingAdd"),
220
+ });
221
+
222
+ const response = await call(socket, {
223
+ id: 4,
224
+ token: TOKEN,
225
+ method: BridgeMethod.PendingAdd,
226
+ });
227
+
228
+ server.stop(true);
229
+
230
+ expect(response).toEqual({
231
+ id: 4,
232
+ ok: false,
233
+ error: "pendingAdd needs an input path",
234
+ });
235
+ });
236
+
237
+ test("answers pendingAdd with the rejection the driver reported beside the refreshed list", async () => {
238
+ const rejected: BridgePendingResult = {
239
+ ...listed,
240
+ rejected: {
241
+ expected: [
242
+ "patch.jar",
243
+ ],
244
+ },
245
+ };
246
+
247
+ const server = createServer({
248
+ ...baseOptions(socket),
249
+ onPendingAdd: async () => rejected,
250
+ });
251
+
252
+ const response = await call(socket, {
253
+ id: 6,
254
+ token: TOKEN,
255
+ method: BridgeMethod.PendingAdd,
256
+ input: ".serverk-staging/pending-upload/wrong.jar",
257
+ });
258
+
259
+ server.stop(true);
260
+
261
+ expect(response).toEqual({
262
+ id: 6,
263
+ ok: true,
264
+ result: rejected,
265
+ });
266
+ });
267
+
268
+ test("names an unknown method so an older agent can degrade", async () => {
269
+ const server = createServer(baseOptions(socket));
270
+
271
+ const response = await call(socket, {
272
+ id: 5,
273
+ token: TOKEN,
274
+ method: "pendingRemove",
275
+ });
276
+
277
+ server.stop(true);
278
+
279
+ expect(response).toEqual({
280
+ id: 5,
281
+ ok: false,
282
+ error: 'unknown method "pendingRemove"',
283
+ });
284
+ });
285
+ });
286
+
287
+ describe("setup dispatch", () => {
288
+ let dir = "";
289
+ let socket = "";
290
+
291
+ beforeEach(async () => {
292
+ dir = await mkdtemp(join(tmpdir(), "bridge-server-"));
293
+ socket = join(dir, "bridge.sock");
294
+ });
295
+
296
+ afterEach(async () => {
297
+ await rm(dir, {
298
+ force: true,
299
+ recursive: true,
300
+ });
301
+ });
302
+
303
+ const runtime: BridgeSetupRuntime = {
304
+ steps: [
305
+ {
306
+ id: "sign-in",
307
+ state: BridgeSetupStepState.Active,
308
+ },
309
+ ],
310
+ prompt: {
311
+ kind: BridgeSetupPromptKind.Link,
312
+ message: {
313
+ ar: "سجل دخولك",
314
+ en: "sign in with your account",
315
+ },
316
+ url: "https://hytale.com/device",
317
+ code: "ABCD-EFGH",
318
+ expiresAt: null,
319
+ actions: [
320
+ {
321
+ id: "renew",
322
+ label: {
323
+ ar: "كود جديد",
324
+ en: "Get a new code",
325
+ },
326
+ },
327
+ ],
328
+ },
329
+ };
330
+
331
+ const reported: BridgeSetupResult = {
332
+ supported: true,
333
+ runtime,
334
+ };
335
+
336
+ test("answers setupStatus with what the driver reported", async () => {
337
+ const server = createServer({
338
+ ...baseOptions(socket),
339
+ onSetupStatus: () => reported,
340
+ });
341
+
342
+ const response = await call(socket, {
343
+ id: 1,
344
+ token: TOKEN,
345
+ method: BridgeMethod.SetupStatus,
346
+ });
347
+
348
+ server.stop(true);
349
+
350
+ expect(response).toEqual({
351
+ id: 1,
352
+ ok: true,
353
+ result: reported,
354
+ });
355
+ });
356
+
357
+ test("answers setupStatus with the unsupported sentinel when the driver declares no setup", async () => {
358
+ const server = createServer({
359
+ ...baseOptions(socket),
360
+ onSetupStatus: () => unsupportedSetup(),
361
+ });
362
+
363
+ const response = await call(socket, {
364
+ id: 2,
365
+ token: TOKEN,
366
+ method: BridgeMethod.SetupStatus,
367
+ });
368
+
369
+ server.stop(true);
370
+
371
+ expect(response).toEqual({
372
+ id: 2,
373
+ ok: true,
374
+ result: {
375
+ supported: false,
376
+ runtime: null,
377
+ },
378
+ });
379
+ });
380
+
381
+ test("passes the step, action and args of a setupSubmit to the driver", async () => {
382
+ const received: {
383
+ step: string;
384
+ action: string;
385
+ args: BridgeValues;
386
+ }[] = [];
387
+
388
+ const server = createServer({
389
+ ...baseOptions(socket),
390
+ onSetupSubmit: async (step, action, args) => {
391
+ received.push({
392
+ step,
393
+ action,
394
+ args,
395
+ });
396
+
397
+ return reported;
398
+ },
399
+ });
400
+
401
+ const response = await call(socket, {
402
+ id: 3,
403
+ token: TOKEN,
404
+ method: BridgeMethod.SetupSubmit,
405
+ module: "sign-in",
406
+ action: "renew",
407
+ args: {
408
+ force: true,
409
+ },
410
+ });
411
+
412
+ server.stop(true);
413
+
414
+ expect(received).toEqual([
415
+ {
416
+ step: "sign-in",
417
+ action: "renew",
418
+ args: {
419
+ force: true,
420
+ },
421
+ },
422
+ ]);
423
+ expect(response).toEqual({
424
+ id: 3,
425
+ ok: true,
426
+ result: reported,
427
+ });
428
+ });
429
+
430
+ test("refuses a setupSubmit carrying no step id", async () => {
431
+ const server = createServer({
432
+ ...baseOptions(socket),
433
+ onSetupSubmit: unreachable("setupSubmit"),
434
+ });
435
+
436
+ const response = await call(socket, {
437
+ id: 4,
438
+ token: TOKEN,
439
+ method: BridgeMethod.SetupSubmit,
440
+ action: "renew",
441
+ });
442
+
443
+ server.stop(true);
444
+
445
+ expect(response).toEqual({
446
+ id: 4,
447
+ ok: false,
448
+ error: "setupSubmit needs a step id",
449
+ });
450
+ });
451
+
452
+ test("answers a rejected setupSubmit with a failed prompt instead of a failed call", async () => {
453
+ const server = createServer({
454
+ ...baseOptions(socket),
455
+ onSetupSubmit: async () =>
456
+ failedSetup(runtime, {
457
+ ar: "الكود ما عاد ينفع",
458
+ en: "this code cannot be renewed any more",
459
+ }),
460
+ });
461
+
462
+ const response = await call(socket, {
463
+ id: 5,
464
+ token: TOKEN,
465
+ method: BridgeMethod.SetupSubmit,
466
+ module: "sign-in",
467
+ action: "renew",
468
+ });
469
+
470
+ server.stop(true);
471
+
472
+ expect(response).toEqual({
473
+ id: 5,
474
+ ok: true,
475
+ result: {
476
+ supported: true,
477
+ runtime: {
478
+ steps: runtime.steps,
479
+ prompt: {
480
+ kind: BridgeSetupPromptKind.Failed,
481
+ message: {
482
+ ar: "الكود ما عاد ينفع",
483
+ en: "this code cannot be renewed any more",
484
+ },
485
+ actions: [
486
+ {
487
+ id: "renew",
488
+ label: {
489
+ ar: "كود جديد",
490
+ en: "Get a new code",
491
+ },
492
+ },
493
+ ],
494
+ },
495
+ },
496
+ },
497
+ });
498
+ });
499
+
500
+ test("answers setupSubmit with the unsupported sentinel when the driver implements no submit", async () => {
501
+ const server = createServer({
502
+ ...baseOptions(socket),
503
+ onSetupSubmit: async () => unsupportedSetup(),
504
+ });
505
+
506
+ const response = await call(socket, {
507
+ id: 6,
508
+ token: TOKEN,
509
+ method: BridgeMethod.SetupSubmit,
510
+ module: "sign-in",
511
+ action: "renew",
512
+ });
513
+
514
+ server.stop(true);
515
+
516
+ expect(response).toEqual({
517
+ id: 6,
518
+ ok: true,
519
+ result: {
520
+ supported: false,
521
+ runtime: null,
522
+ },
523
+ });
524
+ });
525
+ });
526
+
527
+ describe("error serialisation", () => {
528
+ let dir = "";
529
+ let socket = "";
530
+
531
+ beforeEach(async () => {
532
+ dir = await mkdtemp(join(tmpdir(), "bridge-server-"));
533
+ socket = join(dir, "bridge.sock");
534
+ });
535
+
536
+ afterEach(async () => {
537
+ await rm(dir, {
538
+ force: true,
539
+ recursive: true,
540
+ });
541
+ });
542
+
543
+ const thrown = async (error: unknown) => {
544
+ const server = createServer({
545
+ ...baseOptions(socket),
546
+ onPendingAdd: async () => {
547
+ throw error;
548
+ },
549
+ });
550
+
551
+ const response = await call(socket, {
552
+ id: 1,
553
+ token: TOKEN,
554
+ method: BridgeMethod.PendingAdd,
555
+ input: ".serverk-staging/pending-upload/patch.jar",
556
+ });
557
+
558
+ server.stop(true);
559
+
560
+ return response;
561
+ };
562
+
563
+ test("carries the localized text of a user error beside the message", async () => {
564
+ expect(
565
+ await thrown(
566
+ new BridgeUserError({
567
+ ar: "هذا مو الملف اللي ننتظره",
568
+ en: "this is not a file we are waiting for",
569
+ }),
570
+ ),
571
+ ).toEqual({
572
+ id: 1,
573
+ ok: false,
574
+ error: "this is not a file we are waiting for",
575
+ text: {
576
+ ar: "هذا مو الملف اللي ننتظره",
577
+ en: "this is not a file we are waiting for",
578
+ },
579
+ });
580
+ });
581
+
582
+ test("carries the failure code of a failure error and no text", async () => {
583
+ expect(await thrown(new BridgeFailureError(BridgeFailureCode.CatalogRestricted, "the author blocked it"))).toEqual({
584
+ id: 1,
585
+ ok: false,
586
+ error: "the author blocked it",
587
+ failure: BridgeFailureCode.CatalogRestricted,
588
+ });
589
+ });
590
+
591
+ test("carries the message alone for an ordinary error", async () => {
592
+ expect(await thrown(new Error("the file could not be read"))).toEqual({
593
+ id: 1,
594
+ ok: false,
595
+ error: "the file could not be read",
596
+ });
597
+ });
598
+ });
@@ -1,5 +1,5 @@
1
1
  import { timingSafeEqual } from "node:crypto";
2
- import { BridgeFailureError, BridgeSecretError, type BridgeValues } from "../core";
2
+ import { BridgeFailureError, BridgeSecretError, BridgeUserError, type BridgeValues } from "../core";
3
3
  import {
4
4
  BRIDGE_FRAME_LIMIT,
5
5
  type BridgeAnnounceResult,
@@ -8,9 +8,11 @@ import {
8
8
  type BridgeEventsResult,
9
9
  type BridgeManifest,
10
10
  BridgeMethod,
11
+ type BridgePendingResult,
11
12
  type BridgeQuiesceResult,
12
13
  type BridgeResponse,
13
14
  type BridgeSampleResult,
15
+ type BridgeSetupResult,
14
16
  type BridgeStatus,
15
17
  } from "../protocol";
16
18
  import { type BridgeLogger, errorText } from "./bridgeLog";
@@ -30,6 +32,10 @@ export interface ServerOptions {
30
32
  onRelease: () => Promise<void>;
31
33
  onDescribe: () => Promise<BridgeDescribeResult>;
32
34
  onAnnounce: (message: string) => Promise<BridgeAnnounceResult>;
35
+ onPendingList: () => Promise<BridgePendingResult>;
36
+ onPendingAdd: (input: string) => Promise<BridgePendingResult>;
37
+ onSetupStatus: () => BridgeSetupResult;
38
+ onSetupSubmit: (step: string, action: string, args: BridgeValues) => Promise<BridgeSetupResult>;
33
39
  onLog: BridgeLogger;
34
40
  }
35
41
 
@@ -47,7 +53,7 @@ interface IncomingFrame {
47
53
  message?: unknown;
48
54
  }
49
55
 
50
- const asMessage = (value: unknown): string | null => {
56
+ const asText = (value: unknown): string | null => {
51
57
  return typeof value === "string" && value.length > 0 ? value : null;
52
58
  };
53
59
 
@@ -136,7 +142,7 @@ export const createServer = (options: ServerOptions) => {
136
142
  }
137
143
 
138
144
  case BridgeMethod.Announce: {
139
- const message = asMessage(request.message);
145
+ const message = asText(request.message);
140
146
 
141
147
  if (!message) {
142
148
  throw new Error("announce needs a message");
@@ -145,6 +151,34 @@ export const createServer = (options: ServerOptions) => {
145
151
  return await options.onAnnounce(message);
146
152
  }
147
153
 
154
+ case BridgeMethod.PendingList: {
155
+ return await options.onPendingList();
156
+ }
157
+
158
+ case BridgeMethod.PendingAdd: {
159
+ const input = asText(request.input);
160
+
161
+ if (!input) {
162
+ throw new Error("pendingAdd needs an input path");
163
+ }
164
+
165
+ return await options.onPendingAdd(input);
166
+ }
167
+
168
+ case BridgeMethod.SetupStatus: {
169
+ return options.onSetupStatus();
170
+ }
171
+
172
+ case BridgeMethod.SetupSubmit: {
173
+ const step = asText(request.module);
174
+
175
+ if (!step) {
176
+ throw new Error("setupSubmit needs a step id");
177
+ }
178
+
179
+ return await options.onSetupSubmit(step, String(request.action ?? ""), asArgs(request.args));
180
+ }
181
+
148
182
  default: {
149
183
  throw new Error(`unknown method "${method}"`);
150
184
  }
@@ -249,6 +283,11 @@ export const createServer = (options: ServerOptions) => {
249
283
  failure: error.code,
250
284
  }
251
285
  : {}),
286
+ ...(error instanceof BridgeUserError
287
+ ? {
288
+ text: error.text,
289
+ }
290
+ : {}),
252
291
  }),
253
292
  );
254
293
  });