@serverkgg/bridge 0.0.10 → 0.0.12

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.
@@ -9,6 +9,7 @@ import { GuideOpenTab } from "../guides";
9
9
  import type { ManifestPort } from "../manifest";
10
10
  import type { BridgeDriver, BridgePanelModule } from "../modules";
11
11
  import {
12
+ BridgeControl,
12
13
  type BridgeField,
13
14
  BridgeFormTarget,
14
15
  BridgeLayout,
@@ -32,6 +33,7 @@ import {
32
33
  BRIDGE_MODULES_LIMIT,
33
34
  BRIDGE_OPTION_VALUE_LENGTH_LIMIT,
34
35
  BRIDGE_PANEL_TABS_LIMIT,
36
+ BRIDGE_PATTERN_LENGTH_LIMIT,
35
37
  BRIDGE_PLACEHOLDER_LENGTH_LIMIT,
36
38
  BRIDGE_ROW_ACTIONS_LIMIT,
37
39
  BRIDGE_SECTION_ACTIONS_LIMIT,
@@ -121,6 +123,110 @@ const moduleOf = (modules: Record<string, BridgePanelModule>, id: string, where:
121
123
  return module as BridgePanelModule;
122
124
  };
123
125
 
126
+ const compilePattern = (pattern: string): RegExp | null => {
127
+ try {
128
+ return new RegExp(pattern);
129
+ } catch {
130
+ return null;
131
+ }
132
+ };
133
+
134
+ const OPEN_ENDED_REPEAT = /^\{\d+,\}/;
135
+
136
+ const repeatsWithoutBound = (source: string): boolean => {
137
+ let index = 0;
138
+ let inClass = false;
139
+
140
+ while (index < source.length) {
141
+ const char = source[index];
142
+
143
+ if (char === "\\") {
144
+ index += 2;
145
+
146
+ continue;
147
+ }
148
+
149
+ if (inClass) {
150
+ inClass = char !== "]";
151
+ index += 1;
152
+
153
+ continue;
154
+ }
155
+
156
+ if (char === "[") {
157
+ inClass = true;
158
+ index += 1;
159
+
160
+ continue;
161
+ }
162
+
163
+ if (char === "*" || char === "+" || (char === "{" && OPEN_ENDED_REPEAT.test(source.slice(index)))) {
164
+ return true;
165
+ }
166
+
167
+ index += 1;
168
+ }
169
+
170
+ return false;
171
+ };
172
+
173
+ const nestsQuantifiers = (pattern: string): boolean => {
174
+ const opened: number[] = [];
175
+
176
+ let index = 0;
177
+ let inClass = false;
178
+
179
+ while (index < pattern.length) {
180
+ const char = pattern[index];
181
+
182
+ if (char === "\\") {
183
+ index += 2;
184
+
185
+ continue;
186
+ }
187
+
188
+ if (inClass) {
189
+ inClass = char !== "]";
190
+ index += 1;
191
+
192
+ continue;
193
+ }
194
+
195
+ if (char === "[") {
196
+ inClass = true;
197
+ index += 1;
198
+
199
+ continue;
200
+ }
201
+
202
+ if (char === "(") {
203
+ opened.push(index);
204
+ index += 1;
205
+
206
+ continue;
207
+ }
208
+
209
+ if (char === ")") {
210
+ const start = opened.pop();
211
+
212
+ index += 1;
213
+
214
+ const quantified =
215
+ pattern[index] === "*" || pattern[index] === "+" || OPEN_ENDED_REPEAT.test(pattern.slice(index));
216
+
217
+ if (start !== undefined && quantified && repeatsWithoutBound(pattern.slice(start + 1, index - 1))) {
218
+ return true;
219
+ }
220
+
221
+ continue;
222
+ }
223
+
224
+ index += 1;
225
+ }
226
+
227
+ return false;
228
+ };
229
+
124
230
  const validateVisibleWhen = (visibleWhen: BridgeVisibleWhen | undefined, where: string) => {
125
231
  if (!visibleWhen) {
126
232
  return;
@@ -149,12 +255,39 @@ const validateFields = (modules: Record<string, BridgePanelModule>, fields: Brid
149
255
  validateText(field.label, "label", at);
150
256
  validateText(field.help, "help", at);
151
257
  validateText(field.warning, "warning", at);
258
+ validateText(field.patternHint, "pattern hint", at);
152
259
  validateVisibleWhen(field.visibleWhen, at);
153
260
 
154
261
  if (field.placeholder) {
155
262
  lengthIn(field.placeholder, BRIDGE_PLACEHOLDER_LENGTH_LIMIT, "placeholder", at);
156
263
  }
157
264
 
265
+ if (field.pattern !== undefined) {
266
+ if (field.control !== BridgeControl.Text && field.control !== BridgeControl.Secret) {
267
+ fail(`${at} declares a pattern on a "${field.control}" control — only text and secret controls match one`);
268
+ }
269
+
270
+ lengthIn(field.pattern, BRIDGE_PATTERN_LENGTH_LIMIT, "pattern", at);
271
+
272
+ if (!compilePattern(field.pattern)) {
273
+ fail(`${at} declares pattern "${field.pattern}", which is not a valid regular expression`);
274
+ }
275
+
276
+ if (nestsQuantifiers(field.pattern)) {
277
+ fail(
278
+ `${at} declares pattern "${field.pattern}", which repeats a group that already repeats — a value can make it run for minutes`,
279
+ );
280
+ }
281
+ }
282
+
283
+ if (field.patternHint && field.pattern === undefined) {
284
+ fail(`${at} declares a pattern hint with no pattern to explain`);
285
+ }
286
+
287
+ if (field.control === BridgeControl.Secret && field.options) {
288
+ fail(`${at} is a secret control with options — a secret is typed, never chosen from a list`);
289
+ }
290
+
158
291
  if (Array.isArray(field.options)) {
159
292
  countIn(field.options.length, BRIDGE_FIELD_OPTIONS_LIMIT, "options", at);
160
293
 
@@ -515,6 +648,10 @@ const validateSetup = (driver: BridgeDriver) => {
515
648
  }
516
649
  }
517
650
  }
651
+
652
+ if (!setup.submit && setup.steps.some((step) => step.kind === BridgeSetupStepKind.Driver)) {
653
+ fail(`${where} declares a driver step but exports no submit — a driver step is answered through submit`);
654
+ }
518
655
  };
519
656
 
520
657
  const validateTab = (modules: Record<string, BridgePanelModule>, tab: BridgeTab) => {
@@ -1,5 +1,5 @@
1
1
  import type { BridgeProbes } from "../context";
2
- import { probeA2s } from "../probes";
2
+ import { probeA2s, probeMinecraftPing, probeRaknetPing } from "../probes";
3
3
 
4
4
  const PROBE_HOST = "127.0.0.1";
5
5
 
@@ -10,5 +10,17 @@ export const createBridgeProbes = (): BridgeProbes => {
10
10
  async a2s(port) {
11
11
  return await probeA2s(PROBE_HOST, port, PROBE_TIMEOUT_MS);
12
12
  },
13
+ async minecraftPing(port) {
14
+ return await probeMinecraftPing(
15
+ {
16
+ address: PROBE_HOST,
17
+ port,
18
+ },
19
+ PROBE_TIMEOUT_MS,
20
+ );
21
+ },
22
+ async raknetPing(port) {
23
+ return await probeRaknetPing(PROBE_HOST, port, PROBE_TIMEOUT_MS);
24
+ },
13
25
  };
14
26
  };
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { BridgeSetupPromptKind, BridgeSetupStepState } from "../core";
3
3
  import type { BridgeSetupRuntime } from "../protocol";
4
- import { failedSetup, setupSummaryOf, unsupportedSetup } from "./bridgeSetup";
4
+ import { failedSetup, normalizeSetupRuntime, setupSummaryOf, unsupportedSetup } from "./bridgeSetup";
5
5
 
6
6
  const text = (value: string) => ({
7
7
  ar: value,
@@ -187,3 +187,389 @@ describe("a rejected submit", () => {
187
187
  });
188
188
  });
189
189
  });
190
+
191
+ const declared: ReadonlySet<string> = new Set([
192
+ "sign-in",
193
+ "download",
194
+ ]);
195
+
196
+ const normalizing = (runtime: BridgeSetupRuntime | null) => {
197
+ const reasons: string[] = [];
198
+
199
+ const normalized = normalizeSetupRuntime(runtime, {
200
+ declared,
201
+ warn: (_message, fields) => {
202
+ reasons.push(String(fields?.reason));
203
+ },
204
+ });
205
+
206
+ return {
207
+ normalized,
208
+ reasons,
209
+ };
210
+ };
211
+
212
+ const withSteps = (steps: BridgeSetupRuntime["steps"]): BridgeSetupRuntime => ({
213
+ steps,
214
+ prompt: null,
215
+ });
216
+
217
+ const link = (
218
+ prompt: Partial<
219
+ Extract<
220
+ BridgeSetupRuntime["prompt"],
221
+ {
222
+ kind: BridgeSetupPromptKind.Link;
223
+ }
224
+ >
225
+ >,
226
+ ) => {
227
+ return runtimeWith({
228
+ kind: BridgeSetupPromptKind.Link,
229
+ message: text("sign in with your account"),
230
+ url: "https://hytale.com/device",
231
+ code: "ABCD-EFGH",
232
+ expiresAt: null,
233
+ actions: [],
234
+ ...prompt,
235
+ });
236
+ };
237
+
238
+ describe("normalizing a reported runtime", () => {
239
+ test("has nothing to normalize while the driver reported no runtime", () => {
240
+ expect(
241
+ normalizeSetupRuntime(null, {
242
+ declared,
243
+ warn: () => {
244
+ return;
245
+ },
246
+ }),
247
+ ).toBeNull();
248
+ });
249
+
250
+ test("drops a step the driver never declared", () => {
251
+ const { normalized, reasons } = normalizing(
252
+ withSteps([
253
+ {
254
+ id: "sign-in",
255
+ state: BridgeSetupStepState.Active,
256
+ },
257
+ {
258
+ id: "invent",
259
+ state: BridgeSetupStepState.Pending,
260
+ },
261
+ ]),
262
+ );
263
+
264
+ expect(normalized?.steps).toEqual([
265
+ {
266
+ id: "sign-in",
267
+ state: BridgeSetupStepState.Active,
268
+ },
269
+ ]);
270
+ expect(reasons).toEqual([
271
+ "unknown-step",
272
+ ]);
273
+ });
274
+
275
+ test("drops the second report of the same step", () => {
276
+ const { normalized, reasons } = normalizing(
277
+ withSteps([
278
+ {
279
+ id: "sign-in",
280
+ state: BridgeSetupStepState.Done,
281
+ },
282
+ {
283
+ id: "sign-in",
284
+ state: BridgeSetupStepState.Failed,
285
+ },
286
+ ]),
287
+ );
288
+
289
+ expect(normalized?.steps).toEqual([
290
+ {
291
+ id: "sign-in",
292
+ state: BridgeSetupStepState.Done,
293
+ },
294
+ ]);
295
+ expect(reasons).toEqual([
296
+ "duplicate-step",
297
+ ]);
298
+ });
299
+
300
+ test("keeps the first active step and pends the rest", () => {
301
+ const { normalized, reasons } = normalizing(
302
+ withSteps([
303
+ {
304
+ id: "sign-in",
305
+ state: BridgeSetupStepState.Active,
306
+ },
307
+ {
308
+ id: "download",
309
+ state: BridgeSetupStepState.Active,
310
+ },
311
+ ]),
312
+ );
313
+
314
+ expect(normalized?.steps).toEqual([
315
+ {
316
+ id: "sign-in",
317
+ state: BridgeSetupStepState.Active,
318
+ },
319
+ {
320
+ id: "download",
321
+ state: BridgeSetupStepState.Pending,
322
+ },
323
+ ]);
324
+ expect(reasons).toEqual([
325
+ "extra-active-step",
326
+ ]);
327
+ });
328
+
329
+ test("clamps a percent that left the scale", () => {
330
+ expect(
331
+ normalizing(
332
+ runtimeWith({
333
+ kind: BridgeSetupPromptKind.Progress,
334
+ message: text("downloading the game"),
335
+ percent: 140,
336
+ detail: null,
337
+ }),
338
+ ),
339
+ ).toEqual({
340
+ normalized: {
341
+ steps: [
342
+ {
343
+ id: "sign-in",
344
+ state: BridgeSetupStepState.Active,
345
+ },
346
+ {
347
+ id: "download",
348
+ state: BridgeSetupStepState.Pending,
349
+ },
350
+ ],
351
+ prompt: {
352
+ kind: BridgeSetupPromptKind.Progress,
353
+ message: text("downloading the game"),
354
+ percent: 100,
355
+ detail: null,
356
+ },
357
+ },
358
+ reasons: [
359
+ "percent-out-of-range",
360
+ ],
361
+ });
362
+ });
363
+
364
+ test("drops a percent that is not a number", () => {
365
+ const { normalized, reasons } = normalizing(
366
+ runtimeWith({
367
+ kind: BridgeSetupPromptKind.Progress,
368
+ message: text("downloading the game"),
369
+ percent: Number.NaN,
370
+ detail: null,
371
+ }),
372
+ );
373
+
374
+ expect(normalized?.prompt).toEqual({
375
+ kind: BridgeSetupPromptKind.Progress,
376
+ message: text("downloading the game"),
377
+ percent: null,
378
+ detail: null,
379
+ });
380
+ expect(reasons).toEqual([
381
+ "unusable-percent",
382
+ ]);
383
+ });
384
+
385
+ test("cuts a detail line to the length the platform takes", () => {
386
+ const { normalized, reasons } = normalizing(
387
+ runtimeWith({
388
+ kind: BridgeSetupPromptKind.Progress,
389
+ message: text("downloading the game"),
390
+ percent: null,
391
+ detail: "d".repeat(300),
392
+ }),
393
+ );
394
+
395
+ expect(normalized?.prompt).toEqual({
396
+ kind: BridgeSetupPromptKind.Progress,
397
+ message: text("downloading the game"),
398
+ percent: null,
399
+ detail: "d".repeat(256),
400
+ });
401
+ expect(reasons).toEqual([
402
+ "long-detail",
403
+ ]);
404
+ });
405
+
406
+ test("drops an empty detail line", () => {
407
+ const { normalized, reasons } = normalizing(
408
+ runtimeWith({
409
+ kind: BridgeSetupPromptKind.Progress,
410
+ message: text("downloading the game"),
411
+ percent: null,
412
+ detail: "",
413
+ }),
414
+ );
415
+
416
+ expect(normalized?.prompt).toMatchObject({
417
+ detail: null,
418
+ });
419
+ expect(reasons).toEqual([
420
+ "empty-detail",
421
+ ]);
422
+ });
423
+
424
+ test("drops an action whose id is not kebab-case and the duplicate beside it", () => {
425
+ const { normalized, reasons } = normalizing(
426
+ runtimeWith({
427
+ kind: BridgeSetupPromptKind.Action,
428
+ message: text("get a new code"),
429
+ actions: [
430
+ {
431
+ id: "renew",
432
+ label: text("Get a new code"),
433
+ },
434
+ {
435
+ id: "Renew Now",
436
+ label: text("Get a new code"),
437
+ },
438
+ {
439
+ id: "renew",
440
+ label: text("Get a new code"),
441
+ },
442
+ ],
443
+ }),
444
+ );
445
+
446
+ expect(normalized?.prompt).toEqual({
447
+ kind: BridgeSetupPromptKind.Action,
448
+ message: text("get a new code"),
449
+ actions: [
450
+ {
451
+ id: "renew",
452
+ label: text("Get a new code"),
453
+ },
454
+ ],
455
+ });
456
+ expect(reasons).toEqual([
457
+ "unknown-action-id",
458
+ "duplicate-action",
459
+ ]);
460
+ });
461
+
462
+ test("keeps only as many actions as the platform renders", () => {
463
+ const { normalized, reasons } = normalizing(
464
+ runtimeWith({
465
+ kind: BridgeSetupPromptKind.Failed,
466
+ message: text("the sign-in expired"),
467
+ actions: Array.from({
468
+ length: 6,
469
+ }).map((_entry, index) => ({
470
+ id: `action-${index}`,
471
+ label: text(`Action ${index}`),
472
+ })),
473
+ }),
474
+ );
475
+
476
+ expect(normalized?.prompt?.kind).toBe(BridgeSetupPromptKind.Failed);
477
+ expect(normalized?.prompt && "actions" in normalized.prompt ? normalized.prompt.actions.length : 0).toBe(4);
478
+ expect(reasons).toEqual([
479
+ "extra-actions",
480
+ ]);
481
+ });
482
+
483
+ test("cuts a long code and drops an empty one", () => {
484
+ expect(
485
+ normalizing(
486
+ link({
487
+ code: "c".repeat(80),
488
+ }),
489
+ ).normalized?.prompt,
490
+ ).toMatchObject({
491
+ code: "c".repeat(64),
492
+ });
493
+
494
+ expect(
495
+ normalizing(
496
+ link({
497
+ code: "",
498
+ }),
499
+ ).reasons,
500
+ ).toEqual([
501
+ "empty-code",
502
+ ]);
503
+ });
504
+
505
+ test("re-emits an expiry as an iso stamp and drops one it cannot read", () => {
506
+ expect(
507
+ normalizing(
508
+ link({
509
+ expiresAt: "2026-09-04T10:00:00+03:00",
510
+ }),
511
+ ).normalized?.prompt,
512
+ ).toMatchObject({
513
+ expiresAt: "2026-09-04T07:00:00.000Z",
514
+ });
515
+
516
+ expect(
517
+ normalizing(
518
+ link({
519
+ expiresAt: "soon",
520
+ }),
521
+ ).reasons,
522
+ ).toEqual([
523
+ "unusable-expiry",
524
+ ]);
525
+ });
526
+
527
+ test("turns a link the browser cannot open into a failed prompt keeping its actions", () => {
528
+ const { normalized, reasons } = normalizing(
529
+ link({
530
+ url: "javascript:alert(1)",
531
+ actions: [
532
+ {
533
+ id: "renew",
534
+ label: text("Get a new code"),
535
+ },
536
+ ],
537
+ }),
538
+ );
539
+
540
+ expect(normalized?.prompt).toEqual({
541
+ kind: BridgeSetupPromptKind.Failed,
542
+ message: {
543
+ ar: "اللعبة رجّعت رابط دخول ما يشتغل. جرّب مرة ثانية.",
544
+ en: "The game reported a broken sign-in link. Try again.",
545
+ },
546
+ actions: [
547
+ {
548
+ id: "renew",
549
+ label: text("Get a new code"),
550
+ },
551
+ ],
552
+ });
553
+ expect(reasons).toEqual([
554
+ "unusable-link",
555
+ ]);
556
+ });
557
+
558
+ test("turns a url it cannot parse into a failed prompt", () => {
559
+ expect(
560
+ normalizing(
561
+ link({
562
+ url: "not a url",
563
+ }),
564
+ ).normalized?.prompt?.kind,
565
+ ).toBe(BridgeSetupPromptKind.Failed);
566
+ });
567
+
568
+ test("leaves a healthy runtime exactly as the driver reported it", () => {
569
+ const source = link({});
570
+ const { normalized, reasons } = normalizing(source);
571
+
572
+ expect(normalized).toEqual(source);
573
+ expect(reasons).toEqual([]);
574
+ });
575
+ });