@mycadet/gateway 0.12.10 → 0.13.0-dev.202610171617.9ea363e

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 (105) 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/channels.ts +0 -1
  3. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  4. package/node_modules/@mycadet/ces-client/src/http-credentials.ts +18 -1
  5. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  6. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
  7. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  8. package/node_modules/@mycadet/gateway-client/src/admission-policy-contract.ts +1 -2
  9. package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
  10. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  11. package/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
  12. package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  13. package/openapi.json +1 -1
  14. package/package.json +5 -1
  15. package/src/__tests__/actor-token-revocation.test.ts +41 -34
  16. package/src/__tests__/channel-admission-policy-routes.test.ts +4 -17
  17. package/src/__tests__/contact-store-mirror-op-missing.test.ts +33 -70
  18. package/src/__tests__/contacts-control-plane-proxy.test.ts +0 -18
  19. package/src/__tests__/contacts-mirror-op-reporter.test.ts +33 -89
  20. package/src/__tests__/credential-requests.test.ts +4 -4
  21. package/src/__tests__/devices-mine.test.ts +187 -0
  22. package/src/__tests__/edge-auth.test.ts +1 -20
  23. package/src/__tests__/edge-guardian-auth.test.ts +1 -17
  24. package/src/__tests__/feature-flag-resolver.test.ts +2 -30
  25. package/src/__tests__/feature-flag-watcher-callback.test.ts +0 -22
  26. package/src/__tests__/feature-flags-route.test.ts +10 -193
  27. package/src/__tests__/guardian-bootstrap-lookups.test.ts +22 -29
  28. package/src/__tests__/guardian-init-lockfile.test.ts +2 -2
  29. package/src/__tests__/guardian-integrity-reporter.test.ts +51 -82
  30. package/src/__tests__/guardian-integrity.test.ts +16 -20
  31. package/src/__tests__/handle-inbound-trust-verdict.test.ts +1 -14
  32. package/src/__tests__/ipc-feature-flag-routes.test.ts +0 -56
  33. package/src/__tests__/lum-2783-recovery-e2e.test.ts +0 -12
  34. package/src/__tests__/post-assistant-ready-deferred.test.ts +0 -18
  35. package/src/__tests__/refresh-device-binding.test.ts +0 -12
  36. package/src/__tests__/remote-web-ingress-denylist.test.ts +22 -13
  37. package/src/__tests__/remote-web-pairing-token.test.ts +5 -15
  38. package/src/__tests__/route-schema-guard.test.ts +0 -19
  39. package/src/__tests__/runtime-client.test.ts +7 -7
  40. package/src/__tests__/seed-admission-policy.test.ts +0 -6
  41. package/src/__tests__/speech-relay-websocket.test.ts +4 -4
  42. package/src/__tests__/trust-verdict-resolver-guardian-integrity.test.ts +30 -36
  43. package/src/__tests__/twilio-webhooks.test.ts +2 -2
  44. package/src/auth/actor-token-revocation.ts +34 -9
  45. package/src/auth/guardian-bootstrap.ts +1 -1
  46. package/src/channels/inbound-event.ts +1 -3
  47. package/src/channels/types.ts +0 -2
  48. package/src/cli/__tests__/pairing.test.ts +95 -0
  49. package/src/cli/index.ts +1 -1
  50. package/src/cli/pairing.ts +81 -6
  51. package/src/contacts-mirror-op-reporter.ts +19 -36
  52. package/src/db/admission-policy-store.ts +1 -1
  53. package/src/db/connection.ts +0 -10
  54. package/src/db/seed-admission-policy.ts +1 -1
  55. package/src/feature-flag-registry.json +0 -130
  56. package/src/feature-flag-resolver.ts +2 -7
  57. package/src/feature-flag-watcher.ts +1 -14
  58. package/src/guardian-integrity-reporter.ts +19 -47
  59. package/src/handlers/handle-inbound.ts +1 -1
  60. package/src/http/middleware/auth.ts +33 -27
  61. package/src/http/routes/auth-token.ts +1 -1
  62. package/src/http/routes/channel-admission-policy.ts +2 -2
  63. package/src/http/routes/contacts-control-plane-proxy.ts +0 -9
  64. package/src/http/routes/devices-mine.ts +103 -0
  65. package/src/http/routes/feature-flags.ts +1 -8
  66. package/src/http/routes/guardian-pin.ts +2 -11
  67. package/src/http/routes/ipc-runtime-proxy.ts +1 -1
  68. package/src/http/routes/live-voice-websocket.ts +1 -1
  69. package/src/http/routes/runtime-audio-stream.ts +1 -1
  70. package/src/http/routes/runtime-proxy.ts +2 -2
  71. package/src/http/routes/speech-relay-websocket.ts +2 -2
  72. package/src/http/routes/twilio-voice-webhook.test.ts +13 -13
  73. package/src/index.ts +11 -210
  74. package/src/ipc/feature-flag-handlers.ts +2 -9
  75. package/src/remote-web/pairing-challenge-store.ts +15 -0
  76. package/src/risk/command-registry/commands/assistant.ts +0 -24
  77. package/src/risk/command-registry.test.ts +2 -4
  78. package/src/schema.ts +45 -373
  79. package/src/types.ts +0 -1
  80. package/src/__tests__/auth-fallback-count-tracker.test.ts +0 -92
  81. package/src/__tests__/auth-fallback-reporter.test.ts +0 -101
  82. package/src/__tests__/backup-worker-cloud-mode.test.ts +0 -44
  83. package/src/__tests__/backup-worker-export-timeout.test.ts +0 -164
  84. package/src/__tests__/migration-teleport-gcs-proxy.test.ts +0 -216
  85. package/src/__tests__/remote-feature-flag-sync.test.ts +0 -961
  86. package/src/auth-fallback-count-tracker.ts +0 -113
  87. package/src/auth-fallback-reporter.ts +0 -127
  88. package/src/backup/backup-key.ts +0 -138
  89. package/src/backup/backup-routes.ts +0 -161
  90. package/src/backup/backup-worker.ts +0 -425
  91. package/src/backup/list-snapshots.ts +0 -97
  92. package/src/backup/local-writer.ts +0 -87
  93. package/src/backup/offsite-writer.ts +0 -182
  94. package/src/backup/paths.ts +0 -113
  95. package/src/backup/stream-crypt.ts +0 -258
  96. package/src/device-id.ts +0 -18
  97. package/src/feature-flag-remote-store.ts +0 -166
  98. package/src/http/routes/a2a-routes.test.ts +0 -146
  99. package/src/http/routes/a2a-routes.ts +0 -126
  100. package/src/http/routes/migration-proxy.ts +0 -636
  101. package/src/internal-telemetry-client.ts +0 -39
  102. package/src/ipc/debug-export-handlers.test.ts +0 -102
  103. package/src/ipc/debug-export-handlers.ts +0 -81
  104. package/src/remote-feature-flag-sync.ts +0 -440
  105. 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
  });
