@serverkgg/bridge 0.0.21 → 0.1.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.
Files changed (119) hide show
  1. package/package.json +3 -1
  2. package/src/bot/botDeploy.test.ts +363 -0
  3. package/src/bot/botDeploy.ts +136 -0
  4. package/src/bot/botEnvironment.test.ts +189 -0
  5. package/src/bot/botEnvironment.ts +141 -0
  6. package/src/bot/botIdentity.test.ts +251 -0
  7. package/src/bot/botIdentity.ts +140 -0
  8. package/src/bot/botPanel.test.ts +246 -0
  9. package/src/bot/botPanel.ts +129 -0
  10. package/src/bot/botState.test.ts +45 -0
  11. package/src/bot/botState.ts +54 -0
  12. package/src/bot/botTerminal.test.ts +79 -0
  13. package/src/bot/botTerminal.ts +62 -0
  14. package/src/bot/botVariables.test.ts +193 -0
  15. package/src/bot/botVariables.ts +73 -0
  16. package/src/bot/detect/botFramework.ts +23 -0
  17. package/src/bot/detect/botFrameworks.ts +198 -0
  18. package/src/bot/detect/botInspectWarning.ts +15 -0
  19. package/src/bot/detect/botPackageManager.ts +6 -0
  20. package/src/bot/detect/botPlatform.ts +10 -0
  21. package/src/bot/detect/botRuntime.ts +15 -0
  22. package/src/bot/detect/botToken.test.ts +71 -0
  23. package/src/bot/detect/botToken.ts +126 -0
  24. package/src/bot/detect/detect.test.ts +543 -0
  25. package/src/bot/detect/detect.ts +842 -0
  26. package/src/bot/detect/detectResult.ts +40 -0
  27. package/src/bot/detect/detectTable.test.ts +72 -0
  28. package/src/bot/detect/detectTable.ts +166 -0
  29. package/src/bot/detect/envExample.test.ts +29 -0
  30. package/src/bot/detect/envExample.ts +48 -0
  31. package/src/bot/detect/envKey.ts +7 -0
  32. package/src/bot/detect/envScan.test.ts +39 -0
  33. package/src/bot/detect/envScan.ts +87 -0
  34. package/src/bot/detect/index.ts +17 -0
  35. package/src/bot/detect/inferBotPlatform.test.ts +60 -0
  36. package/src/bot/detect/inferBotPlatform.ts +28 -0
  37. package/src/bot/detect/keys.test.ts +28 -0
  38. package/src/bot/detect/keys.ts +64 -0
  39. package/src/bot/detect/sensitiveKey.test.ts +91 -0
  40. package/src/bot/detect/sensitiveKey.ts +44 -0
  41. package/src/bot/detect/serverkJson.test.ts +88 -0
  42. package/src/bot/detect/serverkJson.ts +174 -0
  43. package/src/bot/index.ts +8 -0
  44. package/src/cli/detectTable.test.ts +66 -0
  45. package/src/cli/detectTable.ts +43 -0
  46. package/src/cli/main.ts +12 -4
  47. package/src/cli/renderedFile.ts +35 -0
  48. package/src/cli/schema.ts +7 -26
  49. package/src/cli/validate.test.ts +283 -3
  50. package/src/cli/validate.ts +105 -7
  51. package/src/context/bridgeContext.ts +17 -1
  52. package/src/core/bridgeAction.ts +2 -0
  53. package/src/core/bridgeActivity.ts +9 -0
  54. package/src/core/bridgeAvailability.ts +10 -0
  55. package/src/core/bridgePort.test.ts +45 -0
  56. package/src/core/bridgePort.ts +26 -0
  57. package/src/core/bridgeUserError.test.ts +10 -0
  58. package/src/core/bridgeUserError.ts +6 -2
  59. package/src/core/describeDownloadFailure.test.ts +58 -0
  60. package/src/core/describeDownloadFailure.ts +69 -0
  61. package/src/core/index.ts +3 -0
  62. package/src/guides/compile.ts +2 -2
  63. package/src/manifest/index.ts +1 -0
  64. package/src/manifest/kind.ts +5 -0
  65. package/src/manifest/manifest.test.ts +630 -3
  66. package/src/manifest/manifest.ts +391 -87
  67. package/src/manifest/runtime.ts +2 -2
  68. package/src/manifest/source.ts +4 -4
  69. package/src/modules/bridge.ts +8 -1
  70. package/src/modules/bridgeLifecycleSubject.ts +8 -0
  71. package/src/modules/bridgeModule.ts +19 -1
  72. package/src/modules/bridgeStopSignal.ts +13 -0
  73. package/src/modules/index.ts +2 -0
  74. package/src/panel/bridgeDetail.ts +4 -1
  75. package/src/protocol/bridgeActivity.ts +54 -0
  76. package/src/protocol/bridgeConfig.ts +2 -0
  77. package/src/protocol/bridgeEvents.ts +30 -0
  78. package/src/protocol/bridgeFrame.ts +10 -1
  79. package/src/protocol/index.ts +1 -0
  80. package/src/runtime/bridgeActions.ts +19 -2
  81. package/src/runtime/bridgeActivity.test.ts +318 -0
  82. package/src/runtime/bridgeActivity.ts +268 -0
  83. package/src/runtime/bridgeConfigReader.test.ts +80 -0
  84. package/src/runtime/bridgeConfigReader.ts +20 -0
  85. package/src/runtime/bridgeContext.test.ts +247 -0
  86. package/src/runtime/bridgeContext.ts +43 -10
  87. package/src/runtime/bridgeDriver.test.ts +153 -1
  88. package/src/runtime/bridgeDriver.ts +64 -8
  89. package/src/runtime/bridgeFiles.test.ts +62 -0
  90. package/src/runtime/bridgeFiles.ts +12 -2
  91. package/src/runtime/bridgeLiveConfig.ts +18 -0
  92. package/src/runtime/bridgeLog.ts +10 -0
  93. package/src/runtime/bridgeRedaction.test.ts +410 -0
  94. package/src/runtime/bridgeRedaction.ts +214 -0
  95. package/src/runtime/bridgeRefusalError.ts +13 -0
  96. package/src/runtime/bridgeRegistry.test.ts +1056 -10
  97. package/src/runtime/bridgeRegistry.ts +464 -50
  98. package/src/runtime/bridgeScopedContext.ts +55 -0
  99. package/src/runtime/bridgeServer.test.ts +534 -4
  100. package/src/runtime/bridgeServer.ts +145 -38
  101. package/src/runtime/bridgeSetupHandlers.test.ts +156 -5
  102. package/src/runtime/bridgeSetupHandlers.ts +59 -14
  103. package/src/runtime/bridgeSupervisor.test.ts +670 -2
  104. package/src/runtime/bridgeSupervisor.ts +219 -67
  105. package/src/runtime/serve.ts +87 -11
  106. package/src/steam/steamcmd.test.ts +32 -1
  107. package/src/steam/steamcmd.ts +27 -4
  108. package/src/utils/changedSettingKeys.test.ts +65 -0
  109. package/src/utils/changedSettingKeys.ts +6 -0
  110. package/src/utils/describeExecFailure.test.ts +34 -0
  111. package/src/utils/describeExecFailure.ts +9 -0
  112. package/src/utils/index.ts +3 -0
  113. package/src/utils/sameSettingValue.test.ts +35 -0
  114. package/src/utils/sameSettingValue.ts +40 -0
  115. package/src/wire/activity.test.ts +95 -0
  116. package/src/wire/activity.ts +41 -0
  117. package/src/wire/index.ts +1 -0
  118. package/src/wire/panel.test.ts +67 -1
  119. package/src/wire/panel.ts +11 -0
