@vellumai/assistant 0.12.6-dev.202609301115.aafbefc → 0.12.6-dev.202609301319.c4fc129

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/ARCHITECTURE.md CHANGED
@@ -103,6 +103,8 @@ All HTTP API requests use a single `Authorization: Bearer <jwt>` header for auth
103
103
 
104
104
  **Route policy enforcement:** Every protected endpoint declares required scopes and allowed principal types in `src/runtime/auth/route-policy.ts`. The `enforcePolicy()` function checks the AuthContext against these requirements and returns 403 when access is denied. A guard test ensures every dispatched endpoint has a corresponding policy entry.
105
105
 
106
+ **Trust class enforcement:** A policy's `allowedTrustClasses` names whose turn may call a route, and an absent list means guardian only. A token whose scope profile is trust-checked (`isTrustCheckedScopeProfile` in `@vellumai/gateway-client`: every profile not on its exempt list, which is `contact_client_v1` today plus any unknown profile) reaches only a route whose list admits the trust class its principal resolves to on the `vellum-shared` channel. That class is read per request, so a revoked contact is refused on its next request. A trust-exempt token counts as the guardian with no lookup, so it reaches only a route whose list admits `guardian`, which every route naming no classes does. A refused caller gets 404, as an unmatched path does. The daemon's HTTP router and the gateway's IPC proxy both apply this rule (`tokenMayReachRoute` in `@vellumai/gateway-client`), since the daemon's IPC server runs no policy check of its own.
107
+
106
108
  **Credential storage:** Only hashed tokens are persisted. Access token hashes go in `credential_records`; refresh token hashes in `refresh_token_records`. Raw tokens are returned once and never stored server-side.
107
109
 
108
110
  **Notification scoping:** Guardian-sensitive notifications (`notification_intent`, `notification_conversation_created`) are published with `targetActorPrincipalId`, so the event hub delivers them, live, on reconnect replay, and from the `events/tail` recovery route, only to SSE connections whose verified `actorPrincipalId` is the guardian's. Connections without a principal never receive them. The payload's `targetGuardianPrincipalId` records the scoping.
