@serverkgg/bridge 0.1.1 → 0.1.3

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 (121) hide show
  1. package/package.json +3 -2
  2. package/rules/package.md +60 -0
  3. package/src/blog/articleFrontmatter.test.ts +10 -10
  4. package/src/blog/compile.test.ts +9 -9
  5. package/src/bot/botDeploy.test.ts +16 -16
  6. package/src/bot/botEnvironment.test.ts +8 -8
  7. package/src/bot/botIdentity.test.ts +12 -12
  8. package/src/bot/botPanel.test.ts +41 -11
  9. package/src/bot/botPanel.ts +13 -5
  10. package/src/bot/botState.test.ts +3 -3
  11. package/src/bot/botTerminal.test.ts +5 -5
  12. package/src/bot/botVariables.test.ts +8 -8
  13. package/src/bot/detect/botToken.test.ts +6 -6
  14. package/src/bot/detect/detect.test.ts +21 -21
  15. package/src/bot/detect/detectTable.test.ts +5 -5
  16. package/src/bot/detect/envExample.test.ts +2 -2
  17. package/src/bot/detect/envScan.test.ts +4 -4
  18. package/src/bot/detect/inferBotPlatform.test.ts +4 -4
  19. package/src/bot/detect/keys.test.ts +3 -3
  20. package/src/bot/detect/sensitiveKey.test.ts +5 -5
  21. package/src/bot/detect/serverkJson.test.ts +5 -5
  22. package/src/catalogs/curseforge.test.ts +35 -35
  23. package/src/catalogs/thunderstore.test.ts +9 -9
  24. package/src/cli/detectTable.test.ts +3 -3
  25. package/src/cli/link.test.ts +11 -11
  26. package/src/cli/main.ts +8 -0
  27. package/src/cli/rules.test.ts +198 -0
  28. package/src/cli/rules.ts +188 -0
  29. package/src/cli/validate.test.ts +123 -29
  30. package/src/cli/validate.ts +37 -1
  31. package/src/codecs/ini.test.ts +13 -13
  32. package/src/codecs/javaProperties.test.ts +11 -11
  33. package/src/codecs/sourceCfg.test.ts +14 -14
  34. package/src/codecs/xmlProperties.test.ts +10 -10
  35. package/src/context/bridgeContext.ts +6 -1
  36. package/src/core/bridgeAction.ts +2 -0
  37. package/src/core/bridgeActivity.ts +9 -0
  38. package/src/core/bridgeAvailability.ts +10 -0
  39. package/src/core/bridgePort.test.ts +6 -6
  40. package/src/core/bridgeUserError.test.ts +13 -3
  41. package/src/core/bridgeUserError.ts +6 -2
  42. package/src/core/bridgeValue.test.ts +5 -5
  43. package/src/core/describeDownloadFailure.test.ts +58 -0
  44. package/src/core/describeDownloadFailure.ts +69 -0
  45. package/src/core/index.ts +3 -0
  46. package/src/guides/markdown.test.ts +31 -31
  47. package/src/install/installStamp.test.ts +9 -9
  48. package/src/manifest/extension.ts +36 -0
  49. package/src/manifest/index.ts +1 -0
  50. package/src/manifest/manifest.test.ts +208 -90
  51. package/src/manifest/manifest.ts +34 -0
  52. package/src/modules/bridge.ts +8 -1
  53. package/src/modules/bridgeModule.ts +13 -1
  54. package/src/net/loopback.test.ts +10 -10
  55. package/src/panel/bridgeDetail.ts +2 -1
  56. package/src/presence/rosterSync.test.ts +7 -7
  57. package/src/probes/a2s.test.ts +5 -5
  58. package/src/probes/minecraftPing.test.ts +17 -17
  59. package/src/probes/raknetPing.test.ts +13 -13
  60. package/src/protocol/bridgeActivity.ts +54 -0
  61. package/src/protocol/bridgeEvents.ts +30 -0
  62. package/src/protocol/bridgeFrame.ts +10 -1
  63. package/src/protocol/index.ts +1 -0
  64. package/src/rcon/rconAccess.test.ts +20 -20
  65. package/src/rcon/rconAccess.ts +13 -13
  66. package/src/rcon/rconClient.test.ts +49 -49
  67. package/src/rcon/rconFrame.test.ts +14 -14
  68. package/src/rcon/rconIds.test.ts +4 -4
  69. package/src/rcon/rconWriter.test.ts +6 -6
  70. package/src/runtime/bridgeActions.ts +19 -2
  71. package/src/runtime/bridgeActivity.test.ts +318 -0
  72. package/src/runtime/bridgeActivity.ts +268 -0
  73. package/src/runtime/bridgeBackupLease.test.ts +9 -9
  74. package/src/runtime/bridgeConfigReader.test.ts +5 -5
  75. package/src/runtime/bridgeConsoleHandler.test.ts +8 -8
  76. package/src/runtime/bridgeContext.test.ts +11 -5
  77. package/src/runtime/bridgeContext.ts +8 -1
  78. package/src/runtime/bridgeDriver.test.ts +114 -93
  79. package/src/runtime/bridgeDriver.ts +10 -7
  80. package/src/runtime/bridgeEvents.test.ts +15 -15
  81. package/src/runtime/bridgeFiles.test.ts +54 -54
  82. package/src/runtime/bridgeInstall.test.ts +10 -10
  83. package/src/runtime/bridgeLiveConfig.test.ts +6 -6
  84. package/src/runtime/bridgeLog.ts +10 -0
  85. package/src/runtime/bridgePending.test.ts +6 -6
  86. package/src/runtime/bridgeRedaction.test.ts +16 -16
  87. package/src/runtime/bridgeRefusalError.ts +13 -0
  88. package/src/runtime/bridgeRegistry.test.ts +1077 -34
  89. package/src/runtime/bridgeRegistry.ts +464 -65
  90. package/src/runtime/bridgeScopedContext.ts +55 -0
  91. package/src/runtime/bridgeServer.test.ts +424 -24
  92. package/src/runtime/bridgeServer.ts +125 -35
  93. package/src/runtime/bridgeSetup.test.ts +27 -27
  94. package/src/runtime/bridgeSetup.ts +1 -1
  95. package/src/runtime/bridgeSetupHandlers.test.ts +80 -17
  96. package/src/runtime/bridgeSetupHandlers.ts +45 -10
  97. package/src/runtime/bridgeSignal.test.ts +7 -7
  98. package/src/runtime/bridgeSupervisor.test.ts +42 -42
  99. package/src/runtime/serve.ts +45 -3
  100. package/src/steam/avatars.test.ts +13 -13
  101. package/src/steam/steamApp.test.ts +8 -8
  102. package/src/steam/steamcmd.test.ts +44 -13
  103. package/src/steam/steamcmd.ts +27 -4
  104. package/src/steam/steamcmdNarrator.test.ts +4 -4
  105. package/src/steam/workshop.test.ts +5 -5
  106. package/src/utils/changedSettingKeys.test.ts +65 -0
  107. package/src/utils/changedSettingKeys.ts +6 -0
  108. package/src/utils/describeExecFailure.test.ts +34 -0
  109. package/src/utils/describeExecFailure.ts +9 -0
  110. package/src/utils/index.ts +3 -0
  111. package/src/utils/sameSettingValue.test.ts +35 -0
  112. package/src/utils/sameSettingValue.ts +40 -0
  113. package/src/utils/token.test.ts +7 -7
  114. package/src/wine/protonCommand.test.ts +5 -5
  115. package/src/wine/wineCommand.test.ts +6 -6
  116. package/src/wine/winePrefix.test.ts +6 -6
  117. package/src/wire/activity.test.ts +95 -0
  118. package/src/wire/activity.ts +41 -0
  119. package/src/wire/index.ts +1 -0
  120. package/src/wire/panel.test.ts +91 -25
  121. package/src/wire/panel.ts +11 -0
