@fuzdev/fuz_app 0.53.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 +230 -63
- 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 +106 -95
- 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 +360 -32
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +442 -60
- 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 +32 -19
- 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/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +12 -0
- 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
|
};
|
|
@@ -60,63 +118,151 @@ export const require_request_context = (c) => {
|
|
|
60
118
|
* Check if a request context has an active permit for a given role.
|
|
61
119
|
*
|
|
62
120
|
* Checks the permits already loaded in the context (no DB query).
|
|
121
|
+
* Null-tolerant — `null` ctx (unauthenticated) returns `false`. Symmetric
|
|
122
|
+
* with `has_scoped_role` / `has_any_scoped_role` so the three helpers
|
|
123
|
+
* compose freely in the same predicate (e.g.
|
|
124
|
+
* `has_role(auth, ADMIN) || has_scoped_role(auth, role, scope)`).
|
|
63
125
|
*
|
|
64
|
-
* @param ctx - the request context
|
|
126
|
+
* @param ctx - the request context, or `null` for unauthenticated callers
|
|
65
127
|
* @param role - the role to check
|
|
66
128
|
* @param now - current time (defaults to `new Date()`, pass for testability and hot-path efficiency)
|
|
67
129
|
* @returns `true` if the actor has an active permit for the role
|
|
68
130
|
*/
|
|
69
|
-
export const has_role = (ctx, role, now = new Date()) => ctx
|
|
131
|
+
export const has_role = (ctx, role, now = new Date()) => ctx?.permits.some((p) => p.role === role && is_permit_active(p, now)) ?? false;
|
|
132
|
+
/**
|
|
133
|
+
* Whether the request context holds an active permit for `role` at `scope_id`.
|
|
134
|
+
*
|
|
135
|
+
* Walks the in-memory `ctx.permits` snapshot loaded once per request by
|
|
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.
|
|
143
|
+
*
|
|
144
|
+
* Null-tolerant — `null` ctx (unauthenticated) and account-grain
|
|
145
|
+
* contexts (`actor: null`, empty `permits`) both return `false`. Same
|
|
146
|
+
* convention as `has_role`; lets the helper drop into `auth: 'public'`
|
|
147
|
+
* or account-grain handlers without a manual narrow. See `cell_authorize`
|
|
148
|
+
* for the resource-side analog.
|
|
149
|
+
*
|
|
150
|
+
* `scope_id` semantics: in-memory `permit.scope_id` is `string | null`, so
|
|
151
|
+
* JS `===` matches the SQL `IS NOT DISTINCT FROM` semantics exactly:
|
|
152
|
+
*
|
|
153
|
+
* - `scope_id === null` matches global permits (`scope_id IS NULL`).
|
|
154
|
+
* - `scope_id === '<uuid>'` matches permits bound to that exact scope.
|
|
155
|
+
*
|
|
156
|
+
* @param ctx - the request context, or `null` for unauthenticated callers
|
|
157
|
+
* @param role - the role to check
|
|
158
|
+
* @param scope_id - the scope to check (`null` for global)
|
|
159
|
+
* @param now - current time (defaults to `new Date()`, pass for testability and hot-path efficiency)
|
|
160
|
+
* @returns `true` iff the actor holds an active permit for the role at the requested scope
|
|
161
|
+
*/
|
|
162
|
+
export const has_scoped_role = (ctx, role, scope_id, now = new Date()) => {
|
|
163
|
+
if (!ctx)
|
|
164
|
+
return false;
|
|
165
|
+
return ctx.permits.some((p) => p.role === role && p.scope_id === scope_id && is_permit_active(p, now));
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Whether the request context holds an active permit for any role in `roles`
|
|
169
|
+
* at `scope_id`. Empty `roles` short-circuits to `false` — documents intent
|
|
170
|
+
* at the call site ("zero roles trivially admit no-one"). Same scope and
|
|
171
|
+
* null-tolerance semantics as `has_scoped_role`.
|
|
172
|
+
*
|
|
173
|
+
* @param ctx - the request context, or `null` for unauthenticated callers
|
|
174
|
+
* @param roles - the roles that would admit the caller (any-of)
|
|
175
|
+
* @param scope_id - the scope to check (`null` for global)
|
|
176
|
+
* @param now - current time (defaults to `new Date()`, pass for testability)
|
|
177
|
+
* @returns `true` iff the actor holds an active permit for any role in `roles` at the requested scope
|
|
178
|
+
*/
|
|
179
|
+
export const has_any_scoped_role = (ctx, roles, scope_id, now = new Date()) => {
|
|
180
|
+
if (!ctx)
|
|
181
|
+
return false;
|
|
182
|
+
if (roles.length === 0)
|
|
183
|
+
return false;
|
|
184
|
+
return ctx.permits.some((p) => roles.includes(p.role) && p.scope_id === scope_id && is_permit_active(p, now));
|
|
185
|
+
};
|
|
70
186
|
/**
|
|
71
|
-
*
|
|
187
|
+
* Resolve the acting actor for an authenticated request.
|
|
72
188
|
*
|
|
73
|
-
*
|
|
74
|
-
* the
|
|
75
|
-
*
|
|
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:
|
|
76
192
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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.
|
|
228
|
+
*
|
|
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.
|
|
236
|
+
*
|
|
237
|
+
* Invalid / missing session leaves all keys null and calls `next()` —
|
|
238
|
+
* `require_auth` / `require_role` enforce.
|
|
80
239
|
*
|
|
81
240
|
* @param deps - query dependencies (pool-level db for middleware)
|
|
82
241
|
* @param log - the logger instance
|
|
83
242
|
* @param session_context_key - the Hono context key where session middleware stored the session token
|
|
84
|
-
* @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`
|
|
85
244
|
*/
|
|
86
245
|
export const create_request_context_middleware = (deps, log, session_context_key = 'auth_session_id') => {
|
|
87
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);
|
|
88
252
|
const session_token = c.get(session_context_key) ?? null;
|
|
89
253
|
if (!session_token) {
|
|
90
|
-
c.set(REQUEST_CONTEXT_KEY, null);
|
|
91
|
-
c.set(CREDENTIAL_TYPE_KEY, null);
|
|
92
|
-
c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
|
|
93
|
-
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
94
254
|
await next();
|
|
95
255
|
return;
|
|
96
256
|
}
|
|
97
257
|
const token_hash = hash_session_token(session_token);
|
|
98
258
|
const session = await query_session_get_valid(deps, token_hash);
|
|
99
259
|
if (!session) {
|
|
100
|
-
c.set(REQUEST_CONTEXT_KEY, null);
|
|
101
|
-
c.set(CREDENTIAL_TYPE_KEY, null);
|
|
102
|
-
c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
|
|
103
|
-
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
104
260
|
await next();
|
|
105
261
|
return;
|
|
106
262
|
}
|
|
107
|
-
|
|
108
|
-
if (!ctx) {
|
|
109
|
-
c.set(REQUEST_CONTEXT_KEY, null);
|
|
110
|
-
c.set(CREDENTIAL_TYPE_KEY, null);
|
|
111
|
-
c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
|
|
112
|
-
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
113
|
-
await next();
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
c.set(REQUEST_CONTEXT_KEY, ctx);
|
|
263
|
+
c.set(ACCOUNT_ID_KEY, session.account_id);
|
|
117
264
|
c.set(CREDENTIAL_TYPE_KEY, 'session');
|
|
118
265
|
c.set(AUTH_SESSION_TOKEN_HASH_KEY, token_hash);
|
|
119
|
-
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
120
266
|
// Touch session (fire-and-forget, don't block the request)
|
|
121
267
|
void session_touch_fire_and_forget(deps, token_hash, c.var.pending_effects, log);
|
|
122
268
|
await next();
|
|
@@ -125,11 +271,10 @@ export const create_request_context_middleware = (deps, log, session_context_key
|
|
|
125
271
|
/**
|
|
126
272
|
* Middleware that requires authentication.
|
|
127
273
|
*
|
|
128
|
-
* Returns 401 if
|
|
274
|
+
* Returns 401 if the auth middleware did not set `c.var.auth_account_id`.
|
|
129
275
|
*/
|
|
130
276
|
export const require_auth = async (c, next) => {
|
|
131
|
-
|
|
132
|
-
if (!ctx) {
|
|
277
|
+
if (c.get(ACCOUNT_ID_KEY) == null) {
|
|
133
278
|
return c.json({ error: ERROR_AUTHENTICATION_REQUIRED }, 401);
|
|
134
279
|
}
|
|
135
280
|
await next();
|
|
@@ -137,17 +282,20 @@ export const require_auth = async (c, next) => {
|
|
|
137
282
|
/**
|
|
138
283
|
* Create middleware that requires a specific role.
|
|
139
284
|
*
|
|
140
|
-
* 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`).
|
|
141
289
|
*
|
|
142
290
|
* @param role - the required role
|
|
143
291
|
*/
|
|
144
292
|
export const require_role = (role) => {
|
|
145
293
|
return async (c, next) => {
|
|
146
|
-
|
|
147
|
-
if (!ctx) {
|
|
294
|
+
if (c.get(ACCOUNT_ID_KEY) == null) {
|
|
148
295
|
return c.json({ error: ERROR_AUTHENTICATION_REQUIRED }, 401);
|
|
149
296
|
}
|
|
150
|
-
|
|
297
|
+
const ctx = get_request_context(c);
|
|
298
|
+
if (!ctx || !has_role(ctx, role)) {
|
|
151
299
|
return c.json({ error: ERROR_INSUFFICIENT_PERMISSIONS, required_role: role }, 403);
|
|
152
300
|
}
|
|
153
301
|
await next();
|
|
@@ -161,35 +309,269 @@ export const require_role = (role) => {
|
|
|
161
309
|
* or after receiving a revocation signal.
|
|
162
310
|
*
|
|
163
311
|
* Returns a new `RequestContext` with updated permits — the original
|
|
164
|
-
* 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.
|
|
165
314
|
*
|
|
166
315
|
* @param ctx - the request context to refresh
|
|
167
316
|
* @param deps - query dependencies
|
|
168
317
|
* @returns a new `RequestContext` with fresh permits
|
|
318
|
+
* @throws Error when called on an account-grain context (`actor: null`)
|
|
169
319
|
*/
|
|
170
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
|
+
}
|
|
171
324
|
const permits = await query_permit_find_active_for_actor(deps, ctx.actor.id);
|
|
172
325
|
return { ...ctx, permits };
|
|
173
326
|
};
|
|
174
327
|
/**
|
|
175
|
-
* 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.
|
|
176
336
|
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* 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.
|
|
181
340
|
*
|
|
182
341
|
* @param deps - query dependencies
|
|
183
342
|
* @param account_id - the account to build context for
|
|
184
|
-
* @
|
|
343
|
+
* @param actor_id - the actor this request acts as
|
|
344
|
+
* @returns a request context, or `null` if account/actor not found or mismatched
|
|
185
345
|
*/
|
|
186
|
-
export const build_request_context = async (deps, account_id) => {
|
|
346
|
+
export const build_request_context = async (deps, account_id, actor_id) => {
|
|
187
347
|
const account = await query_account_by_id(deps, account_id);
|
|
188
348
|
if (!account)
|
|
189
349
|
return null;
|
|
190
|
-
const actor = await
|
|
350
|
+
const actor = await query_actor_by_id(deps, actor_id);
|
|
191
351
|
if (!actor)
|
|
192
352
|
return null;
|
|
353
|
+
if (actor.account_id !== account.id)
|
|
354
|
+
return null;
|
|
193
355
|
const permits = await query_permit_find_active_for_actor(deps, actor.id);
|
|
194
356
|
return { account, actor, permits };
|
|
195
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"}
|