@@ -1,17 +1,50 @@
1
1
  import type { BridgeContext } from "../context";
2
- import { BridgeAction, BridgeKind, type BridgeRow, type BridgeValues, isReadAction } from "../core";
3
- import type { BridgeDriver, BridgePanelModule, BridgeSetupModule, BridgeTerminalModule } from "../modules";
4
- import { BridgeLayout } from "../panel";
5
2
  import {
3
+ BRIDGE_AVAILABILITY_KEY_PATTERN,
4
+ BridgeAction,
5
+ type BridgeActivity,
6
+ type BridgeAvailability,
7
+ BridgeKind,
8
+ type BridgeRow,
9
+ type BridgeUnavailable,
10
+ type BridgeValues,
11
+ isLocalizedText,
12
+ isReadAction,
13
+ type LocalizedText,
14
+ } from "../core";
15
+ import type {
16
+ BridgeCatalogModule,
17
+ BridgeCollectionModule,
18
+ BridgeDriver,
19
+ BridgePanelModule,
20
+ BridgeSettingsModule,
21
+ BridgeSetupModule,
22
+ BridgeTerminalModule,
23
+ } from "../modules";
24
+ import { type BridgeDetail, BridgeLayout } from "../panel";
25
+ import {
26
+ BRIDGE_KEY_LENGTH_LIMIT,
27
+ BRIDGE_TEXT_LENGTH_LIMIT,
6
28
  BRIDGE_VERSION,
29
+ BridgeEventName,
7
30
  type BridgeManifest,
8
31
  type BridgeModuleManifest,
9
32
  type BridgeSetupManifest,
10
33
  type BridgeTerminalManifest,
11
34
  } from "../protocol";