@@ -0,0 +1,318 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { BridgeActivity, BridgeValues } from "../core";
3
+ import {
4
+ BRIDGE_ACTIVITY_DETAILS_LIMIT,
5
+ BRIDGE_ACTIVITY_KEYS,
6
+ BRIDGE_ACTIVITY_LIMIT,
7
+ BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT,
8
+ BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT,
9
+ BridgeEventName,
10
+ } from "../protocol";
11
+ import {
12
+ type ActivityNormalizeOptions,
13
+ createActivityReporter,
14
+ createActivityScope,
15
+ normalizeActivity,
16
+ } from "./bridgeActivity";
17
+
18
+ const SECRET = "tok-1234567890-secret";
19
+
20
+ const recorder = () => {
21
+ const warnings: {
22
+ message: string;
23
+ fields?: Record<string, unknown>;
24
+ }[] = [];
25
+
26
+ const emitted: {
27
+ event: string;
28
+ payload: BridgeValues;
29
+ }[] = [];
30
+
31
+ const options: ActivityNormalizeOptions = {
32
+ secretKeys: new Set([
33
+ "RCON_PASSWORD",
34
+ ]),
35
+ redact: (text) => text.split(SECRET).join("••••"),
36
+ warn: (message, fields) => {
37
+ warnings.push({
38
+ message,
39
+ fields,
40
+ });
41
+ },
42
+ };
43
+
44
+ const reporter = createActivityReporter({
45
+ ...options,
46
+ onEmit(event, payload) {
47
+ emitted.push({
48
+ event,
49
+ payload,
50
+ });
51
+ },
52
+ });
53
+
54
+ return {
55
+ options,
56
+ reporter,
57
+ warnings,
58
+ emitted,
59
+ };
60
+ };
61
+
62
+ const granted = (subject = "Steve"): BridgeActivity => ({
63
+ name: BridgeEventName.AccessGranted,
64
+ text: {
65
+ ar: `أضفت ${subject} للقائمة البيضاء`,
66
+ en: `Added ${subject} to the whitelist`,
67
+ },
68
+ subject,
69
+ });
70
+
71
+ describe("normalizeActivity", () => {
72
+ it("keeps a well-formed entry as it is", () => {
73
+ const { options, warnings } = recorder();
74
+
75
+ expect(normalizeActivity(granted(), options)).toEqual(granted());
76
+ expect(warnings).toEqual([]);
77
+ });
78
+
79
+ it("drops a name the protocol does not know, with a warning", () => {
80
+ const { options, warnings } = recorder();
81
+
82
+ expect(
83
+ normalizeActivity(
84
+ {
85
+ ...granted(),
86
+ name: "WhitelistAdded" as BridgeEventName,
87
+ },
88
+ options,
89
+ ),
90
+ ).toBeNull();
91
+ expect(warnings).toEqual([
92
+ {
93
+ message: "activity entry normalized",
94
+ fields: {
95
+ name: "WhitelistAdded",
96
+ reason: "unknown-name",
97
+ },
98
+ },
99
+ ]);
100
+ });
101
+
102
+ it("drops an entry whose text is empty in one language", () => {
103
+ const { options } = recorder();
104
+
105
+ expect(
106
+ normalizeActivity(
107
+ {
108
+ ...granted(),
109
+ text: {
110
+ ar: " ",
111
+ en: "Added Steve",
112
+ },
113
+ },
114
+ options,
115
+ ),
116
+ ).toBeNull();
117
+ });
118
+
119
+ it("cuts the text and the subject to their limits", () => {
120
+ const { options, warnings } = recorder();
121
+
122
+ const normalized = normalizeActivity(
123
+ {
124
+ ...granted(),
125
+ text: {
126
+ ar: "ا".repeat(BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT + 10),
127
+ en: "a".repeat(BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT + 10),
128
+ },
129
+ subject: "s".repeat(BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT + 10),
130
+ },
131
+ options,
132
+ );
133
+
134
+ expect(normalized?.text.ar).toHaveLength(BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT);
135
+ expect(normalized?.text.en).toHaveLength(BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT);
136
+ expect(normalized?.subject).toHaveLength(BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT);
137
+ expect(warnings.map((warning) => warning.fields?.reason)).toEqual([
138
+ "long-text",
139
+ "long-subject",
140
+ ]);
141
+ });
142
+
143
+ it("keeps at most sixteen details", () => {
144
+ const { options } = recorder();
145
+
146
+ const details: BridgeValues = {};
147
+
148
+ for (let index = 0; index < BRIDGE_ACTIVITY_DETAILS_LIMIT + 4; index++) {
149
+ details[`key${index}`] = index;
150
+ }
151
+
152
+ const normalized = normalizeActivity(
153
+ {
154
+ ...granted(),
155
+ details,
156
+ },
157
+ options,
158
+ );
159
+
160
+ expect(Object.keys(normalized?.details ?? {})).toHaveLength(BRIDGE_ACTIVITY_DETAILS_LIMIT);
161
+ });
162
+
163
+ it("removes secret fields, reserved keys and values that are not scalars", () => {
164
+ const { options } = recorder();
165
+
166
+ const normalized = normalizeActivity(
167
+ {
168
+ ...granted(),
169
+ details: {
170
+ RCON_PASSWORD: "hunter22hunter22",
171
+ [BRIDGE_ACTIVITY_KEYS.ar]: "مزوّر",
172
+ "bad key": "x",
173
+ nested: {
174
+ deep: true,
175
+ } as unknown as string,
176
+ count: Number.NaN,
177
+ from: "1.20.4",
178
+ },
179
+ },
180
+ options,
181
+ );
182
+
183
+ expect(normalized?.details).toEqual({
184
+ from: "1.20.4",
185
+ });
186
+ });
187
+
188
+ it("blanks a secret value in the text, the subject and the details", () => {
189
+ const { options } = recorder();
190
+
191
+ const normalized = normalizeActivity(
192
+ {
193
+ name: BridgeEventName.BroadcastSent,
194
+ text: {
195
+ ar: `أرسلت ${SECRET}`,
196
+ en: `Sent ${SECRET}`,
197
+ },
198
+ subject: SECRET,
199
+ details: {
200
+ message: `hello ${SECRET}`,
201
+ },
202
+ },
203
+ options,
204
+ );
205
+
206
+ expect(JSON.stringify(normalized)).not.toContain(SECRET);
207
+ expect(normalized?.details).toEqual({
208
+ message: "hello ••••",
209
+ });
210
+ });
211
+ });
212
+
213
+ describe("createActivityReporter", () => {
214
+ it("emits the entry as an event carrying the text under the reserved keys", () => {
215
+ const { reporter, emitted } = recorder();
216
+
217
+ reporter.report({
218
+ ...granted(),
219
+ details: {
220
+ source: "panel",
221
+ },
222
+ });
223
+
224
+ expect(emitted).toEqual([
225
+ {
226
+ event: BridgeEventName.AccessGranted,
227
+ payload: {
228
+ source: "panel",
229
+ [BRIDGE_ACTIVITY_KEYS.ar]: "أضفت Steve للقائمة البيضاء",
230
+ [BRIDGE_ACTIVITY_KEYS.en]: "Added Steve to the whitelist",
231
+ [BRIDGE_ACTIVITY_KEYS.subject]: "Steve",
232
+ },
233
+ },
234
+ ]);
235
+ });
236
+
237
+ it("emits nothing for an entry normalisation drops", () => {
238
+ const { reporter, emitted } = recorder();
239
+
240
+ reporter.report({
241
+ ...granted(),
242
+ name: "Nope" as BridgeEventName,
243
+ });
244
+
245
+ expect(emitted).toEqual([]);
246
+ });
247
+ });
248
+
249
+ describe("createActivityScope", () => {
250
+ it("collects entries for the answer instead of emitting them", () => {
251
+ const { options, reporter, emitted } = recorder();
252
+ const scope = createActivityScope(reporter, options);
253
+
254
+ scope.reporter.report(granted("Steve"));
255
+ scope.reporter.report(granted("Alex"));
256
+
257
+ expect(scope.reported).toBe(2);
258
+ expect(scope.close()).toEqual([
259
+ granted("Steve"),
260
+ granted("Alex"),
261
+ ]);
262
+ expect(emitted).toEqual([]);
263
+ });
264
+
265
+ it("drains what it collected to the ring when the handler throws", () => {
266
+ const { options, reporter, emitted } = recorder();
267
+ const scope = createActivityScope(reporter, options);
268
+
269
+ scope.reporter.report(granted());
270
+ scope.drain();
271
+
272
+ expect(emitted.map((entry) => entry.event)).toEqual([
273
+ BridgeEventName.AccessGranted,
274
+ ]);
275
+ expect(scope.close()).toEqual([]);
276
+ });
277
+
278
+ it("sends a report made after the answer to the ring", () => {
279
+ const { options, reporter, emitted } = recorder();
280
+ const scope = createActivityScope(reporter, options);
281
+
282
+ expect(scope.close()).toEqual([]);
283
+
284
+ scope.reporter.report(granted());
285
+
286
+ expect(emitted.map((entry) => entry.payload[BRIDGE_ACTIVITY_KEYS.subject])).toEqual([
287
+ "Steve",
288
+ ]);
289
+ });
290
+
291
+ it("keeps at most eight entries on one answer", () => {
292
+ const { options, reporter, warnings } = recorder();
293
+ const scope = createActivityScope(reporter, options);
294
+
295
+ for (let index = 0; index < BRIDGE_ACTIVITY_LIMIT + 2; index++) {
296
+ scope.reporter.report(granted(`player${index}`));
297
+ }
298
+
299
+ expect(scope.close()).toHaveLength(BRIDGE_ACTIVITY_LIMIT);
300
+ expect(warnings.filter((warning) => warning.fields?.reason === "extra-entries")).toHaveLength(2);
301
+ });
302
+
303
+ it("never throws into the caller, whatever the entry holds", () => {
304
+ const { options, reporter, warnings } = recorder();
305
+ const scope = createActivityScope(reporter, options);
306
+
307
+ const hostile = {
308
+ ...granted(),
309
+ get details(): BridgeValues {
310
+ throw new Error("a getter that throws");
311
+ },
312
+ };
313
+
314
+ expect(() => scope.reporter.report(hostile)).not.toThrow();
315
+ expect(() => reporter.report(hostile)).not.toThrow();
316
+ expect(warnings.filter((warning) => warning.message === "activity report failed")).toHaveLength(2);
317
+ });
318
+ });
@@ -0,0 +1,268 @@
1
+ import type { BridgeActivityReporter } from "../context";
2
+ import { type BridgeActivity, type BridgeValue, type BridgeValues, isLocalizedText } from "../core";
3
+ import {
4
+ activityPayload,
5
+ BRIDGE_ACTIVITY_DETAIL_KEY_LENGTH_LIMIT,
6
+ BRIDGE_ACTIVITY_DETAIL_KEY_PATTERN,
7
+ BRIDGE_ACTIVITY_DETAILS_LIMIT,
8
+ BRIDGE_ACTIVITY_KEYS,
9
+ BRIDGE_ACTIVITY_LIMIT,
10
+ BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT,
11
+ BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT,
12
+ isBridgeEventName,
13
+ } from "../protocol";
14
+ import { type BridgeLogger, errorText } from "./bridgeLog";
15
+ import type { BridgeRedact } from "./bridgeRedaction";
16
+
17
+ export interface ActivityNormalizeOptions {
18
+ secretKeys: ReadonlySet<string>;
19
+ redact: BridgeRedact;
20
+ warn: BridgeLogger;
21
+ }
22
+
23
+ export interface ActivityReporterOptions extends ActivityNormalizeOptions {
24
+ onEmit: (event: string, payload: BridgeValues) => void;
25
+ }
26
+
27
+ export interface BridgeActivityAnswer<Result> {
28
+ result: Result;
29
+ activity: BridgeActivity[];
30
+ }
31
+
32
+ export interface BridgeActivityScope {
33
+ readonly reporter: BridgeActivityReporter;
34
+ readonly reported: number;
35
+ close(): BridgeActivity[];
36
+ drain(): void;
37
+ }
38
+
39
+ const RESERVED_KEYS: ReadonlySet<string> = new Set(Object.values(BRIDGE_ACTIVITY_KEYS));
40
+
41
+ const cut = (value: string, limit: number, reason: string, report: (reason: string) => void) => {
42
+ if (value.length <= limit) {
43
+ return value;
44
+ }
45
+
46
+ report(`long-${reason}`);
47
+
48
+ return value.slice(0, limit);
49
+ };
50
+
51
+ const isScalar = (value: unknown): value is BridgeValue => {
52
+ return (
53
+ value === null
54
+ || typeof value === "boolean"
55
+ || typeof value === "string"
56
+ || (typeof value === "number" && Number.isFinite(value))
57
+ );
58
+ };
59
+
60
+ const normalizeDetails = (
61
+ details: unknown,
62
+ options: ActivityNormalizeOptions,
63
+ report: (reason: string) => void,
64
+ ): BridgeValues | null => {
65
+ if (details === undefined || details === null) {
66
+ return null;
67
+ }
68
+
69
+ if (typeof details !== "object" || Array.isArray(details)) {
70
+ report("unusable-details");
71
+
72
+ return null;
73
+ }
74
+
75
+ const kept: BridgeValues = {};
76
+
77
+ let count = 0;
78
+
79
+ for (const [key, value] of Object.entries(details)) {
80
+ if (RESERVED_KEYS.has(key)) {
81
+ report("reserved-detail");
82
+
83
+ continue;
84
+ }
85
+
86
+ if (options.secretKeys.has(key)) {
87
+ report("secret-detail");
88
+
89
+ continue;
90
+ }
91
+
92
+ if (key.length > BRIDGE_ACTIVITY_DETAIL_KEY_LENGTH_LIMIT || !BRIDGE_ACTIVITY_DETAIL_KEY_PATTERN.test(key)) {
93
+ report("unusable-detail-key");
94
+
95
+ continue;
96
+ }
97
+
98
+ if (!isScalar(value)) {
99
+ report("unusable-detail-value");
100
+
101
+ continue;
102
+ }
103
+
104
+ if (count >= BRIDGE_ACTIVITY_DETAILS_LIMIT) {
105
+ report("extra-details");
106
+
107
+ break;
108
+ }
109
+
110
+ kept[key] =
111
+ typeof value === "string"
112
+ ? cut(options.redact(value), BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT, "detail", report)
113
+ : value;
114
+ count += 1;
115
+ }
116
+
117
+ return count > 0 ? kept : null;
118
+ };
119
+
120
+ export const normalizeActivity = (entry: BridgeActivity, options: ActivityNormalizeOptions): BridgeActivity | null => {
121
+ const name = String(entry.name);
122
+ const reported = new Set<string>();
123
+
124
+ const report = (reason: string) => {
125
+ if (reported.has(reason)) {
126
+ return;
127
+ }
128
+
129
+ reported.add(reason);
130
+
131
+ options.warn("activity entry normalized", {
132
+ name,
133
+ reason,
134
+ });
135
+ };
136
+
137
+ if (!isBridgeEventName(name)) {
138
+ report("unknown-name");
139
+
140
+ return null;
141
+ }
142
+
143
+ if (!isLocalizedText(entry.text)) {
144
+ report("unusable-text");
145
+
146
+ return null;
147
+ }
148
+
149
+ const ar = cut(options.redact(entry.text.ar).trim(), BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT, "text", report);
150
+ const en = cut(options.redact(entry.text.en).trim(), BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT, "text", report);
151
+
152
+ if (ar.length === 0 || en.length === 0) {
153
+ report("empty-text");
154
+
155
+ return null;
156
+ }
157
+
158
+ const subject =
159
+ typeof entry.subject === "string"
160
+ ? cut(options.redact(entry.subject).trim(), BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT, "subject", report)
161
+ : "";
162
+
163
+ if (entry.subject !== undefined && subject.length === 0) {
164
+ report("empty-subject");
165
+ }
166
+
167
+ const details = normalizeDetails(entry.details, options, report);
168
+
169
+ return {
170
+ name: entry.name,
171
+ text: {
172
+ ar,
173
+ en,
174
+ },
175
+ ...(subject.length > 0
176
+ ? {
177
+ subject,
178
+ }
179
+ : {}),
180
+ ...(details
181
+ ? {
182
+ details,
183
+ }
184
+ : {}),
185
+ };
186
+ };
187
+
188
+ const safely = (options: ActivityNormalizeOptions, run: () => void) => {
189
+ try {
190
+ run();
191
+ } catch (error) {
192
+ options.warn("activity report failed", {
193
+ error: errorText(error),
194
+ });
195
+ }
196
+ };
197
+
198
+ export const createActivityReporter = (options: ActivityReporterOptions): BridgeActivityReporter => {
199
+ return {
200
+ report(entry) {
201
+ safely(options, () => {
202
+ const normalized = normalizeActivity(entry, options);
203
+
204
+ if (normalized) {
205
+ options.onEmit(normalized.name, activityPayload(normalized));
206
+ }
207
+ });
208
+ },
209
+ };
210
+ };
211
+
212
+ export const createActivityScope = (
213
+ fallback: BridgeActivityReporter,
214
+ options: ActivityNormalizeOptions,
215
+ ): BridgeActivityScope => {
216
+ const entries: BridgeActivity[] = [];
217
+
218
+ let open = true;
219
+
220
+ return {
221
+ reporter: {
222
+ report(entry) {
223
+ safely(options, () => {
224
+ if (!open) {
225
+ fallback.report(entry);
226
+
227
+ return;
228
+ }
229
+
230
+ const normalized = normalizeActivity(entry, options);
231
+
232
+ if (!normalized) {
233
+ return;
234
+ }
235
+
236
+ if (entries.length >= BRIDGE_ACTIVITY_LIMIT) {
237
+ options.warn("activity entry dropped", {
238
+ name: normalized.name,
239
+ reason: "extra-entries",
240
+ });
241
+
242
+ return;
243
+ }
244
+
245
+ entries.push(normalized);
246
+ });
247
+ },
248
+ },
249
+
250
+ get reported() {
251
+ return entries.length;
252
+ },
253
+
254
+ close() {
255
+ open = false;
256
+
257
+ return entries.splice(0);
258
+ },
259
+
260
+ drain() {
261
+ open = false;
262
+
263
+ for (const entry of entries.splice(0)) {
264
+ fallback.report(entry);
265
+ }
266
+ },
267
+ };
268
+ };
@@ -1,8 +1,8 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { describe, expect, it } from "bun:test";
2
2
  import { createBackupLease } from "./bridgeBackupLease";
