@mycadet/gateway 0.12.11 → 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.
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/openapi.json +1 -1
- package/package.json +1 -1
- package/src/__tests__/actor-token-revocation.test.ts +41 -34
- package/src/__tests__/contact-store-mirror-op-missing.test.ts +33 -70
- package/src/__tests__/contacts-mirror-op-reporter.test.ts +33 -89
- package/src/__tests__/credential-requests.test.ts +4 -4
- package/src/__tests__/devices-mine.test.ts +187 -0
- package/src/__tests__/edge-auth.test.ts +1 -20
- package/src/__tests__/edge-guardian-auth.test.ts +1 -17
- package/src/__tests__/feature-flag-resolver.test.ts +2 -30
- package/src/__tests__/feature-flag-watcher-callback.test.ts +0 -22
- package/src/__tests__/feature-flags-route.test.ts +2 -191
- package/src/__tests__/guardian-bootstrap-lookups.test.ts +18 -25
- package/src/__tests__/guardian-integrity-reporter.test.ts +51 -82
- package/src/__tests__/guardian-integrity.test.ts +16 -20
- package/src/__tests__/handle-inbound-trust-verdict.test.ts +1 -14
- package/src/__tests__/ipc-feature-flag-routes.test.ts +0 -56
- package/src/__tests__/lum-2783-recovery-e2e.test.ts +0 -12
- package/src/__tests__/post-assistant-ready-deferred.test.ts +0 -18
- package/src/__tests__/refresh-device-binding.test.ts +0 -12
- package/src/__tests__/remote-web-pairing-token.test.ts +4 -14
- package/src/__tests__/runtime-client.test.ts +7 -7
- package/src/__tests__/speech-relay-websocket.test.ts +4 -4
- package/src/__tests__/trust-verdict-resolver-guardian-integrity.test.ts +30 -36
- package/src/__tests__/twilio-webhooks.test.ts +2 -2
- package/src/auth/actor-token-revocation.ts +34 -9
- package/src/cli/__tests__/pairing.test.ts +95 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/pairing.ts +81 -6
- package/src/contacts-mirror-op-reporter.ts +19 -36
- package/src/db/connection.ts +0 -10
- package/src/feature-flag-resolver.ts +2 -7
- package/src/feature-flag-watcher.ts +1 -14
- package/src/guardian-integrity-reporter.ts +19 -47
- package/src/http/middleware/auth.ts +33 -27
- package/src/http/routes/auth-token.ts +1 -1
- package/src/http/routes/devices-mine.ts +103 -0
- package/src/http/routes/feature-flags.ts +1 -8
- package/src/http/routes/guardian-pin.ts +2 -11
- package/src/http/routes/ipc-runtime-proxy.ts +1 -1
- package/src/http/routes/live-voice-websocket.ts +1 -1
- package/src/http/routes/runtime-audio-stream.ts +1 -1
- package/src/http/routes/runtime-proxy.ts +2 -2
- package/src/http/routes/twilio-voice-webhook.test.ts +13 -13
- package/src/index.ts +11 -198
- package/src/ipc/feature-flag-handlers.ts +2 -9
- package/src/remote-web/pairing-challenge-store.ts +15 -0
- package/src/risk/command-registry/commands/assistant.ts +0 -24
- package/src/risk/command-registry.test.ts +2 -4
- package/src/schema.ts +44 -372
- package/src/__tests__/auth-fallback-count-tracker.test.ts +0 -92
- package/src/__tests__/auth-fallback-reporter.test.ts +0 -101
- package/src/__tests__/backup-worker-cloud-mode.test.ts +0 -44
- package/src/__tests__/backup-worker-export-timeout.test.ts +0 -164
- package/src/__tests__/migration-teleport-gcs-proxy.test.ts +0 -216
- package/src/__tests__/remote-feature-flag-sync.test.ts +0 -961
- package/src/auth-fallback-count-tracker.ts +0 -113
- package/src/auth-fallback-reporter.ts +0 -127
- package/src/backup/backup-key.ts +0 -138
- package/src/backup/backup-routes.ts +0 -161
- package/src/backup/backup-worker.ts +0 -425
- package/src/backup/list-snapshots.ts +0 -97
- package/src/backup/local-writer.ts +0 -87
- package/src/backup/offsite-writer.ts +0 -182
- package/src/backup/paths.ts +0 -113
- package/src/backup/stream-crypt.ts +0 -258
- package/src/device-id.ts +0 -18
- package/src/feature-flag-remote-store.ts +0 -166
- package/src/http/routes/migration-proxy.ts +0 -636
- package/src/internal-telemetry-client.ts +0 -39
- package/src/ipc/debug-export-handlers.test.ts +0 -102
- package/src/ipc/debug-export-handlers.ts +0 -81
- package/src/remote-feature-flag-sync.ts +0 -440
- package/src/watchdog-reporter.ts +0 -110
|
@@ -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("
|
|
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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
169
|
-
//
|
|
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();
|
|
@@ -1,109 +1,78 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fail-loud
|
|
2
|
+
* Fail-loud guardian-integrity reporter (guardian-integrity-reporter.ts):
|
|
3
3
|
*
|
|
4
|
-
* - First report emits one error log
|
|
5
|
-
*
|
|
6
|
-
* - Subsequent reports inside the hourly window are dropped (rate limit)
|
|
7
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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,
|
|
13
|
+
import { beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
|
|
13
14
|
|
|
14
|
-
const errorLogs: unknown[] = [];
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
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(
|
|
65
|
-
|
|
66
|
-
|
|
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",
|
|
77
|
-
reportMissingGuardian({
|
|
78
|
-
reportMissingGuardian({
|
|
79
|
-
|
|
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(
|
|
83
|
-
expect(
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
});
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* files); its logging/relay behavior has its own tests
|
|
13
13
|
* (guardian-integrity-reporter.test.ts).
|
|
14
14
|
*/
|
|
15
|
-
import { beforeEach, afterEach, describe, expect, test } from "bun:test";
|
|
15
|
+
import { beforeEach, afterEach, describe, expect, mock, test } from "bun:test";
|
|
16
16
|
|
|
17
17
|
await import("./test-preload.js");
|
|
18
18
|
const { initGatewayDb, resetGatewayDb, getGatewayDb } =
|
|
@@ -25,10 +25,8 @@ const {
|
|
|
25
25
|
} = await import("../db/schema.js");
|
|
26
26
|
const { seedActorToken, seedContact } =
|
|
27
27
|
await import("./helpers/contact-fixtures.js");
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
setGuardianIntegrityReporterOverridesForTesting,
|
|
31
|
-
} = await import("../guardian-integrity-reporter.js");
|
|
28
|
+
const actualGuardianIntegrityReporter =
|
|
29
|
+
await import("../guardian-integrity-reporter.js");
|
|
32
30
|
const {
|
|
33
31
|
bustGuardianIntegrityCache,
|
|
34
32
|
guardianIntegrityState,
|
|
@@ -37,10 +35,21 @@ const {
|
|
|
37
35
|
const { applyGuardianBindingGatewayWrites } =
|
|
38
36
|
await import("../auth/guardian-bootstrap.js");
|
|
39
37
|
|
|
40
|
-
// The reporter's
|
|
41
|
-
//
|
|
38
|
+
// The reporter's report fns carry the detail payload; capture it by
|
|
39
|
+
// re-registering the reporter module with recorders (bun's spyOn does not
|
|
40
|
+
// intercept calls made through the SUTs' direct ESM imports).
|
|
42
41
|
const reportCalls: Record<string, unknown>[] = [];
|
|
43
42
|
|
|
43
|
+
mock.module("../guardian-integrity-reporter.js", () => ({
|
|
44
|
+
...actualGuardianIntegrityReporter,
|
|
45
|
+
reportMissingGuardian: (detail: Record<string, unknown>) => {
|
|
46
|
+
reportCalls.push(detail);
|
|
47
|
+
},
|
|
48
|
+
reportGuardianMintRefused: (detail: Record<string, unknown>) => {
|
|
49
|
+
reportCalls.push(detail);
|
|
50
|
+
},
|
|
51
|
+
}));
|
|
52
|
+
|
|
44
53
|
function insertRefreshToken(): void {
|
|
45
54
|
const now = Date.now();
|
|
46
55
|
getGatewayDb()
|
|
@@ -71,23 +80,10 @@ beforeEach(async () => {
|
|
|
71
80
|
getGatewayDb().delete(actorRefreshTokenRecords).run();
|
|
72
81
|
bustGuardianIntegrityCache();
|
|
73
82
|
reportCalls.length = 0;
|
|
74
|
-
resetGuardianIntegrityReporterForTesting();
|
|
75
|
-
setGuardianIntegrityReporterOverridesForTesting({
|
|
76
|
-
fetchImpl: async () => new Response("{}"),
|
|
77
|
-
mintToken: () => "svc-token",
|
|
78
|
-
baseUrl: "http://127.0.0.1:7821",
|
|
79
|
-
log: {
|
|
80
|
-
error: (detail) => {
|
|
81
|
-
reportCalls.push(detail);
|
|
82
|
-
},
|
|
83
|
-
warn: () => {},
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
83
|
});
|
|
87
84
|
|
|
88
85
|
afterEach(() => {
|
|
89
86
|
resetGatewayDb();
|
|
90
|
-
resetGuardianIntegrityReporterForTesting();
|
|
91
87
|
});
|
|
92
88
|
|
|
93
89
|
describe("hasEvidenceOfPriorGuardian", () => {
|
|
@@ -85,10 +85,6 @@ const {
|
|
|
85
85
|
} = await import("../db/schema.js");
|
|
86
86
|
const { bustGuardianIntegrityCache } =
|
|
87
87
|
await import("../auth/guardian-integrity.js");
|
|
88
|
-
const {
|
|
89
|
-
resetGuardianIntegrityReporterForTesting,
|
|
90
|
-
setGuardianIntegrityReporterOverridesForTesting,
|
|
91
|
-
} = await import("../guardian-integrity-reporter.js");
|
|
92
88
|
const { handleInbound } = await import("../handlers/handle-inbound.js");
|
|
93
89
|
|
|
94
90
|
const CHANNEL = "telegram";
|
|
@@ -201,22 +197,13 @@ beforeEach(async () => {
|
|
|
201
197
|
runtimePayloads = [];
|
|
202
198
|
forwardToRuntimeMock.mockClear();
|
|
203
199
|
interceptResult = { intercepted: false };
|
|
204
|
-
// The integrity state is TTL-cached process-wide; recompute per test
|
|
205
|
-
// silence the fail-loud reporter (its behavior has its own suites).
|
|
200
|
+
// The integrity state is TTL-cached process-wide; recompute per test.
|
|
206
201
|
bustGuardianIntegrityCache();
|
|
207
|
-
resetGuardianIntegrityReporterForTesting();
|
|
208
|
-
setGuardianIntegrityReporterOverridesForTesting({
|
|
209
|
-
fetchImpl: async () => new Response("{}"),
|
|
210
|
-
mintToken: () => "svc-token",
|
|
211
|
-
baseUrl: "http://127.0.0.1:7821",
|
|
212
|
-
log: { error: () => {}, warn: () => {} },
|
|
213
|
-
});
|
|
214
202
|
});
|
|
215
203
|
|
|
216
204
|
afterEach(() => {
|
|
217
205
|
resetAdmissionPolicyCache();
|
|
218
206
|
resetGatewayDb();
|
|
219
|
-
resetGuardianIntegrityReporterForTesting();
|
|
220
207
|
});
|
|
221
208
|
|
|
222
209
|
describe("handle-inbound trust verdict stamping", () => {
|