@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.
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/ces-client/src/http-credentials.ts +18 -1
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- 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 +22 -29
- package/src/__tests__/guardian-init-lockfile.test.ts +2 -2
- 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 +5 -15
- 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/auth/guardian-bootstrap.ts +1 -1
- 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/speech-relay-websocket.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
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
trimmedNonEmptyString,
|
|
11
11
|
TUNNEL_PROVIDERS,
|
|
12
12
|
type TunnelRecord,
|
|
13
|
-
|
|
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("
|
|
22
|
+
describe("tunnelHostForCloudHost", () => {
|
|
23
23
|
test("maps the prod platform host to the prod tunnel", () => {
|
|
24
|
-
expect(
|
|
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(
|
|
31
|
-
"tunnel
|
|
30
|
+
expect(tunnelHostForCloudHost("staging-cloud.mycadet.ai")).toBe(
|
|
31
|
+
"staging-tunnel.mycadet.ai",
|
|
32
32
|
);
|
|
33
|
-
expect(
|
|
34
|
-
"tunnel
|
|
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(
|
|
40
|
-
expect(
|
|
39
|
+
expect(tunnelHostForCloudHost("localhost")).toBeNull();
|
|
40
|
+
expect(tunnelHostForCloudHost("platform.example.com")).toBeNull();
|
|
41
41
|
expect(
|
|
42
|
-
|
|
42
|
+
tunnelHostForCloudHost("staging-cloud.mycadet.ai.evil.com"),
|
|
43
43
|
).toBeNull();
|
|
44
44
|
});
|
|
45
45
|
});
|
package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts
CHANGED
|
@@ -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` → `
|
|
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
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
22
|
+
describe("tunnelHostForCloudHost", () => {
|
|
23
23
|
test("maps the prod platform host to the prod tunnel", () => {
|
|
24
|
-
expect(
|
|
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(
|
|
31
|
-
"tunnel
|
|
30
|
+
expect(tunnelHostForCloudHost("staging-cloud.mycadet.ai")).toBe(
|
|
31
|
+
"staging-tunnel.mycadet.ai",
|
|
32
32
|
);
|
|
33
|
-
expect(
|
|
34
|
-
"tunnel
|
|
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(
|
|
40
|
-
expect(
|
|
39
|
+
expect(tunnelHostForCloudHost("localhost")).toBeNull();
|
|
40
|
+
expect(tunnelHostForCloudHost("platform.example.com")).toBeNull();
|
|
41
41
|
expect(
|
|
42
|
-
|
|
42
|
+
tunnelHostForCloudHost("staging-cloud.mycadet.ai.evil.com"),
|
|
43
43
|
).toBeNull();
|
|
44
44
|
});
|
|
45
45
|
});
|
package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts
CHANGED
|
@@ -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` → `
|
|
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
|
|
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 ?
|
|
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
|
-
|
|
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("
|
|
22
|
+
describe("tunnelHostForCloudHost", () => {
|
|
23
23
|
test("maps the prod platform host to the prod tunnel", () => {
|
|
24
|
-
expect(
|
|
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(
|
|
31
|
-
"tunnel
|
|
30
|
+
expect(tunnelHostForCloudHost("staging-cloud.mycadet.ai")).toBe(
|
|
31
|
+
"staging-tunnel.mycadet.ai",
|
|
32
32
|
);
|
|
33
|
-
expect(
|
|
34
|
-
"tunnel
|
|
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(
|
|
40
|
-
expect(
|
|
39
|
+
expect(tunnelHostForCloudHost("localhost")).toBeNull();
|
|
40
|
+
expect(tunnelHostForCloudHost("platform.example.com")).toBeNull();
|
|
41
41
|
expect(
|
|
42
|
-
|
|
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` → `
|
|
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
|
|
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 ?
|
|
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.
|
|
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
|
@@ -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
|
|
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(
|
|
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(
|
|
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(
|
|
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");
|