@fuzdev/fuz_app 0.112.0 → 0.113.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 (45) hide show
  1. package/dist/auth/CLAUDE.md +6 -1
  2. package/dist/auth/audit_log_route_schema.d.ts.map +1 -1
  3. package/dist/auth/audit_log_route_schema.js +12 -1
  4. package/dist/auth/role_grant_offer_action_specs.d.ts +2 -1
  5. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  6. package/dist/auth/role_grant_offer_action_specs.js +2 -1
  7. package/dist/db/fact_disk_storage.js +1 -1
  8. package/dist/http/CLAUDE.md +3 -3
  9. package/dist/http/auth_shape.d.ts +19 -6
  10. package/dist/http/auth_shape.d.ts.map +1 -1
  11. package/dist/http/auth_shape.js +19 -6
  12. package/dist/http/surface_query.d.ts +29 -4
  13. package/dist/http/surface_query.d.ts.map +1 -1
  14. package/dist/http/surface_query.js +30 -5
  15. package/dist/realtime/sse_auth_guard.d.ts +9 -3
  16. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  17. package/dist/realtime/sse_auth_guard.js +9 -3
  18. package/dist/testing/CLAUDE.md +1 -1
  19. package/dist/testing/attack_surface.d.ts +5 -2
  20. package/dist/testing/attack_surface.d.ts.map +1 -1
  21. package/dist/testing/attack_surface.js +45 -17
  22. package/dist/testing/auth_apps.d.ts +25 -0
  23. package/dist/testing/auth_apps.d.ts.map +1 -1
  24. package/dist/testing/auth_apps.js +34 -5
  25. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  26. package/dist/testing/cross_backend/testing_reset_actions.js +3 -2
  27. package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
  28. package/dist/testing/cross_backend/token_scope_surface.js +36 -7
  29. package/dist/testing/surface_invariants.d.ts +7 -1
  30. package/dist/testing/surface_invariants.d.ts.map +1 -1
  31. package/dist/testing/surface_invariants.js +11 -5
  32. package/dist/ui/AdminAccounts.svelte +6 -4
  33. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  34. package/dist/ui/AdminInvites.svelte +3 -3
  35. package/dist/ui/AdminInvites.svelte.d.ts.map +1 -1
  36. package/dist/ui/AdminOverview.svelte +10 -10
  37. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  38. package/dist/ui/AdminRoleGrantHistory.svelte +7 -7
  39. package/dist/ui/AdminRoleGrantHistory.svelte.d.ts.map +1 -1
  40. package/dist/ui/AdminSettings.svelte +1 -1
  41. package/dist/ui/RoleGrantOfferInbox.svelte +4 -1
  42. package/dist/ui/RoleGrantOfferInbox.svelte.d.ts.map +1 -1
  43. package/dist/ui/SurfaceExplorer.svelte +33 -17
  44. package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
  45. package/package.json +10 -10
@@ -510,10 +510,15 @@ are excluded.
510
510
  declare `credential_types: ['session']`. `account_session_revoke` is
511
511
  gated alongside `_revoke_all` because a leaked bearer can otherwise
512
512
  compose `account_session_list` + N×revoke to reach the same lockout.
513
+ `role_grant_offer_accept` and `self_service_role_set` carry the gate for
514
+ the privilege-pivot shape — both move the caller's own authority, and the
515
+ resulting grant outlives revoking the token.
513
516
  REST `POST /logout` also declares `credential_types: ['session']`, but
514
517
  for forensic fidelity rather than a threat — a bearer / daemon token
515
518
  holds no session to end, so the gate refuses it instead of returning a
516
- misleading 200 + a phantom `logout` audit row.
519
+ misleading 200 + a phantom `logout` audit row. `GET /audit/stream` carries
520
+ it so the stream stays closable (SSE subscribers are keyed by session
521
+ hash, so `token_revoke` could not close a bearer's stream).
517
522
  Admin token/session revoke specs deliberately stay unrestricted (admin
518
523
  scripting from CLI/bearer is legitimate operator workflow). See
519
524
  ../../../docs/security.md §Credential-channel gating.
@@ -1 +1 @@
1
- {"version":3,"file":"audit_log_route_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_route_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAGvD,2FAA2F;AAC3F,eAAO,MAAM,gBAAgB;;kBAA0C,CAAC;AACxE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE,+DAA+D;AAC/D,eAAO,MAAM,yBAAyB,UAAU,CAAC;AAEjD;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,GACxC,gBAAe,MAAkC,KAC/C,IAAI,CAAC,SAAS,EAAE,SAAS,CAkB1B,CAAC"}
1
+ {"version":3,"file":"audit_log_route_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_route_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAGvD,2FAA2F;AAC3F,eAAO,MAAM,gBAAgB;;kBAA0C,CAAC;AACxE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE,+DAA+D;AAC/D,eAAO,MAAM,yBAAyB,UAAU,CAAC;AAEjD;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,GACxC,gBAAe,MAAkC,KAC/C,IAAI,CAAC,SAAS,EAAE,SAAS,CA6B1B,CAAC"}
@@ -32,11 +32,22 @@ export const create_audit_log_route_shape = (required_role = DEFAULT_AUDIT_STREA
32
32
  account: 'required',
33
33
  actor: 'required',
34
34
  roles: [required_role],
35
+ // The stream is a session channel. A bearer that opens one cannot be
36
+ // closed when it is revoked: `token_revoke` closes WS sockets keyed by
37
+ // token, and SSE subscribers are not keyed that way, so a revoked
38
+ // token would keep receiving audit rows for the life of the
39
+ // connection. Gating the channel is what makes
40
+ // `realtime/sse_auth_guard.ts`'s omission of `token_revoke` correct
41
+ // rather than merely asserted.
42
+ credential_types: ['session'],
35
43
  // Rule 3 — a narrowed token holds no audit feed. Same reasoning as the
36
44
  // WS upgrade: this is a long-lived server→client stream whose contents
37
45
  // are decided by the account's role, not by any per-message scope
38
46
  // check, so there is no point after open at which a narrowing could
39
- // apply.
47
+ // apply. Unreachable behind the credential gate above on the default
48
+ // mount, and kept deliberately: it is the rule-3 declaration the
49
+ // surface census matches on, and it still governs a consumer that
50
+ // widens the channel gate.
40
51
  required_scope: 'surface:audit_stream'
41
52
  },
42
53
  description: 'Subscribe to realtime audit log events',
@@ -11,7 +11,8 @@
11
11
  * required (no roles) and rely on `query_*` IDOR guards or in-handler
12
12
  * policy checks (e.g. `role_grant_offer_list`/`_history` elevate to admin only
13
13
  * when inspecting another account — an input-dependent check that can't be
14
- * expressed at the spec level). `role_grant_revoke` adds `roles: ['admin']` —
14
+ * expressed at the spec level). `role_grant_revoke` and `role_grant_assign` add
15
+ * `roles: ['admin']` —
15
16
  * the RPC dispatcher's per-spec post-authorization auth gate