@@ -0,0 +1,111 @@
1
+ import { describe, expect, mock, test } from "bun:test";
2
+
3
+ import {
4
+ isTrustCheckedScopeProfile,
5
+ routeAdmitsTrustClass,
6
+ tokenMayReachRoute,
7
+ TRUST_EXEMPT_SCOPE_PROFILES,
8
+ } from "../route-trust-class.js";
9
+
10
+ const CONTACT_ROUTE = ["guardian", "trusted_contact", "unverified_contact"];
11
+
12
+ const contactMayReach = (
13
+ allowed: readonly string[] | undefined,
14
+ resolve: () => Promise<string | undefined>,
15
+ ) => tokenMayReachRoute("contact_client_v1", allowed, resolve);
16
+
17
+ describe("routeAdmitsTrustClass", () => {
18
+ test("an absent list admits the guardian only", () => {
19
+ expect(routeAdmitsTrustClass(undefined, "guardian")).toBe(true);
20
+ expect(routeAdmitsTrustClass(undefined, "trusted_contact")).toBe(false);
21
+ expect(routeAdmitsTrustClass(undefined, "unknown")).toBe(false);
22
+ });
23
+
24
+ test("a listed class is admitted and an unlisted one refused", () => {
25
+ expect(routeAdmitsTrustClass(CONTACT_ROUTE, "trusted_contact")).toBe(true);
26
+ expect(routeAdmitsTrustClass(CONTACT_ROUTE, "unknown")).toBe(false);
27
+ });
28
+
29
+ test("a value outside the vocabulary is refused even when listed", () => {
30
+ expect(routeAdmitsTrustClass(["owner"], "owner")).toBe(false);
31
+ expect(routeAdmitsTrustClass(CONTACT_ROUTE, undefined)).toBe(false);
32
+ });
33
+ });
34
+
35
+ describe("tokenMayReachRoute for a contact token", () => {
36
+ test("a route admitting only the guardian refuses without resolving", async () => {
37
+ const resolve = mock(async () => "trusted_contact");
38
+ expect(await contactMayReach(undefined, resolve)).toBe(false);
39
+ expect(await contactMayReach(["guardian"], resolve)).toBe(false);
40
+ expect(resolve).not.toHaveBeenCalled();
41
+ });
42
+
43
+ test.each([
44
+ ["trusted_contact", true],
45
+ ["unverified_contact", true],
46
+ ["unknown", false],
47
+ ["guardian", false],
48
+ [undefined, false],
49
+ ] as const)(
50
+ "a contact route resolving %p admits: %p",
51
+ async (trustClass, admitted) => {
52
+ expect(await contactMayReach(CONTACT_ROUTE, async () => trustClass)).toBe(
53
+ admitted,
54
+ );
55
+ },
56
+ );
57
+
58
+ test("a class the route does not list is refused", async () => {
59
+ expect(
60
+ await contactMayReach(
61
+ ["guardian", "trusted_contact"],
62
+ async () => "unverified_contact",
63
+ ),
64
+ ).toBe(false);
65
+ });
66
+
67
+ test("a resolver that throws refuses", async () => {
68
+ expect(
69
+ await contactMayReach(CONTACT_ROUTE, async () => {
70
+ throw new Error("gateway unreachable");
71
+ }),
72
+ ).toBe(false);
73
+ });
74
+ });
75
+
76
+ describe("isTrustCheckedScopeProfile", () => {
77
+ test("an exempt profile is not trust-checked", () => {
78
+ for (const profile of TRUST_EXEMPT_SCOPE_PROFILES) {
79
+ expect(isTrustCheckedScopeProfile(profile)).toBe(false);
80
+ }
81
+ });
82
+
83
+ test("the contact profile, unknown profiles and prototype keys are", () => {
84
+ for (const profile of [
85
+ "contact_client_v1",
86
+ "bogus_v1",
87
+ "toString",
88
+ "constructor",
89
+ "__proto__",
90
+ ]) {
91
+ expect(isTrustCheckedScopeProfile(profile)).toBe(true);
92
+ }
93
+ });
94
+ });
95
+
96
+ describe("tokenMayReachRoute for a trust-exempt token", () => {
97
+ test.each([...TRUST_EXEMPT_SCOPE_PROFILES])(
98
+ "%s counts as the guardian without a lookup",
99
+ async (profile) => {
100
+ const resolve = mock(async () => "trusted_contact");
101
+ expect(await tokenMayReachRoute(profile, undefined, resolve)).toBe(true);
102
+ expect(await tokenMayReachRoute(profile, CONTACT_ROUTE, resolve)).toBe(
103
+ true,
104
+ );
105
+ expect(
106
+ await tokenMayReachRoute(profile, ["trusted_contact"], resolve),
107
+ ).toBe(false);
108
+ expect(resolve).not.toHaveBeenCalled();
109
+ },
110
+ );
111
+ });
@@ -132,6 +132,15 @@ export type {
132
132
  TrustVerdict,
133
133
  } from "./trust-verdict-contract.js";
134
134
 
135
+ // Route trust class, applied by both the daemon router and the gateway IPC proxy
136
+ export {
137
+ DEFAULT_ROUTE_TRUST_CLASSES,
138
+ isTrustCheckedScopeProfile,
139
+ routeAdmitsTrustClass,
140
+ tokenMayReachRoute,
141
+ TRUST_EXEMPT_SCOPE_PROFILES,
142
+ } from "./route-trust-class.js";
143
+
135
144
  // Invite contract (shared gateway ↔ daemon) — hash/generate helpers,
136
145
  // channel gating, redemption outcome, method map + invite IPC schemas
