@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.
- package/dist/auth/CLAUDE.md +6 -1
- package/dist/auth/audit_log_route_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_route_schema.js +12 -1
- package/dist/auth/role_grant_offer_action_specs.d.ts +2 -1
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +2 -1
- package/dist/db/fact_disk_storage.js +1 -1
- package/dist/http/CLAUDE.md +3 -3
- package/dist/http/auth_shape.d.ts +19 -6
- package/dist/http/auth_shape.d.ts.map +1 -1
- package/dist/http/auth_shape.js +19 -6
- package/dist/http/surface_query.d.ts +29 -4
- package/dist/http/surface_query.d.ts.map +1 -1
- package/dist/http/surface_query.js +30 -5
- package/dist/realtime/sse_auth_guard.d.ts +9 -3
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +9 -3
- package/dist/testing/CLAUDE.md +1 -1
- package/dist/testing/attack_surface.d.ts +5 -2
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +45 -17
- package/dist/testing/auth_apps.d.ts +25 -0
- package/dist/testing/auth_apps.d.ts.map +1 -1
- package/dist/testing/auth_apps.js +34 -5
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +3 -2
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_scope_surface.js +36 -7
- package/dist/testing/surface_invariants.d.ts +7 -1
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/surface_invariants.js +11 -5
- package/dist/ui/AdminAccounts.svelte +6 -4
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminInvites.svelte +3 -3
- package/dist/ui/AdminInvites.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +10 -10
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/AdminRoleGrantHistory.svelte +7 -7
- package/dist/ui/AdminRoleGrantHistory.svelte.d.ts.map +1 -1
- package/dist/ui/AdminSettings.svelte +1 -1
- package/dist/ui/RoleGrantOfferInbox.svelte +4 -1
- package/dist/ui/RoleGrantOfferInbox.svelte.d.ts.map +1 -1
- package/dist/ui/SurfaceExplorer.svelte +33 -17
- package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
- package/package.json +10 -10
package/dist/auth/CLAUDE.md
CHANGED
|
@@ -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,
|
|
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`
|
|
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":"AAAA
|
|
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`
|
|
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/
|
|
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';
|
package/dist/http/CLAUDE.md
CHANGED
|
@@ -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
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
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
|
|
201
|
-
*
|
|
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
|
|
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"}
|
package/dist/http/auth_shape.js
CHANGED
|
@@ -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
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
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
|
|
236
|
-
*
|
|
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
|
|
69
|
-
*
|
|
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
|
|
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;
|
|
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
|
|
91
|
-
*
|
|
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
|
|
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
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
|
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
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -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`, `
|
|
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
|
|
25
|
-
*
|
|
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;
|
|
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 {
|
|
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,
|
|
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
|
|
68
|
-
*
|
|
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
|
|
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
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
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 (
|
|
145
|
-
describe('
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
for
|
|
149
|
-
|
|
150
|
-
|
|
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,
|
|
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;
|
|
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"}
|