16
17
  * (`check_action_auth_post_authorization`) rejects non-admin callers before
17
18
  * the handler runs even though the endpoint hosts non-admin methods
@@ -1 +1 @@
1
- {"version":3,"file":"role_grant_offer_action_specs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_action_specs.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"role_grant_offer_action_specs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_action_specs.ts"],"names":[],"mappings":"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"}
@@ -11,7 +11,8 @@
11
11
  * required (no roles) and rely on `query_*` IDOR guards or in-handler
12
12
  * policy checks (e.g. `role_grant_offer_list`/`_history` elevate to admin only
13
13
  * when inspecting another account — an input-dependent check that can't be
14
- * expressed at the spec level). `role_grant_revoke` adds `roles: ['admin']` —
14
+ * expressed at the spec level). `role_grant_revoke` and `role_grant_assign` add
15
+ * `roles: ['admin']` —
15
16
  * the RPC dispatcher's per-spec post-authorization auth gate
16
17
  * (`check_action_auth_post_authorization`) rejects non-admin callers before
17
18
  * the handler runs even though the endpoint hosts non-admin methods
@@ -29,7 +29,7 @@
29
29
  */
30
30
  import { createHash } from 'node:crypto';
31
31
  import { join } from 'node:path';
32
- import { Blake3Hasher } from '@fuzdev/blake3_wasm';
32
+ import { Blake3Hasher } from '@fuzdev/blake3-wasm';
33
33
  import { to_error_message } from '@fuzdev/fuz_util/error.ts';
34
34
  import { blake3_ready } from '@fuzdev/fuz_util/hash_blake3.ts';
35
35
  import { to_hex } from '@fuzdev/fuz_util/hex.ts';
@@ -264,11 +264,11 @@ the source specs, for tests that need to iterate over raw specs.
264
264
  No side effects, no state — filters and groupings over `AppSurface`:
265
265
 
266
266
  - `filter_protected_routes` / `filter_public_routes`
267
- - `filter_role_routes` / `filter_authenticated_routes` / `filter_keeper_routes` / `filter_routes_for_role(role)`
267
+ - `filter_role_routes` / `filter_authenticated_routes` / `filter_keeper_routes` / `filter_credential_gated_routes` (any `auth.credential_types` gate — superset of keeper) / `filter_routes_for_role(role)`
268
268
  - `filter_routes_by_prefix(prefix)` / `filter_routes_with_input` / `filter_routes_with_params` / `filter_routes_with_query` / `filter_mutation_routes` / `filter_rate_limited_routes`
269
- - `routes_by_auth_type(surface)` — `Map<RouteAuthCategory, Array<AppSurfaceRoute>>` where `RouteAuthCategory = 'none' | 'authenticated' | 'optional' | 'keeper' | 'role:<name>' | 'other'`. Multi-role specs appear under each role bucket
269
+ - `routes_by_auth_type(surface)` — `Map<RouteAuthCategory, Array<AppSurfaceRoute>>` where `RouteAuthCategory = 'none' | 'authenticated' | 'optional' | 'keeper' | 'role:<name>' | 'credential:<type>' | 'other'`. Multi-role / multi-channel specs appear under each bucket. Keeper is tested before the role bucket and the general credential bucket after it — keeper is an authority _level_ and names the route, a session gate is a _channel_ restriction over the authority the route already declares (so the audit SSE stream buckets as `role:admin`)
270
270
  - `format_route_key(route)` → `'METHOD /path'`
271
- - `surface_auth_summary(surface)` — counts per auth type, roles broken out by name
271
+ - `surface_auth_summary(surface)` — counts per auth type, roles and credential channels broken out by name; buckets exactly as `routes_by_auth_type`
272
272
 