35
+ import { changedSettingKeys } from "../utils";
12
36
  import { actionsOf } from "./bridgeActions";
37
+ import {
38
+ type ActivityNormalizeOptions,
39
+ type BridgeActivityAnswer,
40
+ type BridgeActivityScope,
41
+ createActivityScope,
42
+ } from "./bridgeActivity";
13
43
  import { BridgeDriverError } from "./bridgeDriver";
14
44
  import type { BridgeEventRing } from "./bridgeEvents";
45
+ import { errorText } from "./bridgeLog";
46
+ import { BridgeRefusalError } from "./bridgeRefusalError";
47
+ import { scopedContext } from "./bridgeScopedContext";
15
48
  import type { BridgeSupervisor } from "./bridgeSupervisor";
16
49
 
17
50
  interface CacheEntry {
@@ -19,6 +52,11 @@ interface CacheEntry {
19
52
  expiresAt: number;
20
53
  }
21
54
 
55
+ const UNAVAILABLE_TEXT: LocalizedText = {
56
+ ar: "هذا الإجراء مو متاح الحين",
57
+ en: "This action is not available right now",
58
+ };
59
+
22
60
  const requiresRunning = (module: BridgePanelModule) => {
23
61
  if (
24
62
  module.kind === BridgeKind.Actions
@@ -87,38 +125,9 @@ const setupManifestOf = (module: BridgeSetupModule | undefined): BridgeSetupMani
87
125
  };
88
126
  };
89
127
 
90
- const withVariables = (context: BridgeContext, overlay: Record<string, string>): BridgeContext => {
91
- return {
92
- get server() {
93
- return context.server;
94
- },
95
- variable(key) {
96
- return Object.hasOwn(overlay, key) ? (overlay[key] ?? null) : context.variable(key);
97
- },
98
- secret(key) {
99
- return context.secret(key);
100
- },
101
- port(key) {
102
- return context.port(key);
103
- },
104
- files: context.files,
105
- codec: context.codec,
106
- net: context.net,
107
- probe: context.probe,
108
- rcon: context.rcon,
109
- command(input, options) {
110
- return context.command(input, options);
111
- },
112
- exec(command, options) {
113
- return context.exec(command, options);
114
- },
115
- logs: context.logs,
116
- setup: context.setup,
117
- emit(event, payload) {
118
- context.emit(event, payload);
119
- },
120
- log: context.log,
121
- };
128
+ const SETTINGS_CHANGED_TEXT: LocalizedText = {
129
+ ar: "تم تغيير الإعدادات",
130
+ en: "Changed the settings",
122
131
  };
123
132
 
124
133
  const asString = (args: BridgeValues, key: string) => {
@@ -161,6 +170,65 @@ const asPage = (args: BridgeValues) => {
161
170
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
162
171
  };
163
172
 
173
+ const guardedActionsOf = (module: BridgePanelModule): string[] => {
174
+ switch (module.kind) {
175
+ case BridgeKind.Actions: {
176
+ return Object.keys(module.actions);
177
+ }
178
+
179
+ case BridgeKind.Collection:
180
+ case BridgeKind.Detail: {
181
+ return Object.keys(module.actions ?? {});
182
+ }
183
+
184
+ case BridgeKind.Settings: {
185
+ return module.write
186
+ ? [
187
+ BridgeAction.Write,
188
+ ]
189
+ : [];
190
+ }
191
+
192
+ case BridgeKind.Catalog: {
193
+ return [
194
+ BridgeAction.Remove,
195
+ ...(module.toggle
196
+ ? [
197
+ BridgeAction.Toggle,
198
+ ]
199
+ : []),
200
+ ];
201
+ }
202
+
203
+ default: {
204
+ return [];
205
+ }
206
+ }
207
+ };
208
+
209
+ const isAvailabilityKey = (key: string) => {
210
+ return key.length <= BRIDGE_KEY_LENGTH_LIMIT && BRIDGE_AVAILABILITY_KEY_PATTERN.test(key);
211
+ };
212
+
213
+ const refuse = (name: string, action: string, reasons: BridgeUnavailable) => {
214
+ const text = reasons[action];
215
+
216
+ if (text) {
217
+ throw new BridgeRefusalError(`module "${name}" cannot run "${action}" right now`, text);
218
+ }
219
+ };
220
+
221
+ const settingsChanged = (keys: string[]): BridgeActivity => {
222
+ return {
223
+ name: BridgeEventName.SettingsChanged,
224
+ text: SETTINGS_CHANGED_TEXT,
225
+ details: {
226
+ keys: keys.join(","),
227
+ count: keys.length,
228
+ },
229
+ };
230
+ };
231
+
164
232
  const offlineActionsOf = (driver: BridgeDriver): Map<string, Set<string>> => {
165
233
  const offline = new Map<string, Set<string>>();
166
234
 
@@ -186,9 +254,10 @@ const offlineActionsOf = (driver: BridgeDriver): Map<string, Set<string>> => {
186
254
 
187
255
  export const createRegistry = (
188
256
  driver: BridgeDriver,
189
- context: BridgeContext,
257
+ base: BridgeContext,
190
258
  supervisor: BridgeSupervisor,
191
259
  ring: BridgeEventRing,
260
+ activity: ActivityNormalizeOptions,
192
261
  ) => {
193
262
  const cache = new Map<string, CacheEntry>();
194
263
  const inflight = new Map<string, Promise<unknown>>();
@@ -196,7 +265,11 @@ export const createRegistry = (
196
265
  const offlineActions = offlineActionsOf(driver);
197
266
 
198
267
  for (const [id, module] of Object.entries(modules)) {
199
- if (module.kind !== BridgeKind.Actions && module.kind !== BridgeKind.Detail) {
268
+ if (
269
+ module.kind !== BridgeKind.Actions
270
+ && module.kind !== BridgeKind.Collection
271
+ && module.kind !== BridgeKind.Detail
272
+ ) {
200
273
  continue;
201
274
  }
202
275
 
@@ -247,12 +320,286 @@ export const createRegistry = (
247
320
  }
248
321
  };
249
322
 
250
- const dispatch = async (module: BridgePanelModule, name: string, action: string, args: BridgeValues) => {
323
+ const listings = new Map<string, Promise<BridgeRow[]>>();
324
+
325
+ const listFresh = (module: BridgeCollectionModule, name: string, context: BridgeContext) => {
326
+ const rows = Promise.resolve().then(() => module.list(context));
327
+
328
+ listings.set(name, rows);
329
+
330
+ const settle = () => {
331
+ if (listings.get(name) === rows) {
332
+ listings.delete(name);
333
+ }
334
+ };
335
+
336
+ rows.then(settle, settle);
337
+
338
+ return rows;
339
+ };
340
+
341
+ const listForAvailability = (module: BridgeCollectionModule, name: string, context: BridgeContext) => {
342
+ return listings.get(name) ?? listFresh(module, name, context);
343
+ };
344
+
345
+ const reasonOf = (text: LocalizedText): LocalizedText => {
346
+ const ar = activity.redact(text.ar.trim()).slice(0, BRIDGE_TEXT_LENGTH_LIMIT);
347
+ const en = activity.redact(text.en.trim()).slice(0, BRIDGE_TEXT_LENGTH_LIMIT);
348
+
349
+ return ar.length > 0 && en.length > 0
350
+ ? {
351
+ ar,
352
+ en,
353
+ }
354
+ : UNAVAILABLE_TEXT;
355
+ };
356
+
357
+ const unavailableOf = (name: string, raw: unknown, guarded: readonly string[]): BridgeUnavailable => {
358
+ const kept: BridgeUnavailable = {};
359
+
360
+ if (raw === undefined || raw === null) {
361
+ return kept;
362
+ }
363
+
364
+ if (typeof raw !== "object" || Array.isArray(raw)) {
365
+ activity.warn("availability answer is not an object, it is ignored", {
366
+ module: name,
367
+ });
368
+
369
+ return kept;
370
+ }
371
+
372
+ const dropped: string[] = [];
373
+
374
+ for (const [action, text] of Object.entries(raw)) {
375
+ if (!guarded.includes(action) || !isAvailabilityKey(action) || !isLocalizedText(text)) {
376
+ dropped.push(action);
377
+
378
+ continue;
379
+ }
380
+
381
+ kept[action] = reasonOf(text);
382
+ }
383
+
384
+ if (dropped.length > 0) {
385
+ activity.warn("availability names actions the module cannot refuse, they are ignored", {
386
+ module: name,
387
+ actions: dropped.join(","),
388
+ });
389
+ }
390
+
391
+ return kept;
392
+ };
393
+
394
+ const isSubjectId = (name: string, id: unknown): id is string => {
395
+ if (typeof id === "string" && id.length > 0) {
396
+ return true;
397
+ }
398
+
399
+ activity.warn("availability skipped a subject without an id", {
400
+ module: name,
401
+ });
402
+
403
+ return false;
404
+ };
405
+
406
+ const askUnavailable = async (
407
+ name: string,
408
+ guarded: readonly string[],
409
+ ask: () => Promise<unknown> | undefined,
410
+ ): Promise<BridgeUnavailable> => {
411
+ try {
412
+ return unavailableOf(name, await ask(), guarded);
413
+ } catch (error) {
414
+ activity.warn("availability could not be read, the actions count as available", {
415
+ module: name,
416
+ error: errorText(error),
417
+ });
418
+
419
+ return {};
420
+ }
421
+ };
422
+
423
+ const detailUnavailable = (name: string, detail: BridgeDetail, guarded: readonly string[]): BridgeUnavailable => {
424
+ const reasons = unavailableOf(name, detail.unavailable, guarded);
425
+ const pressable = Array.isArray(detail.actions) ? detail.actions : [];
426
+ const closed: BridgeUnavailable = {};
427
+
428
+ for (const action of guarded) {
429
+ if (!pressable.includes(action) && isAvailabilityKey(action)) {
430
+ closed[action] = reasons[action] ?? UNAVAILABLE_TEXT;
431
+ }
432
+ }
433
+
434
+ return closed;
435
+ };
436
+
437
+ const availabilityOf = async (
438
+ module: BridgePanelModule,
439
+ name: string,
440
+ args: BridgeValues,
441
+ context: BridgeContext,
442
+ ): Promise<BridgeAvailability> => {
443
+ const guarded = guardedActionsOf(module);
444
+ const id = asOptional(args, "id");
445
+ const availability: BridgeAvailability = {
446
+ module: {},
447
+ subjects: {},
448
+ };
449
+
450
+ switch (module.kind) {
451
+ case BridgeKind.Collection: {
452
+ if (!module.unavailable) {
453
+ break;
454
+ }
455
+
456
+ for (const row of await listForAvailability(module, name, context)) {
457
+ if (!isSubjectId(name, row.id)) {
458
+ continue;
459
+ }
460
+
461
+ if (id !== null && row.id !== id) {
462
+ continue;
463
+ }
464
+
465
+ const reasons = await askUnavailable(name, guarded, () => module.unavailable?.(context, row));
466
+
467
+ if (Object.keys(reasons).length > 0) {
468
+ availability.subjects[row.id] = reasons;
469
+ }
470
+ }
471
+
472
+ break;
473
+ }
474
+
475
+ case BridgeKind.Catalog: {
476
+ if (!module.unavailable) {
477
+ break;
478
+ }
479
+
480
+ for (const entry of await module.installed(context)) {
481
+ if (!isSubjectId(name, entry.id)) {
482
+ continue;
483
+ }
484
+
485
+ if (id !== null && entry.id !== id) {
486
+ continue;
487
+ }
488
+
489
+ const reasons = await askUnavailable(name, guarded, () => module.unavailable?.(context, entry));
490
+
491
+ if (Object.keys(reasons).length > 0) {
492
+ availability.subjects[entry.id] = reasons;
493
+ }
494
+ }
495
+
496
+ break;
497
+ }
498
+
499
+ case BridgeKind.Actions:
500
+ case BridgeKind.Settings: {
501
+ availability.module = await askUnavailable(name, guarded, () => module.unavailable?.(context));
502
+
503
+ break;
504
+ }
505
+
506
+ case BridgeKind.Detail: {
507
+ const detail = await module.read(context);
508
+
509
+ if (detail) {
510
+ availability.module = detailUnavailable(name, detail, guarded);
511
+ }
512
+
513
+ break;
514
+ }
515
+ }
516
+
517
+ return availability;
518
+ };
519
+
520
+ const refuseEntry = async (
521
+ module: BridgeCatalogModule,
522
+ name: string,
523
+ action: string,
524
+ id: string,
525
+ context: BridgeContext,
526
+ ) => {
527
+ if (!module.unavailable) {
528
+ return;
529
+ }
530
+
531
+ refuse(
532
+ name,
533
+ action,
534
+ await askUnavailable(name, guardedActionsOf(module), async () => {
535
+ const entry = (await module.installed(context)).find((installed) => installed.id === id);
536
+
537
+ return entry ? await module.unavailable?.(context, entry) : undefined;
538
+ }),
539
+ );
540
+ };
541
+
542
+ const readBefore = async (module: BridgeSettingsModule, scoped: BridgeContext, name: string) => {
543
+ try {
544
+ return {
545
+ ...(await module.read(scoped)),
546
+ };
547
+ } catch (error) {
548
+ activity.warn("settings could not be read before the write", {
549
+ module: name,
550
+ error: errorText(error),
551
+ });
552
+
553
+ return null;
554
+ }
555
+ };
556
+
557
+ const reportSettingsDefault = (
558
+ scope: BridgeActivityScope,
559
+ before: BridgeValues | null,
560
+ written: BridgeValues,
561
+ name: string,
562
+ ) => {
563
+ try {
564
+ const keys = changedSettingKeys(before, written);
565
+
566
+ if (scope.reported === 0 && keys.length > 0) {
567
+ scope.reporter.report(settingsChanged(keys));
568
+ }
569
+ } catch (error) {
570
+ activity.warn("settings change could not be reported", {
571
+ module: name,
572
+ error: errorText(error),
573
+ });
574
+ }
575
+ };
576
+
577
+ const dispatch = async (
578
+ module: BridgePanelModule,
579
+ name: string,
580
+ action: string,
581
+ args: BridgeValues,
582
+ scope: BridgeActivityScope,
583
+ ) => {
584
+ const context = scopedContext(base, scope.reporter);
585
+
586
+ if (action === BridgeAction.Availability && actionsOf(module).includes(BridgeAction.Availability)) {
587
+ return await availabilityOf(module, name, args, context);
588
+ }
589
+
251
590
  switch (module.kind) {
252
591
  case BridgeKind.Actions: {
253
592
  const serverAction = module.actions[action];
254
593
 
255
594
  if (serverAction) {
595
+ if (module.unavailable) {
596
+ refuse(
597
+ name,
598
+ action,
599
+ await askUnavailable(name, guardedActionsOf(module), () => module.unavailable?.(context)),
600
+ );
601
+ }
602
+
256
603
  await serverAction(context, args);
257
604
 
258
605
  return null;
@@ -273,7 +620,7 @@ export const createRegistry = (
273
620
  }
274
621
  }
275
622
 
276
- const scoped = withVariables(context, overlay);
623
+ const scoped = scopedContext(base, scope.reporter, overlay);
277
624
 
278
625
  const key = [
279
626
  name,
@@ -296,7 +643,7 @@ export const createRegistry = (
296
643
  }
297
644
  }
298
645
 
299
- const scoped = withVariables(context, overlay);
646
+ const scoped = scopedContext(base, scope.reporter, overlay);
300
647
 
301
648
  return await module.preview(scoped, args);
302
649
  }
@@ -310,8 +657,20 @@ export const createRegistry = (
310
657
  }
311
658
 
312
659
  if (action === BridgeAction.Write && module.write) {
660
+ if (module.unavailable) {
661
+ refuse(
662
+ name,
663
+ action,
664
+ await askUnavailable(name, guardedActionsOf(module), () => module.unavailable?.(context)),
665
+ );
666
+ }
667
+
668
+ const before = await readBefore(module, context, name);
669
+
313
670
  await module.write(context, args);
314
671
 
672
+ reportSettingsDefault(scope, before, args, name);
673
+
315
674
  return null;
316
675
  }
317
676
 
@@ -320,7 +679,7 @@ export const createRegistry = (
320
679
 
321
680
  case BridgeKind.Collection: {
322
681
  if (action === BridgeAction.List) {
323
- return await module.list(context);
682
+ return await listFresh(module, name, context);
324
683
  }
325
684
 
326
685
  if (action === BridgeAction.Add && module.add) {
@@ -342,7 +701,17 @@ export const createRegistry = (
342
701
  : undefined);
343
702
 
344
703
  if (!row) {
345
- throw new BridgeDriverError(`module "${name}" has no row "${id}"`);
704
+ throw new BridgeRefusalError(`module "${name}" has no row "${id}"`);
705
+ }
706
+
707
+ if (module.unavailable) {
708
+ refuse(
709
+ name,
710
+ action,
711
+ await askUnavailable(name, guardedActionsOf(module), () =>
712
+ module.unavailable?.(context, row as BridgeRow),
713
+ ),
714
+ );
346
715
  }
347
716
 
348
717
  await rowAction(context, row as BridgeRow, withoutId(args));
@@ -361,6 +730,18 @@ export const createRegistry = (
361
730
  const detailAction = module.actions?.[action];
362
731
 
363
732
  if (detailAction) {
733
+ const guarded = guardedActionsOf(module);
734
+
735
+ refuse(
736
+ name,
737
+ action,
738
+ await askUnavailable(name, guarded, async () => {
739
+ const detail = await module.read(context);
740
+
741
+ return detail ? detailUnavailable(name, detail, guarded) : undefined;
742
+ }),
743
+ );
744
+
364
745
  return await detailAction(context, args);
365
746
  }
366
747
 
@@ -393,13 +774,19 @@ export const createRegistry = (
393
774
  }
394
775
 
395
776
  if (action === BridgeAction.Remove) {
396
- await module.remove(context, asString(args, "id"));
777
+ const id = asString(args, "id");
778
+
779
+ await refuseEntry(module, name, action, id, context);
780
+ await module.remove(context, id);
397
781
 
398
782
  return null;
399
783
  }
400
784
 
401
785
  if (action === BridgeAction.Toggle && module.toggle) {
402
- await module.toggle(context, asString(args, "id"), args.enabled !== false);
786
+ const id = asString(args, "id");
787
+
788
+ await refuseEntry(module, name, action, id, context);
789
+ await module.toggle(context, id, args.enabled !== false);
403
790
 
404
791
  return null;
405
792
  }
@@ -408,7 +795,7 @@ export const createRegistry = (
408
795
  }
409
796
  }
410
797
 
411
- throw new BridgeDriverError(`module "${name}" does not support "${action}"`);
798
+ throw new BridgeRefusalError(`module "${name}" does not support "${action}"`);
412
799
  };
413
800
 
414
801
  return {
@@ -441,22 +828,49 @@ export const createRegistry = (
441
828
  };
442
829
  },
443
830
 
444
- async invoke(name: string, action: string, args: BridgeValues = {}, recoveryOperationId?: string) {
831
+ async invoke(
832
+ name: string,
833
+ action: string,
834
+ args: BridgeValues = {},
835
+ recoveryOperationId?: string,
836
+ ): Promise<BridgeActivityAnswer<unknown>> {
445
837
  const module = modules[name];
446
838
 
447
839
  if (!module) {
448
- throw new BridgeDriverError(`no module named "${name}"`);
840
+ throw new BridgeRefusalError(`no module named "${name}"`);
449
841
  }
450
842
 
451
843
  if (protectedActionsOf(module).includes(action) && (!recoveryOperationId || supervisor.running)) {
452
- throw new BridgeDriverError("this action needs a recovery operation with the game stopped");
844
+ throw new BridgeRefusalError("this action needs a recovery operation with the game stopped");
453
845
  }
454
846
 
455
847
  if (requiresRunning(module) && !supervisor.running) {
456
- throw new BridgeDriverError(`module "${name}" needs the game to be running`);
848
+ throw new BridgeRefusalError(`module "${name}" needs the game to be running`);
849
+ }
850
+
851
+ const scope = createActivityScope(base.activity, activity);
852
+ const mutation = !isReadAction(action);
853
+
854
+ if (mutation) {
855
+ listings.clear();
457
856
  }
458
857
 
459
- return await dispatch(module, name, action, args);
858
+ try {
859
+ const result = await dispatch(module, name, action, args, scope);
860
+
861
+ return {
862
+ result,
863
+ activity: scope.close(),
864
+ };
865
+ } catch (error) {
866
+ scope.drain();
867
+
868
+ throw error;
869
+ } finally {
870
+ if (mutation) {
871
+ listings.clear();
872
+ }
873
+ }
460
874
  },
461
875
  };
462
876
  };
@@ -0,0 +1,55 @@
1
+ import type { BridgeActivityReporter, BridgeContext } from "../context";
2
+
3
+ export const scopedContext = (
4
+ context: BridgeContext,
5
+ activity: BridgeActivityReporter,
6
+ overlay: Record<string, string> = {},
7
+ ): BridgeContext => {
8
+ return {
9
+ get server() {
10
+ return context.server;
11
+ },
12
+ variable(key) {
13
+ return Object.hasOwn(overlay, key) ? (overlay[key] ?? null) : context.variable(key);
14
+ },
15
+ variables() {
16
+ return {
17
+ ...context.variables(),
18
+ ...overlay,
19
+ };
20
+ },
21
+ secret(key) {
22
+ return context.secret(key);
23
+ },
24
+ secrets() {
25
+ return context.secrets();
26
+ },
27
+ port(key) {
28
+ return context.port(key);
29
+ },
30
+ portBlock(key) {
31
+ return context.portBlock(key);
32
+ },
33
+ files: context.files,
34
+ codec: context.codec,
35
+ net: context.net,
36
+ probe: context.probe,
37
+ rcon: context.rcon,
38
+ command(input, options) {
39
+ return context.command(input, options);
40
+ },
41
+ exec(command, options) {
42
+ return context.exec(command, options);
43
+ },
44
+ logs: context.logs,
45
+ setup: context.setup,
46
+ activity,
47
+ emit(event, payload) {
48
+ context.emit(event, payload);
49
+ },
50
+ restart() {
51
+ return context.restart();
52
+ },
53
+ log: context.log,
54
+ };
55
+ };