@fuzdev/fuz_app 0.55.0 → 0.56.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 +211 -155
- package/dist/actions/action_bridge.d.ts +8 -5
- package/dist/actions/action_bridge.d.ts.map +1 -1
- package/dist/actions/action_bridge.js +1 -11
- package/dist/actions/action_codegen.d.ts +19 -0
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +20 -14
- package/dist/actions/action_registry.d.ts.map +1 -1
- package/dist/actions/action_registry.js +5 -2
- package/dist/actions/action_rpc.d.ts +110 -44
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +92 -287
- package/dist/actions/action_spec.d.ts +55 -16
- package/dist/actions/action_spec.d.ts.map +1 -1
- package/dist/actions/action_spec.js +16 -11
- package/dist/actions/action_types.d.ts +28 -60
- package/dist/actions/action_types.d.ts.map +1 -1
- package/dist/actions/action_types.js +13 -5
- package/dist/actions/broadcast_api.d.ts +2 -2
- package/dist/actions/broadcast_api.js +2 -2
- package/dist/actions/compile_action_registry.d.ts +50 -0
- package/dist/actions/compile_action_registry.d.ts.map +1 -0
- package/dist/actions/compile_action_registry.js +69 -0
- package/dist/actions/heartbeat.d.ts +8 -4
- package/dist/actions/heartbeat.d.ts.map +1 -1
- package/dist/actions/heartbeat.js +5 -4
- package/dist/actions/perform_action.d.ts +145 -0
- package/dist/actions/perform_action.d.ts.map +1 -0
- package/dist/actions/perform_action.js +258 -0
- package/dist/actions/register_action_ws.d.ts +44 -38
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +101 -159
- package/dist/actions/register_ws_endpoint.d.ts +2 -10
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +32 -10
- package/dist/actions/transports_ws_auth_guard.d.ts +1 -1
- package/dist/actions/transports_ws_auth_guard.js +1 -1
- package/dist/actions/transports_ws_backend.d.ts +1 -1
- package/dist/actions/transports_ws_backend.js +1 -1
- package/dist/auth/CLAUDE.md +673 -442
- package/dist/auth/account_action_specs.d.ts +28 -7
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +7 -7
- package/dist/auth/account_actions.d.ts +8 -14
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +26 -32
- package/dist/auth/account_queries.d.ts +46 -13
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +73 -33
- package/dist/auth/account_routes.d.ts +4 -3
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +58 -33
- package/dist/auth/account_schema.d.ts +46 -54
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +21 -48
- package/dist/auth/admin_action_specs.d.ts +55 -21
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +42 -26
- package/dist/auth/admin_actions.d.ts +14 -21
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +47 -44
- package/dist/auth/audit_emitter.d.ts +160 -0
- package/dist/auth/audit_emitter.d.ts.map +1 -0
- package/dist/auth/audit_emitter.js +83 -0
- package/dist/auth/audit_log_queries.d.ts +17 -87
- package/dist/auth/audit_log_queries.d.ts.map +1 -1
- package/dist/auth/audit_log_queries.js +17 -96
- package/dist/auth/audit_log_routes.d.ts +1 -1
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +7 -3
- package/dist/auth/audit_log_schema.d.ts +48 -42
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +56 -43
- package/dist/auth/auth_guard_resolver.d.ts +44 -0
- package/dist/auth/auth_guard_resolver.d.ts.map +1 -0
- package/dist/auth/auth_guard_resolver.js +56 -0
- package/dist/auth/bootstrap_account.d.ts +7 -7
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +7 -7
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +11 -10
- package/dist/auth/cleanup.d.ts +20 -26
- package/dist/auth/cleanup.d.ts.map +1 -1
- package/dist/auth/cleanup.js +33 -47
- package/dist/auth/credential_type_schema.d.ts +115 -0
- package/dist/auth/credential_type_schema.d.ts.map +1 -0
- package/dist/auth/credential_type_schema.js +127 -0
- package/dist/auth/daemon_token_middleware.d.ts +1 -1
- package/dist/auth/daemon_token_middleware.js +3 -3
- package/dist/auth/ddl.d.ts +2 -2
- package/dist/auth/ddl.d.ts.map +1 -1
- package/dist/auth/ddl.js +6 -6
- package/dist/auth/deps.d.ts +7 -32
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/grant_path_schema.d.ts +117 -0
- package/dist/auth/grant_path_schema.d.ts.map +1 -0
- package/dist/auth/grant_path_schema.js +137 -0
- package/dist/auth/invite_queries.d.ts +12 -1
- package/dist/auth/invite_queries.d.ts.map +1 -1
- package/dist/auth/invite_queries.js +12 -1
- package/dist/auth/invite_schema.d.ts +1 -1
- package/dist/auth/invite_schema.d.ts.map +1 -1
- package/dist/auth/invite_schema.js +1 -1
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +5 -2
- package/dist/auth/migrations.d.ts +22 -7
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +64 -25
- package/dist/auth/request_context.d.ts +157 -170
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +224 -268
- package/dist/auth/{permit_offer_action_specs.d.ts → role_grant_offer_action_specs.d.ts} +130 -100
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -0
- package/dist/auth/role_grant_offer_action_specs.js +262 -0
- package/dist/auth/role_grant_offer_actions.d.ts +104 -0
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -0
- package/dist/auth/{permit_offer_actions.js → role_grant_offer_actions.js} +153 -140
- package/dist/auth/{permit_offer_notifications.d.ts → role_grant_offer_notifications.d.ts} +80 -70
- package/dist/auth/role_grant_offer_notifications.d.ts.map +1 -0
- package/dist/auth/role_grant_offer_notifications.js +182 -0
- package/dist/auth/{permit_offer_queries.d.ts → role_grant_offer_queries.d.ts} +64 -64
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -0
- package/dist/auth/{permit_offer_queries.js → role_grant_offer_queries.js} +136 -123
- package/dist/auth/role_grant_offer_schema.d.ts +150 -0
- package/dist/auth/role_grant_offer_schema.d.ts.map +1 -0
- package/dist/auth/{permit_offer_schema.js → role_grant_offer_schema.js} +55 -36
- package/dist/auth/role_grant_queries.d.ts +231 -0
- package/dist/auth/role_grant_queries.d.ts.map +1 -0
- package/dist/auth/role_grant_queries.js +320 -0
- package/dist/auth/role_schema.d.ts +150 -40
- package/dist/auth/role_schema.d.ts.map +1 -1
- package/dist/auth/role_schema.js +144 -45
- package/dist/auth/scope_kind_schema.d.ts +96 -0
- package/dist/auth/scope_kind_schema.d.ts.map +1 -0
- package/dist/auth/scope_kind_schema.js +94 -0
- package/dist/auth/self_service_role_action_specs.d.ts +4 -1
- package/dist/auth/self_service_role_action_specs.d.ts.map +1 -1
- package/dist/auth/self_service_role_action_specs.js +2 -2
- package/dist/auth/self_service_role_actions.d.ts +35 -29
- package/dist/auth/self_service_role_actions.d.ts.map +1 -1
- package/dist/auth/self_service_role_actions.js +58 -48
- package/dist/auth/session_cookie.d.ts +43 -6
- package/dist/auth/session_cookie.d.ts.map +1 -1
- package/dist/auth/session_cookie.js +31 -5
- package/dist/auth/session_middleware.d.ts +37 -3
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +33 -7
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +48 -19
- package/dist/auth/standard_action_specs.d.ts +2 -2
- package/dist/auth/standard_action_specs.js +4 -4
- package/dist/auth/standard_rpc_actions.d.ts +23 -19
- package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
- package/dist/auth/standard_rpc_actions.js +12 -12
- package/dist/db/migrate.d.ts +1 -1
- package/dist/db/migrate.js +1 -1
- package/dist/dev/setup.d.ts +2 -2
- package/dist/dev/setup.d.ts.map +1 -1
- package/dist/dev/setup.js +4 -4
- package/dist/env/load.d.ts +1 -1
- package/dist/env/load.js +1 -1
- package/dist/hono_context.d.ts +27 -45
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/hono_context.js +14 -28
- package/dist/http/CLAUDE.md +235 -121
- package/dist/http/auth_shape.d.ts +191 -0
- package/dist/http/auth_shape.d.ts.map +1 -0
- package/dist/http/auth_shape.js +237 -0
- package/dist/http/common_routes.js +3 -3
- package/dist/http/db_routes.d.ts +4 -0
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +44 -7
- package/dist/http/error_schemas.d.ts +56 -34
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +63 -28
- package/dist/http/pending_effects.d.ts +71 -18
- package/dist/http/pending_effects.d.ts.map +1 -1
- package/dist/http/pending_effects.js +87 -18
- package/dist/http/proxy.d.ts +52 -5
- package/dist/http/proxy.d.ts.map +1 -1
- package/dist/http/proxy.js +92 -14
- package/dist/http/route_spec.d.ts +89 -75
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/route_spec.js +54 -72
- package/dist/http/schema_helpers.d.ts +3 -14
- package/dist/http/schema_helpers.d.ts.map +1 -1
- package/dist/http/schema_helpers.js +2 -14
- package/dist/http/surface.d.ts +2 -10
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +3 -4
- package/dist/http/surface_query.d.ts +39 -35
- package/dist/http/surface_query.d.ts.map +1 -1
- package/dist/http/surface_query.js +79 -36
- package/dist/primitive_schemas.d.ts +39 -0
- package/dist/primitive_schemas.d.ts.map +1 -0
- package/dist/primitive_schemas.js +40 -0
- package/dist/realtime/sse_auth_guard.d.ts +5 -5
- package/dist/realtime/sse_auth_guard.js +9 -9
- package/dist/runtime/mock.d.ts +1 -1
- package/dist/runtime/mock.js +1 -1
- package/dist/server/app_backend.d.ts +14 -11
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +12 -8
- package/dist/server/app_server.d.ts +7 -7
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +35 -40
- package/dist/server/validate_nginx.d.ts +1 -1
- package/dist/server/validate_nginx.js +1 -1
- package/dist/testing/CLAUDE.md +50 -38
- package/dist/testing/admin_integration.d.ts +5 -6
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +87 -85
- package/dist/testing/app_server.d.ts +11 -14
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +16 -15
- package/dist/testing/assertions.d.ts.map +1 -1
- package/dist/testing/assertions.js +2 -1
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +15 -9
- package/dist/testing/audit_completeness.d.ts +2 -2
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +36 -36
- package/dist/testing/auth_apps.d.ts +5 -4
- package/dist/testing/auth_apps.d.ts.map +1 -1
- package/dist/testing/auth_apps.js +22 -19
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +5 -5
- package/dist/testing/db.d.ts +1 -1
- package/dist/testing/db.d.ts.map +1 -1
- package/dist/testing/db.js +4 -4
- package/dist/testing/db_entities.d.ts +22 -0
- package/dist/testing/db_entities.d.ts.map +1 -0
- package/dist/testing/db_entities.js +28 -0
- package/dist/testing/entities.d.ts +8 -7
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +21 -18
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +13 -14
- package/dist/testing/integration_helpers.d.ts +4 -4
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +20 -18
- package/dist/testing/middleware.d.ts +4 -4
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +12 -11
- package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
- package/dist/testing/rpc_attack_surface.js +40 -24
- package/dist/testing/rpc_round_trip.d.ts +1 -1
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +14 -13
- package/dist/testing/sse_round_trip.d.ts +3 -4
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +7 -11
- package/dist/testing/standard.d.ts +1 -1
- package/dist/testing/stubs.d.ts +25 -0
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +43 -2
- package/dist/testing/surface_invariants.d.ts +2 -2
- package/dist/testing/ws_round_trip.d.ts +12 -13
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +19 -11
- package/dist/ui/AdminAccounts.svelte +23 -20
- package/dist/ui/AdminOverview.svelte +15 -13
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/{AdminPermitHistory.svelte → AdminRoleGrantHistory.svelte} +12 -12
- package/dist/ui/AdminRoleGrantHistory.svelte.d.ts +4 -0
- package/dist/ui/AdminRoleGrantHistory.svelte.d.ts.map +1 -0
- package/dist/ui/BootstrapForm.svelte +1 -1
- package/dist/ui/CLAUDE.md +60 -60
- package/dist/ui/{PermitOfferForm.svelte → RoleGrantOfferForm.svelte} +27 -26
- package/dist/ui/{PermitOfferForm.svelte.d.ts → RoleGrantOfferForm.svelte.d.ts} +7 -7
- package/dist/ui/RoleGrantOfferForm.svelte.d.ts.map +1 -0
- package/dist/ui/{PermitOfferHistory.svelte → RoleGrantOfferHistory.svelte} +12 -12
- package/dist/ui/{PermitOfferHistory.svelte.d.ts → RoleGrantOfferHistory.svelte.d.ts} +4 -4
- package/dist/ui/RoleGrantOfferHistory.svelte.d.ts.map +1 -0
- package/dist/ui/{PermitOfferInbox.svelte → RoleGrantOfferInbox.svelte} +14 -14
- package/dist/ui/{PermitOfferInbox.svelte.d.ts → RoleGrantOfferInbox.svelte.d.ts} +4 -4
- package/dist/ui/RoleGrantOfferInbox.svelte.d.ts.map +1 -0
- package/dist/ui/SignupForm.svelte +1 -1
- package/dist/ui/SurfaceExplorer.svelte +35 -15
- package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.d.ts +2 -3
- package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.js +2 -3
- package/dist/ui/admin_accounts_state.svelte.d.ts +18 -18
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +16 -16
- package/dist/ui/admin_rpc_adapters.d.ts +20 -20
- package/dist/ui/admin_rpc_adapters.d.ts.map +1 -1
- package/dist/ui/admin_rpc_adapters.js +17 -17
- package/dist/ui/admin_sessions_state.svelte.d.ts +2 -2
- package/dist/ui/admin_sessions_state.svelte.js +2 -2
- package/dist/ui/audit_log_state.svelte.d.ts +7 -7
- package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
- package/dist/ui/audit_log_state.svelte.js +6 -6
- package/dist/ui/auth_state.svelte.d.ts +3 -3
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +6 -6
- package/dist/ui/format_scope.d.ts +2 -2
- package/dist/ui/format_scope.js +2 -2
- package/dist/ui/{permit_offers_state.svelte.d.ts → role_grant_offers_state.svelte.d.ts} +30 -30
- package/dist/ui/role_grant_offers_state.svelte.d.ts.map +1 -0
- package/dist/ui/{permit_offers_state.svelte.js → role_grant_offers_state.svelte.js} +18 -18
- package/dist/ui/ui_format.js +2 -2
- package/package.json +3 -3
- package/dist/auth/permit_offer_action_specs.d.ts.map +0 -1
- package/dist/auth/permit_offer_action_specs.js +0 -258
- package/dist/auth/permit_offer_actions.d.ts +0 -110
- package/dist/auth/permit_offer_actions.d.ts.map +0 -1
- package/dist/auth/permit_offer_notifications.d.ts.map +0 -1
- package/dist/auth/permit_offer_notifications.js +0 -182
- package/dist/auth/permit_offer_queries.d.ts.map +0 -1
- package/dist/auth/permit_offer_schema.d.ts +0 -125
- package/dist/auth/permit_offer_schema.d.ts.map +0 -1
- package/dist/auth/permit_queries.d.ts +0 -222
- package/dist/auth/permit_queries.d.ts.map +0 -1
- package/dist/auth/permit_queries.js +0 -305
- package/dist/auth/require_keeper.d.ts +0 -20
- package/dist/auth/require_keeper.d.ts.map +0 -1
- package/dist/auth/require_keeper.js +0 -35
- package/dist/auth/route_guards.d.ts +0 -27
- package/dist/auth/route_guards.d.ts.map +0 -1
- package/dist/auth/route_guards.js +0 -38
- package/dist/auth/session_lifecycle.d.ts +0 -37
- package/dist/auth/session_lifecycle.d.ts.map +0 -1
- package/dist/auth/session_lifecycle.js +0 -29
- package/dist/ui/AdminPermitHistory.svelte.d.ts +0 -4
- package/dist/ui/AdminPermitHistory.svelte.d.ts.map +0 -1
- package/dist/ui/PermitOfferForm.svelte.d.ts.map +0 -1
- package/dist/ui/PermitOfferHistory.svelte.d.ts.map +0 -1
- package/dist/ui/PermitOfferInbox.svelte.d.ts.map +0 -1
- package/dist/ui/permit_offers_state.svelte.d.ts.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Request context middleware and
|
|
2
|
+
* Request context middleware and role_grant checking helpers.
|
|
3
3
|
*
|
|
4
4
|
* Two-phase identity resolution:
|
|
5
5
|
*
|
|
@@ -7,41 +7,40 @@
|
|
|
7
7
|
* `bearer_auth`, and `daemon_token_middleware` validate the credential
|
|
8
8
|
* (session cookie, bearer token, daemon token) and set `c.var.account_id`
|
|
9
9
|
* + `c.var.credential_type` on the Hono context. They do not resolve
|
|
10
|
-
* an acting actor or load
|
|
10
|
+
* an acting actor or load role_grants; `REQUEST_CONTEXT_KEY` stays null at
|
|
11
11
|
* this stage, so account-grain identity is the only thing known.
|
|
12
12
|
* 2. **Authorization (route-spec wrapper / RPC dispatcher)** — after input
|
|
13
13
|
* validation, the per-route layer inspects the route. If the input
|
|
14
14
|
* schema declared `acting?: ActingActor` (reference equality with the
|
|
15
|
-
* canonical `ActingActor` schema) or the auth requires
|
|
15
|
+
* canonical `ActingActor` schema) or the auth requires role_grants
|
|
16
16
|
* (`role` / `keeper`), `apply_authorization_phase` resolves the actor
|
|
17
17
|
* against `c.var.account_id` plus the validated `acting` value via
|
|
18
|
-
* `resolve_acting_actor`, builds the `{account, actor,
|
|
18
|
+
* `resolve_acting_actor`, builds the `{account, actor, role_grants}`
|
|
19
19
|
* context via `build_request_context`, and sets it on
|
|
20
20
|
* `REQUEST_CONTEXT_KEY` before auth guards fire. Authenticated routes
|
|
21
21
|
* that don't need an actor still get an account-only context via
|
|
22
22
|
* `build_account_context` so handler signatures stay uniform.
|
|
23
23
|
*
|
|
24
24
|
* Account-grain operations (logout, password_change, account_verify,
|
|
25
|
-
* etc.) declare neither `acting` nor
|
|
25
|
+
* etc.) declare neither `acting` nor role_grant-requiring auth, so no actor
|
|
26
26
|
* is resolved and their handlers see a `RequestContext` with
|
|
27
|
-
* `actor: null` + empty `
|
|
27
|
+
* `actor: null` + empty `role_grants`. They never trigger `actor_required`,
|
|
28
28
|
* which is what makes multi-actor logout work without first picking a
|
|
29
29
|
* persona.
|
|
30
30
|
*
|
|
31
|
-
* `build_request_context` loads `account → actor →
|
|
32
|
-
* the `actor.account_id === account.id` binding. `
|
|
33
|
-
* reloads
|
|
31
|
+
* `build_request_context` loads `account → actor → role_grants` and verifies
|
|
32
|
+
* the `actor.account_id === account.id` binding. `refresh_role_grants`
|
|
33
|
+
* reloads role_grants on an existing context.
|
|
34
34
|
*
|
|
35
35
|
* @module
|
|
36
36
|
*/
|
|
37
|
-
import {
|
|
38
|
-
import { zod_unwrap_to_object } from '@fuzdev/fuz_util/zod.js';
|
|
39
|
-
import { ActingActor, is_permit_active, } from './account_schema.js';
|
|
37
|
+
import { is_role_grant_active } from './account_schema.js';
|
|
40
38
|
import { hash_session_token, session_touch_fire_and_forget, query_session_get_valid, } from './session_queries.js';
|
|
41
39
|
import { query_account_by_id, query_actor_by_id, query_actors_by_account, } from './account_queries.js';
|
|
42
|
-
import {
|
|
43
|
-
import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY,
|
|
44
|
-
import {
|
|
40
|
+
import { query_role_grant_find_active_for_actor } from './role_grant_queries.js';
|
|
41
|
+
import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY, TEST_CONTEXT_PRESET_KEY, } from '../hono_context.js';
|
|
42
|
+
import { is_public_auth, needs_actor } from '../http/auth_shape.js';
|
|
43
|
+
import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INSUFFICIENT_PERMISSIONS, ERROR_CREDENTIAL_TYPE_REQUIRED, ERROR_ACTOR_REQUIRED, ERROR_ACTOR_NOT_ON_ACCOUNT, ERROR_NO_ACTORS_ON_ACCOUNT, ERROR_ACCOUNT_VANISHED, } from '../http/error_schemas.js';
|
|
45
44
|
/** Hono context variable name for the request context. */
|
|
46
45
|
export const REQUEST_CONTEXT_KEY = 'request_context';
|
|
47
46
|
/**
|
|
@@ -83,41 +82,9 @@ export const require_request_context = (c) => {
|
|
|
83
82
|
return ctx;
|
|
84
83
|
};
|
|
85
84
|
/**
|
|
86
|
-
*
|
|
85
|
+
* Check if a request context has an active role_grant for a given role.
|
|
87
86
|
*
|
|
88
|
-
*
|
|
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`?');
|
|
114
|
-
}
|
|
115
|
-
return ctx;
|
|
116
|
-
};
|
|
117
|
-
/**
|
|
118
|
-
* Check if a request context has an active permit for a given role.
|
|
119
|
-
*
|
|
120
|
-
* Checks the permits already loaded in the context (no DB query).
|
|
87
|
+
* Checks the role_grants already loaded in the context (no DB query).
|
|
121
88
|
* Null-tolerant — `null` ctx (unauthenticated) returns `false`. Symmetric
|
|
122
89
|
* with `has_scoped_role` / `has_any_scoped_role` so the three helpers
|
|
123
90
|
* compose freely in the same predicate (e.g.
|
|
@@ -126,15 +93,15 @@ export const require_request_actor = (auth) => {
|
|
|
126
93
|
* @param ctx - the request context, or `null` for unauthenticated callers
|
|
127
94
|
* @param role - the role to check
|
|
128
95
|
* @param now - current time (defaults to `new Date()`, pass for testability and hot-path efficiency)
|
|
129
|
-
* @returns `true` if the actor has an active
|
|
96
|
+
* @returns `true` if the actor has an active role_grant for the role
|
|
130
97
|
*/
|
|
131
|
-
export const has_role = (ctx, role, now = new Date()) => ctx?.
|
|
98
|
+
export const has_role = (ctx, role, now = new Date()) => ctx?.role_grants.some((p) => p.role === role && is_role_grant_active(p, now)) ?? false;
|
|
132
99
|
/**
|
|
133
|
-
* Whether the request context holds an active
|
|
100
|
+
* Whether the request context holds an active role_grant for `role` at `scope_id`.
|
|
134
101
|
*
|
|
135
|
-
* Walks the in-memory `ctx.
|
|
102
|
+
* Walks the in-memory `ctx.role_grants` snapshot loaded once per request by
|
|
136
103
|
* the route-spec / RPC dispatcher's authorization phase (when the route
|
|
137
|
-
* declares `acting?: ActingActor` or has
|
|
104
|
+
* declares `acting?: ActingActor` or has role_grant-requiring auth); zero DB
|
|
138
105
|
* roundtrip per check. The "freshness" framing of a SQL re-query is
|
|
139
106
|
* illusory because the race window is between predicate and the actual
|
|
140
107
|
* mutation, not predicate and authorization load. Closing that race needs
|
|
@@ -142,30 +109,31 @@ export const has_role = (ctx, role, now = new Date()) => ctx?.permits.some((p) =
|
|
|
142
109
|
* provides.
|
|
143
110
|
*
|
|
144
111
|
* Null-tolerant — `null` ctx (unauthenticated) and account-grain
|
|
145
|
-
* contexts (`actor: null`, empty `
|
|
146
|
-
* convention as `has_role`; lets the helper drop into
|
|
147
|
-
*
|
|
148
|
-
*
|
|
112
|
+
* contexts (`actor: null`, empty `role_grants`) both return `false`. Same
|
|
113
|
+
* convention as `has_role`; lets the helper drop into public
|
|
114
|
+
* (`{account: 'none', actor: 'none'}`) and account-grain
|
|
115
|
+
* (`{account: 'required', actor: 'none'}`) handlers without a manual
|
|
116
|
+
* narrow. See `cell_authorize` for the resource-side analog.
|
|
149
117
|
*
|
|
150
|
-
* `scope_id` semantics: in-memory `
|
|
118
|
+
* `scope_id` semantics: in-memory `role_grant.scope_id` is `string | null`, so
|
|
151
119
|
* JS `===` matches the SQL `IS NOT DISTINCT FROM` semantics exactly:
|
|
152
120
|
*
|
|
153
|
-
* - `scope_id === null` matches global
|
|
154
|
-
* - `scope_id === '<uuid>'` matches
|
|
121
|
+
* - `scope_id === null` matches global role_grants (`scope_id IS NULL`).
|
|
122
|
+
* - `scope_id === '<uuid>'` matches role_grants bound to that exact scope.
|
|
155
123
|
*
|
|
156
124
|
* @param ctx - the request context, or `null` for unauthenticated callers
|
|
157
125
|
* @param role - the role to check
|
|
158
126
|
* @param scope_id - the scope to check (`null` for global)
|
|
159
127
|
* @param now - current time (defaults to `new Date()`, pass for testability and hot-path efficiency)
|
|
160
|
-
* @returns `true` iff the actor holds an active
|
|
128
|
+
* @returns `true` iff the actor holds an active role_grant for the role at the requested scope
|
|
161
129
|
*/
|
|
162
130
|
export const has_scoped_role = (ctx, role, scope_id, now = new Date()) => {
|
|
163
131
|
if (!ctx)
|
|
164
132
|
return false;
|
|
165
|
-
return ctx.
|
|
133
|
+
return ctx.role_grants.some((p) => p.role === role && p.scope_id === scope_id && is_role_grant_active(p, now));
|
|
166
134
|
};
|
|
167
135
|
/**
|
|
168
|
-
* Whether the request context holds an active
|
|
136
|
+
* Whether the request context holds an active role_grant for any role in `roles`
|
|
169
137
|
* at `scope_id`. Empty `roles` short-circuits to `false` — documents intent
|
|
170
138
|
* at the call site ("zero roles trivially admit no-one"). Same scope and
|
|
171
139
|
* null-tolerance semantics as `has_scoped_role`.
|
|
@@ -174,14 +142,14 @@ export const has_scoped_role = (ctx, role, scope_id, now = new Date()) => {
|
|
|
174
142
|
* @param roles - the roles that would admit the caller (any-of)
|
|
175
143
|
* @param scope_id - the scope to check (`null` for global)
|
|
176
144
|
* @param now - current time (defaults to `new Date()`, pass for testability)
|
|
177
|
-
* @returns `true` iff the actor holds an active
|
|
145
|
+
* @returns `true` iff the actor holds an active role_grant for any role in `roles` at the requested scope
|
|
178
146
|
*/
|
|
179
147
|
export const has_any_scoped_role = (ctx, roles, scope_id, now = new Date()) => {
|
|
180
148
|
if (!ctx)
|
|
181
149
|
return false;
|
|
182
150
|
if (roles.length === 0)
|
|
183
151
|
return false;
|
|
184
|
-
return ctx.
|
|
152
|
+
return ctx.role_grants.some((p) => roles.includes(p.role) && p.scope_id === scope_id && is_role_grant_active(p, now));
|
|
185
153
|
};
|
|
186
154
|
/**
|
|
187
155
|
* Resolve the acting actor for an authenticated request.
|
|
@@ -229,7 +197,7 @@ export const resolve_acting_actor = async (deps, account_id, acting_actor_id) =>
|
|
|
229
197
|
* Reads the session identity (set by session middleware), looks up the
|
|
230
198
|
* `auth_session`, and on a valid session sets `c.var.auth_account_id`,
|
|
231
199
|
* `CREDENTIAL_TYPE_KEY = 'session'`, and `AUTH_SESSION_TOKEN_HASH_KEY`.
|
|
232
|
-
* Touches the session (fire-and-forget). Does not load actor or
|
|
200
|
+
* Touches the session (fire-and-forget). Does not load actor or role_grants;
|
|
233
201
|
* `REQUEST_CONTEXT_KEY` is left null — the route-spec / RPC dispatcher
|
|
234
202
|
* authorization phase resolves the acting actor and builds the full
|
|
235
203
|
* `RequestContext` when the route needs one.
|
|
@@ -280,55 +248,99 @@ export const require_auth = async (c, next) => {
|
|
|
280
248
|
await next();
|
|
281
249
|
};
|
|
282
250
|
/**
|
|
283
|
-
* Create middleware that requires
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
251
|
+
* Create middleware that requires the actor to hold any of the given
|
|
252
|
+
* roles globally (`scope_id IS NULL`).
|
|
253
|
+
*
|
|
254
|
+
* Returns 401 if unauthenticated, 403 if none of the roles are present.
|
|
255
|
+
* Reads `REQUEST_CONTEXT_KEY` because role-gated routes always run the
|
|
256
|
+
* dispatcher's authorization phase before this guard (the phase sets
|
|
257
|
+
* the actor-bound `RequestContext`).
|
|
258
|
+
*
|
|
259
|
+
* Uses `has_any_scoped_role(ctx, roles, null)` so the gate matches
|
|
260
|
+
* **global / unscoped role_grants only**. A scoped role_grant
|
|
261
|
+
* (`{role: 'admin', scope_id: <some uuid>}`) does not unlock route-spec
|
|
262
|
+
* gates that are inherently global. The same scope-aware check is
|
|
263
|
+
* mirrored in `actions/action_rpc.ts` (HTTP RPC dispatcher) and
|
|
264
|
+
* `actions/register_action_ws.ts` (WS dispatcher) so all three
|
|
265
|
+
* transports agree.
|
|
266
|
+
*
|
|
267
|
+
* Multi-role disjunction (any-of) lets `auth.roles: ['admin', 'steward']`
|
|
268
|
+
* specs translate to one middleware that admits either role. Single-role
|
|
269
|
+
* routes pass `[role_name]`; the array shape is uniform.
|
|
270
|
+
*
|
|
271
|
+
* @param roles - the roles to admit (any-of)
|
|
291
272
|
*/
|
|
292
|
-
export const require_role = (
|
|
273
|
+
export const require_role = (roles) => {
|
|
293
274
|
return async (c, next) => {
|
|
294
275
|
if (c.get(ACCOUNT_ID_KEY) == null) {
|
|
295
276
|
return c.json({ error: ERROR_AUTHENTICATION_REQUIRED }, 401);
|
|
296
277
|
}
|
|
297
278
|
const ctx = get_request_context(c);
|
|
298
|
-
if (!ctx || !
|
|
299
|
-
return c.json({ error: ERROR_INSUFFICIENT_PERMISSIONS,
|
|
279
|
+
if (!ctx || !has_any_scoped_role(ctx, roles, null)) {
|
|
280
|
+
return c.json({ error: ERROR_INSUFFICIENT_PERMISSIONS, required_roles: roles }, 403);
|
|
300
281
|
}
|
|
301
282
|
await next();
|
|
302
283
|
};
|
|
303
284
|
};
|
|
304
285
|
/**
|
|
305
|
-
*
|
|
286
|
+
* Create middleware that requires the request's `credential_type` to be
|
|
287
|
+
* one of the given values.
|
|
288
|
+
*
|
|
289
|
+
* Returns 401 if unauthenticated, 403 with
|
|
290
|
+
* `ERROR_CREDENTIAL_TYPE_REQUIRED` + `required_credential_types` echoing
|
|
291
|
+
* the spec's allowlist when the wire-side credential isn't in it.
|
|
292
|
+
* Body shape is symmetric with the role gate (`ERROR_INSUFFICIENT_PERMISSIONS` +
|
|
293
|
+
* `required_roles`) and matches what the RPC dispatcher's post-auth
|
|
294
|
+
* gate emits for the same condition. Today's only credential gate is
|
|
295
|
+
* keeper (`['daemon_token']`); future gates (`agent_token`,
|
|
296
|
+
* `group_actor_token`) reuse this literal and label themselves through
|
|
297
|
+
* the array.
|
|
298
|
+
*
|
|
299
|
+
* @param credential_types - allowed credential types (any-of)
|
|
300
|
+
*/
|
|
301
|
+
export const require_credential_types = (credential_types) => {
|
|
302
|
+
return async (c, next) => {
|
|
303
|
+
if (c.get(ACCOUNT_ID_KEY) == null) {
|
|
304
|
+
return c.json({ error: ERROR_AUTHENTICATION_REQUIRED }, 401);
|
|
305
|
+
}
|
|
306
|
+
const credential_type = c.get(CREDENTIAL_TYPE_KEY) ?? null;
|
|
307
|
+
if (!credential_type || !credential_types.includes(credential_type)) {
|
|
308
|
+
return c.json({
|
|
309
|
+
error: ERROR_CREDENTIAL_TYPE_REQUIRED,
|
|
310
|
+
required_credential_types: credential_types,
|
|
311
|
+
}, 403);
|
|
312
|
+
}
|
|
313
|
+
await next();
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
/**
|
|
317
|
+
* Reload active role_grants from the database, returning a new request context.
|
|
306
318
|
*
|
|
307
|
-
* Useful for long-lived WebSocket connections where
|
|
319
|
+
* Useful for long-lived WebSocket connections where role_grants may change
|
|
308
320
|
* (grant or revoke) during the connection lifetime. Call periodically
|
|
309
321
|
* or after receiving a revocation signal.
|
|
310
322
|
*
|
|
311
|
-
* Returns a new `RequestContext` with updated
|
|
323
|
+
* Returns a new `RequestContext` with updated role_grants — the original
|
|
312
324
|
* context is not mutated, making concurrent calls safe. Throws when
|
|
313
|
-
* `ctx.actor` is null; account-grain contexts have no
|
|
325
|
+
* `ctx.actor` is null; account-grain contexts have no role_grants to refresh.
|
|
314
326
|
*
|
|
315
327
|
* @param ctx - the request context to refresh
|
|
316
328
|
* @param deps - query dependencies
|
|
317
|
-
* @returns a new `RequestContext` with fresh
|
|
329
|
+
* @returns a new `RequestContext` with fresh role_grants
|
|
318
330
|
* @throws Error when called on an account-grain context (`actor: null`)
|
|
319
331
|
*/
|
|
320
|
-
export const
|
|
332
|
+
export const refresh_role_grants = async (ctx, deps) => {
|
|
321
333
|
if (!ctx.actor) {
|
|
322
|
-
throw new Error('
|
|
334
|
+
throw new Error('refresh_role_grants: account-grain context has no actor / role_grants to refresh');
|
|
323
335
|
}
|
|
324
|
-
const
|
|
325
|
-
return { ...ctx,
|
|
336
|
+
const role_grants = await query_role_grant_find_active_for_actor(deps, ctx.actor.id);
|
|
337
|
+
return { ...ctx, role_grants };
|
|
326
338
|
};
|
|
327
339
|
/**
|
|
328
340
|
* Build a full `RequestContext` from an account id and an explicit
|
|
329
341
|
* actor id (already resolved via `resolve_acting_actor`).
|
|
330
342
|
*
|
|
331
|
-
* Loads `account` + the named `actor` + the actor's active
|
|
343
|
+
* Loads `account` + the named `actor` + the actor's active role_grants.
|
|
332
344
|
* Verifies the `actor.account_id === account.id` binding so downstream
|
|
333
345
|
* handlers can trust `ctx.actor.account_id === ctx.account.id`. Returns
|
|
334
346
|
* `null` when the account is missing, the actor is missing, or the
|
|
@@ -352,17 +364,17 @@ export const build_request_context = async (deps, account_id, actor_id) => {
|
|
|
352
364
|
return null;
|
|
353
365
|
if (actor.account_id !== account.id)
|
|
354
366
|
return null;
|
|
355
|
-
const
|
|
356
|
-
return { account, actor,
|
|
367
|
+
const role_grants = await query_role_grant_find_active_for_actor(deps, actor.id);
|
|
368
|
+
return { account, actor, role_grants };
|
|
357
369
|
};
|
|
358
370
|
/**
|
|
359
|
-
* Build an account-only `RequestContext` (no actor, no
|
|
371
|
+
* Build an account-only `RequestContext` (no actor, no role_grants) from
|
|
360
372
|
* an account id.
|
|
361
373
|
*
|
|
362
374
|
* Used by the dispatcher's authorization phase for authenticated routes
|
|
363
375
|
* that don't need an acting actor — account-grain operations (logout,
|
|
364
376
|
* password change, account self-service). Lets handlers read
|
|
365
|
-
* `auth.account.id` / `auth.account.username` uniformly with
|
|
377
|
+
* `auth.account.id` / `auth.account.username` uniformly with role_grant-bound
|
|
366
378
|
* routes; the cost is one extra `query_account_by_id` per request.
|
|
367
379
|
*
|
|
368
380
|
* Returns `null` when the account row is missing (e.g. deleted between
|
|
@@ -377,201 +389,145 @@ export const build_account_context = async (deps, account_id) => {
|
|
|
377
389
|
const account = await query_account_by_id(deps, account_id);
|
|
378
390
|
if (!account)
|
|
379
391
|
return null;
|
|
380
|
-
return { account, actor: null,
|
|
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;
|
|
392
|
+
return { account, actor: null, role_grants: [] };
|
|
427
393
|
};
|
|
428
394
|
/**
|
|
429
|
-
* Apply the dispatcher's authorization phase
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
* -
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
* `
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
* Returns `undefined` on success.
|
|
462
|
-
*
|
|
463
|
-
* @mutates Hono context - sets `REQUEST_CONTEXT_KEY` on success
|
|
395
|
+
* Apply the dispatcher's authorization phase against the flat-record
|
|
396
|
+
* `RouteAuth` shape. Shared by the route-spec wrapper, the HTTP RPC
|
|
397
|
+
* dispatcher, and the per-message WS dispatcher. Phase order:
|
|
398
|
+
* pre-validation 401 → input validation 400 → authorization phase →
|
|
399
|
+
* post-authorization 403.
|
|
400
|
+
*
|
|
401
|
+
* Pure data — the function does not touch a Hono context. Each transport
|
|
402
|
+
* passes `account_id` (extracted from its own credential surface) and
|
|
403
|
+
* binds the returned `AuthorizationResult` to its wire shape. The REST
|
|
404
|
+
* pipeline additionally writes `REQUEST_CONTEXT_KEY` on `c` for downstream
|
|
405
|
+
* `require_role` / `require_credential_types` middleware that still reads
|
|
406
|
+
* the resolved context off the Hono context.
|
|
407
|
+
*
|
|
408
|
+
* Branching by `auth.account` × `auth.actor`:
|
|
409
|
+
*
|
|
410
|
+
* - Both `'none'` → `{ok: true, request_context: null}`. Public actions
|
|
411
|
+
* never see a `RequestContext`.
|
|
412
|
+
* - `account_id == null` on any non-public route → same null
|
|
413
|
+
* `request_context`. The `'required'` callers were already rejected at
|
|
414
|
+
* the pre-validation gate in the dispatcher; only genuine anonymous
|
|
415
|
+
* access on an `'optional'` axis lands here.
|
|
416
|
+
* - `actor === 'none'` → builds account-only context via
|
|
417
|
+
* `build_account_context`. Null lookup → `account_vanished` 500 failure.
|
|
418
|
+
* - `actor === 'required'` → resolves the actor from `acting_value` (or
|
|
419
|
+
* single-actor account); failures map to 400 / 500.
|
|
420
|
+
* - `actor === 'optional'` → same as `'required'` except multi-actor
|
|
421
|
+
* accounts without an `acting` value fall back to account-only context
|
|
422
|
+
* (no `actor_required` 400). Bad `acting` ids still 400.
|
|
423
|
+
*
|
|
424
|
+
* 500 branches stay distinct: `ERROR_NO_ACTORS_ON_ACCOUNT` (signup
|
|
425
|
+
* invariant violation), `ERROR_ACCOUNT_VANISHED` (torn read after
|
|
426
|
+
* resolve).
|
|
464
427
|
*/
|
|
465
|
-
export const apply_authorization_phase = async (deps,
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
428
|
+
export const apply_authorization_phase = async (deps, account_id, auth, acting_value) => {
|
|
429
|
+
if (is_public_auth(auth))
|
|
430
|
+
return { ok: true, request_context: null };
|
|
431
|
+
if (account_id == null) {
|
|
432
|
+
// Optional-auth route hit without a credential — leave `RequestContext`
|
|
433
|
+
// null so the handler can branch on it. `'required'` callers already
|
|
434
|
+
// got rejected at the pre-validation gate.
|
|
435
|
+
return { ok: true, request_context: null };
|
|
436
|
+
}
|
|
437
|
+
if (!needs_actor(auth)) {
|
|
438
|
+
const ctx = await build_account_context(deps, account_id);
|
|
439
|
+
if (!ctx)
|
|
440
|
+
return { ok: false, status: 500, body: { error: ERROR_ACCOUNT_VANISHED } };
|
|
441
|
+
return { ok: true, request_context: ctx };
|
|
442
|
+
}
|
|
443
|
+
// actor 'required' or 'optional' — resolve.
|
|
444
|
+
const acting = await resolve_acting_actor(deps, account_id, acting_value);
|
|
445
|
+
if (!acting.ok) {
|
|
446
|
+
if (acting.reason === 'actor_required') {
|
|
447
|
+
if (auth.actor === 'optional') {
|
|
448
|
+
// Multi-actor account, no pick — fall back to account-only context.
|
|
449
|
+
const ctx = await build_account_context(deps, account_id);
|
|
450
|
+
if (!ctx)
|
|
451
|
+
return { ok: false, status: 500, body: { error: ERROR_ACCOUNT_VANISHED } };
|
|
452
|
+
return { ok: true, request_context: ctx };
|
|
488
453
|
}
|
|
489
|
-
return {
|
|
454
|
+
return {
|
|
455
|
+
ok: false,
|
|
456
|
+
status: 400,
|
|
457
|
+
body: { error: ERROR_ACTOR_REQUIRED, available: acting.available },
|
|
458
|
+
};
|
|
490
459
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
return;
|
|
460
|
+
if (acting.reason === 'actor_not_on_account') {
|
|
461
|
+
return { ok: false, status: 400, body: { error: ERROR_ACTOR_NOT_ON_ACCOUNT } };
|
|
462
|
+
}
|
|
463
|
+
return { ok: false, status: 500, body: { error: ERROR_NO_ACTORS_ON_ACCOUNT } };
|
|
496
464
|
}
|
|
497
|
-
const ctx = await
|
|
465
|
+
const ctx = await build_request_context(deps, account_id, acting.actor_id);
|
|
498
466
|
if (!ctx)
|
|
499
|
-
return { status: 500, body: { error: ERROR_ACCOUNT_VANISHED } };
|
|
500
|
-
|
|
467
|
+
return { ok: false, status: 500, body: { error: ERROR_ACCOUNT_VANISHED } };
|
|
468
|
+
return { ok: true, request_context: ctx };
|
|
501
469
|
};
|
|
502
470
|
/**
|
|
503
471
|
* Create the route-spec authorization handler used by `apply_route_specs`.
|
|
504
472
|
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
* the phase entirely
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
* the same final response as if the validation step had parsed first.
|
|
473
|
+
* Reads `acting` off `c.var.validated_input` (or `c.var.validated_query`
|
|
474
|
+
* for GET routes) — input validation runs first, so the authorization
|
|
475
|
+
* phase consumes the typed Zod field instead of pre-parsing the body.
|
|
476
|
+
* Public routes (`auth.account === 'none' && auth.actor === 'none'`)
|
|
477
|
+
* skip the phase entirely.
|
|
478
|
+
*
|
|
479
|
+
* Per registry-time invariant 2, `auth.actor !== 'none'` ⟺ the input
|
|
480
|
+
* (or query) schema declares `acting?: ActingActor` — so reading from
|
|
481
|
+
* `c.var.validated_input.acting` / `c.var.validated_query.acting` is
|
|
482
|
+
* type-safe.
|
|
483
|
+
*
|
|
484
|
+
* Resolved contexts land on `REQUEST_CONTEXT_KEY` so the post-authorization
|
|
485
|
+
* REST middleware (`require_role`, `require_credential_types`) reads the
|
|
486
|
+
* actor-bound context off `c.var`. The HTTP RPC and WS dispatchers consume
|
|
487
|
+
* the `apply_authorization_phase` outcome directly without round-tripping
|
|
488
|
+
* through `c.var`.
|
|
522
489
|
*/
|
|
523
490
|
export const create_fuz_authorization_handler = (deps) => {
|
|
524
491
|
return async (c, spec) => {
|
|
525
|
-
|
|
492
|
+
// Test escape hatch: harnesses that pre-populate `REQUEST_CONTEXT_KEY`
|
|
493
|
+
// flag `TEST_CONTEXT_PRESET_KEY = true` so the authorization phase
|
|
494
|
+
// trusts the supplied context instead of running DB-backed resolution.
|
|
495
|
+
// Production middleware never sets this flag.
|
|
496
|
+
if (c.get(TEST_CONTEXT_PRESET_KEY))
|
|
526
497
|
return;
|
|
527
|
-
|
|
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)
|
|
498
|
+
if (is_public_auth(spec.auth))
|
|
536
499
|
return;
|
|
537
|
-
|
|
500
|
+
const acting_value = needs_actor(spec.auth) ? extract_validated_acting(c) : undefined;
|
|
501
|
+
const account_id = c.get(ACCOUNT_ID_KEY) ?? null;
|
|
502
|
+
const result = await apply_authorization_phase(deps, account_id, spec.auth, acting_value);
|
|
503
|
+
if (!result.ok)
|
|
504
|
+
return c.json(result.body, result.status);
|
|
505
|
+
if (result.request_context !== null) {
|
|
506
|
+
c.set(REQUEST_CONTEXT_KEY, result.request_context);
|
|
507
|
+
}
|
|
508
|
+
// `request_context: null` — public action or unauthenticated optional axis.
|
|
509
|
+
// Leave `REQUEST_CONTEXT_KEY` null; downstream `require_role` /
|
|
510
|
+
// `require_credential_types` enforce.
|
|
511
|
+
return;
|
|
538
512
|
};
|
|
539
513
|
};
|
|
540
514
|
/**
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
*
|
|
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.
|
|
515
|
+
* Read `acting` off the validated input (or validated query) on the Hono
|
|
516
|
+
* context. Input/query validation runs before the authorization phase,
|
|
517
|
+
* so this reads a typed Zod field — not the raw body.
|
|
518
|
+
*
|
|
519
|
+
* Returns `undefined` when `validated_input` / `validated_query` isn't
|
|
520
|
+
* set or doesn't carry `acting`. Per registry-time invariant 2, the
|
|
521
|
+
* dispatcher only calls this when `auth.actor !== 'none'`, which by
|
|
522
|
+
* the biconditional means the input schema declares
|
|
523
|
+
* `acting?: ActingActor`.
|
|
562
524
|
*/
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
return body.acting;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
catch {
|
|
574
|
-
c.set(CACHED_REQUEST_BODY_KEY, { ok: false });
|
|
575
|
-
}
|
|
525
|
+
const extract_validated_acting = (c) => {
|
|
526
|
+
const validated_input = c.get('validated_input');
|
|
527
|
+
if (validated_input && typeof validated_input.acting === 'string')
|
|
528
|
+
return validated_input.acting;
|
|
529
|
+
const validated_query = c.get('validated_query');
|
|
530
|
+
if (validated_query && typeof validated_query.acting === 'string')
|
|
531
|
+
return validated_query.acting;
|
|
576
532
|
return undefined;
|
|
577
533
|
};
|