273
273
  The per-route auth predicates these filters compose over (`is_public_auth`,
274
274
  `is_role_auth`, `is_credential_gated_auth`, `is_keeper_auth`,
@@ -189,16 +189,29 @@ export declare const is_role_auth: (auth: RouteAuth) => boolean;
189
189
  /** True iff the route declares any credential-type gate (`auth.credential_types?.length`). */
190
190
  export declare const is_credential_gated_auth: (auth: RouteAuth) => boolean;
191
191
  /**
192
- * True iff the route is the keeper bucket — credential gate restricted
193
- * to `daemon_token`. Keeper is the only credential gate today; if more
194
- * land, this filter widens. Knows the `'daemon_token'` literal directly
195
- * (the keeper composition is fuz_app's only registered credential gate).
192
+ * True iff the route is the keeper bucket — a credential gate admitting
193
+ * `daemon_token`. Knows the literal directly, because keeper is a named
194
+ * authority level rather than one channel among many: it is the ceiling
195
+ * sessions and api tokens cannot reach.
196
+ *
197
+ * Narrower than `is_credential_gated_auth`, which is true of every channel
198
+ * gate — including the session-only gates pointing the other way (the
199
+ * credential-minting actions, `POST /logout` / `POST /password`, the audit
200
+ * SSE stream). Reach for the general predicate when the question is "does a
201
+ * channel gate answer here"; reach for this one only when the question is
202
+ * about keeper specifically.
196
203
  */
197
204
  export declare const is_keeper_auth: (auth: RouteAuth) => boolean;
198
205
  /**
199
206
  * True iff the route is plain authenticated — `account === 'required'`
200
- * with no role gate and no credential gate. Account-grain authenticated
201
- * routes (logout, password change, account self-service) fall here.
207
+ * with no role gate and no credential gate: any credential the auth
208
+ * middleware resolved gets in.
209
+ *
210
+ * A channel gate takes a route out of this bucket, which is why the
211
+ * account-grain routes one might expect here are not — `POST /logout`,
212
+ * `POST /password`, and the `account_*` credential-lifecycle actions all
213
+ * declare `credential_types: ['session']` and answer to
214
+ * `is_credential_gated_auth` instead.
202
215
  */
203
216
  export declare const is_plain_authenticated_auth: (auth: RouteAuth) => boolean;
204
217
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"auth_shape.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/auth_shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,WAAW,6DAGtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,SAGtD,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa;;;;EAA2C,CAAC;AACtE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;IAMpB;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;;kBAyCF,CAAC;AACJ,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAUlD;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,SAAS,KAAG,OACA,CAAC;AAElD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,SAAS,KAAG,OAAgC,CAAC;AAE/E;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,SAAS,KAAG,OAAkC,CAAC;AAEnF,wEAAwE;AACxE,eAAO,MAAM,YAAY,GAAI,MAAM,SAAS,KAAG,OAA+B,CAAC;AAE/E,8FAA8F;AAC9F,eAAO,MAAM,wBAAwB,GAAI,MAAM,SAAS,KAAG,OAC3B,CAAC;AAEjC;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,SAAS,KAAG,OACQ,CAAC;AAE1D;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,GAAI,MAAM,SAAS,KAAG,OACwB,CAAC;AAYvF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAIhE,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,sCAAsC,GAClD,MAAM,SAAS,EACf,OAAO,WAAW,EAClB,SAAS,MAAM,KACb,IAmBF,CAAC"}
1
+ {"version":3,"file":"auth_shape.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/auth_shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,WAAW,6DAGtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,SAGtD,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa;;;;EAA2C,CAAC;AACtE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;IAMpB;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;;kBAyCF,CAAC;AACJ,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAUlD;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,SAAS,KAAG,OACA,CAAC;AAElD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,SAAS,KAAG,OAAgC,CAAC;AAE/E;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,SAAS,KAAG,OAAkC,CAAC;AAEnF,wEAAwE;AACxE,eAAO,MAAM,YAAY,GAAI,MAAM,SAAS,KAAG,OAA+B,CAAC;AAE/E,8FAA8F;AAC9F,eAAO,MAAM,wBAAwB,GAAI,MAAM,SAAS,KAAG,OAC3B,CAAC;AAEjC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,SAAS,KAAG,OACQ,CAAC;AAE1D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,2BAA2B,GAAI,MAAM,SAAS,KAAG,OACwB,CAAC;AAYvF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAIhE,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,sCAAsC,GAClD,MAAM,SAAS,EACf,OAAO,WAAW,EAClB,SAAS,MAAM,KACb,IAmBF,CAAC"}
@@ -224,16 +224,29 @@ export const is_role_auth = (auth) => !!auth.roles?.length;
224
224
  /** True iff the route declares any credential-type gate (`auth.credential_types?.length`). */
225
225
  export const is_credential_gated_auth = (auth) => !!auth.credential_types?.length;
226
226
  /**
227
- * True iff the route is the keeper bucket — credential gate restricted
228
- * to `daemon_token`. Keeper is the only credential gate today; if more
229
- * land, this filter widens. Knows the `'daemon_token'` literal directly
230
- * (the keeper composition is fuz_app's only registered credential gate).
227
+ * True iff the route is the keeper bucket — a credential gate admitting
228
+ * `daemon_token`. Knows the literal directly, because keeper is a named
229
+ * authority level rather than one channel among many: it is the ceiling
230
+ * sessions and api tokens cannot reach.
231
+ *
232
+ * Narrower than `is_credential_gated_auth`, which is true of every channel
233
+ * gate — including the session-only gates pointing the other way (the
234
+ * credential-minting actions, `POST /logout` / `POST /password`, the audit
235
+ * SSE stream). Reach for the general predicate when the question is "does a
236
+ * channel gate answer here"; reach for this one only when the question is
237
+ * about keeper specifically.
231
238
  */
232
239
  export const is_keeper_auth = (auth) => auth.credential_types?.includes('daemon_token') ?? false;
233
240
  /**
234
241
  * True iff the route is plain authenticated — `account === 'required'`
235
- * with no role gate and no credential gate. Account-grain authenticated
236
- * routes (logout, password change, account self-service) fall here.
242
+ * with no role gate and no credential gate: any credential the auth
243
+ * middleware resolved gets in.
244
+ *
245
+ * A channel gate takes a route out of this bucket, which is why the
246
+ * account-grain routes one might expect here are not — `POST /logout`,
247
+ * `POST /password`, and the `account_*` credential-lifecycle actions all
248
+ * declare `credential_types: ['session']` and answer to
249
+ * `is_credential_gated_auth` instead.
237
250
  */
238
251
  export const is_plain_authenticated_auth = (auth) => auth.account === 'required' && !is_role_auth(auth) && !is_credential_gated_auth(auth);
239
252
  // --- Registry-time invariant 2 enforcement ---
@@ -27,6 +27,17 @@ export declare const filter_role_routes: (surface: AppSurface) => Array<AppSurfa
27
27
  export declare const filter_authenticated_routes: (surface: AppSurface) => Array<AppSurfaceRoute>;
28
28
  /** Filter routes that require keeper credentials (`daemon_token`). */
29
29
  export declare const filter_keeper_routes: (surface: AppSurface) => Array<AppSurfaceRoute>;
30
+ /**
31
+ * Filter routes gated to a credential channel (any non-empty
32
+ * `auth.credential_types`), whichever channel that is.
33
+ *
34
+ * Superset of `filter_keeper_routes`: keeper is the `daemon_token` gate, and
35
+ * the same axis points the other way on the session-only routes (the
36
+ * credential-minting actions, `POST /logout` / `POST /password`, the audit
37
+ * SSE stream). Adversarial runners need the whole bucket, since every route
38
+ * in it answers `credential_type_required` to a channel it does not admit.
39
+ */
40
+ export declare const filter_credential_gated_routes: (surface: AppSurface) => Array<AppSurfaceRoute>;
30
41
  /** Filter routes whose `auth.roles` includes the named role. */
31
42
  export declare const filter_routes_for_role: (surface: AppSurface, role: string) => Array<AppSurfaceRoute>;
32
43
  /**
@@ -38,11 +49,22 @@ export declare const filter_routes_for_role: (surface: AppSurface, role: string)
38
49
  * - `'role:<name>'` for each role declared on `auth.roles` (multi-role specs
39
50
  * are emitted multiple times; callers that need single-bucket grouping
40
51
  * should pre-collapse)
52
+ * - `'credential:<type>'` for each channel a role-less route's
53
+ * `credential_types` admits — the session-only gates (`POST /logout`,
54
+ * `POST /password`, the `account_*` credential-lifecycle actions), plus any
55
+ * channel a consumer registers
41
56
  * - `'authenticated'` for `account === 'required'` without role / credential gate
42
57
  * - `'optional'` when either axis is `'optional'` and no other bucket fits
43
58
  * - `'other'` as a last-resort bucket for shapes that don't match above
59
+ *
60
+ * Keeper is tested before the role bucket and the general credential bucket
61
+ * after it, which is not an inconsistency: keeper is an authority *level* — a
62
+ * ceiling no session or api token reaches — so it names the route, while a
63
+ * session gate is a *channel* restriction layered over whatever authority the
64
+ * route already declares. So the audit SSE stream, gated to the session
65
+ * channel and to the admin role, buckets as `role:admin`.
44
66
  */
