@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
@@ -10,7 +10,7 @@ import {
10
10
  trimmedNonEmptyString,
11
11
  TUNNEL_PROVIDERS,
12
12
  type TunnelRecord,
13
- tunnelHostForPlatformHost,
13
+ tunnelHostForCloudHost,
14
14
  } from "../ingress.js";
15
15
  import {
16
16
  buildTwilioMediaStreamUrl,
@@ -19,27 +19,27 @@ import {
19
19
  resolveTwilioPublicBaseUrl,
20
20
  } from "../twilio-ingress.js";
21
21
 
22
- describe("tunnelHostForPlatformHost", () => {
22
+ describe("tunnelHostForCloudHost", () => {
23
23
  test("maps the prod platform host to the prod tunnel", () => {
24
- expect(tunnelHostForPlatformHost("cloud.mycadet.ai")).toBe(
24
+ expect(tunnelHostForCloudHost("cloud.mycadet.ai")).toBe(
25
25
  "tunnel.mycadet.ai",
26
26
  );
27
27
  });
28
28
 
29
29
  test("maps env-prefixed platform hosts to their env tunnel", () => {
30
- expect(tunnelHostForPlatformHost("staging-cloud.mycadet.ai")).toBe(
31
- "tunnel-staging.mycadet.ai",
30
+ expect(tunnelHostForCloudHost("staging-cloud.mycadet.ai")).toBe(
31
+ "staging-tunnel.mycadet.ai",
32
32
  );
33
- expect(tunnelHostForPlatformHost("dev-cloud.mycadet.ai")).toBe(
34
- "tunnel-dev.mycadet.ai",
33
+ expect(tunnelHostForCloudHost("dev-cloud.mycadet.ai")).toBe(
34
+ "dev-tunnel.mycadet.ai",
35
35
  );
36
36
  });
37
37
 
38
38
  test("returns null for hosts outside the deployment convention", () => {
39
- expect(tunnelHostForPlatformHost("localhost")).toBeNull();
40
- expect(tunnelHostForPlatformHost("platform.example.com")).toBeNull();
39
+ expect(tunnelHostForCloudHost("localhost")).toBeNull();
40
+ expect(tunnelHostForCloudHost("platform.example.com")).toBeNull();
41
41
  expect(
42
- tunnelHostForPlatformHost("staging-cloud.mycadet.ai.evil.com"),
42
+ tunnelHostForCloudHost("staging-cloud.mycadet.ai.evil.com"),
43
43
  ).toBeNull();
44
44
  });
45
45
  });
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * Map a Cadet Cloud host onto its environment's tunnel host, following the
3
3
  * deployment naming convention (`cloud.mycadet.ai` → `tunnel.mycadet.ai`,
4
- * `{env}-cloud.mycadet.ai` → `tunnel-{env}.mycadet.ai`). Returns null for
4
+ * `{env}-cloud.mycadet.ai` → `{env}-tunnel.mycadet.ai`). Returns null for
5
5
  * hosts outside that convention (localhost, custom domains). Shared by the
6
6
  * gateway speech relay and the web live-voice client so the two ends of the
7
7
  * cloud-speech transport can't drift.
8
8
  */
9
- export function tunnelHostForPlatformHost(host: string): string | null {
9
+ export function tunnelHostForCloudHost(host: string): string | null {
10
10
  if (host === "cloud.mycadet.ai") {
11
11
  return "tunnel.mycadet.ai";
12
12
  }
13
13
  const match = /^([a-z0-9-]+)-cloud\.mycadet\.ai$/.exec(host);
14
- return match ? `tunnel-${match[1]}.mycadet.ai` : null;
14
+ return match ? `${match[1]}-tunnel.mycadet.ai` : null;
15
15
  }
16
16
 
17
17
  export function normalizePublicBaseUrl(value: unknown): string | undefined {
@@ -143,6 +143,19 @@ export interface CesHttpCredentialClient {
143
143
  // Factory
144
144
  // ---------------------------------------------------------------------------
145
145
 
146
+ function listErrorContext(
147
+ response: Response,
148
+ path: string,
149
+ ): Record<string, unknown> {
150
+ const requestId = response.headers.get("x-ces-error-id");
151
+ return {
152
+ status: response.status,
153
+ method: "GET",
154
+ path,
155
+ ...(requestId && /^[a-f0-9-]{36}$/i.test(requestId) ? { requestId } : {}),
156
+ };
157
+ }
158
+
146
159
  export function createCesHttpCredentialClient(
147
160
  config: CesHttpCredentialConfig,
148
161
  logger: CesHttpLogger,
@@ -298,7 +311,7 @@ export function createCesHttpCredentialClient(
298
311
  if (!res) return { accounts: [], unreachable: true };
299
312
  if (!res.ok) {
300
313
  logger.warn(
301
- { status: res.status },
314
+ listErrorContext(res, "/v1/credentials"),
302
315
  "CES credential list returned non-OK status",
303
316
  );
304
317
  return { accounts: [], unreachable: true };
@@ -399,6 +412,10 @@ export function createCesHttpCredentialClient(
399
412
  return { records: [], unreachable: true };
400
413
  }
401
414
  if (!res.ok) {
415
+ logger.warn(
416
+ listErrorContext(res, "/v1/metadata"),
417
+ "CES metadata list returned non-OK status",
418
+ );
402
419
  return { records: [], unreachable: true };
403
420
  }
404
421
  const data = (await res.json()) as {
@@ -10,7 +10,7 @@ import {
10
10
  trimmedNonEmptyString,
11
11
  TUNNEL_PROVIDERS,
12
12
  type TunnelRecord,
13
- tunnelHostForPlatformHost,
13
+ tunnelHostForCloudHost,
14
14
  } from "../ingress.js";
15
15
  import {
16
16
  buildTwilioMediaStreamUrl,
@@ -19,27 +19,27 @@ import {
19
19
  resolveTwilioPublicBaseUrl,
20
20
  } from "../twilio-ingress.js";
21
21
 
22
- describe("tunnelHostForPlatformHost", () => {
22
+ describe("tunnelHostForCloudHost", () => {
23
23
  test("maps the prod platform host to the prod tunnel", () => {
24
- expect(tunnelHostForPlatformHost("cloud.mycadet.ai")).toBe(
24
+ expect(tunnelHostForCloudHost("cloud.mycadet.ai")).toBe(
25
25
  "tunnel.mycadet.ai",
26
26
  );
27
27
  });
28
28
 
29
29
  test("maps env-prefixed platform hosts to their env tunnel", () => {
30
- expect(tunnelHostForPlatformHost("staging-cloud.mycadet.ai")).toBe(
31
- "tunnel-staging.mycadet.ai",
30
+ expect(tunnelHostForCloudHost("staging-cloud.mycadet.ai")).toBe(
31
+ "staging-tunnel.mycadet.ai",
32
32
  );
33
- expect(tunnelHostForPlatformHost("dev-cloud.mycadet.ai")).toBe(
34
- "tunnel-dev.mycadet.ai",
33
+ expect(tunnelHostForCloudHost("dev-cloud.mycadet.ai")).toBe(
34
+ "dev-tunnel.mycadet.ai",
35
35
  );
36
36
  });
37
37
 
38
38
  test("returns null for hosts outside the deployment convention", () => {
39
- expect(tunnelHostForPlatformHost("localhost")).toBeNull();
40
- expect(tunnelHostForPlatformHost("platform.example.com")).toBeNull();
39
+ expect(tunnelHostForCloudHost("localhost")).toBeNull();
40
+ expect(tunnelHostForCloudHost("platform.example.com")).toBeNull();
41
41
  expect(
42
- tunnelHostForPlatformHost("staging-cloud.mycadet.ai.evil.com"),
42
+ tunnelHostForCloudHost("staging-cloud.mycadet.ai.evil.com"),
43
43
  ).toBeNull();
44
44
  });
45
45
  });
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * Map a Cadet Cloud host onto its environment's tunnel host, following the
3
3
  * deployment naming convention (`cloud.mycadet.ai` → `tunnel.mycadet.ai`,
4
- * `{env}-cloud.mycadet.ai` → `tunnel-{env}.mycadet.ai`). Returns null for
4
+ * `{env}-cloud.mycadet.ai` → `{env}-tunnel.mycadet.ai`). Returns null for
5
5
  * hosts outside that convention (localhost, custom domains). Shared by the
6
6
  * gateway speech relay and the web live-voice client so the two ends of the
7
7
  * cloud-speech transport can't drift.
8
8
  */
9
- export function tunnelHostForPlatformHost(host: string): string | null {
9
+ export function tunnelHostForCloudHost(host: string): string | null {
10
10
  if (host === "cloud.mycadet.ai") {
11
11
  return "tunnel.mycadet.ai";
12
12
  }
13
13
  const match = /^([a-z0-9-]+)-cloud\.mycadet\.ai$/.exec(host);
14
- return match ? `tunnel-${match[1]}.mycadet.ai` : null;
14
+ return match ? `${match[1]}-tunnel.mycadet.ai` : null;
15
15
  }
16
16
 
17
17
  export function normalizePublicBaseUrl(value: unknown): string | undefined {
@@ -68,26 +68,6 @@ export type GatewayLogsTailRouteParams = z.infer<
68
68
  typeof GatewayLogsTailRouteParamsSchema
69
69
  >;
70
70
 
71
- /**
72
- * `gateway_debug_export`: a consistent copy of the gateway database plus its
73
- * recent log files, as one gzipped tar, for a debug bundle. No params.
74
- */
75
- export const GatewayDebugExportIpcParamsSchema = z
76
- .object({})
77
- .strict()
78
- .default({});
79
-
80
- export const GatewayDebugExportIpcResponseSchema = z.object({
81
- ok: z.literal(true),
82
- /** `tar.gz` bytes, base64. Contains gateway.sqlite and gateway-logs/. */
83
- archive_base64: z.string(),
84
- size_bytes: z.number().int().nonnegative(),
85
- });
86
-
87
- export type GatewayDebugExportIpcResponse = z.infer<
88
- typeof GatewayDebugExportIpcResponseSchema
89
- >;
90
-
91
71
  export const GatewayLogsTailIpcResponseSchema = z.object({
92
72
  lines: z.array(z.record(z.string(), z.unknown())),
93
73
  truncated: z.boolean(),
@@ -10,7 +10,7 @@ import {
10
10
  trimmedNonEmptyString,
11
11
  TUNNEL_PROVIDERS,
12
12
  type TunnelRecord,
13
- tunnelHostForPlatformHost,
13
+ tunnelHostForCloudHost,
14
14
  } from "../ingress.js";
15
15
  import {
16
16
  buildTwilioMediaStreamUrl,
@@ -19,27 +19,27 @@ import {
19
19
  resolveTwilioPublicBaseUrl,
20
20
  } from "../twilio-ingress.js";
21
21
 
22
- describe("tunnelHostForPlatformHost", () => {
22
+ describe("tunnelHostForCloudHost", () => {
23
23
  test("maps the prod platform host to the prod tunnel", () => {
24
- expect(tunnelHostForPlatformHost("cloud.mycadet.ai")).toBe(
24
+ expect(tunnelHostForCloudHost("cloud.mycadet.ai")).toBe(
25
25
  "tunnel.mycadet.ai",
26
26
  );
27
27
  });
28
28
 
29
29
  test("maps env-prefixed platform hosts to their env tunnel", () => {
30
- expect(tunnelHostForPlatformHost("staging-cloud.mycadet.ai")).toBe(
31
- "tunnel-staging.mycadet.ai",
30
+ expect(tunnelHostForCloudHost("staging-cloud.mycadet.ai")).toBe(
31
+ "staging-tunnel.mycadet.ai",
32
32
  );
33
- expect(tunnelHostForPlatformHost("dev-cloud.mycadet.ai")).toBe(
34
- "tunnel-dev.mycadet.ai",
33
+ expect(tunnelHostForCloudHost("dev-cloud.mycadet.ai")).toBe(
34
+ "dev-tunnel.mycadet.ai",
35
35
  );
36
36
  });
37
37
 
38
38
  test("returns null for hosts outside the deployment convention", () => {
39
- expect(tunnelHostForPlatformHost("localhost")).toBeNull();
40
- expect(tunnelHostForPlatformHost("platform.example.com")).toBeNull();
39
+ expect(tunnelHostForCloudHost("localhost")).toBeNull();
40
+ expect(tunnelHostForCloudHost("platform.example.com")).toBeNull();
41
41
  expect(
42
- tunnelHostForPlatformHost("staging-cloud.mycadet.ai.evil.com"),
42
+ tunnelHostForCloudHost("staging-cloud.mycadet.ai.evil.com"),
43
43
  ).toBeNull();
44
44
  });
45
45
  });
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * Map a Cadet Cloud host onto its environment's tunnel host, following the
3
3
  * deployment naming convention (`cloud.mycadet.ai` → `tunnel.mycadet.ai`,
4
- * `{env}-cloud.mycadet.ai` → `tunnel-{env}.mycadet.ai`). Returns null for
4
+ * `{env}-cloud.mycadet.ai` → `{env}-tunnel.mycadet.ai`). Returns null for
5
5
  * hosts outside that convention (localhost, custom domains). Shared by the
6
6
  * gateway speech relay and the web live-voice client so the two ends of the
7
7
  * cloud-speech transport can't drift.
8
8
  */
9
- export function tunnelHostForPlatformHost(host: string): string | null {
9
+ export function tunnelHostForCloudHost(host: string): string | null {
10
10
  if (host === "cloud.mycadet.ai") {
11
11
  return "tunnel.mycadet.ai";
12
12
  }
13
13
  const match = /^([a-z0-9-]+)-cloud\.mycadet\.ai$/.exec(host);
14
- return match ? `tunnel-${match[1]}.mycadet.ai` : null;
14
+ return match ? `${match[1]}-tunnel.mycadet.ai` : null;
15
15
  }
16
16
 
17
17
  export function normalizePublicBaseUrl(value: unknown): string | undefined {
package/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Cadet Gateway API",
5
- "version": "0.12.8",
5
+ "version": "0.12.9",
6
6
  "description": "Auto-generated OpenAPI specification for the Cadet Gateway HTTP endpoints."
7
7
  },
8
8
  "servers": [{ "url": "", "description": "Same-origin — the gateway" }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mycadet/gateway",
3
- "version": "0.12.10",
3
+ "version": "0.13.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -30,10 +30,6 @@ const { createRuntimeProxyHandler } =
30
30
  await import("../http/routes/runtime-proxy.js");
31
31
  const { bustGuardianIntegrityCache } =
32
32
  await import("../auth/guardian-integrity.js");
33
- const {
34
- resetGuardianIntegrityReporterForTesting,
35
- setGuardianIntegrityReporterOverridesForTesting,
36
- } = await import("../guardian-integrity-reporter.js");
37
33
 
38
34
  const ACTOR_SUB = "actor:self:guardian-001";
39
35
  const actorClaims = { sub: ACTOR_SUB } as TokenClaims;
@@ -128,20 +124,12 @@ beforeEach(async () => {
128
124
  await initGatewayDb();
129
125
  __resetLastUsedDebounceForTests();
130
126
  // The integrity state is module-cached across tests; token rows seeded here
131
- // are integrity evidence, so keep the reporter silenced and the cache cold.
127
+ // are integrity evidence, so keep the cache cold.
132
128
  bustGuardianIntegrityCache();
133
- resetGuardianIntegrityReporterForTesting();
134
- setGuardianIntegrityReporterOverridesForTesting({
135
- fetchImpl: async () => new Response("{}"),
136
- mintToken: () => "svc-token",
137
- baseUrl: "http://127.0.0.1:7821",
138
- log: { error: () => {}, warn: () => {} },
139
- });
140
129
  });
141
130
 
142
131
  afterEach(() => {
143
132
  resetGatewayDb();
144
- resetGuardianIntegrityReporterForTesting();
145
133
  bustGuardianIntegrityCache();
146
134
  if (savedSecurityDir === undefined) {
147
135
  delete process.env.GATEWAY_SECURITY_DIR;
@@ -158,28 +146,41 @@ afterEach(() => {
158
146
  describe("admitActorToken: revocation verdict", () => {
159
147
  test("rejects an actor token whose record is revoked", () => {
160
148
  insertTokenRecord("token-revoked", "revoked");
161
- expect(admitActorToken("token-revoked", actorClaims)).toBe(false);
149
+ expect(admitActorToken("token-revoked", actorClaims).admitted).toBe(false);
162
150
  });
163
151
 
164
152
  test("admits an actor token whose record is active", () => {
165
153
  insertTokenRecord("token-active", "active");
166
- expect(admitActorToken("token-active", actorClaims)).toBe(true);
154
+ expect(admitActorToken("token-active", actorClaims).admitted).toBe(true);
155
+ });
156
+
157
+ test("hands the loaded record back for route reuse", () => {
158
+ insertTokenRecord("token-recorded", "active");
159
+ const { record } = admitActorToken("token-recorded", actorClaims);
160
+ expect(record).toMatchObject({
161
+ status: "active",
162
+ guardianPrincipalId: "guardian-001",
163
+ hashedDeviceId: hashToken("device-A"),
164
+ platform: "web",
165
+ });
166
+ expect(record?.issuedAt).toBeNumber();
167
+ expect(record?.expiresAt).toBeNumber();
167
168
  });
168
169
 
169
170
  test("admits (fail-open) an actor token with no record", () => {
170
- expect(admitActorToken("token-unknown", actorClaims)).toBe(true);
171
+ expect(admitActorToken("token-unknown", actorClaims).admitted).toBe(true);
171
172
  });
172
173
 
173
174
  test("never checks non-actor tokens (svc)", () => {
174
175
  // Even if a row with this hash were revoked, a svc sub must be ignored.
175
176
  insertTokenRecord("svc-token", "revoked");
176
177
  const svcClaims = { sub: "svc:gateway:self" } as TokenClaims;
177
- expect(admitActorToken("svc-token", svcClaims)).toBe(true);
178
+ expect(admitActorToken("svc-token", svcClaims).admitted).toBe(true);
178
179
  });
179
180
 
180
181
  test("admits (fail-open) when the gateway DB is unavailable", () => {
181
182
  resetGatewayDb();
182
- expect(admitActorToken("token-anything", actorClaims)).toBe(true);
183
+ expect(admitActorToken("token-anything", actorClaims).admitted).toBe(true);
183
184
  });
184
185
 
185
186
  test("still detects revocation when the token has surrounding whitespace", () => {
@@ -187,8 +188,10 @@ describe("admitActorToken: revocation verdict", () => {
187
188
  // supplied with trailing whitespace (e.g. a `?token=<jwt>%20` WS param)
188
189
  // must still resolve to the revoked record.
189
190
  insertTokenRecord("token-revoked", "revoked");
190
- expect(admitActorToken("token-revoked ", actorClaims)).toBe(false);
191
- expect(admitActorToken(" token-revoked\n", actorClaims)).toBe(false);
191
+ expect(admitActorToken("token-revoked ", actorClaims).admitted).toBe(false);
192
+ expect(admitActorToken(" token-revoked\n", actorClaims).admitted).toBe(
193
+ false,
194
+ );
192
195
  });
193
196
 
194
197
  test("a revoked token is rejected without stamping any row", () => {
@@ -197,7 +200,7 @@ describe("admitActorToken: revocation verdict", () => {
197
200
  insertTokenRecord("token-stale", "revoked");
198
201
  insertTokenRecord("token-fresh", "active");
199
202
 
200
- expect(admitActorToken("token-stale", actorClaims)).toBe(false);
203
+ expect(admitActorToken("token-stale", actorClaims).admitted).toBe(false);
201
204
 
202
205
  expect(readRow("token-stale")?.lastUsedAt).toBeNull();
203
206
  expect(readRow("token-fresh")?.lastUsedAt).toBeNull();
@@ -228,19 +231,19 @@ describe("signature-encoding canonicalization (revocation bypass)", () => {
228
231
  insertTokenRecord(jwt, "revoked"); // stored under the canonical token hash
229
232
 
230
233
  // Baseline: the canonical token is detected as revoked.
231
- expect(admitActorToken(jwt, actorClaims)).toBe(false);
234
+ expect(admitActorToken(jwt, actorClaims).admitted).toBe(false);
232
235
 
233
236
  // Bypass attempt: same token, signature re-encoded (different string, same
234
237
  // bytes). Must still resolve to the revoked record.
235
238
  const padded = padSignature(jwt);
236
239
  expect(padded).not.toBe(jwt);
237
- expect(admitActorToken(padded, actorClaims)).toBe(false);
240
+ expect(admitActorToken(padded, actorClaims).admitted).toBe(false);
238
241
  });
239
242
 
240
243
  test("does not falsely revoke an active token re-encoded with padding", () => {
241
244
  const jwt = mintActorJwt();
242
245
  insertTokenRecord(jwt, "active");
243
- expect(admitActorToken(padSignature(jwt), actorClaims)).toBe(true);
246
+ expect(admitActorToken(padSignature(jwt), actorClaims).admitted).toBe(true);
244
247
  });
245
248
  });
246
249
 
@@ -365,12 +368,12 @@ describe("/auth/token revocation", () => {
365
368
  .get();
366
369
 
367
370
  expect(derivedRecord?.status).toBe("derived");
368
- expect(admitActorToken(derivedJwt, actorClaims)).toBe(true);
371
+ expect(admitActorToken(derivedJwt, actorClaims).admitted).toBe(true);
369
372
 
370
373
  revokeActorTokensByDevice("guardian-001", hashToken("device-A"));
371
374
 
372
- expect(admitActorToken(sourceJwt, actorClaims)).toBe(false);
373
- expect(admitActorToken(derivedJwt, actorClaims)).toBe(false);
375
+ expect(admitActorToken(sourceJwt, actorClaims).admitted).toBe(false);
376
+ expect(admitActorToken(derivedJwt, actorClaims).admitted).toBe(false);
374
377
  });
375
378
 
376
379
  test("fails closed with a repairable 401 when guardian rows are lost over evidence (no divergent mint)", async () => {
@@ -528,7 +531,7 @@ describe("admitActorToken: device last-used stamping", () => {
528
531
  const before = readRow("token-used");
529
532
  expect(before?.lastUsedAt).toBeNull();
530
533
 
531
- expect(admitActorToken("token-used", actorClaims)).toBe(true);
534
+ expect(admitActorToken("token-used", actorClaims).admitted).toBe(true);
532
535
 
533
536
  const after = readRow("token-used");
534
537
  expect(after?.lastUsedAt).toBeGreaterThan(0);
@@ -540,7 +543,7 @@ describe("admitActorToken: device last-used stamping", () => {
540
543
  insertTokenRecord("token-counted", "active");
541
544
 
542
545
  const sql = captureSql(() => {
543
- expect(admitActorToken("token-counted", actorClaims)).toBe(true);
546
+ expect(admitActorToken("token-counted", actorClaims).admitted).toBe(true);
544
547
  });
545
548
 
546
549
  // The verdict and the stamp share one SELECT; the stamp adds the UPDATE.
@@ -560,7 +563,9 @@ describe("admitActorToken: device last-used stamping", () => {
560
563
  .run();
561
564
 
562
565
  const sql = captureSql(() => {
563
- expect(admitActorToken("token-debounced", actorClaims)).toBe(true);
566
+ expect(admitActorToken("token-debounced", actorClaims).admitted).toBe(
567
+ true,
568
+ );
564
569
  });
565
570
 
566
571
  expect(readRow("token-debounced")?.lastUsedAt).toBe(sentinel);
@@ -593,7 +598,7 @@ describe("admitActorToken: device last-used stamping", () => {
593
598
  const svcClaims = { sub: "svc:gateway:self" } as TokenClaims;
594
599
 
595
600
  const sql = captureSql(() => {
596
- expect(admitActorToken("svc-token", svcClaims)).toBe(true);
601
+ expect(admitActorToken("svc-token", svcClaims).admitted).toBe(true);
597
602
  });
598
603
 
599
604
  expect(readRow("svc-token")?.lastUsedAt).toBeNull();
@@ -604,7 +609,9 @@ describe("admitActorToken: device last-used stamping", () => {
604
609
  test("is a no-op for an unrecorded token hash", () => {
605
610
  insertTokenRecord("token-recorded", "active");
606
611
 
607
- expect(admitActorToken("token-unrecorded", actorClaims)).toBe(true);
612
+ expect(admitActorToken("token-unrecorded", actorClaims).admitted).toBe(
613
+ true,
614
+ );
608
615
  expect(readRow("token-recorded")?.lastUsedAt).toBeNull();
609
616
  });
610
617
 
@@ -623,7 +630,7 @@ describe("admitActorToken: device last-used stamping", () => {
623
630
  "CREATE TRIGGER fail_last_used BEFORE UPDATE ON actor_token_records BEGIN SELECT RAISE(ABORT, 'stamp failed'); END",
624
631
  );
625
632
 
626
- expect(admitActorToken("token-retry", actorClaims)).toBe(true);
633
+ expect(admitActorToken("token-retry", actorClaims).admitted).toBe(true);
627
634
  expect(readRow("token-retry")?.lastUsedAt).toBeNull();
628
635
 
629
636
  rawDb.exec("DROP TRIGGER fail_last_used");
@@ -657,7 +664,7 @@ describe("admitActorToken: device last-used stamping", () => {
657
664
  ).$client;
658
665
  rawDb.exec("ALTER TABLE actor_token_records RENAME TO actor_token_stash");
659
666
 
660
- expect(admitActorToken("token-transient", actorClaims)).toBe(true);
667
+ expect(admitActorToken("token-transient", actorClaims).admitted).toBe(true);
661
668
 
662
669
  rawDb.exec("ALTER TABLE actor_token_stash RENAME TO actor_token_records");
663
670
  insertTokenRecord("token-transient", "active");