@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.
Files changed (81) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  2. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  3. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  4. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  5. package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
  6. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  7. package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  8. package/openapi.json +1 -1
  9. package/package.json +1 -1
  10. package/src/__tests__/actor-token-revocation.test.ts +41 -34
  11. package/src/__tests__/contact-store-mirror-op-missing.test.ts +33 -70
  12. package/src/__tests__/contacts-mirror-op-reporter.test.ts +33 -89
  13. package/src/__tests__/credential-requests.test.ts +4 -4
  14. package/src/__tests__/devices-mine.test.ts +187 -0
  15. package/src/__tests__/edge-auth.test.ts +1 -20
  16. package/src/__tests__/edge-guardian-auth.test.ts +1 -17
  17. package/src/__tests__/feature-flag-resolver.test.ts +2 -30
  18. package/src/__tests__/feature-flag-watcher-callback.test.ts +0 -22
  19. package/src/__tests__/feature-flags-route.test.ts +2 -191
  20. package/src/__tests__/guardian-bootstrap-lookups.test.ts +18 -25
  21. package/src/__tests__/guardian-integrity-reporter.test.ts +51 -82
  22. package/src/__tests__/guardian-integrity.test.ts +16 -20
  23. package/src/__tests__/handle-inbound-trust-verdict.test.ts +1 -14
  24. package/src/__tests__/ipc-feature-flag-routes.test.ts +0 -56
  25. package/src/__tests__/lum-2783-recovery-e2e.test.ts +0 -12
  26. package/src/__tests__/post-assistant-ready-deferred.test.ts +0 -18
  27. package/src/__tests__/refresh-device-binding.test.ts +0 -12
  28. package/src/__tests__/remote-web-pairing-token.test.ts +4 -14
  29. package/src/__tests__/runtime-client.test.ts +7 -7
  30. package/src/__tests__/speech-relay-websocket.test.ts +4 -4
  31. package/src/__tests__/trust-verdict-resolver-guardian-integrity.test.ts +30 -36
  32. package/src/__tests__/twilio-webhooks.test.ts +2 -2
  33. package/src/auth/actor-token-revocation.ts +34 -9
  34. package/src/cli/__tests__/pairing.test.ts +95 -0
  35. package/src/cli/index.ts +1 -1
  36. package/src/cli/pairing.ts +81 -6
  37. package/src/contacts-mirror-op-reporter.ts +19 -36
  38. package/src/db/connection.ts +0 -10
  39. package/src/feature-flag-resolver.ts +2 -7
  40. package/src/feature-flag-watcher.ts +1 -14
  41. package/src/guardian-integrity-reporter.ts +19 -47
  42. package/src/http/middleware/auth.ts +33 -27
  43. package/src/http/routes/auth-token.ts +1 -1
  44. package/src/http/routes/devices-mine.ts +103 -0
  45. package/src/http/routes/feature-flags.ts +1 -8
  46. package/src/http/routes/guardian-pin.ts +2 -11
  47. package/src/http/routes/ipc-runtime-proxy.ts +1 -1
  48. package/src/http/routes/live-voice-websocket.ts +1 -1
  49. package/src/http/routes/runtime-audio-stream.ts +1 -1
  50. package/src/http/routes/runtime-proxy.ts +2 -2
  51. package/src/http/routes/twilio-voice-webhook.test.ts +13 -13
  52. package/src/index.ts +11 -198
  53. package/src/ipc/feature-flag-handlers.ts +2 -9
  54. package/src/remote-web/pairing-challenge-store.ts +15 -0
  55. package/src/risk/command-registry/commands/assistant.ts +0 -24
  56. package/src/risk/command-registry.test.ts +2 -4
  57. package/src/schema.ts +44 -372
  58. package/src/__tests__/auth-fallback-count-tracker.test.ts +0 -92
  59. package/src/__tests__/auth-fallback-reporter.test.ts +0 -101
  60. package/src/__tests__/backup-worker-cloud-mode.test.ts +0 -44
  61. package/src/__tests__/backup-worker-export-timeout.test.ts +0 -164
  62. package/src/__tests__/migration-teleport-gcs-proxy.test.ts +0 -216
  63. package/src/__tests__/remote-feature-flag-sync.test.ts +0 -961
  64. package/src/auth-fallback-count-tracker.ts +0 -113
  65. package/src/auth-fallback-reporter.ts +0 -127
  66. package/src/backup/backup-key.ts +0 -138
  67. package/src/backup/backup-routes.ts +0 -161
  68. package/src/backup/backup-worker.ts +0 -425
  69. package/src/backup/list-snapshots.ts +0 -97
  70. package/src/backup/local-writer.ts +0 -87
  71. package/src/backup/offsite-writer.ts +0 -182
  72. package/src/backup/paths.ts +0 -113
  73. package/src/backup/stream-crypt.ts +0 -258
  74. package/src/device-id.ts +0 -18
  75. package/src/feature-flag-remote-store.ts +0 -166
  76. package/src/http/routes/migration-proxy.ts +0 -636
  77. package/src/internal-telemetry-client.ts +0 -39
  78. package/src/ipc/debug-export-handlers.test.ts +0 -102
  79. package/src/ipc/debug-export-handlers.ts +0 -81
  80. package/src/remote-feature-flag-sync.ts +0 -440
  81. package/src/watchdog-reporter.ts +0 -110