45
- export type RouteAuthCategory = 'none' | 'authenticated' | 'optional' | 'keeper' | `role:${string}` | 'other';
67
+ export type RouteAuthCategory = 'none' | 'authenticated' | 'optional' | 'keeper' | `role:${string}` | `credential:${string}` | 'other';
46
68
  /**
47
69
  * Group routes by auth category (see `RouteAuthCategory`). Multi-role specs
48
70
  * appear under each of their role buckets.
@@ -65,16 +87,19 @@ export declare const format_route_key: (route: AppSurfaceRoute) => string;
65
87
  /**
66
88
  * Summarize route auth distribution across the surface.
67
89
  *
68
- * Categorical view over the flat auth record. Multi-role specs
69
- * contribute one count per role they admit.
90
+ * Categorical view over the flat auth record, bucketing exactly as
91
+ * `routes_by_auth_type`. Multi-role and multi-channel specs contribute one
92
+ * count per role / channel they admit.
70
93
  *
71
- * @returns counts by auth category, with role counts broken out by role name
94
+ * @returns counts by auth category, with role and channel counts broken out
95
+ * by name
72
96
  */
73
97
  export declare const surface_auth_summary: (surface: AppSurface) => {
74
98
  none: number;
75
99
  authenticated: number;
76
100
  optional: number;
77
101
  role: Map<string, number>;
102
+ credential: Map<string, number>;
78
103
  keeper: number;
79
104
  other: number;
80
105
  };
@@ -1 +1 @@
1
- {"version":3,"file":"surface_query.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/surface_query.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAQhE,6DAA6D;AAC7D,eAAO,MAAM,uBAAuB,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CAC7B,CAAC;AAEvD,2EAA2E;AAC3E,eAAO,MAAM,oBAAoB,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CAC3B,CAAC;AAEtD,0EAA0E;AAC1E,eAAO,MAAM,kBAAkB,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CAC3B,CAAC;AAEpD;;;GAGG;AACH,eAAO,MAAM,2BAA2B,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CACrB,CAAC;AAEnE,sEAAsE;AACtE,eAAO,MAAM,oBAAoB,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CAC3B,CAAC;AAEtD,gEAAgE;AAChE,eAAO,MAAM,sBAAsB,GAAI,SAAS,UAAU,EAAE,MAAM,MAAM,KAAG,KAAK,CAAC,eAAe,CAC5B,CAAC;AAErE;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,iBAAiB,GAC5B,MAAM,GAAG,eAAe,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,MAAM,EAAE,GAAG,OAAO,CAAC;AAE/E;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAAI,SAAS,UAAU,KAAG,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,eAAe,CAAC,CAmC3F,CAAC;AAEF,qDAAqD;AACrD,eAAO,MAAM,uBAAuB,GACnC,SAAS,UAAU,EACnB,QAAQ,MAAM,KACZ,KAAK,CAAC,eAAe,CAA4D,CAAC;AAErF,uDAAuD;AACvD,eAAO,MAAM,wBAAwB,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CAC9B,CAAC;AAEvD,wDAAwD;AACxD,eAAO,MAAM,yBAAyB,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CAC9B,CAAC;AAExD,uDAAuD;AACvD,eAAO,MAAM,wBAAwB,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CAC9B,CAAC;AAEvD,mEAAmE;AACnE,eAAO,MAAM,sBAAsB,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CACtC,CAAC;AAE7C,gDAAgD;AAChD,eAAO,MAAM,0BAA0B,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CAC9B,CAAC;AAEzD,iEAAiE;AACjE,eAAO,MAAM,gBAAgB,GAAI,OAAO,eAAe,KAAG,MAAyC,CAAC;AAEpG;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAChC,SAAS,UAAU,KACjB;IACF,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CAqCd,CAAC"}
1
+ {"version":3,"file":"surface_query.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/surface_query.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAShE,6DAA6D;AAC7D,eAAO,MAAM,uBAAuB,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CAC7B,CAAC;AAEvD,2EAA2E;AAC3E,eAAO,MAAM,oBAAoB,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CAC3B,CAAC;AAEtD,0EAA0E;AAC1E,eAAO,MAAM,kBAAkB,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CAC3B,CAAC;AAEpD;;;GAGG;AACH,eAAO,MAAM,2BAA2B,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CACrB,CAAC;AAEnE,sEAAsE;AACtE,eAAO,MAAM,oBAAoB,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CAC3B,CAAC;AAEtD;;;;;;;;;GASG;AACH,eAAO,MAAM,8BAA8B,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CAC3B,CAAC;AAEhE,gEAAgE;AAChE,eAAO,MAAM,sBAAsB,GAAI,SAAS,UAAU,EAAE,MAAM,MAAM,KAAG,KAAK,CAAC,eAAe,CAC5B,CAAC;AAErE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,MAAM,iBAAiB,GAC1B,MAAM,GACN,eAAe,GACf,UAAU,GACV,QAAQ,GACR,QAAQ,MAAM,EAAE,GAChB,cAAc,MAAM,EAAE,GACtB,OAAO,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAAI,SAAS,UAAU,KAAG,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,eAAe,CAAC,CAwC3F,CAAC;AAEF,qDAAqD;AACrD,eAAO,MAAM,uBAAuB,GACnC,SAAS,UAAU,EACnB,QAAQ,MAAM,KACZ,KAAK,CAAC,eAAe,CAA4D,CAAC;AAErF,uDAAuD;AACvD,eAAO,MAAM,wBAAwB,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CAC9B,CAAC;AAEvD,wDAAwD;AACxD,eAAO,MAAM,yBAAyB,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CAC9B,CAAC;AAExD,uDAAuD;AACvD,eAAO,MAAM,wBAAwB,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CAC9B,CAAC;AAEvD,mEAAmE;AACnE,eAAO,MAAM,sBAAsB,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CACtC,CAAC;AAE7C,gDAAgD;AAChD,eAAO,MAAM,0BAA0B,GAAI,SAAS,UAAU,KAAG,KAAK,CAAC,eAAe,CAC9B,CAAC;AAEzD,iEAAiE;AACjE,eAAO,MAAM,gBAAgB,GAAI,OAAO,eAAe,KAAG,MAAyC,CAAC;AAEpG;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,GAChC,SAAS,UAAU,KACjB;IACF,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1B,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CA4Cd,CAAC"}
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * @module
15
15
  */