137
146
  export {
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Which trust classes a route admits, shared by the daemon's HTTP router and
3
+ * the gateway's IPC proxy so both enforcement points apply one rule.
4
+ *
5
+ * A route policy's `allowedTrustClasses` reaches the gateway on the IPC route
6
+ * schema as plain strings, and is absent from a null policy or a schema served
7
+ * by a daemon that predates the field. Absent means guardian only.
8
+ */
9
+
10
+ import { isTrustClass, type TrustClass } from "./trust-verdict-contract.js";
11
+
12
+ /**
13
+ * Scope profiles whose holders are not trust-checked: the guardian's own
14
+ * client, and service, local and single-route grants that route policy
15
+ * already governs. Any profile not listed here is trust-checked, so an
16
+ * unknown or newly added profile fails closed until it is judged exempt.
17
+ */
18
+ export const TRUST_EXEMPT_SCOPE_PROFILES: readonly string[] = [
19
+ "actor_client_v1",
20
+ "gateway_ingress_v1",
21
+ "gateway_service_v1",
22
+ "local_v1",
23
+ "oauth_proxy_v1",
24
+ "speech_relay_v1",
25
+ "ui_page_v1",
26
+ ];
27
+
28
+ const TRUST_EXEMPT_SET: ReadonlySet<string> = new Set(
29
+ TRUST_EXEMPT_SCOPE_PROFILES,
30
+ );
31
+
32
+ /**
33
+ * True when a token of `profile` reaches a route only if the route admits
34
+ * its holder's trust class.
35
+ */
36
+ export function isTrustCheckedScopeProfile(profile: string): boolean {
37
+ return !TRUST_EXEMPT_SET.has(profile);
38
+ }
39
+
40
+ /** Trust classes a route admits when its policy names none. */
41
+ export const DEFAULT_ROUTE_TRUST_CLASSES: readonly TrustClass[] = ["guardian"];
42
+
43
+ /**
44
+ * Whether a route admitting `allowedTrustClasses` admits a caller of
45
+ * `trustClass`. A value outside the vocabulary is refused, since a wire-sourced
46
+ * class can carry anything.
47
+ */
48
+ export function routeAdmitsTrustClass(
49
+ allowedTrustClasses: readonly string[] | undefined,
50
+ trustClass: string | undefined,
51
+ ): boolean {
52
+ if (typeof trustClass !== "string" || !isTrustClass(trustClass)) {
53
+ return false;
54
+ }
55
+ return (allowedTrustClasses ?? DEFAULT_ROUTE_TRUST_CLASSES).includes(
56
+ trustClass,
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Whether a caller whose token carries `scopeProfile` may reach a route
62
+ * admitting `allowedTrustClasses`.
63
+ *
64
+ * A trust-exempt profile stands for the guardian, so it reaches exactly the
65
+ * routes that admit `guardian`, with no lookup. That is every route naming no
66
+ * classes.
67
+ *
68
+ * A trust-checked token never speaks for the guardian, so a route admitting no
69
+ * other class refuses it without calling `resolveTrustClass`. Otherwise the
70
+ * resolved class must be one the route admits and must not be `guardian`. A
71
+ * resolver that throws or resolves nothing refuses.
72
+ */
73
+ export async function tokenMayReachRoute(
74
+ scopeProfile: string,
75
+ allowedTrustClasses: readonly string[] | undefined,
76
+ resolveTrustClass: () => Promise<string | undefined>,
77
+ ): Promise<boolean> {
78
+ if (!isTrustCheckedScopeProfile(scopeProfile)) {
79
+ return routeAdmitsTrustClass(allowedTrustClasses, "guardian");
80
+ }
81
+ const allowed = allowedTrustClasses ?? DEFAULT_ROUTE_TRUST_CLASSES;
82
+ if (!allowed.some((trustClass) => trustClass !== "guardian")) {
83
+ return false;
84
+ }
85
+ let trustClass: string | undefined;
86
+ try {
87
+ trustClass = await resolveTrustClass();
88
+ } catch {
89
+ return false;
90
+ }
91
+ return (
92
+ trustClass !== "guardian" && routeAdmitsTrustClass(allowed, trustClass)
93
+ );
94
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.12.6-dev.202609301115.aafbefc",
3
+ "version": "0.12.6-dev.202609301319.c4fc129",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -41,6 +41,7 @@ interface SchemaEntry {
41
41
  policy: {
42
42
  requiredScopes: string[];
43
43
  allowedPrincipalTypes: string[];
44
+ allowedTrustClasses: string[];
44
45
  } | null;
45
46
  }
46
47
 
@@ -130,6 +131,7 @@ describe("routeDefinitionsToIpcMethods — schema shape", () => {
130
131
  .object({
131
132
  requiredScopes: z.array(z.string()),
132
133
  allowedPrincipalTypes: z.array(z.string()),
134
+ allowedTrustClasses: z.array(z.string()),
133
135
  })
134
136
  .nullable(),
135
137
  });
@@ -241,4 +243,42 @@ describe("routeDefinitionsToIpcMethods — policy serialization", () => {
241
243
  expect(schema[0].policy?.requiredScopes).toEqual(["settings.write"]);
242
244
  expect(schema[1].policy?.requiredScopes).toEqual(["settings.write"]);
243
245
  });
246
+
247
+ test("a policy declaring no trust classes ships guardian only", async () => {
248
+ // The gateway is the sole policy check on the IPC path, so the
249
+ // effective value must cross the wire rather than be re-derived
250
+ // there from an absent field.
251
+ const routes: RouteDefinition[] = [
252
+ defineRoute({
253
+ operationId: "settings_read",
254
+ endpoint: "settings",
255
+ policy: {
256
+ requiredScopes: ["settings.read"],
257
+ allowedPrincipalTypes: ["actor"],
258
+ },
259
+ }),
260
+ ];
261
+ const schema = await getSchema(routes);
262
+ expect(schema[0].policy?.allowedTrustClasses).toEqual(["guardian"]);
263
+ });
264
+
265
+ test("declared trust classes ship verbatim", async () => {
266
+ const routes: RouteDefinition[] = [
267
+ defineRoute({
268
+ operationId: "chat_send",
269
+ endpoint: "chat",
270
+ method: "POST",
271
+ policy: {
272
+ requiredScopes: ["chat.write"],
273
+ allowedPrincipalTypes: ["actor"],
274
+ allowedTrustClasses: ["guardian", "trusted_contact"],
275
+ },
276
+ }),
277
+ ];
278
+ const schema = await getSchema(routes);
279
+ expect(schema[0].policy?.allowedTrustClasses).toEqual([
280
+ "guardian",
281
+ "trusted_contact",
282
+ ]);
283
+ });
244
284
  });
@@ -8,6 +8,7 @@
8
8
  * RouteDefinition; no lookup or derivation happens here.
9
9
  */
10
10
 
11
+ import { GUARDIAN_ONLY } from "../../runtime/auth/route-policy.js";
11
12
  import type { RouteDefinition } from "../../runtime/routes/types.js";
12
13
 
13
14
  function isIpcEligible(r: RouteDefinition): boolean {
@@ -22,6 +23,10 @@ function isIpcEligible(r: RouteDefinition): boolean {
22
23
  * unprotected (e.g. health, debug). The gateway respects that and
23
24
  * skips enforcement. `policy: { ... }` carries the same scopes /
24
25
  * principal types the daemon's HTTP path enforces via `enforcePolicy()`.
26
+ *
27
+ * `allowedTrustClasses` is always present on a non-null policy, resolved
28
+ * to {@link GUARDIAN_ONLY} when the route declares none, so the wire
29
+ * carries the effective value and no default is duplicated gateway-side.
25
30
  */
26
31
  interface IpcRouteSchemaEntry {
27
32
  operationId: string;
@@ -30,6 +35,7 @@ interface IpcRouteSchemaEntry {
30
35
  policy: {
31
36
  requiredScopes: string[];
32
37
  allowedPrincipalTypes: string[];
38
+ allowedTrustClasses: string[];
33
39
  } | null;
34
40
  }
35
41
 
@@ -44,6 +50,9 @@ function toSchemaEntry(r: RouteDefinition): IpcRouteSchemaEntry {
44
50
  // shape doesn't carry the `Scope` / `PrincipalType` narrowing.
45
51
  requiredScopes: [...r.policy.requiredScopes],
46
52
  allowedPrincipalTypes: [...r.policy.allowedPrincipalTypes],
53
+ allowedTrustClasses: [
54
+ ...(r.policy.allowedTrustClasses ?? GUARDIAN_ONLY),
55
+ ],
47
56
  }
48
57
  : null,
49
58
  };
@@ -0,0 +1,360 @@
1
+ /**
2
+ * The router refuses a contact-role token on every route that does not admit
3
+ * its trust class, with a 404, and leaves every other token untouched.
4
+ *
5
+ * The real route table is served with stub handlers, so each route's own
6
+ * endpoint and policy are what is exercised without running its handler. A
7
+ * probe route that opts into contacts covers the admitted path, which no
8
+ * shipped route takes yet.
9
+ */
10
+
11
+ import {
12
+ afterAll,
13
+ beforeAll,
14
+ beforeEach,
15
+ describe,
16
+ expect,
17
+ mock,
18
+ test,
19
+ } from "bun:test";
20
+
21
+ let authDisabled = false;
22
+ const actualEnv = await import("../../config/env.js");
23
+ mock.module("../../config/env.js", () => ({
24
+ ...actualEnv,
25
+ isHttpAuthDisabled: () => authDisabled,
26
+ }));
27
+
28
+ type Trust = { trustClass: string };
29
+ const resolveSharedPrincipalFresh = mock(
30
+ async (_principalId: string): Promise<Trust> => ({
31
+ trustClass: "trusted_contact",
32
+ }),
33
+ );
34
+ const actualLookup = await import("../shared-principal-lookup.js");
35
+ mock.module("../shared-principal-lookup.js", () => ({
36
+ ...actualLookup,
37
+ resolveSharedPrincipalFresh,
38
+ }));
39
+
40
+ const { isContactTrustClass } = await import("../trust-class.js");
41
+ const { CONTACT_ALLOWED, enforcePolicy } =
42
+ await import("../auth/route-policy.js");
43
+ const actualRoutes = await import("../routes/index.js");
44
+ type RouteDefinition = (typeof actualRoutes.ROUTES)[number];
45
+
46
+ let reached: string | undefined;
47
+ const stub = (route: RouteDefinition): RouteDefinition => ({
48
+ ...route,
49
+ requireGuardian: false,
50
+ handler: () => {
51
+ reached = route.operationId;
52
+ return { ok: true };
53
+ },
54
+ });
55
+
56
+ const PROBE: RouteDefinition = {
57
+ operationId: "trust_probe",
58
+ endpoint: "trust-probe",
59
+ method: "POST",
60
+ policy: {
61
+ requiredScopes: ["chat.write"],
62
+ allowedPrincipalTypes: ["actor"],
63
+ allowedTrustClasses: CONTACT_ALLOWED,
64
+ },
65
+ handler: () => ({ ok: true }),
66
+ };
67
+
68
+ const CONTACT_ONLY_PROBE: RouteDefinition = {
69
+ ...PROBE,
70
+ operationId: "contact_only_probe",
71
+ endpoint: "contact-only-probe",
72
+ policy: { ...PROBE.policy!, allowedTrustClasses: ["trusted_contact"] },
73
+ };
74
+
75
+ const REAL_ROUTES = actualRoutes.ROUTES.map(stub);
76
+ const GUARDIAN_ROUTES = REAL_ROUTES.filter(
77
+ (route) => !route.policy?.allowedTrustClasses?.some(isContactTrustClass),
78
+ );
79
+ mock.module("../routes/index.js", () => ({
80
+ ...actualRoutes,
81
+ ROUTES: [...REAL_ROUTES, stub(PROBE), stub(CONTACT_ONLY_PROBE)],
82
+ }));
83
+
84
+ const { resolveScopeProfile } = await import("../auth/scopes.js");
85
+ const { HttpRouter } = await import("../http-router.js");
86
+ import type { AuthContext, ScopeProfile } from "../auth/types.js";
87
+
88
+ function context(
89
+ subject: string,
90
+ scopeProfile: ScopeProfile,
91
+ principalType: AuthContext["principalType"] = "actor",
92
+ ): AuthContext {
93
+ const actorPrincipalId =
94
+ principalType === "actor" ? subject.split(":")[2] : undefined;
95
+ return {
96
+ subject,
97
+ principalType,
98
+ assistantId: "self",
99
+ actorPrincipalId,
100
+ scopeProfile,
101
+ scopes: resolveScopeProfile(scopeProfile),
102
+ policyEpoch: 0,
103
+ };
104
+ }
105
+
106
+ const CONTACT = context("actor:self:principal-alice", "contact_client_v1");
107
+ const GUARDIAN = context("actor:self:principal-bob", "actor_client_v1");
108
+ const GATEWAY = context(
109
+ "svc:gateway:self",
110
+ "gateway_service_v1",
111
+ "svc_gateway",
112
+ );
113
+ const LOCAL = context("local:self:conv-xyz", "local_v1", "local");
114
+
115
+ const UUID = "123e4567-e89b-42d3-a456-426614174000";
116
+
117
+ /** A concrete path for a route's endpoint pattern. */
118
+ function concretePath(route: RouteDefinition): string {
119
+ const types = new Map(
120
+ (route.pathParams ?? []).map((param) => [param.name, param.type]),
121
+ );
122
+ return route.endpoint
123
+ .split("/")
124
+ .map((segment) => {
125
+ if (!segment.startsWith(":")) {
126
+ return segment;
127
+ }
128
+ if (segment.endsWith("*")) {
129
+ return "a/b";
130
+ }
131
+ return types.get(segment.slice(1)) === "uuid" ? UUID : "x";
132
+ })
133
+ .join("/");
134
+ }
135
+
136
+ let server: ReturnType<typeof Bun.serve>;
137
+ let router: InstanceType<typeof HttpRouter>;
138
+
139
+ beforeAll(() => {
140
+ server = Bun.serve({ port: 0, fetch: () => new Response("unused") });
141
+ router = new HttpRouter();
142
+ });
143
+
144
+ afterAll(() => {
145
+ server.stop(true);
146
+ });
147
+
148
+ beforeEach(() => {
149
+ authDisabled = false;
150
+ reached = undefined;
151
+ resolveSharedPrincipalFresh.mockReset();
152
+ resolveSharedPrincipalFresh.mockImplementation(async () => ({
153
+ trustClass: "trusted_contact",
154
+ }));
155
+ });
156
+
157
+ async function dispatch(
158
+ method: string,
159
+ endpoint: string,
160
+ authContext: AuthContext,
161
+ ): Promise<Response> {
162
+ const url = new URL(`http://127.0.0.1/v1/${endpoint}`);
163
+ const req = new Request(url, {
164
+ method,
165
+ headers: { "content-type": "application/json" },
166
+ body: method === "GET" || method === "HEAD" ? undefined : "{}",
167
+ });
168
+ const response = await router.dispatch(
169
+ endpoint,
170
+ req,
171
+ url,
172
+ server,
173
+ authContext,
174
+ );
175
+ if (!response) {
176
+ throw new Error(`no route matched ${method} ${endpoint}`);
177
+ }
178
+ return response;
179
+ }
180
+
181
+ describe("contact-role tokens", () => {
182
+ test("POST /v1/messages is a 404", async () => {
183
+ const response = await dispatch("POST", "messages", CONTACT);
184
+
185
+ expect(response.status).toBe(404);
186
+ const body = (await response.json()) as { error: { code: string } };
187
+ expect(body.error.code).toBe("NOT_FOUND");
188
+ expect(reached).toBeUndefined();
189
+ });
190
+
191
+ test("every route not admitting contacts is a 404, without a trust lookup", async () => {
192
+ expect(GUARDIAN_ROUTES.length).toBeGreaterThan(0);
193
+ const leaks: string[] = [];
194
+ for (const route of GUARDIAN_ROUTES) {
195
+ reached = undefined;
196
+ const response = await dispatch(
197
+ route.method,
198
+ concretePath(route),
199
+ CONTACT,
200
+ );
201
+ if (response.status !== 404 || reached !== undefined) {
202
+ leaks.push(`${route.method} ${route.endpoint}: ${response.status}`);
203
+ }
204
+ }
205
+ expect(leaks).toEqual([]);
206
+ expect(resolveSharedPrincipalFresh).not.toHaveBeenCalled();
207
+ });
208
+
209
+ test("a malformed path on a guardian route is a 404, not a 400", async () => {
210
+ const route = GUARDIAN_ROUTES.find(
211
+ (r) => r.endpoint.includes(":") && !r.pathParams?.length,
212
+ )!;
213
+ const path = route.endpoint.replace(/:[^/]+/, "a%zz");
214
+
215
+ expect((await dispatch(route.method, path, CONTACT)).status).toBe(404);
216
+ expect((await dispatch(route.method, path, GUARDIAN)).status).toBe(400);
217
+ });
218
+
219
+ test("an unrecognized profile is trust-checked too", async () => {
220
+ const response = await dispatch(
221
+ "POST",
222
+ "messages",
223
+ context("actor:self:principal-alice", "bogus_v1" as ScopeProfile),
224
+ );
225
+ expect(response.status).toBe(404);
226
+ expect(reached).toBeUndefined();
227
+ });
228
+
229
+ test("refusal holds under the dev auth bypass", async () => {
230
+ authDisabled = true;
231
+ const response = await dispatch("POST", "messages", CONTACT);
232
+ expect(response.status).toBe(404);
233
+ expect(reached).toBeUndefined();
234
+ });
235
+
236
+ test.each(["trusted_contact", "unverified_contact"])(
237
+ "a route admitting contacts serves a %s",
238
+ async (trustClass) => {
239
+ resolveSharedPrincipalFresh.mockImplementation(async () => ({
240
+ trustClass,
241
+ }));
242
+ const response = await dispatch("POST", "trust-probe", CONTACT);
243
+
244
+ expect(response.status).toBe(200);
245
+ expect(reached).toBe("trust_probe");
246
+ expect(resolveSharedPrincipalFresh).toHaveBeenCalledWith(
247
+ "principal-alice",
248
+ );
249
+ },
250
+ );
251
+
252
+ test.each(["unknown", "guardian"])(
253
+ "a route admitting contacts refuses a principal resolving %s",
254
+ async (trustClass) => {
255
+ resolveSharedPrincipalFresh.mockImplementation(async () => ({
256
+ trustClass,
257
+ }));
258
+ const response = await dispatch("POST", "trust-probe", CONTACT);
259
+
260
+ expect(response.status).toBe(404);
261
+ expect(reached).toBeUndefined();
262
+ },
263
+ );
264
+
265
+ test("a revoked contact is refused on its next request", async () => {
266
+ expect((await dispatch("POST", "trust-probe", CONTACT)).status).toBe(200);
267
+
268
+ resolveSharedPrincipalFresh.mockImplementation(async () => ({
269
+ trustClass: "unknown",
270
+ }));
271
+ reached = undefined;
272
+ expect((await dispatch("POST", "trust-probe", CONTACT)).status).toBe(404);
273
+ expect(reached).toBeUndefined();
274
+ expect(resolveSharedPrincipalFresh).toHaveBeenCalledTimes(2);
275
+ });
276
+
277
+ test("a failed trust lookup refuses", async () => {
278
+ resolveSharedPrincipalFresh.mockImplementation(async () => {
279
+ throw new Error("gateway unreachable");
280
+ });
281
+ const response = await dispatch("POST", "trust-probe", CONTACT);
282
+
283
+ expect(response.status).toBe(404);
284
+ expect(reached).toBeUndefined();
285
+ });
286
+
287
+ test("a contact token naming no actor principal refuses without a lookup", async () => {
288
+ const response = await dispatch(
289
+ "POST",
290
+ "trust-probe",
291
+ context("svc:gateway:self", "contact_client_v1", "svc_gateway"),
292
+ );
293
+
294
+ expect(response.status).toBe(404);
295
+ expect(resolveSharedPrincipalFresh).not.toHaveBeenCalled();
296
+ });
297
+ });
298
+
299
+ describe("other tokens", () => {
300
+ test("a guardian token reaches POST /v1/messages", async () => {
301
+ const response = await dispatch("POST", "messages", GUARDIAN);
302
+
303
+ expect(response.status).toBe(202);
304
+ expect(reached).toBe("messages_post");
305
+ });
306
+
307
+ test("a guardian token reaches a route admitting guardians and contacts", async () => {
308
+ const response = await dispatch("POST", "trust-probe", GUARDIAN);
309
+
310
+ expect(response.status).toBe(200);
311
+ expect(reached).toBe("trust_probe");
312
+ expect(resolveSharedPrincipalFresh).not.toHaveBeenCalled();
313
+ });
314
+
315
+ test("a guardian token gets 404 from a contact-only route, without a lookup", async () => {
316
+ const response = await dispatch("POST", "contact-only-probe", GUARDIAN);
317
+
318
+ expect(response.status).toBe(404);
319
+ const body = (await response.json()) as { error: { code: string } };
320
+ expect(body.error.code).toBe("NOT_FOUND");
321
+ expect(reached).toBeUndefined();
322
+ expect(resolveSharedPrincipalFresh).not.toHaveBeenCalled();
323
+ });
324
+
325
+ test("the dev-bypass guardian is refused on a contact-only route too", async () => {
326
+ authDisabled = true;
327
+ const response = await dispatch("POST", "contact-only-probe", GUARDIAN);
328
+ expect(response.status).toBe(404);
329
+ });
330
+
331
+ test("a contact token reaches a contact-only route", async () => {
332
+ const response = await dispatch("POST", "contact-only-probe", CONTACT);
333
+
334
+ expect(response.status).toBe(200);
335
+ expect(reached).toBe("contact_only_probe");
336
+ });
337
+
338
+ test.each([
339
+ ["guardian", GUARDIAN],
340
+ ["svc_gateway", GATEWAY],
341
+ ["local", LOCAL],
342
+ ] as const)(
343
+ "a %s token gets what the scope policy alone decides on every route",
344
+ async (_label, ctx) => {
345
+ const drift: string[] = [];
346
+ for (const route of REAL_ROUTES) {
347
+ reached = undefined;
348
+ const response = await dispatch(route.method, concretePath(route), ctx);
349
+ const denied = enforcePolicy(route.endpoint, route.policy, ctx);
350
+ const expected = denied ? denied.status : route.operationId;
351
+ const actual = reached ?? response.status;
352
+ if (actual !== expected) {
353
+ drift.push(`${route.method} ${route.endpoint}: ${actual}`);
354
+ }
355
+ }
356
+ expect(drift).toEqual([]);
357
+ expect(resolveSharedPrincipalFresh).not.toHaveBeenCalled();
358
+ },
359
+ );
360
+ });