3
3
 
4
4
  describe("backup save ownership", () => {
5
- test("keeps a long backup frozen while renewed and releases when its owner disappears", async () => {
5
+ it("keeps a long backup frozen while renewed and releases when its owner disappears", async () => {
6
6
  let now = 0;
7
7
  let releases = 0;
8
8
  const lease = createBackupLease(
@@ -25,7 +25,7 @@ describe("backup save ownership", () => {
25
25
  expect(releases).toBe(1);
26
26
  expect(lease.active).toBe(false);
27
27
  });
28
- test("refuses overlapping holds and retains ownership after a failed resume", async () => {
28
+ it("refuses overlapping holds and retains ownership after a failed resume", async () => {
29
29
  let fail = true;
30
30
  const lease = createBackupLease(async () => {
31
31
  if (fail) {
@@ -43,7 +43,7 @@ describe("backup save ownership", () => {
43
43
  });
44
44
  });
45
45
 
46
- test("reserves ownership while the quiesce operation is still awaiting confirmation", async () => {
46
+ it("reserves ownership while the quiesce operation is still awaiting confirmation", async () => {
47
47
  const prepared = Promise.withResolvers<void>();
48
48
  const lease = createBackupLease(async () => {});
49
49
  const owner = lease.begin();
@@ -58,7 +58,7 @@ test("reserves ownership while the quiesce operation is still awaiting confirmat
58
58
  await lease.finish(owner);
59
59
  });
60
60
 
61
- test("expiration waits for in-flight preparation before resuming saves", async () => {
61
+ it("expiration waits for in-flight preparation before resuming saves", async () => {
62
62
  let now = 0;
63
63
  let releases = 0;
64
64
  const prepared = Promise.withResolvers<void>();
@@ -83,7 +83,7 @@ test("expiration waits for in-flight preparation before resuming saves", async (
83
83
  expect(lease.active).toBe(false);
84
84
  });
85
85
 
86
- test("late releases and tokenless callers cannot release a newer owner", async () => {
86
+ it("late releases and tokenless callers cannot release a newer owner", async () => {
87
87
  let releases = 0;
88
88
  const lease = createBackupLease(async () => {
89
89
  releases++;
@@ -99,7 +99,7 @@ test("late releases and tokenless callers cannot release a newer owner", async (
99
99
  expect(releases).toBe(2);
100
100
  });
101
101
 
102
- test("legacy release is accepted only for a legacy hold", async () => {
102
+ it("legacy release is accepted only for a legacy hold", async () => {
103
103
  let releases = 0;
104
104
  const lease = createBackupLease(async () => {
105
105
  releases++;
@@ -111,7 +111,7 @@ test("legacy release is accepted only for a legacy hold", async () => {
111
111
  expect(releases).toBe(1);
112
112
  });
113
113
 
114
- test("failed quiesce cleanup keeps the owner until resume succeeds", async () => {
114
+ it("failed quiesce cleanup keeps the owner until resume succeeds", async () => {
115
115
  let releases = 0;
116
116
  const lease = createBackupLease(async () => {
117
117
  releases++;
@@ -127,7 +127,7 @@ test("failed quiesce cleanup keeps the owner until resume succeeds", async () =>
127
127
  expect(lease.active).toBe(false);
128
128
  });
129
129
 
130
- test("legacy owners retain the historical hold until explicit release without renewal", async () => {
130
+ it("legacy owners retain the historical hold until explicit release without renewal", async () => {
131
131
  let now = 0;
132
132
  let releases = 0;
133
133
  const lease = createBackupLease(
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { describe, expect, it } from "bun:test";
2
2
  import { BridgeConfigError, parseConfig } from "./bridgeConfigReader";
3
3
 
4
4
  const written = (extra: Record<string, unknown> = {}) => {
@@ -22,7 +22,7 @@ const written = (extra: Record<string, unknown> = {}) => {
22
22
  };
23
23
 
24
24
  describe("parseConfig", () => {
25
- test("reads a file an older agent wrote, with no public address and no port counts", () => {
25
+ it("reads a file an older agent wrote, with no public address and no port counts", () => {
26
26
  const config = parseConfig(written());
27
27
 
28
28
  expect(config.publicAddress).toBeUndefined();
@@ -33,7 +33,7 @@ describe("parseConfig", () => {
33
33
  });
34
34
  });
35
35
 
36
- test("reads the public address and the port counts when the agent wrote them", () => {
36
+ it("reads the public address and the port counts when the agent wrote them", () => {
37
37
  const config = parseConfig(
38
38
  written({
39
39
  publicAddress: "203.0.113.7",
@@ -51,7 +51,7 @@ describe("parseConfig", () => {
51
51
  });
52
52
  });
53
53
 
54
- test("ignores a malformed public address and drops non-numeric counts", () => {
54
+ it("ignores a malformed public address and drops non-numeric counts", () => {
55
55
  const config = parseConfig(
56
56
  written({
57
57
  publicAddress: 7,
@@ -68,7 +68,7 @@ describe("parseConfig", () => {
68
68
  });
69
69
  });
70
70
 
71
- test("still refuses a game that would run as root", () => {
71
+ it("still refuses a game that would run as root", () => {
72
72
  expect(() =>
73
73
  parseConfig(
74
74
  written({