16
- import { is_keeper_auth, is_plain_authenticated_auth, is_public_auth, is_role_auth } from './auth_shape.js';
16
+ import { is_credential_gated_auth, is_keeper_auth, is_plain_authenticated_auth, is_public_auth, is_role_auth } from './auth_shape.js';
17
17
  /** Filter routes that require any form of authentication. */
18
18
  export const filter_protected_routes = (surface) => surface.routes.filter((r) => !is_public_auth(r.auth));
19
19
  /** Filter routes that are publicly accessible (no auth surface at all). */
@@ -27,6 +27,17 @@ export const filter_role_routes = (surface) => surface.routes.filter((r) => is_r
27
27
  export const filter_authenticated_routes = (surface) => surface.routes.filter((r) => is_plain_authenticated_auth(r.auth));
28
28
  /** Filter routes that require keeper credentials (`daemon_token`). */
29
29
  export const filter_keeper_routes = (surface) => surface.routes.filter((r) => is_keeper_auth(r.auth));
30
+ /**
31
+ * Filter routes gated to a credential channel (any non-empty
32
+ * `auth.credential_types`), whichever channel that is.
33
+ *
34
+ * Superset of `filter_keeper_routes`: keeper is the `daemon_token` gate, and
35
+ * the same axis points the other way on the session-only routes (the
36
+ * credential-minting actions, `POST /logout` / `POST /password`, the audit
37
+ * SSE stream). Adversarial runners need the whole bucket, since every route
38
+ * in it answers `credential_type_required` to a channel it does not admit.
39
+ */
40
+ export const filter_credential_gated_routes = (surface) => surface.routes.filter((r) => is_credential_gated_auth(r.auth));
30
41
  /** Filter routes whose `auth.roles` includes the named role. */
31
42
  export const filter_routes_for_role = (surface, role) => surface.routes.filter((r) => r.auth.roles?.includes(role) ?? false);
32
43
  /**
@@ -58,6 +69,11 @@ export const routes_by_auth_type = (surface) => {
58
69
  push(`role:${role}`, r);
59
70
  continue;
60
71
  }
72
+ if (is_credential_gated_auth(auth)) {
73
+ for (const credential_type of auth.credential_types)
74
+ push(`credential:${credential_type}`, r);
75
+ continue;
76
+ }
61
77
  if (is_plain_authenticated_auth(auth)) {
62
78
  push('authenticated', r);
63
79
  continue;
@@ -87,16 +103,19 @@ export const format_route_key = (route) => `${route.method} ${route.path}`;
87
103
  /**
88
104
  * Summarize route auth distribution across the surface.
89
105
  *
90
- * Categorical view over the flat auth record. Multi-role specs
91
- * contribute one count per role they admit.
106
+ * Categorical view over the flat auth record, bucketing exactly as
107
+ * `routes_by_auth_type`. Multi-role and multi-channel specs contribute one
108
+ * count per role / channel they admit.
92
109
  *
93
- * @returns counts by auth category, with role counts broken out by role name
110
+ * @returns counts by auth category, with role and channel counts broken out
111
+ * by name
94
112
  */
95
113
  export const surface_auth_summary = (surface) => {
96
114
  let none = 0;
97
115
  let authenticated = 0;
98
116
  let optional = 0;
99
117
  const role = new Map();
118
+ const credential = new Map();
100
119
  let keeper = 0;
101
120
  let other = 0;
102
121
  for (const r of surface.routes) {
@@ -115,6 +134,12 @@ export const surface_auth_summary = (surface) => {
115
134
  }
116
135
  continue;
117
136
  }
137
+ if (is_credential_gated_auth(auth)) {
138
+ for (const credential_type of auth.credential_types) {
139
+ credential.set(credential_type, (credential.get(credential_type) ?? 0) + 1);
140
+ }
141
+ continue;
142
+ }
118
143
  if (is_plain_authenticated_auth(auth)) {
119
144
  authenticated++;
120
145
  continue;
@@ -125,5 +150,5 @@ export const surface_auth_summary = (surface) => {
125
150
  }
126
151
  other++;
127
152
  }
128
- return { none, authenticated, optional, role, keeper, other };
153
+ return { none, authenticated, optional, role, credential, keeper, other };
129
154
  };
@@ -31,9 +31,15 @@ export declare const AUDIT_LOG_CHANNEL = "audit_log";
31
31
  * `session_revoke` closes only the stream tied to the specific revoked session
32
32
  * (matched by the blake3 session hash in `event.metadata.session_id`) — closing
33
33
  * all of a user's streams for a single-session revoke would be over-aggressive.
34
- * The single `token_revoke` the WS half handles is omitted: an SSE stream is
35
- * opened under a cookie session, never an API token, so no stream is keyed by a
36
- * single token id.
34
+ * The single `token_revoke` the WS half handles is omitted, and what makes that
35
+ * correct is a gate rather than an assumption: the audit route declares
36
+ * `credential_types: ['session']` (`auth/audit_log_route_schema.ts`), so a
37
+ * bearer is refused at the channel and no stream is ever keyed by a single
38
+ * token id. **A consumer that widens that gate must key its subscribers by
39
+ * api-token id and add `token_revoke` here** — otherwise a revoked token keeps
40
+ * receiving audit rows for the life of the connection. The Rust twin enforces
41
+ * the same pairing structurally, via the `RevocationScope` column on
42
+ * `AUDIT_EVENT_SPECS`.
37
43
  */
38
44
  export declare const disconnect_event_types: ReadonlySet<string>;
39
45
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"sse_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/realtime/sse_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,EAGN,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACrF,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAEtE,2DAA2D;AAC3D,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAE7C;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAOrD,CAAC;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,qBAAqB,GAAI,CAAC,EACtC,UAAU,kBAAkB,CAAC,CAAC,CAAC,EAC/B,eAAe,MAAM,GAAG,IAAI,EAC5B,KAAK,MAAM,KACT,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CA6CjC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,8FAA8F;IAC9F,SAAS,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,KAAK,MAAM,IAAI,CAAC;IAC1F,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC;IACZ,yJAAyJ;IACzJ,cAAc,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C,yEAAyE;IACzE,QAAQ,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;CAC9C;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,SAAS,CAOlD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS;IAC7C,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,KAAG,WAgBH,CAAC"}
1
+ {"version":3,"file":"sse_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/realtime/sse_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,EAGN,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACrF,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAEtE,2DAA2D;AAC3D,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAOrD,CAAC;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,qBAAqB,GAAI,CAAC,EACtC,UAAU,kBAAkB,CAAC,CAAC,CAAC,EAC/B,eAAe,MAAM,GAAG,IAAI,EAC5B,KAAK,MAAM,KACT,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CA6CjC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,8FAA8F;IAC9F,SAAS,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,KAAK,MAAM,IAAI,CAAC;IAC1F,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC;IACZ,yJAAyJ;IACzJ,cAAc,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C,yEAAyE;IACzE,QAAQ,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;CAC9C;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,SAAS,CAOlD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS;IAC7C,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,KAAG,WAgBH,CAAC"}
@@ -29,9 +29,15 @@ export const AUDIT_LOG_CHANNEL = 'audit_log';
29
29
  * `session_revoke` closes only the stream tied to the specific revoked session
