@fuzdev/fuz_app 0.54.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 +214 -103
- 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 +32 -0
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +35 -15
- 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 +141 -22
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +106 -187
- 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 +46 -40
- 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 +15 -10
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +54 -7
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +0 -4
- 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 +794 -410
- 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 +7 -13
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +26 -35
- package/dist/auth/account_queries.d.ts +52 -16
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +87 -38
- package/dist/auth/account_routes.d.ts +9 -11
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +118 -46
- package/dist/auth/account_schema.d.ts +46 -35
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +21 -28
- package/dist/auth/admin_action_specs.d.ts +100 -32
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +64 -33
- package/dist/auth/admin_actions.d.ts +13 -19
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +37 -41
- 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 -48
- package/dist/auth/audit_log_queries.d.ts.map +1 -1
- package/dist/auth/audit_log_queries.js +20 -56
- 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 +92 -32
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +75 -46
- 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/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/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 -42
- 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 +23 -11
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +28 -22
- 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 -18
- 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 +9 -4
- package/dist/auth/migrations.d.ts +37 -14
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +79 -32
- package/dist/auth/request_context.d.ts +331 -61
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +378 -95
- package/dist/auth/{permit_offer_action_specs.d.ts → role_grant_offer_action_specs.d.ts} +163 -94
- 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/role_grant_offer_actions.js +473 -0
- package/dist/auth/{permit_offer_notifications.d.ts → role_grant_offer_notifications.d.ts} +90 -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/role_grant_offer_queries.d.ts +242 -0
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -0
- package/dist/auth/role_grant_offer_queries.js +533 -0
- 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} +60 -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 +6 -1
- package/dist/auth/self_service_role_action_specs.d.ts.map +1 -1
- package/dist/auth/self_service_role_action_specs.js +3 -1
- package/dist/auth/self_service_role_actions.d.ts +34 -27
- package/dist/auth/self_service_role_actions.d.ts.map +1 -1
- package/dist/auth/self_service_role_actions.js +68 -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 +12 -8
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +10 -7
- package/dist/dev/setup.d.ts +2 -2
- package/dist/dev/setup.d.ts.map +1 -1
- package/dist/dev/setup.js +9 -7
- package/dist/env/load.d.ts +1 -1
- package/dist/env/load.js +1 -1
- package/dist/hono_context.d.ts +64 -5
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/hono_context.js +38 -2
- package/dist/http/CLAUDE.md +264 -87
- 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 +132 -19
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +132 -40
- 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/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 +113 -41
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/route_spec.js +130 -52
- package/dist/http/schema_helpers.d.ts +3 -2
- package/dist/http/schema_helpers.d.ts.map +1 -1
- package/dist/http/schema_helpers.js +9 -2
- package/dist/http/surface.d.ts +2 -1
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +1 -2
- 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 +36 -31
- package/dist/server/validate_nginx.d.ts +1 -1
- package/dist/server/validate_nginx.js +1 -1
- package/dist/testing/CLAUDE.md +73 -55
- 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 +100 -96
- package/dist/testing/adversarial_headers.js +1 -1
- 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 +18 -17
- 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 +53 -39
- 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 +28 -22
- 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 +10 -8
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +22 -18
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +13 -14
- package/dist/testing/integration_helpers.d.ts +8 -6
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +29 -23
- package/dist/testing/middleware.d.ts +15 -11
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +75 -32
- package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
- package/dist/testing/rpc_attack_surface.js +40 -24
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +3 -1
- 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 +24 -12
- 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 +65 -59
- package/dist/ui/{PermitOfferForm.svelte → RoleGrantOfferForm.svelte} +37 -22
- package/dist/ui/RoleGrantOfferForm.svelte.d.ts +20 -0
- 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 +25 -18
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +28 -17
- 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} +39 -31
- 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} +25 -19
- 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 -227
- 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_actions.js +0 -452
- 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 +0 -183
- package/dist/auth/permit_offer_queries.d.ts.map +0 -1
- package/dist/auth/permit_offer_queries.js +0 -408
- package/dist/auth/permit_offer_schema.d.ts +0 -103
- package/dist/auth/permit_offer_schema.d.ts.map +0 -1
- package/dist/auth/permit_queries.d.ts +0 -210
- package/dist/auth/permit_queries.d.ts.map +0 -1
- package/dist/auth/permit_queries.js +0 -294
- 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 -21
- package/dist/auth/route_guards.d.ts.map +0 -1
- package/dist/auth/route_guards.js +0 -32
- 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 +0 -14
- 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,22 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Request context middleware and
|
|
2
|
+
* Request context middleware and role_grant 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 role_grants; `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 role_grants
|
|
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, role_grants}`
|
|
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 role_grant-requiring auth, so no actor
|
|
26
|
+
* is resolved and their handlers see a `RequestContext` with
|
|
27
|
+
* `actor: null` + empty `role_grants`. 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 → role_grants` and verifies
|
|
32
|
+
* the `actor.account_id === account.id` binding. `refresh_role_grants`
|
|
33
|
+
* reloads role_grants on an existing context.
|
|
11
34
|
*
|
|
12
35
|
* @module
|
|
13
36
|
*/
|
|
14
|
-
import {
|
|
37
|
+
import { is_role_grant_active } from './account_schema.js';
|
|
15
38
|
import { hash_session_token, session_touch_fire_and_forget, query_session_get_valid, } from './session_queries.js';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY } from '../hono_context.js';
|
|
19
|
-
import {
|
|
39
|
+
import { query_account_by_id, query_actor_by_id, query_actors_by_account, } from './account_queries.js';
|
|
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';
|
|
20
44
|
/** Hono context variable name for the request context. */
|
|
21
45
|
export const REQUEST_CONTEXT_KEY = 'request_context';
|
|
22
46
|
/**
|
|
@@ -41,25 +65,26 @@ export const get_request_context = (c) => {
|
|
|
41
65
|
/**
|
|
42
66
|
* Get the request context, throwing if unauthenticated.
|
|
43
67
|
*
|
|
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
|
|
68
|
+
* Use in route handlers where the dispatcher's authorization phase guarantees
|
|
69
|
+
* a context exists (i.e., routes with `auth: {type: 'authenticated'}` or
|
|
70
|
+
* stricter). Prefer this over `get_request_context(c)!` for explicit error
|
|
71
|
+
* handling.
|
|
47
72
|
*
|
|
48
73
|
* @param c - the Hono context
|
|
49
74
|
* @returns the request context (never null)
|
|
50
|
-
* @throws Error if no request context is set (
|
|
75
|
+
* @throws Error if no request context is set (dispatcher misconfiguration)
|
|
51
76
|
*/
|
|
52
77
|
export const require_request_context = (c) => {
|
|
53
78
|
const ctx = get_request_context(c);
|
|
54
79
|
if (!ctx) {
|
|
55
|
-
throw new Error('require_request_context: no request context — is
|
|
80
|
+
throw new Error('require_request_context: no request context — is the dispatcher authorization phase wired?');
|
|
56
81
|
}
|
|
57
82
|
return ctx;
|
|
58
83
|
};
|
|
59
84
|
/**
|
|
60
|
-
* Check if a request context has an active
|
|
85
|
+
* Check if a request context has an active role_grant for a given role.
|
|
61
86
|
*
|
|
62
|
-
* Checks the
|
|
87
|
+
* Checks the role_grants already loaded in the context (no DB query).
|
|
63
88
|
* Null-tolerant — `null` ctx (unauthenticated) returns `false`. Symmetric
|
|
64
89
|
* with `has_scoped_role` / `has_any_scoped_role` so the three helpers
|
|
65
90
|
* compose freely in the same predicate (e.g.
|
|
@@ -68,43 +93,47 @@ export const require_request_context = (c) => {
|
|
|
68
93
|
* @param ctx - the request context, or `null` for unauthenticated callers
|
|
69
94
|
* @param role - the role to check
|
|
70
95
|
* @param now - current time (defaults to `new Date()`, pass for testability and hot-path efficiency)
|
|
71
|
-
* @returns `true` if the actor has an active
|
|
96
|
+
* @returns `true` if the actor has an active role_grant for the role
|
|
72
97
|
*/
|
|
73
|
-
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;
|
|
74
99
|
/**
|
|
75
|
-
* Whether the request context holds an active
|
|
100
|
+
* Whether the request context holds an active role_grant for `role` at `scope_id`.
|
|
76
101
|
*
|
|
77
|
-
* Walks the in-memory `ctx.
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
102
|
+
* Walks the in-memory `ctx.role_grants` snapshot loaded once per request by
|
|
103
|
+
* the route-spec / RPC dispatcher's authorization phase (when the route
|
|
104
|
+
* declares `acting?: ActingActor` or has role_grant-requiring auth); zero DB
|
|
105
|
+
* roundtrip per check. The "freshness" framing of a SQL re-query is
|
|
106
|
+
* illusory because the race window is between predicate and the actual
|
|
107
|
+
* mutation, not predicate and authorization load. Closing that race needs
|
|
108
|
+
* a transactional re-check inside the UPDATE/INSERT, which neither style
|
|
109
|
+
* provides.
|
|
83
110
|
*
|
|
84
|
-
* Null-tolerant — `null` ctx (unauthenticated)
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
111
|
+
* Null-tolerant — `null` ctx (unauthenticated) and account-grain
|
|
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.
|
|
88
117
|
*
|
|
89
|
-
* `scope_id` semantics: in-memory `
|
|
118
|
+
* `scope_id` semantics: in-memory `role_grant.scope_id` is `string | null`, so
|
|
90
119
|
* JS `===` matches the SQL `IS NOT DISTINCT FROM` semantics exactly:
|
|
91
120
|
*
|
|
92
|
-
* - `scope_id === null` matches global
|
|
93
|
-
* - `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.
|
|
94
123
|
*
|
|
95
124
|
* @param ctx - the request context, or `null` for unauthenticated callers
|
|
96
125
|
* @param role - the role to check
|
|
97
126
|
* @param scope_id - the scope to check (`null` for global)
|
|
98
127
|
* @param now - current time (defaults to `new Date()`, pass for testability and hot-path efficiency)
|
|
99
|
-
* @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
|
|
100
129
|
*/
|
|
101
130
|
export const has_scoped_role = (ctx, role, scope_id, now = new Date()) => {
|
|
102
131
|
if (!ctx)
|
|
103
132
|
return false;
|
|
104
|
-
return ctx.
|
|
133
|
+
return ctx.role_grants.some((p) => p.role === role && p.scope_id === scope_id && is_role_grant_active(p, now));
|
|
105
134
|
};
|
|
106
135
|
/**
|
|
107
|
-
* Whether the request context holds an active
|
|
136
|
+
* Whether the request context holds an active role_grant for any role in `roles`
|
|
108
137
|
* at `scope_id`. Empty `roles` short-circuits to `false` — documents intent
|
|
109
138
|
* at the call site ("zero roles trivially admit no-one"). Same scope and
|
|
110
139
|
* null-tolerance semantics as `has_scoped_role`.
|
|
@@ -113,65 +142,95 @@ export const has_scoped_role = (ctx, role, scope_id, now = new Date()) => {
|
|
|
113
142
|
* @param roles - the roles that would admit the caller (any-of)
|
|
114
143
|
* @param scope_id - the scope to check (`null` for global)
|
|
115
144
|
* @param now - current time (defaults to `new Date()`, pass for testability)
|
|
116
|
-
* @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
|
|
117
146
|
*/
|
|
118
147
|
export const has_any_scoped_role = (ctx, roles, scope_id, now = new Date()) => {
|
|
119
148
|
if (!ctx)
|
|
120
149
|
return false;
|
|
121
150
|
if (roles.length === 0)
|
|
122
151
|
return false;
|
|
123
|
-
return ctx.
|
|
152
|
+
return ctx.role_grants.some((p) => roles.includes(p.role) && p.scope_id === scope_id && is_role_grant_active(p, now));
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Resolve the acting actor for an authenticated request.
|
|
156
|
+
*
|
|
157
|
+
* Called from the route-spec / RPC dispatcher's authorization phase
|
|
158
|
+
* with the authenticated account id and the validated `acting` value
|
|
159
|
+
* (from the request payload). Applies the uniform resolution rules:
|
|
160
|
+
*
|
|
161
|
+
* - `acting_actor_id` omitted + 1 actor → use it.
|
|
162
|
+
* - `acting_actor_id` omitted + 0 actors → `no_actors` (defensive —
|
|
163
|
+
* signup / bootstrap always create an actor in the same tx, so this
|
|
164
|
+
* is a server error).
|
|
165
|
+
* - `acting_actor_id` omitted + multiple actors → `actor_required` with
|
|
166
|
+
* the available list so the client can prompt; never pick silently.
|
|
167
|
+
* - `acting_actor_id` present + matches an actor on the account → use it.
|
|
168
|
+
* - `acting_actor_id` present + does not match → `actor_not_on_account`.
|
|
169
|
+
* The available list is intentionally not echoed in this branch (treat
|
|
170
|
+
* as opaque rejection).
|
|
171
|
+
*
|
|
172
|
+
* @param deps - query dependencies
|
|
173
|
+
* @param account_id - the authenticated account
|
|
174
|
+
* @param acting_actor_id - the requested acting actor id, or `undefined`
|
|
175
|
+
*/
|
|
176
|
+
export const resolve_acting_actor = async (deps, account_id, acting_actor_id) => {
|
|
177
|
+
const actors = await query_actors_by_account(deps, account_id);
|
|
178
|
+
if (actors.length === 0)
|
|
179
|
+
return { ok: false, reason: 'no_actors' };
|
|
180
|
+
if (acting_actor_id == null) {
|
|
181
|
+
if (actors.length === 1)
|
|
182
|
+
return { ok: true, actor_id: actors[0].id };
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
reason: 'actor_required',
|
|
186
|
+
available: actors.map((a) => ({ id: a.id, name: a.name })),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const match = actors.find((a) => a.id === acting_actor_id);
|
|
190
|
+
if (!match)
|
|
191
|
+
return { ok: false, reason: 'actor_not_on_account' };
|
|
192
|
+
return { ok: true, actor_id: match.id };
|
|
124
193
|
};
|
|
125
194
|
/**
|
|
126
|
-
* Create middleware that
|
|
195
|
+
* Create middleware that authenticates the account from a session cookie.
|
|
127
196
|
*
|
|
128
|
-
* Reads the session identity (set by session middleware), looks up
|
|
129
|
-
*
|
|
130
|
-
*
|
|
197
|
+
* Reads the session identity (set by session middleware), looks up the
|
|
198
|
+
* `auth_session`, and on a valid session sets `c.var.auth_account_id`,
|
|
199
|
+
* `CREDENTIAL_TYPE_KEY = 'session'`, and `AUTH_SESSION_TOKEN_HASH_KEY`.
|
|
200
|
+
* Touches the session (fire-and-forget). Does not load actor or role_grants;
|
|
201
|
+
* `REQUEST_CONTEXT_KEY` is left null — the route-spec / RPC dispatcher
|
|
202
|
+
* authorization phase resolves the acting actor and builds the full
|
|
203
|
+
* `RequestContext` when the route needs one.
|
|
131
204
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* `require_role` or `require_auth` for enforcement.
|
|
205
|
+
* Invalid / missing session leaves all keys null and calls `next()` —
|
|
206
|
+
* `require_auth` / `require_role` enforce.
|
|
135
207
|
*
|
|
136
208
|
* @param deps - query dependencies (pool-level db for middleware)
|
|
137
209
|
* @param log - the logger instance
|
|
138
210
|
* @param session_context_key - the Hono context key where session middleware stored the session token
|
|
139
|
-
* @mutates Hono context - sets `
|
|
211
|
+
* @mutates Hono context - sets `ACCOUNT_ID_KEY`, `CREDENTIAL_TYPE_KEY`, `AUTH_SESSION_TOKEN_HASH_KEY`, and `AUTH_API_TOKEN_ID_KEY`
|
|
140
212
|
*/
|
|
141
213
|
export const create_request_context_middleware = (deps, log, session_context_key = 'auth_session_id') => {
|
|
142
214
|
return async (c, next) => {
|
|
215
|
+
c.set(REQUEST_CONTEXT_KEY, null);
|
|
216
|
+
c.set(ACCOUNT_ID_KEY, null);
|
|
217
|
+
c.set(CREDENTIAL_TYPE_KEY, null);
|
|
218
|
+
c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
|
|
219
|
+
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
143
220
|
const session_token = c.get(session_context_key) ?? null;
|
|
144
221
|
if (!session_token) {
|
|
145
|
-
c.set(REQUEST_CONTEXT_KEY, null);
|
|
146
|
-
c.set(CREDENTIAL_TYPE_KEY, null);
|
|
147
|
-
c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
|
|
148
|
-
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
149
222
|
await next();
|
|
150
223
|
return;
|
|
151
224
|
}
|
|
152
225
|
const token_hash = hash_session_token(session_token);
|
|
153
226
|
const session = await query_session_get_valid(deps, token_hash);
|
|
154
227
|
if (!session) {
|
|
155
|
-
c.set(REQUEST_CONTEXT_KEY, null);
|
|
156
|
-
c.set(CREDENTIAL_TYPE_KEY, null);
|
|
157
|
-
c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
|
|
158
|
-
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
159
|
-
await next();
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
const ctx = await build_request_context(deps, session.account_id);
|
|
163
|
-
if (!ctx) {
|
|
164
|
-
c.set(REQUEST_CONTEXT_KEY, null);
|
|
165
|
-
c.set(CREDENTIAL_TYPE_KEY, null);
|
|
166
|
-
c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
|
|
167
|
-
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
168
228
|
await next();
|
|
169
229
|
return;
|
|
170
230
|
}
|
|
171
|
-
c.set(
|
|
231
|
+
c.set(ACCOUNT_ID_KEY, session.account_id);
|
|
172
232
|
c.set(CREDENTIAL_TYPE_KEY, 'session');
|
|
173
233
|
c.set(AUTH_SESSION_TOKEN_HASH_KEY, token_hash);
|
|
174
|
-
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
175
234
|
// Touch session (fire-and-forget, don't block the request)
|
|
176
235
|
void session_touch_fire_and_forget(deps, token_hash, c.var.pending_effects, log);
|
|
177
236
|
await next();
|
|
@@ -180,71 +239,295 @@ export const create_request_context_middleware = (deps, log, session_context_key
|
|
|
180
239
|
/**
|
|
181
240
|
* Middleware that requires authentication.
|
|
182
241
|
*
|
|
183
|
-
* Returns 401 if
|
|
242
|
+
* Returns 401 if the auth middleware did not set `c.var.auth_account_id`.
|
|
184
243
|
*/
|
|
185
244
|
export const require_auth = async (c, next) => {
|
|
186
|
-
|
|
187
|
-
if (!ctx) {
|
|
245
|
+
if (c.get(ACCOUNT_ID_KEY) == null) {
|
|
188
246
|
return c.json({ error: ERROR_AUTHENTICATION_REQUIRED }, 401);
|
|
189
247
|
}
|
|
190
248
|
await next();
|
|
191
249
|
};
|
|
192
250
|
/**
|
|
193
|
-
* Create middleware that requires
|
|
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`).
|
|
194
258
|
*
|
|
195
|
-
*
|
|
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.
|
|
196
266
|
*
|
|
197
|
-
*
|
|
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)
|
|
198
272
|
*/
|
|
199
|
-
export const require_role = (
|
|
273
|
+
export const require_role = (roles) => {
|
|
200
274
|
return async (c, next) => {
|
|
275
|
+
if (c.get(ACCOUNT_ID_KEY) == null) {
|
|
276
|
+
return c.json({ error: ERROR_AUTHENTICATION_REQUIRED }, 401);
|
|
277
|
+
}
|
|
201
278
|
const ctx = get_request_context(c);
|
|
202
|
-
if (!ctx) {
|
|
279
|
+
if (!ctx || !has_any_scoped_role(ctx, roles, null)) {
|
|
280
|
+
return c.json({ error: ERROR_INSUFFICIENT_PERMISSIONS, required_roles: roles }, 403);
|
|
281
|
+
}
|
|
282
|
+
await next();
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
/**
|
|
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) {
|
|
203
304
|
return c.json({ error: ERROR_AUTHENTICATION_REQUIRED }, 401);
|
|
204
305
|
}
|
|
205
|
-
|
|
206
|
-
|
|
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);
|
|
207
312
|
}
|
|
208
313
|
await next();
|
|
209
314
|
};
|
|
210
315
|
};
|
|
211
316
|
/**
|
|
212
|
-
* Reload active
|
|
317
|
+
* Reload active role_grants from the database, returning a new request context.
|
|
213
318
|
*
|
|
214
|
-
* Useful for long-lived WebSocket connections where
|
|
319
|
+
* Useful for long-lived WebSocket connections where role_grants may change
|
|
215
320
|
* (grant or revoke) during the connection lifetime. Call periodically
|
|
216
321
|
* or after receiving a revocation signal.
|
|
217
322
|
*
|
|
218
|
-
* Returns a new `RequestContext` with updated
|
|
219
|
-
* context is not mutated, making concurrent calls safe.
|
|
323
|
+
* Returns a new `RequestContext` with updated role_grants — the original
|
|
324
|
+
* context is not mutated, making concurrent calls safe. Throws when
|
|
325
|
+
* `ctx.actor` is null; account-grain contexts have no role_grants to refresh.
|
|
220
326
|
*
|
|
221
327
|
* @param ctx - the request context to refresh
|
|
222
328
|
* @param deps - query dependencies
|
|
223
|
-
* @returns a new `RequestContext` with fresh
|
|
329
|
+
* @returns a new `RequestContext` with fresh role_grants
|
|
330
|
+
* @throws Error when called on an account-grain context (`actor: null`)
|
|
224
331
|
*/
|
|
225
|
-
export const
|
|
226
|
-
|
|
227
|
-
|
|
332
|
+
export const refresh_role_grants = async (ctx, deps) => {
|
|
333
|
+
if (!ctx.actor) {
|
|
334
|
+
throw new Error('refresh_role_grants: account-grain context has no actor / role_grants to refresh');
|
|
335
|
+
}
|
|
336
|
+
const role_grants = await query_role_grant_find_active_for_actor(deps, ctx.actor.id);
|
|
337
|
+
return { ...ctx, role_grants };
|
|
228
338
|
};
|
|
229
339
|
/**
|
|
230
|
-
* Build a full `RequestContext` from an account id
|
|
340
|
+
* Build a full `RequestContext` from an account id and an explicit
|
|
341
|
+
* actor id (already resolved via `resolve_acting_actor`).
|
|
342
|
+
*
|
|
343
|
+
* Loads `account` + the named `actor` + the actor's active role_grants.
|
|
344
|
+
* Verifies the `actor.account_id === account.id` binding so downstream
|
|
345
|
+
* handlers can trust `ctx.actor.account_id === ctx.account.id`. Returns
|
|
346
|
+
* `null` when the account is missing, the actor is missing, or the
|
|
347
|
+
* actor doesn't belong to the supplied account.
|
|
231
348
|
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* the account or actor is not found.
|
|
349
|
+
* Called by the route-spec / RPC dispatcher's authorization phase for
|
|
350
|
+
* routes that need an acting actor; account-grain routes use
|
|
351
|
+
* `build_account_context` instead.
|
|
236
352
|
*
|
|
237
353
|
* @param deps - query dependencies
|
|
238
354
|
* @param account_id - the account to build context for
|
|
239
|
-
* @
|
|
355
|
+
* @param actor_id - the actor this request acts as
|
|
356
|
+
* @returns a request context, or `null` if account/actor not found or mismatched
|
|
240
357
|
*/
|
|
241
|
-
export const build_request_context = async (deps, account_id) => {
|
|
358
|
+
export const build_request_context = async (deps, account_id, actor_id) => {
|
|
242
359
|
const account = await query_account_by_id(deps, account_id);
|
|
243
360
|
if (!account)
|
|
244
361
|
return null;
|
|
245
|
-
const actor = await
|
|
362
|
+
const actor = await query_actor_by_id(deps, actor_id);
|
|
246
363
|
if (!actor)
|
|
247
364
|
return null;
|
|
248
|
-
|
|
249
|
-
|
|
365
|
+
if (actor.account_id !== account.id)
|
|
366
|
+
return null;
|
|
367
|
+
const role_grants = await query_role_grant_find_active_for_actor(deps, actor.id);
|
|
368
|
+
return { account, actor, role_grants };
|
|
369
|
+
};
|
|
370
|
+
/**
|
|
371
|
+
* Build an account-only `RequestContext` (no actor, no role_grants) from
|
|
372
|
+
* an account id.
|
|
373
|
+
*
|
|
374
|
+
* Used by the dispatcher's authorization phase for authenticated routes
|
|
375
|
+
* that don't need an acting actor — account-grain operations (logout,
|
|
376
|
+
* password change, account self-service). Lets handlers read
|
|
377
|
+
* `auth.account.id` / `auth.account.username` uniformly with role_grant-bound
|
|
378
|
+
* routes; the cost is one extra `query_account_by_id` per request.
|
|
379
|
+
*
|
|
380
|
+
* Returns `null` when the account row is missing (e.g. deleted between
|
|
381
|
+
* the auth middleware's session lookup and the dispatcher) — caller
|
|
382
|
+
* surfaces that as a 500 since it represents a torn read.
|
|
383
|
+
*
|
|
384
|
+
* @param deps - query dependencies
|
|
385
|
+
* @param account_id - the account to build context for
|
|
386
|
+
* @returns an account-only request context, or `null` if the account is missing
|
|
387
|
+
*/
|
|
388
|
+
export const build_account_context = async (deps, account_id) => {
|
|
389
|
+
const account = await query_account_by_id(deps, account_id);
|
|
390
|
+
if (!account)
|
|
391
|
+
return null;
|
|
392
|
+
return { account, actor: null, role_grants: [] };
|
|
393
|
+
};
|
|
394
|
+
/**
|
|
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).
|
|
427
|
+
*/
|
|
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 };
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
ok: false,
|
|
456
|
+
status: 400,
|
|
457
|
+
body: { error: ERROR_ACTOR_REQUIRED, available: acting.available },
|
|
458
|
+
};
|
|
459
|
+
}
|
|
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 } };
|
|
464
|
+
}
|
|
465
|
+
const ctx = await build_request_context(deps, account_id, acting.actor_id);
|
|
466
|
+
if (!ctx)
|
|
467
|
+
return { ok: false, status: 500, body: { error: ERROR_ACCOUNT_VANISHED } };
|
|
468
|
+
return { ok: true, request_context: ctx };
|
|
469
|
+
};
|
|
470
|
+
/**
|
|
471
|
+
* Create the route-spec authorization handler used by `apply_route_specs`.
|
|
472
|
+
*
|
|
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`.
|
|
489
|
+
*/
|
|
490
|
+
export const create_fuz_authorization_handler = (deps) => {
|
|
491
|
+
return async (c, spec) => {
|
|
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))
|
|
497
|
+
return;
|
|
498
|
+
if (is_public_auth(spec.auth))
|
|
499
|
+
return;
|
|
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;
|
|
512
|
+
};
|
|
513
|
+
};
|
|
514
|
+
/**
|
|
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`.
|
|
524
|
+
*/
|
|
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;
|
|
532
|
+
return undefined;
|
|
250
533
|
};
|