@mycadet/gateway 0.12.10 → 0.13.0

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 (85) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  2. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  3. package/node_modules/@mycadet/ces-client/src/http-credentials.ts +18 -1
  4. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  5. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  6. package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
  7. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  8. package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  9. package/openapi.json +1 -1
  10. package/package.json +1 -1
  11. package/src/__tests__/actor-token-revocation.test.ts +41 -34
  12. package/src/__tests__/contact-store-mirror-op-missing.test.ts +33 -70
  13. package/src/__tests__/contacts-mirror-op-reporter.test.ts +33 -89
  14. package/src/__tests__/credential-requests.test.ts +4 -4
  15. package/src/__tests__/devices-mine.test.ts +187 -0
  16. package/src/__tests__/edge-auth.test.ts +1 -20
  17. package/src/__tests__/edge-guardian-auth.test.ts +1 -17
  18. package/src/__tests__/feature-flag-resolver.test.ts +2 -30
  19. package/src/__tests__/feature-flag-watcher-callback.test.ts +0 -22
  20. package/src/__tests__/feature-flags-route.test.ts +2 -191
  21. package/src/__tests__/guardian-bootstrap-lookups.test.ts +22 -29
  22. package/src/__tests__/guardian-init-lockfile.test.ts +2 -2
  23. package/src/__tests__/guardian-integrity-reporter.test.ts +51 -82
  24. package/src/__tests__/guardian-integrity.test.ts +16 -20
  25. package/src/__tests__/handle-inbound-trust-verdict.test.ts +1 -14
  26. package/src/__tests__/ipc-feature-flag-routes.test.ts +0 -56
  27. package/src/__tests__/lum-2783-recovery-e2e.test.ts +0 -12
  28. package/src/__tests__/post-assistant-ready-deferred.test.ts +0 -18
  29. package/src/__tests__/refresh-device-binding.test.ts +0 -12
  30. package/src/__tests__/remote-web-pairing-token.test.ts +5 -15
  31. package/src/__tests__/runtime-client.test.ts +7 -7
  32. package/src/__tests__/speech-relay-websocket.test.ts +4 -4
  33. package/src/__tests__/trust-verdict-resolver-guardian-integrity.test.ts +30 -36
  34. package/src/__tests__/twilio-webhooks.test.ts +2 -2
  35. package/src/auth/actor-token-revocation.ts +34 -9
  36. package/src/auth/guardian-bootstrap.ts +1 -1
  37. package/src/cli/__tests__/pairing.test.ts +95 -0
  38. package/src/cli/index.ts +1 -1
  39. package/src/cli/pairing.ts +81 -6
  40. package/src/contacts-mirror-op-reporter.ts +19 -36
  41. package/src/db/connection.ts +0 -10
  42. package/src/feature-flag-resolver.ts +2 -7
  43. package/src/feature-flag-watcher.ts +1 -14
  44. package/src/guardian-integrity-reporter.ts +19 -47
  45. package/src/http/middleware/auth.ts +33 -27
  46. package/src/http/routes/auth-token.ts +1 -1
  47. package/src/http/routes/devices-mine.ts +103 -0
  48. package/src/http/routes/feature-flags.ts +1 -8
  49. package/src/http/routes/guardian-pin.ts +2 -11
  50. package/src/http/routes/ipc-runtime-proxy.ts +1 -1
  51. package/src/http/routes/live-voice-websocket.ts +1 -1
  52. package/src/http/routes/runtime-audio-stream.ts +1 -1
  53. package/src/http/routes/runtime-proxy.ts +2 -2
  54. package/src/http/routes/speech-relay-websocket.ts +2 -2
  55. package/src/http/routes/twilio-voice-webhook.test.ts +13 -13
  56. package/src/index.ts +11 -198
  57. package/src/ipc/feature-flag-handlers.ts +2 -9
  58. package/src/remote-web/pairing-challenge-store.ts +15 -0
  59. package/src/risk/command-registry/commands/assistant.ts +0 -24
  60. package/src/risk/command-registry.test.ts +2 -4
  61. package/src/schema.ts +44 -372
  62. package/src/__tests__/auth-fallback-count-tracker.test.ts +0 -92
  63. package/src/__tests__/auth-fallback-reporter.test.ts +0 -101
  64. package/src/__tests__/backup-worker-cloud-mode.test.ts +0 -44
  65. package/src/__tests__/backup-worker-export-timeout.test.ts +0 -164
  66. package/src/__tests__/migration-teleport-gcs-proxy.test.ts +0 -216
  67. package/src/__tests__/remote-feature-flag-sync.test.ts +0 -961
  68. package/src/auth-fallback-count-tracker.ts +0 -113
  69. package/src/auth-fallback-reporter.ts +0 -127
  70. package/src/backup/backup-key.ts +0 -138
  71. package/src/backup/backup-routes.ts +0 -161
  72. package/src/backup/backup-worker.ts +0 -425
  73. package/src/backup/list-snapshots.ts +0 -97
  74. package/src/backup/local-writer.ts +0 -87
  75. package/src/backup/offsite-writer.ts +0 -182
  76. package/src/backup/paths.ts +0 -113
  77. package/src/backup/stream-crypt.ts +0 -258
  78. package/src/device-id.ts +0 -18
  79. package/src/feature-flag-remote-store.ts +0 -166
  80. package/src/http/routes/migration-proxy.ts +0 -636
  81. package/src/internal-telemetry-client.ts +0 -39
  82. package/src/ipc/debug-export-handlers.test.ts +0 -102
  83. package/src/ipc/debug-export-handlers.ts +0 -81
  84. package/src/remote-feature-flag-sync.ts +0 -440
  85. package/src/watchdog-reporter.ts +0 -110