30
30
  * (matched by the blake3 session hash in `event.metadata.session_id`) — closing
31
31
  * all of a user's streams for a single-session revoke would be over-aggressive.
32
- * The single `token_revoke` the WS half handles is omitted: an SSE stream is
33
- * opened under a cookie session, never an API token, so no stream is keyed by a
34
- * single token id.
32
+ * The single `token_revoke` the WS half handles is omitted, and what makes that
33
+ * correct is a gate rather than an assumption: the audit route declares
34
+ * `credential_types: ['session']` (`auth/audit_log_route_schema.ts`), so a
35
+ * bearer is refused at the channel and no stream is ever keyed by a single
36
+ * token id. **A consumer that widens that gate must key its subscribers by
37
+ * api-token id and add `token_revoke` here** — otherwise a revoked token keeps
38
+ * receiving audit rows for the life of the connection. The Rust twin enforces
39
+ * the same pairing structurally, via the `RevocationScope` column on
40
+ * `AUDIT_EVENT_SPECS`.
35
41
  */
36
42
  export const disconnect_event_types = new Set([
37
43
  'role_grant_revoke', // role revoked — user lost access
@@ -398,7 +398,7 @@ case per route):
398
398
 
399
399
  1. **attack surface snapshot** — `matches committed snapshot`, `is deterministic`.
400
400
  2. **attack surface structure** — `only expected public routes`, `full middleware stack on API routes`, `surface invariants`, `rpc/ws surface invariants`, `security policy`, `error schema tightness` (logs counts and asserts against `default_error_schema_tightness` by default; pass an override config or `null` via `error_schema_tightness`).
401
- 3. **adversarial HTTP auth enforcement** — `unauthenticated → 401`, `wrong role → 403` × roles, `authenticated without role → 403`, `keeper routes reject session credential → 403`, `correct auth passes guard`.
401
+ 3. **adversarial HTTP auth enforcement** — `unauthenticated → 401`, `wrong role → 403` × roles, `authenticated without role → 403`, `credential-gated routes reject a disallowed credential → 403`, `correct auth passes guard`. The role blocks cover role routes the session-credentialed role apps reach (a keeper route's channel gate answers first); the credential block probes every `auth.credential_types` route on the first builtin channel its allowlist omits, using `AuthTestApps.by_credential_type` (one keeper-role app per builtin channel; `keeper` is its `daemon_token` entry).
402
402
  4. **adversarial input validation** — delegated to `describe_adversarial_input`.
403
403
  5. **adversarial 404 response validation** — delegated to `describe_adversarial_404`.
404
404
 
@@ -21,8 +21,11 @@ export interface AdversarialTestOptions {
21
21
  *
22
22
  * Describe blocks:
23
23
  * - unauthenticated → 401 — every protected route
24
- * - wrong role → 403 — every role route, tested with all non-matching roles
25
- * - authenticated without role → 403 — every role route, no-role context
24
+ * - wrong role → 403 — every role route the role apps' session credential
25
+ * reaches, tested with all non-matching roles
26
+ * - authenticated without role → 403 — the same routes, no-role context
27
+ * - credential-gated routes reject a disallowed credential → 403 — every
28
+ * route declaring `auth.credential_types`, probed on a channel it refuses
26
29
  * - correct auth passes guard — every protected route, assert not 401/403
27
30
  */
28
31
  export declare const describe_adversarial_auth: (options: AdversarialTestOptions) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"attack_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/attack_surface.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAoB7B,OAAO,EAQN,KAAK,4BAA4B,EACjC,KAAK,2BAA2B,EAChC,MAAM,yBAAyB,CAAC;AAqBjC,OAAO,EAA6B,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAoDpF,oFAAoF;AACpF,MAAM,WAAW,sBAAsB;IACtC,+EAA+E;IAC/E,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,sBAAsB,KAAG,IAsI3E,CAAC;AAIF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,uCAAuC,GACnD,UAAU,2BAA2B,GAAG,IAAI,GAAG,SAAS,KACtD,2BAA2B,GAAG,IAWhC,CAAC;AAEF,0DAA0D;AAC1D,MAAM,WAAW,4BAA4B;IAC5C,+EAA+E;IAC/E,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,yDAAyD;IACzD,aAAa,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,sBAAsB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACtC,gHAAgH;IAChH,uBAAuB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACvC,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iEAAiE;IACjE,eAAe,CAAC,EAAE,4BAA4B,CAAC;IAC/C;;;;;;;;;;;OAWG;IACH,sBAAsB,CAAC,EAAE,2BAA2B,GAAG,IAAI,CAAC;IAC5D;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,sCAAsC,GAClD,SAAS,4BAA4B,KACnC,IA4EF,CAAC"}
1
+ {"version":3,"file":"attack_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/attack_surface.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAoB7B,OAAO,EAQN,KAAK,4BAA4B,EACjC,KAAK,2BAA2B,EAChC,MAAM,yBAAyB,CAAC;AAejC,OAAO,EAA6B,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAsDpF,oFAAoF;AACpF,MAAM,WAAW,sBAAsB;IACtC,+EAA+E;IAC/E,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,sBAAsB,KAAG,IA2J3E,CAAC;AAIF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,uCAAuC,GACnD,UAAU,2BAA2B,GAAG,IAAI,GAAG,SAAS,KACtD,2BAA2B,GAAG,IAWhC,CAAC;AAEF,0DAA0D;AAC1D,MAAM,WAAW,4BAA4B;IAC5C,+EAA+E;IAC/E,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,yDAAyD;IACzD,aAAa,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,sBAAsB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACtC,gHAAgH;IAChH,uBAAuB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACvC,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iEAAiE;IACjE,eAAe,CAAC,EAAE,4BAA4B,CAAC;IAC/C;;;;;;;;;;;OAWG;IACH,sBAAsB,CAAC,EAAE,2BAA2B,GAAG,IAAI,CAAC;IAC5D;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,sCAAsC,GAClD,SAAS,4BAA4B,KACnC,IA4EF,CAAC"}
@@ -18,12 +18,14 @@ import { test, assert, describe } from 'vitest';
18
18
  import { assert_surface_invariants, assert_rpc_ws_surface_invariants, assert_surface_security_policy, audit_error_schema_tightness, assert_error_schema_tightness, default_error_schema_tightness, fuz_app_stock_route_tightness_allowlist } from './surface_invariants.js';
19
19
  import { describe_adversarial_input } from './adversarial_input.js';
20
20
  import { describe_adversarial_404 } from './adversarial_404.js';
21
- import { create_test_app_from_specs, create_test_request_context, create_auth_test_apps, create_route_skip_filter, select_auth_app } from './auth_apps.js';
21
+ import { create_auth_test_apps, create_route_skip_filter, select_auth_app } from './auth_apps.js';
22
22
  import { resolve_valid_path } from './schema_generators.js';
23
23
  import { assert_surface_matches_snapshot, assert_surface_deterministic, assert_only_expected_public_routes, assert_full_middleware_stack, assert_error_schema_valid } from './assertions.js';
24
24
  import { merge_error_schemas } from '../http/schema_helpers.js';
25
25
  import { collect_middleware_errors } from '../http/surface.js';
26
- import { filter_protected_routes, filter_role_routes, filter_keeper_routes } from '../http/surface_query.js';
26
+ import { filter_protected_routes, filter_role_routes, filter_credential_gated_routes } from '../http/surface_query.js';
27
+ import { CREDENTIAL_TYPES } from '../hono_context.js';
28
+ import { CREDENTIAL_TYPE_SESSION } from '../auth/credential_type_schema.js';
27
29
  import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INSUFFICIENT_PERMISSIONS, ERROR_CREDENTIAL_TYPE_REQUIRED } from '../http/error_schemas.js';
28
30
  // --- Adversarial test runner ---
29
31
  /**
@@ -64,8 +66,11 @@ const build_route_spec_lookup = (route_specs) => {
64
66
  *
65
67
  * Describe blocks:
66
68
  * - unauthenticated → 401 — every protected route
67
- * - wrong role → 403 — every role route, tested with all non-matching roles
68
- * - authenticated without role → 403 — every role route, no-role context
69
+ * - wrong role → 403 — every role route the role apps' session credential
70
+ * reaches, tested with all non-matching roles
71
+ * - authenticated without role → 403 — the same routes, no-role context
72
+ * - credential-gated routes reject a disallowed credential → 403 — every
73
+ * route declaring `auth.credential_types`, probed on a channel it refuses
69
74
  * - correct auth passes guard — every protected route, assert not 401/403
70
75
  */
71
76
  export const describe_adversarial_auth = (options) => {
@@ -76,7 +81,7 @@ export const describe_adversarial_auth = (options) => {
76
81
  if (protected_routes.length === 0)
77
82
  return;
78
83
  const role_routes = filter_role_routes(surface).filter((r) => !is_skipped(r));
79
- const keeper_routes = filter_keeper_routes(surface).filter((r) => !is_skipped(r));
84
+ const credential_gated_routes = filter_credential_gated_routes(surface).filter((r) => !is_skipped(r));
80
85
  // merged error schemas (auto-derived + middleware + handler-specific) for response validation
81
86
  const error_schema_lookup = build_error_schema_lookup(route_specs, middleware_specs);
82
87
  // The route's own spec, so a path param with a declared format (a
@@ -100,10 +105,17 @@ export const describe_adversarial_auth = (options) => {
100
105
  });
101
106
  }
102
107
  });
