@fuzdev/fuz_app 0.54.0 → 0.55.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/actions/CLAUDE.md +68 -13
- package/dist/actions/action_codegen.d.ts +13 -0
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +15 -1
- package/dist/actions/action_rpc.d.ts +60 -7
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +158 -44
- package/dist/actions/register_action_ws.d.ts +4 -4
- package/dist/actions/register_action_ws.js +6 -6
- package/dist/actions/register_ws_endpoint.d.ts +20 -7
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +30 -5
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +0 -4
- package/dist/auth/CLAUDE.md +219 -66
- package/dist/auth/account_actions.d.ts +6 -6
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +8 -11
- package/dist/auth/account_queries.d.ts +6 -3
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +14 -5
- package/dist/auth/account_routes.d.ts +7 -10
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +70 -23
- package/dist/auth/account_schema.d.ts +19 -0
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +20 -0
- package/dist/auth/admin_action_specs.d.ts +45 -11
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +23 -8
- package/dist/auth/admin_actions.d.ts +8 -7
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +11 -18
- package/dist/auth/audit_log_queries.d.ts +53 -14
- package/dist/auth/audit_log_queries.d.ts.map +1 -1
- package/dist/auth/audit_log_queries.js +45 -2
- package/dist/auth/audit_log_schema.d.ts +55 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +19 -3
- package/dist/auth/bearer_auth.d.ts +9 -7
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +13 -21
- package/dist/auth/cleanup.d.ts.map +1 -1
- package/dist/auth/cleanup.js +5 -0
- package/dist/auth/daemon_token_middleware.d.ts +23 -11
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +26 -20
- package/dist/auth/deps.d.ts +14 -0
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +4 -2
- package/dist/auth/migrations.d.ts +15 -7
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +15 -7
- package/dist/auth/permit_offer_action_specs.d.ts +45 -6
- package/dist/auth/permit_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/permit_offer_action_specs.js +38 -7
- package/dist/auth/permit_offer_actions.d.ts +2 -2
- package/dist/auth/permit_offer_actions.d.ts.map +1 -1
- package/dist/auth/permit_offer_actions.js +98 -90
- package/dist/auth/permit_offer_notifications.d.ts +10 -0
- package/dist/auth/permit_offer_notifications.d.ts.map +1 -1
- package/dist/auth/permit_offer_queries.d.ts +68 -9
- package/dist/auth/permit_offer_queries.d.ts.map +1 -1
- package/dist/auth/permit_offer_queries.js +147 -35
- package/dist/auth/permit_offer_schema.d.ts +23 -1
- package/dist/auth/permit_offer_schema.d.ts.map +1 -1
- package/dist/auth/permit_offer_schema.js +5 -0
- package/dist/auth/permit_queries.d.ts +17 -5
- package/dist/auth/permit_queries.d.ts.map +1 -1
- package/dist/auth/permit_queries.js +19 -8
- package/dist/auth/request_context.d.ts +321 -38
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +393 -66
- package/dist/auth/route_guards.d.ts +10 -4
- package/dist/auth/route_guards.d.ts.map +1 -1
- package/dist/auth/route_guards.js +14 -8
- package/dist/auth/self_service_role_action_specs.d.ts +2 -0
- package/dist/auth/self_service_role_action_specs.d.ts.map +1 -1
- package/dist/auth/self_service_role_action_specs.js +2 -0
- package/dist/auth/self_service_role_actions.d.ts +6 -5
- package/dist/auth/self_service_role_actions.d.ts.map +1 -1
- package/dist/auth/self_service_role_actions.js +18 -8
- package/dist/db/migrate.d.ts +11 -7
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +9 -6
- package/dist/dev/setup.d.ts.map +1 -1
- package/dist/dev/setup.js +5 -3
- package/dist/hono_context.d.ts +77 -0
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/hono_context.js +50 -0
- package/dist/http/CLAUDE.md +80 -17
- package/dist/http/error_schemas.d.ts +92 -1
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +73 -16
- package/dist/http/jsonrpc_errors.d.ts +27 -2
- package/dist/http/jsonrpc_errors.d.ts.map +1 -1
- package/dist/http/jsonrpc_errors.js +26 -2
- package/dist/http/route_spec.d.ts +62 -4
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/route_spec.js +117 -21
- package/dist/http/schema_helpers.d.ts +13 -1
- package/dist/http/schema_helpers.d.ts.map +1 -1
- package/dist/http/schema_helpers.js +21 -2
- package/dist/http/surface.d.ts +10 -1
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +2 -2
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +11 -1
- package/dist/testing/CLAUDE.md +23 -17
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +15 -13
- package/dist/testing/adversarial_headers.js +1 -1
- package/dist/testing/app_server.js +2 -2
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +21 -7
- package/dist/testing/auth_apps.d.ts.map +1 -1
- package/dist/testing/auth_apps.js +6 -3
- package/dist/testing/entities.d.ts +2 -1
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +1 -0
- package/dist/testing/integration_helpers.d.ts +4 -2
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +9 -5
- package/dist/testing/middleware.d.ts +12 -8
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +67 -25
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +3 -1
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +5 -1
- package/dist/ui/CLAUDE.md +16 -10
- package/dist/ui/PermitOfferForm.svelte +14 -0
- package/dist/ui/PermitOfferForm.svelte.d.ts +6 -0
- package/dist/ui/PermitOfferForm.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.d.ts +8 -1
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +14 -3
- package/dist/ui/permit_offers_state.svelte.d.ts +9 -1
- package/dist/ui/permit_offers_state.svelte.d.ts.map +1 -1
- package/dist/ui/permit_offers_state.svelte.js +7 -1
- package/package.json +1 -1
|
@@ -1,22 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Request context middleware and permit checking helpers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* for every authenticated request. Downstream handlers check
|
|
6
|
-
* permits, never flags.
|
|
4
|
+
* Two-phase identity resolution:
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* 1. **Authentication (middleware)** — `create_request_context_middleware`,
|
|
7
|
+
* `bearer_auth`, and `daemon_token_middleware` validate the credential
|
|
8
|
+
* (session cookie, bearer token, daemon token) and set `c.var.account_id`
|
|
9
|
+
* + `c.var.credential_type` on the Hono context. They do not resolve
|
|
10
|
+
* an acting actor or load permits; `REQUEST_CONTEXT_KEY` stays null at
|
|
11
|
+
* this stage, so account-grain identity is the only thing known.
|
|
12
|
+
* 2. **Authorization (route-spec wrapper / RPC dispatcher)** — after input
|
|
13
|
+
* validation, the per-route layer inspects the route. If the input
|
|
14
|
+
* schema declared `acting?: ActingActor` (reference equality with the
|
|
15
|
+
* canonical `ActingActor` schema) or the auth requires permits
|
|
16
|
+
* (`role` / `keeper`), `apply_authorization_phase` resolves the actor
|
|
17
|
+
* against `c.var.account_id` plus the validated `acting` value via
|
|
18
|
+
* `resolve_acting_actor`, builds the `{account, actor, permits}`
|
|
19
|
+
* context via `build_request_context`, and sets it on
|
|
20
|
+
* `REQUEST_CONTEXT_KEY` before auth guards fire. Authenticated routes
|
|
21
|
+
* that don't need an actor still get an account-only context via
|
|
22
|
+
* `build_account_context` so handler signatures stay uniform.
|
|
23
|
+
*
|
|
24
|
+
* Account-grain operations (logout, password_change, account_verify,
|
|
25
|
+
* etc.) declare neither `acting` nor permit-requiring auth, so no actor
|
|
26
|
+
* is resolved and their handlers see a `RequestContext` with
|
|
27
|
+
* `actor: null` + empty `permits`. They never trigger `actor_required`,
|
|
28
|
+
* which is what makes multi-actor logout work without first picking a
|
|
29
|
+
* persona.
|
|
30
|
+
*
|
|
31
|
+
* `build_request_context` loads `account → actor → permits` and verifies
|
|
32
|
+
* the `actor.account_id === account.id` binding. `refresh_permits`
|
|
33
|
+
* reloads permits on an existing context.
|
|
11
34
|
*
|
|
12
35
|
* @module
|
|
13
36
|
*/
|
|
14
|
-
import {
|
|
37
|
+
import { z } from 'zod';
|
|
38
|
+
import { zod_unwrap_to_object } from '@fuzdev/fuz_util/zod.js';
|
|
39
|
+
import { ActingActor, is_permit_active, } from './account_schema.js';
|
|
15
40
|
import { hash_session_token, session_touch_fire_and_forget, query_session_get_valid, } from './session_queries.js';
|
|
16
|
-
import {
|
|
41
|
+
import { query_account_by_id, query_actor_by_id, query_actors_by_account, } from './account_queries.js';
|
|
17
42
|
import { query_permit_find_active_for_actor } from './permit_queries.js';
|
|
18
|
-
import { AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY } from '../hono_context.js';
|
|
19
|
-
import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INSUFFICIENT_PERMISSIONS, } from '../http/error_schemas.js';
|
|
43
|
+
import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CACHED_REQUEST_BODY_KEY, CREDENTIAL_TYPE_KEY, TEST_CONTEXT_PRESET_KEY, } from '../hono_context.js';
|
|
44
|
+
import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INSUFFICIENT_PERMISSIONS, ERROR_ACTOR_REQUIRED, ERROR_ACTOR_NOT_ON_ACCOUNT, ERROR_NO_ACTORS_ON_ACCOUNT, ERROR_ACCOUNT_VANISHED, } from '../http/error_schemas.js';
|
|
20
45
|
/** Hono context variable name for the request context. */
|
|
21
46
|
export const REQUEST_CONTEXT_KEY = 'request_context';
|
|
22
47
|
/**
|
|
@@ -41,18 +66,51 @@ export const get_request_context = (c) => {
|
|
|
41
66
|
/**
|
|
42
67
|
* Get the request context, throwing if unauthenticated.
|
|
43
68
|
*
|
|
44
|
-
* Use in route handlers where
|
|
45
|
-
* (i.e., routes with `auth: {type: 'authenticated'}` or
|
|
46
|
-
* Prefer this over `get_request_context(c)!` for explicit error
|
|
69
|
+
* Use in route handlers where the dispatcher's authorization phase guarantees
|
|
70
|
+
* a context exists (i.e., routes with `auth: {type: 'authenticated'}` or
|
|
71
|
+
* stricter). Prefer this over `get_request_context(c)!` for explicit error
|
|
72
|
+
* handling.
|
|
47
73
|
*
|
|
48
74
|
* @param c - the Hono context
|
|
49
75
|
* @returns the request context (never null)
|
|
50
|
-
* @throws Error if no request context is set (
|
|
76
|
+
* @throws Error if no request context is set (dispatcher misconfiguration)
|
|
51
77
|
*/
|
|
52
78
|
export const require_request_context = (c) => {
|
|
53
79
|
const ctx = get_request_context(c);
|
|
54
80
|
if (!ctx) {
|
|
55
|
-
throw new Error('require_request_context: no request context — is
|
|
81
|
+
throw new Error('require_request_context: no request context — is the dispatcher authorization phase wired?');
|
|
82
|
+
}
|
|
83
|
+
return ctx;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Narrow `RequestContext | null` to a non-null context (auth invariant).
|
|
87
|
+
*
|
|
88
|
+
* Use in RPC action handlers whose spec is non-public — the dispatcher's
|
|
89
|
+
* pre-validation auth gate has already short-circuited unauthenticated
|
|
90
|
+
* callers, so `ctx.auth` is non-null by the time the handler runs.
|
|
91
|
+
*
|
|
92
|
+
* @throws Error when called from a public-auth handler (programmer error)
|
|
93
|
+
*/
|
|
94
|
+
export const require_request_auth = (auth) => {
|
|
95
|
+
if (!auth) {
|
|
96
|
+
throw new Error('require_request_auth: no auth — is this handler bound to a non-public action spec?');
|
|
97
|
+
}
|
|
98
|
+
return auth;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Narrow `RequestContext | null` to `RequestActorContext` (actor invariant).
|
|
102
|
+
*
|
|
103
|
+
* Use in RPC action handlers whose spec declares `auth: 'keeper' | {role}`
|
|
104
|
+
* or whose input declares `acting?: ActingActor` — the dispatcher's
|
|
105
|
+
* authorization phase resolves an actor before the handler runs. Replaces
|
|
106
|
+
* the `ctx.auth!.actor!.id` chain that the type system can't otherwise see.
|
|
107
|
+
*
|
|
108
|
+
* @throws Error when the handler runs without actor resolution (programmer error)
|
|
109
|
+
*/
|
|
110
|
+
export const require_request_actor = (auth) => {
|
|
111
|
+
const ctx = require_request_auth(auth);
|
|
112
|
+
if (!ctx.actor) {
|
|
113
|
+
throw new Error('require_request_actor: no actor — is this handler bound to an actor-implying spec (keeper/role) or one whose input declares `acting`?');
|
|
56
114
|
}
|
|
57
115
|
return ctx;
|
|
58
116
|
};
|
|
@@ -75,16 +133,19 @@ export const has_role = (ctx, role, now = new Date()) => ctx?.permits.some((p) =
|
|
|
75
133
|
* Whether the request context holds an active permit for `role` at `scope_id`.
|
|
76
134
|
*
|
|
77
135
|
* Walks the in-memory `ctx.permits` snapshot loaded once per request by
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
136
|
+
* the route-spec / RPC dispatcher's authorization phase (when the route
|
|
137
|
+
* declares `acting?: ActingActor` or has permit-requiring auth); zero DB
|
|
138
|
+
* roundtrip per check. The "freshness" framing of a SQL re-query is
|
|
139
|
+
* illusory because the race window is between predicate and the actual
|
|
140
|
+
* mutation, not predicate and authorization load. Closing that race needs
|
|
141
|
+
* a transactional re-check inside the UPDATE/INSERT, which neither style
|
|
142
|
+
* provides.
|
|
83
143
|
*
|
|
84
|
-
* Null-tolerant — `null` ctx (unauthenticated)
|
|
144
|
+
* Null-tolerant — `null` ctx (unauthenticated) and account-grain
|
|
145
|
+
* contexts (`actor: null`, empty `permits`) both return `false`. Same
|
|
85
146
|
* convention as `has_role`; lets the helper drop into `auth: 'public'`
|
|
86
|
-
* handlers without a manual narrow. See `cell_authorize`
|
|
87
|
-
* resource-side analog.
|
|
147
|
+
* or account-grain handlers without a manual narrow. See `cell_authorize`
|
|
148
|
+
* for the resource-side analog.
|
|
88
149
|
*
|
|
89
150
|
* `scope_id` semantics: in-memory `permit.scope_id` is `string | null`, so
|
|
90
151
|
* JS `===` matches the SQL `IS NOT DISTINCT FROM` semantics exactly:
|
|
@@ -123,55 +184,85 @@ export const has_any_scoped_role = (ctx, roles, scope_id, now = new Date()) => {
|
|
|
123
184
|
return ctx.permits.some((p) => roles.includes(p.role) && p.scope_id === scope_id && is_permit_active(p, now));
|
|
124
185
|
};
|
|
125
186
|
/**
|
|
126
|
-
*
|
|
187
|
+
* Resolve the acting actor for an authenticated request.
|
|
188
|
+
*
|
|
189
|
+
* Called from the route-spec / RPC dispatcher's authorization phase
|
|
190
|
+
* with the authenticated account id and the validated `acting` value
|
|
191
|
+
* (from the request payload). Applies the uniform resolution rules:
|
|
192
|
+
*
|
|
193
|
+
* - `acting_actor_id` omitted + 1 actor → use it.
|
|
194
|
+
* - `acting_actor_id` omitted + 0 actors → `no_actors` (defensive —
|
|
195
|
+
* signup / bootstrap always create an actor in the same tx, so this
|
|
196
|
+
* is a server error).
|
|
197
|
+
* - `acting_actor_id` omitted + multiple actors → `actor_required` with
|
|
198
|
+
* the available list so the client can prompt; never pick silently.
|
|
199
|
+
* - `acting_actor_id` present + matches an actor on the account → use it.
|
|
200
|
+
* - `acting_actor_id` present + does not match → `actor_not_on_account`.
|
|
201
|
+
* The available list is intentionally not echoed in this branch (treat
|
|
202
|
+
* as opaque rejection).
|
|
203
|
+
*
|
|
204
|
+
* @param deps - query dependencies
|
|
205
|
+
* @param account_id - the authenticated account
|
|
206
|
+
* @param acting_actor_id - the requested acting actor id, or `undefined`
|
|
207
|
+
*/
|
|
208
|
+
export const resolve_acting_actor = async (deps, account_id, acting_actor_id) => {
|
|
209
|
+
const actors = await query_actors_by_account(deps, account_id);
|
|
210
|
+
if (actors.length === 0)
|
|
211
|
+
return { ok: false, reason: 'no_actors' };
|
|
212
|
+
if (acting_actor_id == null) {
|
|
213
|
+
if (actors.length === 1)
|
|
214
|
+
return { ok: true, actor_id: actors[0].id };
|
|
215
|
+
return {
|
|
216
|
+
ok: false,
|
|
217
|
+
reason: 'actor_required',
|
|
218
|
+
available: actors.map((a) => ({ id: a.id, name: a.name })),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const match = actors.find((a) => a.id === acting_actor_id);
|
|
222
|
+
if (!match)
|
|
223
|
+
return { ok: false, reason: 'actor_not_on_account' };
|
|
224
|
+
return { ok: true, actor_id: match.id };
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* Create middleware that authenticates the account from a session cookie.
|
|
127
228
|
*
|
|
128
|
-
* Reads the session identity (set by session middleware), looks up
|
|
129
|
-
*
|
|
130
|
-
*
|
|
229
|
+
* Reads the session identity (set by session middleware), looks up the
|
|
230
|
+
* `auth_session`, and on a valid session sets `c.var.auth_account_id`,
|
|
231
|
+
* `CREDENTIAL_TYPE_KEY = 'session'`, and `AUTH_SESSION_TOKEN_HASH_KEY`.
|
|
232
|
+
* Touches the session (fire-and-forget). Does not load actor or permits;
|
|
233
|
+
* `REQUEST_CONTEXT_KEY` is left null — the route-spec / RPC dispatcher
|
|
234
|
+
* authorization phase resolves the acting actor and builds the full
|
|
235
|
+
* `RequestContext` when the route needs one.
|
|
131
236
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* `require_role` or `require_auth` for enforcement.
|
|
237
|
+
* Invalid / missing session leaves all keys null and calls `next()` —
|
|
238
|
+
* `require_auth` / `require_role` enforce.
|
|
135
239
|
*
|
|
136
240
|
* @param deps - query dependencies (pool-level db for middleware)
|
|
137
241
|
* @param log - the logger instance
|
|
138
242
|
* @param session_context_key - the Hono context key where session middleware stored the session token
|
|
139
|
-
* @mutates Hono context - sets `
|
|
243
|
+
* @mutates Hono context - sets `ACCOUNT_ID_KEY`, `CREDENTIAL_TYPE_KEY`, `AUTH_SESSION_TOKEN_HASH_KEY`, and `AUTH_API_TOKEN_ID_KEY`
|
|
140
244
|
*/
|
|
141
245
|
export const create_request_context_middleware = (deps, log, session_context_key = 'auth_session_id') => {
|
|
142
246
|
return async (c, next) => {
|
|
247
|
+
c.set(REQUEST_CONTEXT_KEY, null);
|
|
248
|
+
c.set(ACCOUNT_ID_KEY, null);
|
|
249
|
+
c.set(CREDENTIAL_TYPE_KEY, null);
|
|
250
|
+
c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
|
|
251
|
+
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
143
252
|
const session_token = c.get(session_context_key) ?? null;
|
|
144
253
|
if (!session_token) {
|
|
145
|
-
c.set(REQUEST_CONTEXT_KEY, null);
|
|
146
|
-
c.set(CREDENTIAL_TYPE_KEY, null);
|
|
147
|
-
c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
|
|
148
|
-
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
149
254
|
await next();
|
|
150
255
|
return;
|
|
151
256
|
}
|
|
152
257
|
const token_hash = hash_session_token(session_token);
|
|
153
258
|
const session = await query_session_get_valid(deps, token_hash);
|
|
154
259
|
if (!session) {
|
|
155
|
-
c.set(REQUEST_CONTEXT_KEY, null);
|
|
156
|
-
c.set(CREDENTIAL_TYPE_KEY, null);
|
|
157
|
-
c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
|
|
158
|
-
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
159
|
-
await next();
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
const ctx = await build_request_context(deps, session.account_id);
|
|
163
|
-
if (!ctx) {
|
|
164
|
-
c.set(REQUEST_CONTEXT_KEY, null);
|
|
165
|
-
c.set(CREDENTIAL_TYPE_KEY, null);
|
|
166
|
-
c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
|
|
167
|
-
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
168
260
|
await next();
|
|
169
261
|
return;
|
|
170
262
|
}
|
|
171
|
-
c.set(
|
|
263
|
+
c.set(ACCOUNT_ID_KEY, session.account_id);
|
|
172
264
|
c.set(CREDENTIAL_TYPE_KEY, 'session');
|
|
173
265
|
c.set(AUTH_SESSION_TOKEN_HASH_KEY, token_hash);
|
|
174
|
-
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
175
266
|
// Touch session (fire-and-forget, don't block the request)
|
|
176
267
|
void session_touch_fire_and_forget(deps, token_hash, c.var.pending_effects, log);
|
|
177
268
|
await next();
|
|
@@ -180,11 +271,10 @@ export const create_request_context_middleware = (deps, log, session_context_key
|
|
|
180
271
|
/**
|
|
181
272
|
* Middleware that requires authentication.
|
|
182
273
|
*
|
|
183
|
-
* Returns 401 if
|
|
274
|
+
* Returns 401 if the auth middleware did not set `c.var.auth_account_id`.
|
|
184
275
|
*/
|
|
185
276
|
export const require_auth = async (c, next) => {
|
|
186
|
-
|
|
187
|
-
if (!ctx) {
|
|
277
|
+
if (c.get(ACCOUNT_ID_KEY) == null) {
|
|
188
278
|
return c.json({ error: ERROR_AUTHENTICATION_REQUIRED }, 401);
|
|
189
279
|
}
|
|
190
280
|
await next();
|
|
@@ -192,17 +282,20 @@ export const require_auth = async (c, next) => {
|
|
|
192
282
|
/**
|
|
193
283
|
* Create middleware that requires a specific role.
|
|
194
284
|
*
|
|
195
|
-
* Returns 401 if unauthenticated, 403 if the role is missing.
|
|
285
|
+
* Returns 401 if unauthenticated, 403 if the role is missing. Reads
|
|
286
|
+
* `REQUEST_CONTEXT_KEY` because role-gated routes always run the
|
|
287
|
+
* dispatcher's authorization phase before this guard (the phase sets the
|
|
288
|
+
* actor-bound `RequestContext`).
|
|
196
289
|
*
|
|
197
290
|
* @param role - the required role
|
|
198
291
|
*/
|
|
199
292
|
export const require_role = (role) => {
|
|
200
293
|
return async (c, next) => {
|
|
201
|
-
|
|
202
|
-
if (!ctx) {
|
|
294
|
+
if (c.get(ACCOUNT_ID_KEY) == null) {
|
|
203
295
|
return c.json({ error: ERROR_AUTHENTICATION_REQUIRED }, 401);
|
|
204
296
|
}
|
|
205
|
-
|
|
297
|
+
const ctx = get_request_context(c);
|
|
298
|
+
if (!ctx || !has_role(ctx, role)) {
|
|
206
299
|
return c.json({ error: ERROR_INSUFFICIENT_PERMISSIONS, required_role: role }, 403);
|
|
207
300
|
}
|
|
208
301
|
await next();
|
|
@@ -216,35 +309,269 @@ export const require_role = (role) => {
|
|
|
216
309
|
* or after receiving a revocation signal.
|
|
217
310
|
*
|
|
218
311
|
* Returns a new `RequestContext` with updated permits — the original
|
|
219
|
-
* context is not mutated, making concurrent calls safe.
|
|
312
|
+
* context is not mutated, making concurrent calls safe. Throws when
|
|
313
|
+
* `ctx.actor` is null; account-grain contexts have no permits to refresh.
|
|
220
314
|
*
|
|
221
315
|
* @param ctx - the request context to refresh
|
|
222
316
|
* @param deps - query dependencies
|
|
223
317
|
* @returns a new `RequestContext` with fresh permits
|
|
318
|
+
* @throws Error when called on an account-grain context (`actor: null`)
|
|
224
319
|
*/
|
|
225
320
|
export const refresh_permits = async (ctx, deps) => {
|
|
321
|
+
if (!ctx.actor) {
|
|
322
|
+
throw new Error('refresh_permits: account-grain context has no actor / permits to refresh');
|
|
323
|
+
}
|
|
226
324
|
const permits = await query_permit_find_active_for_actor(deps, ctx.actor.id);
|
|
227
325
|
return { ...ctx, permits };
|
|
228
326
|
};
|
|
229
327
|
/**
|
|
230
|
-
* Build a full `RequestContext` from an account id
|
|
328
|
+
* Build a full `RequestContext` from an account id and an explicit
|
|
329
|
+
* actor id (already resolved via `resolve_acting_actor`).
|
|
330
|
+
*
|
|
331
|
+
* Loads `account` + the named `actor` + the actor's active permits.
|
|
332
|
+
* Verifies the `actor.account_id === account.id` binding so downstream
|
|
333
|
+
* handlers can trust `ctx.actor.account_id === ctx.account.id`. Returns
|
|
334
|
+
* `null` when the account is missing, the actor is missing, or the
|
|
335
|
+
* actor doesn't belong to the supplied account.
|
|
231
336
|
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* the account or actor is not found.
|
|
337
|
+
* Called by the route-spec / RPC dispatcher's authorization phase for
|
|
338
|
+
* routes that need an acting actor; account-grain routes use
|
|
339
|
+
* `build_account_context` instead.
|
|
236
340
|
*
|
|
237
341
|
* @param deps - query dependencies
|
|
238
342
|
* @param account_id - the account to build context for
|
|
239
|
-
* @
|
|
343
|
+
* @param actor_id - the actor this request acts as
|
|
344
|
+
* @returns a request context, or `null` if account/actor not found or mismatched
|
|
240
345
|
*/
|
|
241
|
-
export const build_request_context = async (deps, account_id) => {
|
|
346
|
+
export const build_request_context = async (deps, account_id, actor_id) => {
|
|
242
347
|
const account = await query_account_by_id(deps, account_id);
|
|
243
348
|
if (!account)
|
|
244
349
|
return null;
|
|
245
|
-
const actor = await
|
|
350
|
+
const actor = await query_actor_by_id(deps, actor_id);
|
|
246
351
|
if (!actor)
|
|
247
352
|
return null;
|
|
353
|
+
if (actor.account_id !== account.id)
|
|
354
|
+
return null;
|
|
248
355
|
const permits = await query_permit_find_active_for_actor(deps, actor.id);
|
|
249
356
|
return { account, actor, permits };
|
|
250
357
|
};
|
|
358
|
+
/**
|
|
359
|
+
* Build an account-only `RequestContext` (no actor, no permits) from
|
|
360
|
+
* an account id.
|
|
361
|
+
*
|
|
362
|
+
* Used by the dispatcher's authorization phase for authenticated routes
|
|
363
|
+
* that don't need an acting actor — account-grain operations (logout,
|
|
364
|
+
* password change, account self-service). Lets handlers read
|
|
365
|
+
* `auth.account.id` / `auth.account.username` uniformly with permit-bound
|
|
366
|
+
* routes; the cost is one extra `query_account_by_id` per request.
|
|
367
|
+
*
|
|
368
|
+
* Returns `null` when the account row is missing (e.g. deleted between
|
|
369
|
+
* the auth middleware's session lookup and the dispatcher) — caller
|
|
370
|
+
* surfaces that as a 500 since it represents a torn read.
|
|
371
|
+
*
|
|
372
|
+
* @param deps - query dependencies
|
|
373
|
+
* @param account_id - the account to build context for
|
|
374
|
+
* @returns an account-only request context, or `null` if the account is missing
|
|
375
|
+
*/
|
|
376
|
+
export const build_account_context = async (deps, account_id) => {
|
|
377
|
+
const account = await query_account_by_id(deps, account_id);
|
|
378
|
+
if (!account)
|
|
379
|
+
return null;
|
|
380
|
+
return { account, actor: null, permits: [] };
|
|
381
|
+
};
|
|
382
|
+
/**
|
|
383
|
+
* Whether the supplied auth descriptor implies an acting actor must be
|
|
384
|
+
* resolved (i.e., permit-requiring auth: `'role'` or `'keeper'`).
|
|
385
|
+
*
|
|
386
|
+
* The dispatcher's authorization phase uses this to decide whether to
|
|
387
|
+
* walk the actor list when the input schema doesn't already declare
|
|
388
|
+
* `acting?: ActingActor`. Accepts either auth shape — the route-spec
|
|
389
|
+
* `RouteAuth` (`{type: 'role' | 'keeper' | ...}`) or the action-spec
|
|
390
|
+
* `ActionAuth` (`'keeper' | {role}`) — so HTTP and RPC dispatchers share
|
|
391
|
+
* one source of truth for the "permit-bound" rule.
|
|
392
|
+
*/
|
|
393
|
+
export const is_actor_implying_auth = (auth) => {
|
|
394
|
+
if (typeof auth === 'string')
|
|
395
|
+
return auth === 'keeper';
|
|
396
|
+
if ('type' in auth)
|
|
397
|
+
return auth.type === 'role' || auth.type === 'keeper';
|
|
398
|
+
return 'role' in auth;
|
|
399
|
+
};
|
|
400
|
+
/**
|
|
401
|
+
* Whether an input schema declares the canonical `acting?: ActingActor`
|
|
402
|
+
* field. Reference-equality on the exported `ActingActor` schema —
|
|
403
|
+
* consumer schemas with unrelated `acting` fields don't trip this check.
|
|
404
|
+
*
|
|
405
|
+
* Peels through Zod wrappers (`optional`, `nullable`, `default`,
|
|
406
|
+
* `transform`, `pipe`, `prefault`) via `zod_unwrap_to_object` so a spec
|
|
407
|
+
* authored as `z.optional(z.strictObject({acting: ActingActor}))` or
|
|
408
|
+
* `z.strictObject({acting: ActingActor}).default({})` still trips the
|
|
409
|
+
* predicate. The wrapper-tolerant lookup is defense-in-depth — the
|
|
410
|
+
* canonical shape is the un-wrapped `z.strictObject({acting: ActingActor})`,
|
|
411
|
+
* but variant B in `~/dev/grimoire/lore/fuz_app/TODO_PUBLIC_AUTH_PHASE.md`
|
|
412
|
+
* makes this predicate authorization-correctness load-bearing for
|
|
413
|
+
* `auth: 'public'` actions, so missing a wrapper-bound declaration
|
|
414
|
+
* would silently skip actor resolution. The reference-equality check
|
|
415
|
+
* on `ActingActor` keeps consumer schemas with unrelated `acting`
|
|
416
|
+
* fields from tripping the predicate even after the wrapper peel.
|
|
417
|
+
*
|
|
418
|
+
* The dispatcher's authorization phase uses this to decide whether to
|
|
419
|
+
* pull the actor id from validated input (so multi-actor users can pick
|
|
420
|
+
* a persona on actor-needing routes).
|
|
421
|
+
*/
|
|
422
|
+
export const input_schema_declares_acting = (schema) => {
|
|
423
|
+
const obj = zod_unwrap_to_object(schema);
|
|
424
|
+
if (!obj)
|
|
425
|
+
return false;
|
|
426
|
+
return obj.shape.acting === ActingActor;
|
|
427
|
+
};
|
|
428
|
+
/**
|
|
429
|
+
* Apply the dispatcher's authorization phase. Shared by the route-spec
|
|
430
|
+
* wrapper and the RPC dispatcher.
|
|
431
|
+
*
|
|
432
|
+
* - When `c.var.auth_account_id` is `null`, returns `void` so the
|
|
433
|
+
* downstream auth guard can fire 401 (less-helpful than `actor_required`
|
|
434
|
+
* for the unauthenticated case).
|
|
435
|
+
* - When `needs_actor` is true, resolves the actor against the account
|
|
436
|
+
* plus the supplied `acting` value, then builds the full
|
|
437
|
+
* `{account, actor, permits}` context.
|
|
438
|
+
* - When `needs_actor` is false, builds an account-only context so
|
|
439
|
+
* handler signatures stay uniform across the surface.
|
|
440
|
+
*
|
|
441
|
+
* On resolution failure returns an `AuthorizationFailure` (`{status, body}`)
|
|
442
|
+
* the caller wraps in a transport-appropriate response. Three 500 branches
|
|
443
|
+
* are kept distinct so the wire shape names what actually went wrong:
|
|
444
|
+
*
|
|
445
|
+
* - 500 `ERROR_NO_ACTORS_ON_ACCOUNT` — `resolve_acting_actor` returned
|
|
446
|
+
* `no_actors`. The actor enumeration succeeded and came back empty;
|
|
447
|
+
* signup / bootstrap should have created one in the same transaction,
|
|
448
|
+
* so this is a real corruption signal.
|
|
449
|
+
* - 500 `ERROR_ACCOUNT_VANISHED` — `build_request_context` /
|
|
450
|
+
* `build_account_context` returned null after a successful
|
|
451
|
+
* `resolve_acting_actor`. The account or actor row was deleted between
|
|
452
|
+
* the credential check and authorization (torn read race), or — in
|
|
453
|
+
* the `build_request_context` actor↔account mismatch sub-branch — the
|
|
454
|
+
* binding flipped under us. Reachability of the mismatch sub-branch in
|
|
455
|
+
* production is essentially zero (`resolve_acting_actor` already
|
|
456
|
+
* verified the actor was on this account, and `actor.account_id` only
|
|
457
|
+
* changes via row-level edits no production path makes), so collapsing
|
|
458
|
+
* that case into the torn-read shape costs nothing.
|
|
459
|
+
*
|
|
460
|
+
* Other failure paths: 400 `ERROR_ACTOR_REQUIRED` / `ERROR_ACTOR_NOT_ON_ACCOUNT`.
|
|
461
|
+
* Returns `undefined` on success.
|
|
462
|
+
*
|
|
463
|
+
* @mutates Hono context - sets `REQUEST_CONTEXT_KEY` on success
|
|
464
|
+
*/
|
|
465
|
+
export const apply_authorization_phase = async (deps, c, needs_actor, acting_value) => {
|
|
466
|
+
// Test escape hatch: when a harness pre-populates `REQUEST_CONTEXT_KEY`
|
|
467
|
+
// it must also flag `TEST_CONTEXT_PRESET_KEY = true` (set by
|
|
468
|
+
// `create_test_app_from_specs` / `create_fake_hono_context` / per-test
|
|
469
|
+
// middleware). Production middleware never sets this flag, so future
|
|
470
|
+
// production code that consults `REQUEST_CONTEXT_KEY` cannot silently
|
|
471
|
+
// bypass the live build the way an implicit presence probe would.
|
|
472
|
+
if (c.get(TEST_CONTEXT_PRESET_KEY))
|
|
473
|
+
return;
|
|
474
|
+
const account_id = c.get(ACCOUNT_ID_KEY) ?? null;
|
|
475
|
+
if (account_id == null)
|
|
476
|
+
return; // auth guard handles 401
|
|
477
|
+
if (needs_actor) {
|
|
478
|
+
const acting = await resolve_acting_actor(deps, account_id, acting_value);
|
|
479
|
+
if (!acting.ok) {
|
|
480
|
+
if (acting.reason === 'actor_required') {
|
|
481
|
+
return {
|
|
482
|
+
status: 400,
|
|
483
|
+
body: { error: ERROR_ACTOR_REQUIRED, available: acting.available },
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
if (acting.reason === 'actor_not_on_account') {
|
|
487
|
+
return { status: 400, body: { error: ERROR_ACTOR_NOT_ON_ACCOUNT } };
|
|
488
|
+
}
|
|
489
|
+
return { status: 500, body: { error: ERROR_NO_ACTORS_ON_ACCOUNT } };
|
|
490
|
+
}
|
|
491
|
+
const ctx = await build_request_context(deps, account_id, acting.actor_id);
|
|
492
|
+
if (!ctx)
|
|
493
|
+
return { status: 500, body: { error: ERROR_ACCOUNT_VANISHED } };
|
|
494
|
+
c.set(REQUEST_CONTEXT_KEY, ctx);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const ctx = await build_account_context(deps, account_id);
|
|
498
|
+
if (!ctx)
|
|
499
|
+
return { status: 500, body: { error: ERROR_ACCOUNT_VANISHED } };
|
|
500
|
+
c.set(REQUEST_CONTEXT_KEY, ctx);
|
|
501
|
+
};
|
|
502
|
+
/**
|
|
503
|
+
* Create the route-spec authorization handler used by `apply_route_specs`.
|
|
504
|
+
*
|
|
505
|
+
* Decides whether the route needs actor resolution from `spec.auth` plus
|
|
506
|
+
* `spec.input` introspection, extracts the raw `acting` value (string
|
|
507
|
+
* typeguard, no schema validation), and delegates to
|
|
508
|
+
* `apply_authorization_phase`. Public routes (`auth.type === 'none'`) skip
|
|
509
|
+
* the phase entirely; their handlers see no `RequestContext`.
|
|
510
|
+
*
|
|
511
|
+
* Authorization runs before input validation (matches the RPC dispatcher's
|
|
512
|
+
* order). For GET routes `acting` comes from the URL query string; for
|
|
513
|
+
* mutating methods it comes from a pre-parse of the JSON body. The pre-
|
|
514
|
+
* parse result lands on `c.var.cached_request_body` so the subsequent
|
|
515
|
+
* `create_input_validation` step reads the parsed value from there
|
|
516
|
+
* without re-running `JSON.parse` — explicit cache, independent of
|
|
517
|
+
* Hono's internal `bodyCache` behavior. A malformed body fails the
|
|
518
|
+
* pre-parse silently (`acting` treated as undefined, cache flagged
|
|
519
|
+
* `{ok: false}`) and is then rejected with `ERROR_INVALID_JSON_BODY`
|
|
520
|
+
* by the input-validation step that reads the failure flag — producing
|
|
521
|
+
* the same final response as if the validation step had parsed first.
|
|
522
|
+
*/
|
|
523
|
+
export const create_fuz_authorization_handler = (deps) => {
|
|
524
|
+
return async (c, spec) => {
|
|
525
|
+
if (spec.auth.type === 'none')
|
|
526
|
+
return;
|
|
527
|
+
const declares_acting = input_schema_declares_acting(spec.input);
|
|
528
|
+
const needs_actor = is_actor_implying_auth(spec.auth) || declares_acting;
|
|
529
|
+
let acting_value;
|
|
530
|
+
if (declares_acting) {
|
|
531
|
+
const raw_acting = await read_raw_acting(c, spec.method);
|
|
532
|
+
acting_value = typeof raw_acting === 'string' ? raw_acting : undefined;
|
|
533
|
+
}
|
|
534
|
+
const failure = await apply_authorization_phase(deps, c, needs_actor, acting_value);
|
|
535
|
+
if (!failure)
|
|
536
|
+
return;
|
|
537
|
+
return c.json(failure.body, failure.status);
|
|
538
|
+
};
|
|
539
|
+
};
|
|
540
|
+
/**
|
|
541
|
+
* Extract the raw `acting` value from a request before input validation
|
|
542
|
+
* has run. Returns `undefined` on parse failure or non-object body; the
|
|
543
|
+
* downstream input-validation step then rejects malformed bodies with
|
|
544
|
+
* `ERROR_INVALID_JSON_BODY`.
|
|
545
|
+
*
|
|
546
|
+
* Writes the parse result to `c.var.cached_request_body` so the
|
|
547
|
+
* input-validation step does not re-run `JSON.parse` on the same Hono-
|
|
548
|
+
* cached body text. Hono's internal `bodyCache` keeps the body text
|
|
549
|
+
* alive across multiple `c.req.json()` calls, but each call still
|
|
550
|
+
* re-parses — caching the parsed value here decouples our pipeline
|
|
551
|
+
* from that undocumented detail (and saves the second parse).
|
|
552
|
+
*
|
|
553
|
+
* Three cache states:
|
|
554
|
+
*
|
|
555
|
+
* - GET (early return) — no cache write; the input-validation step is
|
|
556
|
+
* a no-op for GET so nothing reads the cache anyway.
|
|
557
|
+
* - Successful parse (any JSON value) — `{ok: true, body}`. The
|
|
558
|
+
* input-validation step reads `body` and runs the non-object check
|
|
559
|
+
* itself.
|
|
560
|
+
* - Parse failure — `{ok: false}`. The input-validation step short-
|
|
561
|
+
* circuits with `ERROR_INVALID_JSON_BODY` without re-parsing.
|
|
562
|
+
*/
|
|
563
|
+
const read_raw_acting = async (c, method) => {
|
|
564
|
+
if (method === 'GET')
|
|
565
|
+
return c.req.query('acting');
|
|
566
|
+
try {
|
|
567
|
+
const body = await c.req.json();
|
|
568
|
+
c.set(CACHED_REQUEST_BODY_KEY, { ok: true, body });
|
|
569
|
+
if (typeof body === 'object' && body !== null && !Array.isArray(body)) {
|
|
570
|
+
return body.acting;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
c.set(CACHED_REQUEST_BODY_KEY, { ok: false });
|
|
575
|
+
}
|
|
576
|
+
return undefined;
|
|
577
|
+
};
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth guard resolver for the route spec system.
|
|
3
3
|
*
|
|
4
|
-
* Maps `RouteAuth` discriminants to auth middleware
|
|
4
|
+
* Maps `RouteAuth` discriminants to two-phase auth middleware sets.
|
|
5
|
+
* `pre_validation` carries the 401 check (`require_auth`) so
|
|
6
|
+
* unauthenticated callers never see route-shape information from input
|
|
7
|
+
* parse failures. `post_authorization` carries the 403 role / keeper
|
|
8
|
+
* checks because they read the `RequestContext` populated by the
|
|
9
|
+
* dispatcher's authorization phase.
|
|
10
|
+
*
|
|
5
11
|
* Injected into `apply_route_specs` to decouple the generic HTTP
|
|
6
12
|
* framework (`http/route_spec.ts`) from auth-specific middleware.
|
|
7
13
|
*
|
|
@@ -13,9 +19,9 @@ import type { AuthGuardResolver } from '../http/route_spec.js';
|
|
|
13
19
|
*
|
|
14
20
|
* Maps `RouteAuth` to middleware:
|
|
15
21
|
* - `none` → no guards
|
|
16
|
-
* - `authenticated` → `require_auth`
|
|
17
|
-
* - `role` → `require_role(role)`
|
|
18
|
-
* - `keeper` → `require_keeper`
|
|
22
|
+
* - `authenticated` → pre-validation `require_auth`
|
|
23
|
+
* - `role` → pre-validation `require_auth` + post-authorization `require_role(role)`
|
|
24
|
+
* - `keeper` → pre-validation `require_auth` + post-authorization `require_keeper`
|
|
19
25
|
*/
|
|
20
26
|
export declare const fuz_auth_guard_resolver: AuthGuardResolver;
|
|
21
27
|
//# sourceMappingURL=route_guards.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route_guards.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/route_guards.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"route_guards.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/route_guards.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,uBAAuB,CAAC;AAE7D;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,EAAE,iBAWrC,CAAC"}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth guard resolver for the route spec system.
|
|
3
3
|
*
|
|
4
|
-
* Maps `RouteAuth` discriminants to auth middleware
|
|
4
|
+
* Maps `RouteAuth` discriminants to two-phase auth middleware sets.
|
|
5
|
+
* `pre_validation` carries the 401 check (`require_auth`) so
|
|
6
|
+
* unauthenticated callers never see route-shape information from input
|
|
7
|
+
* parse failures. `post_authorization` carries the 403 role / keeper
|
|
8
|
+
* checks because they read the `RequestContext` populated by the
|
|
9
|
+
* dispatcher's authorization phase.
|
|
10
|
+
*
|
|
5
11
|
* Injected into `apply_route_specs` to decouple the generic HTTP
|
|
6
12
|
* framework (`http/route_spec.ts`) from auth-specific middleware.
|
|
7
13
|
*
|
|
@@ -14,19 +20,19 @@ import { require_keeper } from './require_keeper.js';
|
|
|
14
20
|
*
|
|
15
21
|
* Maps `RouteAuth` to middleware:
|
|
16
22
|
* - `none` → no guards
|
|
17
|
-
* - `authenticated` → `require_auth`
|
|
18
|
-
* - `role` → `require_role(role)`
|
|
19
|
-
* - `keeper` → `require_keeper`
|
|
23
|
+
* - `authenticated` → pre-validation `require_auth`
|
|
24
|
+
* - `role` → pre-validation `require_auth` + post-authorization `require_role(role)`
|
|
25
|
+
* - `keeper` → pre-validation `require_auth` + post-authorization `require_keeper`
|
|
20
26
|
*/
|
|
21
27
|
export const fuz_auth_guard_resolver = (auth) => {
|
|
22
28
|
switch (auth.type) {
|
|
23
29
|
case 'none':
|
|
24
|
-
return [];
|
|
30
|
+
return { pre_validation: [], post_authorization: [] };
|
|
25
31
|
case 'authenticated':
|
|
26
|
-
return [require_auth];
|
|
32
|
+
return { pre_validation: [require_auth], post_authorization: [] };
|
|
27
33
|
case 'role':
|
|
28
|
-
return [require_role(auth.role)];
|
|
34
|
+
return { pre_validation: [require_auth], post_authorization: [require_role(auth.role)] };
|
|
29
35
|
case 'keeper':
|
|
30
|
-
return [require_keeper];
|
|
36
|
+
return { pre_validation: [require_auth], post_authorization: [require_keeper] };
|
|
31
37
|
}
|
|
32
38
|
};
|
|
@@ -15,6 +15,7 @@ export declare const ERROR_ROLE_NOT_SELF_SERVICE_ELIGIBLE: "role_not_self_servic
|
|
|
15
15
|
export declare const SelfServiceRoleSetInput: z.ZodObject<{
|
|
16
16
|
role: z.ZodString;
|
|
17
17
|
enabled: z.ZodBoolean;
|
|
18
|
+
acting: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
18
19
|
}, z.core.$strict>;
|
|
19
20
|
export type SelfServiceRoleSetInput = z.infer<typeof SelfServiceRoleSetInput>;
|
|
20
21
|
/**
|
|
@@ -37,6 +38,7 @@ export declare const self_service_role_set_action_spec: {
|
|
|
37
38
|
input: z.ZodObject<{
|
|
38
39
|
role: z.ZodString;
|
|
39
40
|
enabled: z.ZodBoolean;
|
|
41
|
+
acting: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
40
42
|
}, z.core.$strict>;
|
|
41
43
|
output: z.ZodObject<{
|
|
42
44
|
ok: z.ZodLiteral<true>;
|