@@ -50,7 +50,6 @@ beforeEach(() => {
50
50
  _setRegistryCandidateOverrides([defaultsPath]);
51
51
  resetFeatureFlagDefaultsCache();
52
52
  clearFeatureFlagStoreCache();
53
- clearRemoteFeatureFlagStoreCache();
54
53
  });
55
54
 
56
55
  afterEach(() => {
@@ -63,7 +62,6 @@ afterEach(() => {
63
62
  _setRegistryCandidateOverrides(null);
64
63
  resetFeatureFlagDefaultsCache();
65
64
  clearFeatureFlagStoreCache();
66
- clearRemoteFeatureFlagStoreCache();
67
65
  resetEnvOverridesCache();
68
66
  delete process.env.CADET_FLAG_BROWSER;
69
67
  delete process.env.CADET_FLAG_A2A_CHANNEL;
@@ -76,8 +74,6 @@ const { resetFeatureFlagDefaultsCache, _setRegistryCandidateOverrides } =
76
74
  await import("../feature-flag-defaults.js");
77
75
  const { clearFeatureFlagStoreCache, writeFeatureFlag } =
78
76
  await import("../feature-flag-store.js");
79
- const { clearRemoteFeatureFlagStoreCache, writeRemoteFeatureFlags } =
80
- await import("../feature-flag-remote-store.js");
81
77
  const { resetEnvOverridesCache } =
82
78
  await import("../feature-flag-env-overrides.js");
83
79
 
@@ -95,19 +91,8 @@ describe("isFeatureFlagEnabled", () => {
95
91
  expect(isFeatureFlagEnabled("a2a-channel")).toBe(true);
96
92
  });
97
93
 
98
- test("uses explicit remote values for declared flags", () => {
99
- writeRemoteFeatureFlags({
100
- browser: false,
101
- "a2a-channel": true,
102
- });
103
-
104
- expect(isFeatureFlagEnabled("browser")).toBe(false);
105
- expect(isFeatureFlagEnabled("a2a-channel")).toBe(true);
106
- });
107
-
108
- test("ignores persisted and remote values for undeclared flags", () => {
94
+ test("ignores persisted values for undeclared flags", () => {
109
95
  writeFeatureFlag("unknown", true);
110
- writeRemoteFeatureFlags({ unknown: true });
111
96
 
112
97
  expect(isFeatureFlagEnabled("unknown")).toBe(false);
113
98
  });
@@ -146,19 +131,7 @@ describe("getFeatureFlagValue", () => {
146
131
  expect(getFeatureFlagValue("default-model")).toBe("gpt-4");
147
132
  });
148
133
 
149
- test("remote string value overrides default when no persisted value", () => {
150
- writeRemoteFeatureFlags({ "default-model": "gpt-4" });
151
- expect(getFeatureFlagValue("default-model")).toBe("gpt-4");
152
- });
153
-
154
- test("persisted takes precedence over remote for string flags", () => {
155
- writeRemoteFeatureFlags({ "default-model": "remote-model" });
156
- writeFeatureFlag("default-model", "persisted-model");
157
- expect(getFeatureFlagValue("default-model")).toBe("persisted-model");
158
- });
159
-
160
- test("env override wins over persisted, remote, and default values", () => {
161
- writeRemoteFeatureFlags({ browser: false });
134
+ test("env override wins over persisted and default values", () => {
162
135
  writeFeatureFlag("browser", false);
163
136
 
164
137
  process.env.CADET_FLAG_BROWSER = "true";
@@ -168,7 +141,6 @@ describe("getFeatureFlagValue", () => {
168
141
  });
169
142
 
170
143
  test("env override wins for string flags", () => {
171
- writeRemoteFeatureFlags({ "default-model": "remote-model" });
172
144
  writeFeatureFlag("default-model", "persisted-model");
173
145
 
174
146
  process.env.CADET_FLAG_DEFAULT_MODEL = "env-model";
@@ -13,12 +13,6 @@ mock.module("../feature-flag-store.js", () => ({
13
13
  getFeatureFlagStorePath: () => join(flagDir, "feature-flags.json"),
14
14
  }));
15
15
 
16
- mock.module("../feature-flag-remote-store.js", () => ({
17
- refreshRemoteFeatureFlagStoreCache: mock(() => {}),
18
- getRemoteFeatureFlagStorePath: () =>
19
- join(flagDir, "remote-feature-flags.json"),
20
- }));
21
-
22
16
  const { FeatureFlagWatcher } = await import("../feature-flag-watcher.js");
23
17
 
24
18
  describe("FeatureFlagWatcher onChanged callback", () => {
@@ -52,22 +46,6 @@ describe("FeatureFlagWatcher onChanged callback", () => {
52
46
  watcher.stop();
53
47
  });
54
48
 
55
- test("calls onChanged after debounce fires on remote flag file change", async () => {
56
- const onChanged = mock(() => {});
57
- const watcher = new FeatureFlagWatcher({ onChanged });
58
- watcher.start();
59
-
60
- writeFileSync(
61
- join(flagDir, "remote-feature-flags.json"),
62
- JSON.stringify({ remote: true }),
63
- );
64
-
65
- await new Promise((r) => setTimeout(r, 700));
66
-
67
- expect(onChanged).toHaveBeenCalledTimes(1);
68
- watcher.stop();
69
- });
70
-
71
49
  test("does not call onChanged when no callback is provided", async () => {
72
50
  const watcher = new FeatureFlagWatcher();
73
51
  watcher.start();
@@ -11,10 +11,6 @@ import { testSecurityDir } from "./test-preload.js";
11
11
 
12
12
  const protectedDir = testSecurityDir;
13
13
  const featureFlagStorePath = join(protectedDir, "feature-flags.json");
14
- const remoteFeatureFlagStorePath = join(
15
- protectedDir,
16
- "feature-flags-remote.json",
17
- );
18
14
 
19
15
  // Write the test registry to an isolated temp path so we never touch
20
16
  // the committed gateway/src/feature-flag-registry.json file.
@@ -65,7 +61,6 @@ beforeEach(() => {
65
61
  _setRegistryCandidateOverrides([defaultsPath]);
66
62
  resetFeatureFlagDefaultsCache();
67
63
  clearFeatureFlagStoreCache();
68
- clearRemoteFeatureFlagStoreCache();
69
64
  });
70
65
 
71
66
  afterEach(() => {
@@ -79,7 +74,6 @@ afterEach(() => {
79
74
  _setRegistryCandidateOverrides(null);
80
75
  resetFeatureFlagDefaultsCache();
81
76
  clearFeatureFlagStoreCache();
82
- clearRemoteFeatureFlagStoreCache();
83
77
  resetEnvOverridesCache();
84
78
  delete process.env.CADET_FLAG_A2A_CHANNEL;
85
79
  });
@@ -93,8 +87,6 @@ const {
93
87
  } = await import("../feature-flag-defaults.js");
94
88
  const { clearFeatureFlagStoreCache, readPersistedFeatureFlags } =
95
89
  await import("../feature-flag-store.js");
96
- const { clearRemoteFeatureFlagStoreCache, writeRemoteFeatureFlags } =
97
- await import("../feature-flag-remote-store.js");
98
90
  const { resetEnvOverridesCache } =
99
91
  await import("../feature-flag-env-overrides.js");
100
92
 
@@ -283,132 +275,13 @@ describe("GET /v1/feature-flags handler", () => {
283
275
  expect(browserFlag.defaultEnabled).toBe(true);
284
276
  });
285
277
 
286
- test("remote values fill in when no local override exists", async () => {
287
- // Write a remote store with a2a-channel enabled (overriding registry default of false)
288
- writeFileSync(
289
- remoteFeatureFlagStorePath,
290
- JSON.stringify({
291
- version: 1,
292
- values: {
293
- "a2a-channel": true,
294
- },
295
- }),
296
- );
297
- clearRemoteFeatureFlagStoreCache();
298
-
299
- // No local override for a2a-channel
300
- if (existsSync(featureFlagStorePath)) {
301
- rmSync(featureFlagStorePath);
302
- }
303
- clearFeatureFlagStoreCache();
304
-
305
- const handler = createFeatureFlagsGetHandler();
306
- const res = await handler(
307
- new Request("http://gateway.test/v1/feature-flags"),
308
- );
309
-
310
- expect(res.status).toBe(200);
311
- const body = await res.json();
312
-
313
- const a2aFlag = body.flags.find(
314
- (f: { key: string }) => f.key === "a2a-channel",
315
- );
316
- expect(a2aFlag).toBeDefined();
317
- // Remote value (true) overrides registry default (false)
318
- expect(a2aFlag.enabled).toBe(true);
319
- });
320
-
321
- test("local overrides take precedence over remote values", async () => {
322
- // Set remote value to true
323
- writeFileSync(
324
- remoteFeatureFlagStorePath,
325
- JSON.stringify({
326
- version: 1,
327
- values: {
328
- "a2a-channel": true,
329
- },
330
- }),
331
- );
332
- clearRemoteFeatureFlagStoreCache();
333
-
334
- // Set local override to false
335
- writeFileSync(
336
- featureFlagStorePath,
337
- JSON.stringify({
338
- version: 1,
339
- values: {
340
- "a2a-channel": false,
341
- },
342
- }),
343
- );
344
- clearFeatureFlagStoreCache();
345
-
346
- const handler = createFeatureFlagsGetHandler();
347
- const res = await handler(
348
- new Request("http://gateway.test/v1/feature-flags"),
349
- );
350
-
351
- expect(res.status).toBe(200);
352
- const body = await res.json();
353
-
354
- const a2aFlag = body.flags.find(
355
- (f: { key: string }) => f.key === "a2a-channel",
356
- );
357
- expect(a2aFlag).toBeDefined();
358
- // Local override (false) takes precedence over remote (true)
359
- expect(a2aFlag.enabled).toBe(false);
360
- });
361
-
362
- test("reflects updated flags after remote sync writes new values (stale cache regression)", async () => {
363
- // Scenario: the remote poller (RemoteFeatureFlagSync) writes
364
- // a2a-channel: false, the gateway caches it, then a subsequent
365
- // poll writes a2a-channel: true. The GET handler should return
366
- // the updated value because writeRemoteFeatureFlags() updates
367
- // both disk and the in-memory cache.
368
-
369
- // Step 1: First poll writes a2a-channel: false (simulated via
370
- // writeRemoteFeatureFlags, which is what the poller calls internally).
371
- writeRemoteFeatureFlags({ "a2a-channel": false });
372
-
373
- const handler = createFeatureFlagsGetHandler();
374
- const res1 = await handler(
375
- new Request("http://gateway.test/v1/feature-flags"),
376
- );
377
- const body1 = await res1.json();
378
- const a2aFlag1 = body1.flags.find(
379
- (f: { key: string }) => f.key === "a2a-channel",
380
- );
381
- expect(a2aFlag1.enabled).toBe(false);
382
-
383
- // Step 2: Second poll writes a2a-channel: true — the poller
384
- // calls writeRemoteFeatureFlags which updates file + cache.
385
- writeRemoteFeatureFlags({ "a2a-channel": true });
386
-
387
- // Step 3: The GET handler should immediately reflect the update
388
- // without needing a file-watcher round-trip.
389
- const res2 = await handler(
390
- new Request("http://gateway.test/v1/feature-flags"),
391
- );
392
- const body2 = await res2.json();
393
- const a2aFlag2 = body2.flags.find(
394
- (f: { key: string }) => f.key === "a2a-channel",
395
- );
396
- expect(a2aFlag2.enabled).toBe(true);
397
- });
398
-
399
- test("registry default used when neither local nor remote is set", async () => {
278
+ test("registry default used when no local override is set", async () => {
400
279
  // No local override
401
280
  if (existsSync(featureFlagStorePath)) {
402
281
  rmSync(featureFlagStorePath);
403
282
  }
404
283
  clearFeatureFlagStoreCache();
405
284
 
406
- // No remote value (empty remote store)
407
- if (existsSync(remoteFeatureFlagStorePath)) {
408
- rmSync(remoteFeatureFlagStorePath);
409
- }
410
- clearRemoteFeatureFlagStoreCache();
411
-
412
285
  const handler = createFeatureFlagsGetHandler();
413
286
  const res = await handler(
414
287
  new Request("http://gateway.test/v1/feature-flags"),
@@ -417,11 +290,11 @@ describe("GET /v1/feature-flags handler", () => {
417
290
  expect(res.status).toBe(200);
418
291
  const body = await res.json();
419
292
 
420
- // a2a-channel has defaultEnabled: false in registry
421
293
  const a2aFlag = body.flags.find(
422
294
  (f: { key: string }) => f.key === "a2a-channel",
423
295
  );
424
296
  expect(a2aFlag).toBeDefined();
297
+ // Registry default (false) applies with no local override
425
298
  expect(a2aFlag.enabled).toBe(false);
426
299
  expect(a2aFlag.defaultEnabled).toBe(false);
427
300
 
@@ -434,51 +307,9 @@ describe("GET /v1/feature-flags handler", () => {
434
307
  expect(browserFlag.defaultEnabled).toBe(true);
435
308
  });
436
309
 
437
- test("declared flags missing from a remote snapshot use their registry defaults", async () => {
438
- // No local override
439
- if (existsSync(featureFlagStorePath)) {
440
- rmSync(featureFlagStorePath);
441
- }
442
- clearFeatureFlagStoreCache();
443
-
444
- // Remote snapshot exists, but browser is absent as it would be when the
445
- // platform has no LaunchDarkly value for that key.
446
- writeFileSync(
447
- remoteFeatureFlagStorePath,
448
- JSON.stringify({
449
- version: 1,
450
- values: { "a2a-channel": true },
451
- }),
452
- );
453
- clearRemoteFeatureFlagStoreCache();
454
-
455
- const handler = createFeatureFlagsGetHandler();
456
- const res = await handler(
457
- new Request("http://gateway.test/v1/feature-flags"),
458
- );
459
-
460
- expect(res.status).toBe(200);
461
- const body = await res.json();
462
-
463
- const a2aFlag = body.flags.find(
464
- (f: { key: string }) => f.key === "a2a-channel",
465
- );
466
- expect(a2aFlag.enabled).toBe(true);
467
-
468
- const browserFlag = body.flags.find(
469
- (f: { key: string }) => f.key === "browser",
470
- );
471
- expect(browserFlag).toBeDefined();
472
- expect(browserFlag.enabled).toBe(true);
473
- expect(browserFlag.defaultEnabled).toBe(true);
474
- });
475
-
476
310
  test("string flag uses registry default when no override exists", async () => {
477
311
  if (existsSync(featureFlagStorePath)) rmSync(featureFlagStorePath);
478
- if (existsSync(remoteFeatureFlagStorePath))
479
- rmSync(remoteFeatureFlagStorePath);
480
312
  clearFeatureFlagStoreCache();
481
- clearRemoteFeatureFlagStoreCache();
482
313
 
483
314
  const handler = createFeatureFlagsGetHandler();
484
315
  const res = await handler(
@@ -496,26 +327,6 @@ describe("GET /v1/feature-flags handler", () => {
496
327
  expect(modelFlag.defaultEnabled).toBe("claude-sonnet-4-6");
497
328
  });
498
329
 
499
- test("remote string value overrides registry default for string flag", async () => {
500
- writeRemoteFeatureFlags({ "default-model": "gpt-4" });
501
- if (existsSync(featureFlagStorePath)) rmSync(featureFlagStorePath);
502
- clearFeatureFlagStoreCache();
503
-
504
- const handler = createFeatureFlagsGetHandler();
505
- const res = await handler(
506
- new Request("http://gateway.test/v1/feature-flags"),
507
- );
508
-
509
- expect(res.status).toBe(200);
510
- const body = await res.json();
511
-
512
- const modelFlag = body.flags.find(
513
- (f: { key: string }) => f.key === "default-model",
514
- );
515
- expect(modelFlag).toBeDefined();
516
- expect(modelFlag.enabled).toBe("gpt-4");
517
- });
518
-
519
330
  test("env override takes precedence over persisted and default values", async () => {
520
331
  // Persisted value sets a2a-channel to false
521
332
  writeFileSync(
@@ -5,9 +5,9 @@
5
5
  * (via ensureCadetGuardianBinding / bootstrapGuardian) and its divergent
6
6
  * re-mint guard (evidence of a prior guardian refuses the implicit mint).
7
7
  *
8
- * The guardian-integrity reporter is silenced/observed via its test-only
9
- * overrides, and the integrity cache is busted between tests — same seam
10
- * conventions as the guardian-integrity test trio (no mock.module).
8
+ * The guardian-integrity reporter's report fns are observed via a pass-through
9
+ * spy on the module's live ESM bindings, and the integrity cache is busted
10
+ * between tests.
11
11
  *
12
12
  * Guardian rows are seeded ONLY in the gateway DB. By default the assistant
13
13
  * DB proxy is swapped for a backend that throws, proving the lookups never
@@ -25,7 +25,6 @@ import {
25
25
  expect,
26
26
  beforeAll,
27
27
  beforeEach,
28
- afterEach,
29
28
  afterAll,
30
29
  mock,
31
30
  } from "bun:test";
@@ -84,10 +83,7 @@ import {
84
83
  actorTokenRecords,
85
84
  actorRefreshTokenRecords,
86
85
  } from "../db/schema.js";
87
- import {
88
- resetGuardianIntegrityReporterForTesting,
89
- setGuardianIntegrityReporterOverridesForTesting,
90
- } from "../guardian-integrity-reporter.js";
86
+ import * as guardianIntegrityReporter from "../guardian-integrity-reporter.js";
91
87
  import { seedActorToken, seedContact } from "./helpers/contact-fixtures.js";
92
88
 
93
89
  // Initialize signing key so bootstrapGuardian's JWT minting works.
@@ -165,10 +161,22 @@ beforeAll(async () => {
165
161
  await initGatewayDb();
166
162
  });
167
163
 
168
- // Captured guardian-integrity reporter detail payloads (via the log
169
- // override): missing-guardian and mint-refused reports both land here.
164
+ // Captured guardian-integrity reporter detail payloads (via module
165
+ // re-registration — bun's spyOn does not intercept calls made through the
166
+ // SUTs' direct ESM imports): missing-guardian and mint-refused reports both
167
+ // land here.
170
168
  const reportCalls: Record<string, unknown>[] = [];
171
169
 
170
+ mock.module("../guardian-integrity-reporter.js", () => ({
171
+ ...guardianIntegrityReporter,
172
+ reportMissingGuardian: (detail: Record<string, unknown>) => {
173
+ reportCalls.push(detail);
174
+ },
175
+ reportGuardianMintRefused: (detail: Record<string, unknown>) => {
176
+ reportCalls.push(detail);
177
+ },
178
+ }));
179
+
172
180
  beforeEach(() => {
173
181
  const db = getGatewayDb();
174
182
  db.delete(contactChannels).run();
@@ -180,23 +188,8 @@ beforeEach(() => {
180
188
  bustGuardianIntegrityCache();
181
189
  assistantBackend = throwingBackend;
182
190
  reportCalls.length = 0;
183
- resetGuardianIntegrityReporterForTesting();
184
- setGuardianIntegrityReporterOverridesForTesting({
185
- fetchImpl: async () => new Response("{}"),
186
- mintToken: () => "svc-token",
187
- baseUrl: "http://127.0.0.1:7821",
188
- log: {
189
- error: (detail) => {
190
- reportCalls.push(detail);
191
- },
192
- warn: () => {},
193
- },
194
- });
195
191
  });
196
192
 
197
- afterEach(() => {
198
- resetGuardianIntegrityReporterForTesting();
199
- });
200
193
 
201
194
  afterAll(() => {
202
195
  resetGatewayDb();
@@ -337,7 +330,7 @@ describe("resolve-or-mint (resolveOrCreateCadetGuardian)", () => {
337
330
  assistantBackend = makeAssistantBackend();
338
331
 
339
332
  const principalId = await ensureCadetGuardianBinding();
340
- expect(principalId).toMatch(/^cadet-principal-/);
333
+ expect(principalId).toMatch(/^principal-/);
341
334
 
342
335
  // Gateway now has the row; the assistant DB must not be read again.
343
336
  assistantBackend = throwingBackend;
@@ -350,7 +343,7 @@ describe("resolve-or-mint (resolveOrCreateCadetGuardian)", () => {
350
343
 
351
344
  const principalId = await ensureCadetGuardianBinding();
352
345
 
353
- expect(principalId).toMatch(/^cadet-principal-/);
346
+ expect(principalId).toMatch(/^principal-/);
354
347
  const gwRows = gatewayCadetGuardians();
355
348
  expect(gwRows).toHaveLength(1);
356
349
  expect(gwRows[0]?.principalId).toBe(principalId);
@@ -383,7 +376,7 @@ describe("resolve-or-mint (resolveOrCreateCadetGuardian)", () => {
383
376
  deviceId: "device-1",
384
377
  });
385
378
 
386
- expect(result.guardianPrincipalId).toMatch(/^cadet-principal-/);
379
+ expect(result.guardianPrincipalId).toMatch(/^principal-/);
387
380
  expect(result.isNew).toBe(true);
388
381
  expect(result.mintedOverPriorEvidence).toBe(false);
389
382
  });
@@ -606,7 +599,7 @@ describe("resolve-or-mint (resolveOrCreateCadetGuardian)", () => {
606
599
  deviceId: "device-1",
607
600
  });
608
601
 
609
- expect(result.guardianPrincipalId).toMatch(/^cadet-principal-/);
602
+ expect(result.guardianPrincipalId).toMatch(/^principal-/);
610
603
  expect(result.isNew).toBe(true);
611
604
  expect(result.mintedOverPriorEvidence).toBe(true);
612
605
  expect(gatewayCadetGuardians()).toHaveLength(1);
@@ -271,7 +271,7 @@ describe("guardian/init bootstrap secret", () => {
271
271
 
272
272
  expect(res.status).toBe(200);
273
273
  const body = await res.json();
274
- expect(body.guardianPrincipalId).toMatch(/^cadet-principal-/);
274
+ expect(body.guardianPrincipalId).toMatch(/^principal-/);
275
275
  expect(body.accessToken).toBeTruthy();
276
276
  expect(body.refreshToken).toBeTruthy();
277
277
  expect(body.isNew).toBe(true);
@@ -391,7 +391,7 @@ describe("guardian/init one-time-use lockfile", () => {
391
391
 
392
392
  expect(res.status).toBe(200);
393
393
  const body = await res.json();
394
- expect(body.guardianPrincipalId).toMatch(/^cadet-principal-/);
394
+ expect(body.guardianPrincipalId).toMatch(/^principal-/);
395
395
  expect(body.accessToken).toBeTruthy();
396
396
  expect(body.refreshToken).toBeTruthy();
397
397
  expect(lockFileExists()).toBe(true);
@@ -1,109 +1,78 @@
1
1
  /**
2
- * Fail-loud missing-guardian reporter (guardian-integrity-reporter.ts):
2
+ * Fail-loud guardian-integrity reporter (guardian-integrity-reporter.ts):
3
3
  *
4
- * - First report emits one error log and one telemetry relay POST with the
5
- * `gateway_guardian_missing` check name and the caller's detail.
6
- * - Subsequent reports inside the hourly window are dropped (rate limit).
7
- * - Relay failures never throw out of the caller.
4
+ * - First report per check emits one error log carrying the caller's detail
5
+ * and the failure copy.
6
+ * - Subsequent reports inside the hourly window are dropped (rate limit);
7
+ * the sibling check still reports (per-check rate limit).
8
8
  *
9
- * Logging is observed through the reporter's test-only `log` override —
10
- * bun's mock.module is process-global and would leak into other test files.
9
+ * Logs are observed by mocking the logger module (a side-effect boundary —
10
+ * the same convention as sleep-wake-detector.test.ts). Per-file CI truth:
11
+ * the mock is process-global, so this file pins nothing else.
11
12
  */
12
- import { beforeEach, afterEach, describe, expect, test } from "bun:test";
13
+ import { beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
13
14
 
14
- const errorLogs: unknown[] = [];
15
- const warnLogs: unknown[] = [];
15
+ const errorLogs: [Record<string, unknown>, string][] = [];
16
+
17
+ mock.module("../logger.js", () => ({
18
+ getLogger: () => ({
19
+ error: (detail: Record<string, unknown>, msg: string) => {
20
+ errorLogs.push([detail, msg]);
21
+ },
22
+ warn: () => {},
23
+ }),
24
+ }));
16
25
 
17
26
  await import("./test-preload.js");
18
- const {
19
- GUARDIAN_MISSING_CHECK_NAME,
20
- flushGuardianIntegrityReporterForTesting,
21
- reportMissingGuardian,
22
- resetGuardianIntegrityReporterForTesting,
23
- setGuardianIntegrityReporterOverridesForTesting,
24
- } = await import("../guardian-integrity-reporter.js");
27
+ const { reportGuardianMintRefused, reportMissingGuardian } =
28
+ await import("../guardian-integrity-reporter.js");
25
29
 
26
- type FetchCall = { url: string; init: RequestInit };
27
- let fetchCalls: FetchCall[] = [];
28
- let fetchResult: () => Promise<Response> = async () => new Response("{}");
30
+ let fakeNow = 1_000_000_000_000;
29
31
 
30
32
  beforeEach(() => {
31
- fetchCalls = [];
32
- fetchResult = async () => new Response("{}");
33
33
  errorLogs.length = 0;
34
- warnLogs.length = 0;
35
- resetGuardianIntegrityReporterForTesting();
36
- setGuardianIntegrityReporterOverridesForTesting({
37
- fetchImpl: async (url, init) => {
38
- fetchCalls.push({ url: String(url), init: init ?? {} });
39
- return fetchResult();
40
- },
41
- mintToken: () => "svc-token",
42
- baseUrl: "http://127.0.0.1:7821",
43
- log: {
44
- error: (detail, msg) => {
45
- errorLogs.push([detail, msg]);
46
- },
47
- warn: (detail, msg) => {
48
- warnLogs.push([detail, msg]);
49
- },
50
- },
51
- });
52
- });
53
-
54
- afterEach(() => {
55
- resetGuardianIntegrityReporterForTesting();
34
+ fakeNow += 24 * 60 * 60 * 1000; // start each test outside prior windows
35
+ spyOn(Date, "now").mockImplementation(() => fakeNow);
56
36
  });
57
37
 
58
38
  describe("reportMissingGuardian", () => {
59
- test("first report logs an error and relays one watchdog event", async () => {
39
+ test("first report logs one error with the detail + copy", () => {
60
40
  reportMissingGuardian({ has_contacts: true, has_actor_tokens: false });
61
- await flushGuardianIntegrityReporterForTesting();
62
41
 
63
42
  expect(errorLogs.length).toBe(1);
64
- expect(fetchCalls.length).toBe(1);
65
- expect(fetchCalls[0].url).toBe(
66
- "http://127.0.0.1:7821/v1/internal/telemetry/watchdog",
67
- );
68
- const headers = fetchCalls[0].init.headers as Record<string, string>;
69
- expect(headers.Authorization).toBe("Bearer svc-token");
70
- expect(JSON.parse(String(fetchCalls[0].init.body))).toEqual({
71
- check_name: GUARDIAN_MISSING_CHECK_NAME,
72
- detail: { has_contacts: true, has_actor_tokens: false },
43
+ expect(errorLogs[0]![0]).toMatchObject({
44
+ check: "gateway_guardian_missing",
45
+ has_contacts: true,
46
+ has_actor_tokens: false,
73
47
  });
48
+ expect(errorLogs[0]![1]).toContain("until the guardian is re-seeded");
74
49
  });
75
50
 
76
- test("repeat reports inside the hourly window are dropped", async () => {
77
- reportMissingGuardian({ has_contacts: true, has_actor_tokens: false });
78
- reportMissingGuardian({ has_contacts: true, has_actor_tokens: false });
79
- reportMissingGuardian({ has_contacts: true, has_actor_tokens: true });
80
- await flushGuardianIntegrityReporterForTesting();
51
+ test("repeat reports inside the hourly window are dropped; the sibling check still fires", () => {
52
+ reportMissingGuardian({});
53
+ reportMissingGuardian({});
54
+ reportGuardianMintRefused({ integrity_state: "ok" });
81
55
 
82
- expect(errorLogs.length).toBe(1);
83
- expect(fetchCalls.length).toBe(1);
56
+ expect(errorLogs.length).toBe(2);
57
+ expect(errorLogs[0]![0]).toMatchObject({
58
+ check: "gateway_guardian_missing",
59
+ });
60
+ expect(errorLogs[1]![0]).toMatchObject({
61
+ check: "gateway_guardian_mint_refused",
62
+ integrity_state: "ok",
63
+ });
84
64
  });
65
+ });
85
66
 
86
- test("a rejected relay never throws out of the caller", async () => {
87
- fetchResult = async () => {
88
- throw new Error("daemon unreachable");
89
- };
90
-
91
- expect(() =>
92
- reportMissingGuardian({ has_contacts: false, has_actor_tokens: true }),
93
- ).not.toThrow();
94
- await flushGuardianIntegrityReporterForTesting();
67
+ describe("reportGuardianMintRefused", () => {
68
+ test("first report logs one error with the detail + copy", () => {
69
+ reportGuardianMintRefused({ integrity_state: "ok" });
95
70
 
96
71
  expect(errorLogs.length).toBe(1);
97
- expect(warnLogs.length).toBe(1);
98
- });
99
-
100
- test("a non-ok relay response is swallowed with a warning", async () => {
101
- fetchResult = async () => new Response("nope", { status: 500 });
102
-
103
- reportMissingGuardian({ has_contacts: true, has_actor_tokens: true });
104
- await flushGuardianIntegrityReporterForTesting();
105
-
106
- expect(fetchCalls.length).toBe(1);
107
- expect(warnLogs.length).toBe(1);
72
+ expect(errorLogs[0]![0]).toMatchObject({
73
+ check: "gateway_guardian_mint_refused",
74
+ integrity_state: "ok",
75
+ });
76
+ expect(errorLogs[0]![1]).toContain("guardian_repair_required");
108
77
  });
109
78
  });