@@ -39,7 +39,6 @@ export const CHANNEL_IDS = [
39
39
  "slack",
40
40
  "email",
41
41
  "platform",
42
- "a2a",
43
42
  "discord",
44
43
  "plugin",
45
44
  // Internal id for conversations a guardian shares with a contact. It has no
@@ -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
  });
@@ -39,7 +39,6 @@ export const CHANNEL_IDS = [
39
39
  "slack",
40
40
  "email",
41
41
  "platform",
42
- "a2a",
43
42
  "discord",
44
43
  "plugin",
45
44
  // Internal id for conversations a guardian shares with a contact. It has no
@@ -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 {
@@ -52,7 +52,7 @@ export const ADMISSION_FLOOR: Record<AdmissionPolicy, number> = {
52
52
  * Hard-exempt internal channels — never subject to PUT policy, omitted from
53
53
  * GET list, runtime admission stage short-circuits without floor check.
54
54
  *
55
- * `platform` / `a2a` are peer/internal channels with no human-trust model.
55
+ * `platform` is the peer/internal channel with no human-trust model.
56
56
  *
57
57
  * `phone` is NOT exempt — voice ingress enforces the admission floor.
58
58
  *
@@ -62,7 +62,6 @@ export const ADMISSION_FLOOR: Record<AdmissionPolicy, number> = {
62
62
  */
63
63
  export const ADMISSION_POLICY_EXEMPT_CHANNELS: ReadonlySet<string> = new Set([
64
64
  "platform",
65
- "a2a",
66
65
  ]);
67
66
 
68
67
  export function isAdmissionPolicyExemptChannel(channelType: string): boolean {
@@ -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
  });
@@ -39,7 +39,6 @@ export const CHANNEL_IDS = [
39
39
  "slack",
40
40
  "email",
41
41
  "platform",
42
- "a2a",
43
42
  "discord",
44
43
  "plugin",
45
44
  // Internal id for conversations a guardian shares with a contact. It has no
@@ -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,10 @@
1
1
  {
2
2
  "name": "@mycadet/gateway",
3
- "version": "0.12.10",
3
+ "version": "0.13.0-dev.202610171617.9ea363e",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/vargasjr-dev/mycadet-temp.git"
7
+ },
4
8
  "license": "MIT",
5
9
  "type": "module",
6
10
  "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");
@@ -125,12 +125,10 @@ describe("GET /v1/channel-admission-policy", () => {
125
125
  expect(phone?.policy).toBe(ADMISSION_POLICY_DEFAULT);
126
126
  });
127
127
 
128
- test("omits exempt `a2a` and hidden `cadet`/`whatsapp` but includes enforced `phone`", async () => {
129
- // a2a stays exempt (no runtime admission model). cadet/whatsapp are still
130
- // enforced at runtime but hidden from the Channel Trust Floors UI, so they
131
- // must not surface even when a row is persisted. phone is now enforced and
132
- // visible, so it must surface.
133
- store.set("a2a", "guardian_only");
128
+ test("omits hidden `cadet`/`whatsapp` but includes enforced `phone`", async () => {
129
+ // cadet/whatsapp are still enforced at runtime but hidden from the
130
+ // Channel Trust Floors UI, so they must not surface even when a row is
131
+ // persisted. phone is now enforced and visible, so it must surface.
134
132
  store.set("cadet", "guardian_only");
135
133
  store.set("whatsapp", "guardian_only");
136
134
  store.set("cadet-shared", "guardian_only");
@@ -263,17 +261,6 @@ describe("PUT /v1/channel-admission-policy/:channelType", () => {
263
261
  }
264
262
  });
265
263
 
266
- test("§8.1: rejects PUT for exempt channel `a2a` with 403", async () => {
267
- const handler = createChannelAdmissionPolicySetHandler();
268
- const res = await handler(
269
- jsonRequest("http://localhost/v1/channel-admission-policy/a2a", "PUT", {
270
- policy: "guardian_only",
271
- }),
272
- "a2a",
273
- );
274
- expect(res.status).toBe(403);
275
- expect(store.get("a2a")).toBe(ADMISSION_POLICY_DEFAULT);
276
- });
277
264
 
278
265
  test("upserts `phone` like any enforced channel (voice ingress wired)", async () => {
279
266
  const handler = createChannelAdmissionPolicySetHandler();