103
- // Role-only routes (no credential gate). Keeper routes have a credential
104
- // gate that fires before the role gate, so they get tested separately
105
- // in the keeper block below.
106
- const role_only_routes = role_routes.filter((r) => !(r.auth.credential_types?.length ?? 0));
108
+ // Role routes the role apps can actually reach. Those apps carry a
109
+ // session credential, so a route gated to another channel (keeper) is
110
+ // refused by the pre-authorization credential gate before its role gate
111
+ // ever runs — probing it here would assert `insufficient_permissions`
112
+ // and see `credential_type_required`. A route gated *to* the session
113
+ // channel (the audit SSE stream) stays in: its role gate is reachable,
114
+ // and dropping it would silently retire this coverage the day a route
115
+ // gained a session gate. Either way the credential block below probes
116
+ // the gate itself.
117
+ const role_only_routes = role_routes.filter((r) => !r.auth.credential_types?.length ||
118
+ r.auth.credential_types.includes(CREDENTIAL_TYPE_SESSION));
107
119
  if (role_only_routes.length > 0) {
108
120
  describe('wrong role → 403', () => {
109
121
  for (const route of role_only_routes) {
@@ -141,19 +153,35 @@ export const describe_adversarial_auth = (options) => {
141
153
  }
142
154
  });
143
155
  }
144
- if (keeper_routes.length > 0) {
145
- describe('keeper routes reject session credential → 403', () => {
146
- // keeper role via session cookie should fail (wrong credential type)
147
- const app_session_keeper = create_test_app_from_specs(route_specs, create_test_request_context('keeper'), 'session');
148
- for (const route of keeper_routes) {
149
- test(`${route.method} ${route.path}`, async () => {
150
- const res = await app_session_keeper.request(test_path(route), {
156
+ if (credential_gated_routes.length > 0) {
157
+ describe('credential-gated routes reject a disallowed credential → 403', () => {
158
+ // Each route is probed on the first channel its own allowlist
159
+ // omits, in `CREDENTIAL_TYPES` order: a session for keeper's
160
+ // `daemon_token` gate, an api token for the session-only routes —
161
+ // which is the bearer channel those gates exist to refuse. The
162
+ // channel apps hold the keeper role, so no route's role gate can
163
+ // be what answers; it could not be anyway, the credential gate
164
+ // being pre-authorization.
165
+ for (const route of credential_gated_routes) {
166
+ const admitted = route.auth.credential_types ?? [];
167
+ const refused = CREDENTIAL_TYPES.find((t) => !admitted.includes(t));
168
+ if (!refused) {
169
+ // A gate listing every builtin channel refuses none of them, so
170
+ // there is no probe to make. Reported as a skip rather than
171
+ // dropped silently: a route that reads as gated but is not is
172
+ // worth seeing in the run, and a `continue` here would hide
173
+ // exactly the shape this block exists to surface.
174
+ test.skip(`${route.method} ${route.path} (gate admits every builtin channel — nothing to refuse)`, () => { });
175
+ continue;
176
+ }
177
+ test(`${route.method} ${route.path} (${refused})`, async () => {
178
+ const res = await apps.by_credential_type.get(refused).request(test_path(route), {
151
179
  method: route.method
152
180
  });
153
181
  assert.strictEqual(res.status, 403, `${route.method} ${route.path}`);
154
182
  const body = await res.json();
155
183
  assert.strictEqual(body.error, ERROR_CREDENTIAL_TYPE_REQUIRED);
156
- assert.deepStrictEqual(body.required_credential_types, route.auth.credential_types ?? []);
184
+ assert.deepStrictEqual(body.required_credential_types, admitted);
157
185
  assert_error_schema_valid(error_schema_lookup, route, 403, body);
158
186
  });
159
187
  }
@@ -29,6 +29,16 @@ export interface AuthTestApps {
29
29
  public: Hono;
30
30
  authed: Hono;
31
31
  keeper: Hono;
32
+ /**
33
+ * One authenticated app per builtin credential type, each holding the
34
+ * keeper role — the channel axis, where `by_role` is the authority axis.
35
+ *
36
+ * A credential gate is pre-authorization, so probing it needs an app on a
37
+ * channel the route refuses, and proving a gated route still works needs
38
+ * one on a channel it admits. `keeper` is the `'daemon_token'` entry of
39
+ * this map, not a second app.
40
+ */
41
+ by_credential_type: Map<CredentialType, Hono>;
32
42
  by_role: Map<string, Hono>;
33
43
  }
34
44
  /**
@@ -41,9 +51,24 @@ export declare const create_auth_test_apps: (route_specs: Array<RouteSpec>, role
41
51
  /**
42
52
  * Select the Hono test app with correct auth for a route.
43
53
  *
54
+ * The credential channel is decided first, because the channel gate is
55
+ * pre-authorization: an app whose credential the route refuses never reaches
56
+ * the role gate, so a role-correct app on the wrong channel answers
57
+ * `credential_type_required`, and a "correct auth passes guard" case reads it
58
+ * as a failure of the role gate that never ran.
59
+ *
60
+ * A gate admitting `'session'` falls through to the `by_role` /
61
+ * `authed` apps, which carry that channel. Any other gate leaves the
62
+ * `by_credential_type` app as the only candidate — those hold the keeper
63
+ * role, which is why a route pairing a non-session channel with some *other*
64
+ * role has no app and says so.
65
+ *
44
66
  * @throws Error if `auth.roles` names a role not present in `apps.by_role` —
45
67
  * surfaces a missing entry in the `roles` array passed to
46
68
  * `create_auth_test_apps`.
69
+ * @throws Error if the route pairs a non-session channel with a role the
70
+ * channel apps don't hold. Silently handing back a session app would turn
71
+ * every case on the route into a misattributed 403.
47
72
  */
48
73
  export declare const select_auth_app: (apps: AuthTestApps, auth: RouteAuth) => Hono;
49
74
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"auth_apps.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/auth_apps.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;GAOG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAEvE,OAAO,EAGN,KAAK,cAAc,EACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAIN,KAAK,cAAc,EACnB,MAAM,oBAAoB,CAAC;AAI5B;;GAEG;AACH,eAAO,MAAM,2BAA2B,GAAI,OAAO,MAAM,KAAG,cAI1D,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,GACtC,aAAa,KAAK,CAAC,SAAS,CAAC,EAC7B,WAAW,cAAc,EACzB,kBAAkB,cAAc,KAC9B,IAuBF,CAAC;AAEF,sFAAsF;AACtF,MAAM,WAAW,YAAY;IAC5B,MAAM,EAAE,IAAI,CAAC;IACb,MAAM,EAAE,IAAI,CAAC;IACb,MAAM,EAAE,IAAI,CAAC;IACb,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;CAC3B;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GACjC,aAAa,KAAK,CAAC,SAAS,CAAC,EAC7B,OAAO,KAAK,CAAC,MAAM,CAAC,KAClB,YAeF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,YAAY,EAAE,MAAM,SAAS,KAAG,IAarE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GACpC,aAAa,KAAK,CAAC,MAAM,CAAC,GAAG,SAAS,KACpC,CAAC,CAAC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,KAAK,OAAO,CAGvD,CAAC"}
1
+ {"version":3,"file":"auth_apps.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/auth_apps.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;GAOG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAOvE,OAAO,EAGN,KAAK,cAAc,EACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAKN,KAAK,cAAc,EACnB,MAAM,oBAAoB,CAAC;AAI5B;;GAEG;AACH,eAAO,MAAM,2BAA2B,GAAI,OAAO,MAAM,KAAG,cAI1D,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,GACtC,aAAa,KAAK,CAAC,SAAS,CAAC,EAC7B,WAAW,cAAc,EACzB,kBAAkB,cAAc,KAC9B,IAuBF,CAAC;AAEF,sFAAsF;AACtF,MAAM,WAAW,YAAY;IAC5B,MAAM,EAAE,IAAI,CAAC;IACb,MAAM,EAAE,IAAI,CAAC;IACb,MAAM,EAAE,IAAI,CAAC;IACb;;;;;;;;OAQG;IACH,kBAAkB,EAAE,GAAG,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC;IAC9C,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;CAC3B;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GACjC,aAAa,KAAK,CAAC,SAAS,CAAC,EAC7B,OAAO,KAAK,CAAC,MAAM,CAAC,KAClB,YAsBF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,YAAY,EAAE,MAAM,SAAS,KAAG,IAwBrE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GACpC,aAAa,KAAK,CAAC,MAAM,CAAC,GAAG,SAAS,KACpC,CAAC,CAAC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,KAAK,OAAO,CAGvD,CAAC"}