@@ -1,121 +1,65 @@
1
1
  /**
2
2
  * Fail-loud mirror-op-missing reporter (contacts-mirror-op-reporter.ts):
3
3
  *
4
- * - First report per op emits one error log and one telemetry relay POST with
5
- * the `contacts_mirror_op_missing` check name and `{ op, ...detail }`.
4
+ * - First report per op emits one error log carrying `{ op, ...detail }`.
6
5
  * - Repeat reports for the same op inside the hourly window are dropped; a
7
6
  * DIFFERENT op still reports (per-op rate limit).
8
- * - Relay failures never throw out of the caller.
9
7
  * - `isUnknownIpcMethodError` matches only the IPC server's unknown-method
10
8
  * rejection shape.
11
9
  *
12
- * Logging is observed through the reporter's test-only `log` override —
13
- * bun's mock.module is process-global and would leak into other test files.
10
+ * Logs are observed by mocking the logger module (a side-effect boundary —
11
+ * the same convention as sleep-wake-detector.test.ts). Per-file CI truth:
12
+ * the mock is process-global, so this file pins nothing else.
14
13
  */
15
- import { beforeEach, afterEach, describe, expect, test } from "bun:test";
14
+ import { beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
16
15
 
17
- const errorLogs: unknown[] = [];
18
- const warnLogs: unknown[] = [];
16
+ const errorLogs: [Record<string, unknown>, string][] = [];
17
+
18
+ mock.module("../logger.js", () => ({
19
+ getLogger: () => ({
20
+ error: (detail: Record<string, unknown>, msg: string) => {
21
+ errorLogs.push([detail, msg]);
22
+ },
23
+ warn: () => {},
24
+ }),
25
+ }));
19
26
 
20
27
  await import("./test-preload.js");
21
- const {
22
- MIRROR_OP_MISSING_CHECK_NAME,
23
- flushMirrorOpReporterForTesting,
24
- isUnknownIpcMethodError,
25
- reportMirrorOpMissing,
26
- resetMirrorOpReporterForTesting,
27
- setMirrorOpReporterOverridesForTesting,
28
- } = await import("../contacts-mirror-op-reporter.js");
28
+ const { isUnknownIpcMethodError, reportMirrorOpMissing } =
29
+ await import("../contacts-mirror-op-reporter.js");
29
30
 
30
- type FetchCall = { url: string; init: RequestInit };
31
- let fetchCalls: FetchCall[] = [];
32
- let fetchResult: () => Promise<Response> = async () => new Response("{}");
31
+ let fakeNow = 1_000_000_000_000;
33
32
 
34
33
  beforeEach(() => {
35
- fetchCalls = [];
36
- fetchResult = async () => new Response("{}");
37
34
  errorLogs.length = 0;
38
- warnLogs.length = 0;
39
- resetMirrorOpReporterForTesting();
40
- setMirrorOpReporterOverridesForTesting({
41
- fetchImpl: async (url, init) => {
42
- fetchCalls.push({ url: String(url), init: init ?? {} });
43
- return fetchResult();
44
- },
45
- mintToken: () => "svc-token",
46
- baseUrl: "http://127.0.0.1:7821",
47
- log: {
48
- error: (detail, msg) => {
49
- errorLogs.push([detail, msg]);
50
- },
51
- warn: (detail, msg) => {
52
- warnLogs.push([detail, msg]);
53
- },
54
- },
55
- });
56
- });
57
-
58
- afterEach(() => {
59
- resetMirrorOpReporterForTesting();
35
+ fakeNow += 24 * 60 * 60 * 1000; // start each test outside prior windows
36
+ spyOn(Date, "now").mockImplementation(() => fakeNow);
60
37
  });
61
38
 
62
39
  describe("reportMirrorOpMissing", () => {
63
- test("first report logs an error and relays one watchdog event", async () => {
64
- reportMirrorOpMissing("contacts_mirror_merge_contact", {
65
- keepId: "ct_keep",
66
- mergeId: "ct_merge",
67
- });
68
- await flushMirrorOpReporterForTesting();
69
-
70
- expect(errorLogs.length).toBe(1);
71
- expect(fetchCalls.length).toBe(1);
72
- expect(fetchCalls[0].url).toBe(
73
- "http://127.0.0.1:7821/v1/internal/telemetry/watchdog",
74
- );
75
- const headers = fetchCalls[0].init.headers as Record<string, string>;
76
- expect(headers.Authorization).toBe("Bearer svc-token");
77
- expect(JSON.parse(String(fetchCalls[0].init.body))).toEqual({
78
- check_name: MIRROR_OP_MISSING_CHECK_NAME,
79
- detail: {
80
- op: "contacts_mirror_merge_contact",
40
+ test("first report logs one error with the op + detail", () => {
41
+ expect(() =>
42
+ reportMirrorOpMissing("contacts_mirror_merge_contact", {
81
43
  keepId: "ct_keep",
82
44
  mergeId: "ct_merge",
83
- },
45
+ }),
46
+ ).not.toThrow();
47
+
48
+ expect(errorLogs.length).toBe(1);
49
+ expect(errorLogs[0]![0]).toMatchObject({
50
+ op: "contacts_mirror_merge_contact",
51
+ keepId: "ct_keep",
52
+ mergeId: "ct_merge",
84
53
  });
54
+ expect(errorLogs[0]![1]).toContain("divergence until the daemon updates");
85
55
  });
86
56
 
87
- test("repeat reports for the same op are dropped; a different op still fires", async () => {
57
+ test("repeat reports for the same op are dropped; a different op still fires", () => {
88
58
  reportMirrorOpMissing("contacts_mirror_merge_contact", { keepId: "a" });
89
59
  reportMirrorOpMissing("contacts_mirror_merge_contact", { keepId: "b" });
90
60
  reportMirrorOpMissing("contacts_mirror_upsert_full", { contactId: "c" });
91
- await flushMirrorOpReporterForTesting();
92
61
 
93
62
  expect(errorLogs.length).toBe(2);
94
- expect(fetchCalls.length).toBe(2);
95
- });
96
-
97
- test("a rejected relay never throws out of the caller", async () => {
98
- fetchResult = async () => {
99
- throw new Error("daemon unreachable");
100
- };
101
-
102
- expect(() =>
103
- reportMirrorOpMissing("contacts_mirror_upsert_full", { contactId: "x" }),
104
- ).not.toThrow();
105
- await flushMirrorOpReporterForTesting();
106
-
107
- expect(errorLogs.length).toBe(1);
108
- expect(warnLogs.length).toBe(1);
109
- });
110
-
111
- test("a non-ok relay response is swallowed with a warning", async () => {
112
- fetchResult = async () => new Response("nope", { status: 500 });
113
-
114
- reportMirrorOpMissing("contacts_mirror_upsert_full", { contactId: "x" });
115
- await flushMirrorOpReporterForTesting();
116
-
117
- expect(fetchCalls.length).toBe(1);
118
- expect(warnLogs.length).toBe(1);
119
63
  });
120
64
  });
121
65
 
@@ -121,7 +121,7 @@ describe("credential-request mint (IPC)", () => {
121
121
  test("makes public ingress live on the successful mint path", async () => {
122
122
  const ensurePublicIngressLive = mock(async () => {});
123
123
  const result = await createCredentialRequest(
124
- fakeGatewayConfig("https://tunnel-dev.mycadet.ai"),
124
+ fakeGatewayConfig("https://dev-tunnel.mycadet.ai"),
125
125
  fakeConfigFile(undefined),
126
126
  { service: "github", field: "api_token" },
127
127
  {
@@ -152,7 +152,7 @@ describe("credential-request mint (IPC)", () => {
152
152
  test("does not read the cloud assistant id when the config URL resolves", async () => {
153
153
  const getCloudAssistantId = mock(async () => "assistant-123");
154
154
  const result = await createCredentialRequest(
155
- fakeGatewayConfig("https://tunnel-dev.mycadet.ai"),
155
+ fakeGatewayConfig("https://dev-tunnel.mycadet.ai"),
156
156
  fakeConfigFile("https://assistant.example.com"),
157
157
  { service: "github", field: "api_token" },
158
158
  { getCloudAssistantId },
@@ -165,7 +165,7 @@ describe("credential-request mint (IPC)", () => {
165
165
  // ingress.publicBaseUrl is Tunnel-managed and cleared on tunnel flaps, so
166
166
  // the mint must still resolve a URL from tunnelBaseUrl + the assistant id.
167
167
  const result = await createCredentialRequest(
168
- fakeGatewayConfig("https://tunnel-dev.mycadet.ai"),
168
+ fakeGatewayConfig("https://dev-tunnel.mycadet.ai"),
169
169
  fakeConfigFile(undefined),
170
170
  { service: "github", field: "api_token" },
171
171
  { getCloudAssistantId: async () => "assistant-123" },
@@ -173,7 +173,7 @@ describe("credential-request mint (IPC)", () => {
173
173
 
174
174
  if (!result.ok) throw new Error(`mint failed: ${result.error}`);
175
175
  expect(result.url).toBe(
176
- `https://tunnel-dev.mycadet.ai/assistant-123/assistant/credentials/enter#token=${encodeURIComponent(result.token)}`,
176
+ `https://dev-tunnel.mycadet.ai/assistant-123/assistant/credentials/enter#token=${encodeURIComponent(result.token)}`,
177
177
  );
178
178
  });
179
179
 
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Tests for GET /v1/devices/mine — the caller's own paired-device row. The
3
+ * bearer IS the credential (browser-reachable by design), and the response
4
+ * mirrors the /v1/devices projection for the caller's device so a
5
+ * `devices/mine` answer matches that device's row in the host-side
6
+ * `cadet devices` list field for field — including the cross-status
7
+ * max(lastUsedAt) that keeps activity history across token rotation.
8
+ */
9
+ import { describe, test, expect, mock, beforeEach, afterEach } from "bun:test";
10
+ import { randomUUID } from "node:crypto";
11
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { eq } from "drizzle-orm";
15
+
16
+ import { initSigningKey } from "../auth/token-service.js";
17
+
18
+ initSigningKey(Buffer.from("test-signing-key-at-least-32-bytes-long-xx"));
19
+
20
+ // The pair guardian-lookup reads the gateway DB; the assistant DB proxy is
21
+ // mocked so any incidental assistant access stays inert in tests.
22
+ mock.module("../db/assistant-db-proxy.js", () => ({
23
+ assistantDbQuery: mock(),
24
+ assistantDbRun: mock(),
25
+ assistantDbExec: mock(),
26
+ }));
27
+
28
+ const { initGatewayDb, resetGatewayDb, getGatewayDb } = await import(
29
+ "../db/connection.js"
30
+ );
31
+ const { actorTokenRecords, contacts, contactChannels } = await import(
32
+ "../db/schema.js"
33
+ );
34
+ const { hashToken, mintAndRecordBrowserTokenPair } = await import(
35
+ "../auth/guardian-bootstrap.js"
36
+ );
37
+ const { handleGetMyDevice } = await import("../http/routes/devices-mine.js");
38
+
39
+ const GUARDIAN_ID = "guardian-001";
40
+
41
+ let testRoot: string;
42
+
43
+ function mineRequest(bearer: string | null): Request {
44
+ const headers = new Headers({ host: "localhost:7830" });
45
+ if (bearer) {
46
+ headers.set("authorization", `Bearer ${bearer}`);
47
+ }
48
+ return new Request("http://localhost:7830/v1/devices/mine", {
49
+ method: "GET",
50
+ headers,
51
+ });
52
+ }
53
+
54
+ function rowForToken(token: string) {
55
+ return getGatewayDb()
56
+ .select()
57
+ .from(actorTokenRecords)
58
+ .where(eq(actorTokenRecords.tokenHash, hashToken(token)))
59
+ .get();
60
+ }
61
+
62
+ beforeEach(async () => {
63
+ testRoot = mkdtempSync(join(tmpdir(), "devices-mine-test-"));
64
+ const securityDir = join(testRoot, "protected");
65
+ mkdirSync(securityDir, { recursive: true });
66
+ process.env.GATEWAY_SECURITY_DIR = securityDir;
67
+ await initGatewayDb();
68
+
69
+ // The route answers for any actor-token row; the guardian contact seed
70
+ // mirrors the devices.test.ts fixture shape.
71
+ const now = Date.now();
72
+ getGatewayDb()
73
+ .insert(contacts)
74
+ .values({
75
+ id: GUARDIAN_ID,
76
+ displayName: "Guardian",
77
+ role: "guardian",
78
+ principalId: GUARDIAN_ID,
79
+ createdAt: now,
80
+ updatedAt: now,
81
+ })
82
+ .run();
83
+ getGatewayDb()
84
+ .insert(contactChannels)
85
+ .values({
86
+ id: `ch-${GUARDIAN_ID}`,
87
+ contactId: GUARDIAN_ID,
88
+ type: "cadet",
89
+ address: "guardian-cadet",
90
+ isPrimary: false,
91
+ createdAt: now,
92
+ updatedAt: now,
93
+ })
94
+ .run();
95
+ });
96
+
97
+ afterEach(() => {
98
+ resetGatewayDb();
99
+ delete process.env.GATEWAY_SECURITY_DIR;
100
+ rmSync(testRoot, { recursive: true, force: true });
101
+ });
102
+
103
+ describe("GET /v1/devices/mine", () => {
104
+ test("returns the caller's own device row", async () => {
105
+ const pair = mintAndRecordBrowserTokenPair({
106
+ guardianPrincipalId: GUARDIAN_ID,
107
+ platform: "web",
108
+ browserRefreshCookiePath: "/",
109
+ identity: { pairingUserAgent: "Mozilla/5.0 TestBrowser" },
110
+ });
111
+
112
+ const response = await handleGetMyDevice(mineRequest(pair.accessToken));
113
+ expect(response.status).toBe(200);
114
+
115
+ const body = (await response.json()) as { device: Record<string, unknown> };
116
+ const stored = rowForToken(pair.accessToken);
117
+ expect(stored).toBeTruthy();
118
+ expect(body.device.hashedDeviceId).toBe(stored!.hashedDeviceId);
119
+ expect(body.device.platform).toBe("web");
120
+ expect(body.device.pairingUserAgent).toBe("Mozilla/5.0 TestBrowser");
121
+ expect(body.device.issuedAt).toBe(stored!.issuedAt);
122
+ });
123
+
124
+ test("lastUsedAt is the cross-status max for the device", async () => {
125
+ const pair = mintAndRecordBrowserTokenPair({
126
+ guardianPrincipalId: GUARDIAN_ID,
127
+ platform: "web",
128
+ browserRefreshCookiePath: "/",
129
+ });
130
+ const stored = rowForToken(pair.accessToken);
131
+ expect(stored).toBeTruthy();
132
+
133
+ // A rotation revokes the old row and mints an unstamped replacement,
134
+ // so the newest activity stamp can live on the revoked row — the same
135
+ // rule the /v1/devices list applies.
136
+ const now = Date.now();
137
+ getGatewayDb()
138
+ .insert(actorTokenRecords)
139
+ .values({
140
+ id: randomUUID(),
141
+ tokenHash: hashToken(`rotated-${stored!.hashedDeviceId}`),
142
+ guardianPrincipalId: GUARDIAN_ID,
143
+ hashedDeviceId: stored!.hashedDeviceId,
144
+ platform: "web",
145
+ status: "revoked",
146
+ issuedAt: now - 86_400_000,
147
+ expiresAt: now + 86_400_000,
148
+ lastUsedAt: now - 60_000,
149
+ createdAt: now,
150
+ updatedAt: now,
151
+ })
152
+ .run();
153
+
154
+ const response = await handleGetMyDevice(mineRequest(pair.accessToken));
155
+ expect(response.status).toBe(200);
156
+ const body = (await response.json()) as { device: { lastUsedAt: number | null } };
157
+ expect(body.device.lastUsedAt).toBe(now - 60_000);
158
+ });
159
+
160
+ test("an unknown bearer answers 404, not an error", async () => {
161
+ const response = await handleGetMyDevice(
162
+ mineRequest("not-a-token-anyone-minted"),
163
+ );
164
+ expect(response.status).toBe(404);
165
+ });
166
+
167
+ test("a cloud-hosted assistant answers 404 before any row lookup", async () => {
168
+ process.env.DISABLE_HTTP_AUTH = "true";
169
+ process.env.IS_CLOUD = "true";
170
+ try {
171
+ const response = await handleGetMyDevice(mineRequest(null));
172
+ expect(response.status).toBe(404);
173
+ const body = (await response.json()) as { error: { code: string } };
174
+ expect(body.error.code).toBe("NOT_AVAILABLE_ON_CLOUD");
175
+ } finally {
176
+ delete process.env.DISABLE_HTTP_AUTH;
177
+ delete process.env.IS_CLOUD;
178
+ }
179
+ });
180
+
181
+ test("non-GET answers method not allowed", async () => {
182
+ const response = await handleGetMyDevice(
183
+ new Request("http://localhost:7830/v1/devices/mine", { method: "POST" }),
184
+ );
185
+ expect(response.status).toBe(405);
186
+ });
187
+ });
@@ -64,8 +64,7 @@ mock.module("../auth/token-exchange.js", () => ({
64
64
  }));
65
65
 
66
66
  const { AuthRateLimiter } = await import("../auth-rate-limiter.js");
67
- const { createAuthMiddleware, loopbackFallbackCountTracker } =
68
- await import("../http/middleware/auth.js");
67
+ const { createAuthMiddleware } = await import("../http/middleware/auth.js");
69
68
  const { initGatewayDb, resetGatewayDb, getGatewayDb } =
70
69
  await import("../db/connection.js");
71
70
  const { actorTokenRecords } = await import("../db/schema.js");
@@ -96,7 +95,6 @@ beforeEach(() => {
96
95
  mockReadCredential = mock(async () => undefined);
97
96
  mockPlatformUserIdUnreachable = false;
98
97
  mockValidateEdgeToken = mock(() => ({ ok: false, reason: "noop" }));
99
- loopbackFallbackCountTracker.reset();
100
98
  });
101
99
 
102
100
  afterEach(() => {
@@ -238,21 +236,6 @@ describe("requireEdgeAuth — JWT mode", () => {
238
236
  );
239
237
  expect(res?.status).toBe(401);
240
238
  expect(mockValidateEdgeToken).not.toHaveBeenCalled();
241
- expect(loopbackFallbackCountTracker.snapshot()).toEqual([]);
242
- });
243
-
244
- test("a loopback fallback is counted by (guard, path, failureKind)", async () => {
245
- const { requireEdgeAuth } = makeMiddleware();
246
- await requireEdgeAuth(makeReq(), makeLoopbackServer());
247
- await requireEdgeAuth(makeReq(), makeLoopbackServer());
248
- expect(loopbackFallbackCountTracker.snapshot()).toEqual([
249
- {
250
- guard: "edge",
251
- path: "/v1/something",
252
- failureKind: "missing_authorization",
253
- count: 2,
254
- },
255
- ]);
256
239
  });
257
240
 
258
241
  test("null on valid bearer token", async () => {
@@ -389,7 +372,6 @@ describe("requireEdgeAuthWithScope — JWT mode", () => {
389
372
  );
390
373
  expect(res?.status).toBe(401);
391
374
  expect(mockValidateEdgeToken).not.toHaveBeenCalled();
392
- expect(loopbackFallbackCountTracker.snapshot()).toEqual([]);
393
375
  });
394
376
 
395
377
  test("403 when token's scope_profile lacks the required scope", async () => {
@@ -512,7 +494,6 @@ describe("edge auth refuses a single-route grant", () => {
512
494
  makeLoopbackServer(),
513
495
  );
514
496
  expect(res?.status).toBe(401);
515
- expect(loopbackFallbackCountTracker.snapshot()).toEqual([]);
516
497
  });
517
498
 
518
499
  test("requireEdgeAuthWithScope 401s the grant before the scope check", async () => {
@@ -53,8 +53,7 @@ mock.module("../auth/token-exchange.js", () => ({
53
53
  }));
54
54
 
55
55
  const { AuthRateLimiter } = await import("../auth-rate-limiter.js");
56
- const { createAuthMiddleware, loopbackFallbackCountTracker } =
57
- await import("../http/middleware/auth.js");
56
+ const { createAuthMiddleware } = await import("../http/middleware/auth.js");
58
57
 
59
58
  const PLATFORM_USER_ID = "user-abc-123";
60
59
  const GUARDIAN_PRINCIPAL = "actor-guardian-xyz";
@@ -82,7 +81,6 @@ beforeEach(() => {
82
81
  mockPlatformUserIdUnreachable = false;
83
82
  mockFindCadetGuardian = mock(async () => null);
84
83
  mockValidateEdgeToken = mock(() => ({ ok: false, reason: "noop" }));
85
- loopbackFallbackCountTracker.reset();
86
84
  });
87
85
 
88
86
  afterEach(() => {
@@ -210,7 +208,6 @@ describe("requireEdgeGuardianAuth — actor principal mode", () => {
210
208
  expect(res?.status).toBe(401);
211
209
  expect(mockValidateEdgeToken).not.toHaveBeenCalled();
212
210
  expect(mockFindCadetGuardian).not.toHaveBeenCalled();
213
- expect(loopbackFallbackCountTracker.snapshot()).toEqual([]);
214
211
  });
215
212
 
216
213
  test("invalid bearer token falls back to loopback", async () => {
@@ -224,19 +221,6 @@ describe("requireEdgeGuardianAuth — actor principal mode", () => {
224
221
  expect(mockFindCadetGuardian).not.toHaveBeenCalled();
225
222
  });
226
223
 
227
- test("a guardian loopback fallback is counted under the edge-guardian guard", async () => {
228
- const { requireEdgeGuardianAuth } = makeMiddleware();
229
- await requireEdgeGuardianAuth(makeReq(), makeLoopbackServer());
230
- expect(loopbackFallbackCountTracker.snapshot()).toEqual([
231
- {
232
- guard: "edge-guardian",
233
- path: "/v1/contact-channels/abc/verify",
234
- failureKind: "missing_authorization",
235
- count: 1,
236
- },
237
- ]);
238
- });
239
-
240
224
  test("returns 503 when findCadetGuardian throws (transient assistant DB outage)", async () => {
241
225
  mockValidateEdgeToken = mock(() => ({
242
226
  ok: true,
@@ -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();