@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
|
@@ -2,19 +2,24 @@
|
|
|
2
2
|
* Unified self-service role toggle RPC action.
|
|
3
3
|
*
|
|
4
4
|
* One static `request_response` action — `self_service_role_set` — that
|
|
5
|
-
* takes `{role, enabled}` and toggles a global
|
|
5
|
+
* takes `{role, enabled}` and toggles a global role_grant on the caller for an
|
|
6
6
|
* allowlisted role. Idempotent in both directions: re-enabling an
|
|
7
7
|
* already-held role returns `changed: false`; disabling a role the caller
|
|
8
8
|
* doesn't hold returns `changed: false`.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Eligibility is derived by default from `RoleSpec.grant_paths` —
|
|
11
|
+
* every role whose `grant_paths` includes `'self_service'`
|
|
12
|
+
* (`GRANT_PATH_SELF_SERVICE`) is eligible. The factory accepts an
|
|
13
|
+
* optional `eligible_roles` override (validated against the supplied
|
|
14
|
+
* `roles.role_specs` at factory time so typos surface at startup
|
|
15
|
+
* instead of at first call) for deployments that want to lock the
|
|
16
|
+
* surface down further than the role spec declares. Roles outside
|
|
17
|
+
* the eligible set are rejected with `forbidden` + reason
|
|
18
|
+
* `role_not_self_service_eligible`.
|
|
14
19
|
*
|
|
15
20
|
* Audit metadata carries `self_service: true` so admin reviewers can
|
|
16
|
-
* distinguish self-toggled
|
|
17
|
-
* `
|
|
21
|
+
* distinguish self-toggled role_grants from admin grants/offers. The
|
|
22
|
+
* `role_grant_create` / `role_grant_revoke` metadata schemas declare
|
|
18
23
|
* `self_service: z.boolean().optional()` explicitly, so the field is
|
|
19
24
|
* part of the documented schema surface and is round-trip-validated by
|
|
20
25
|
* `query_audit_log`.
|
|
@@ -22,7 +27,7 @@
|
|
|
22
27
|
* Static method name — `role` lives in the input, not the method name —
|
|
23
28
|
* so the spec is codegen-compatible (`satisfies RequestResponseActionSpec`)
|
|
24
29
|
* and the surface stays constant as consumers add eligible roles. Mirrors
|
|
25
|
-
* the existing `
|
|
30
|
+
* the existing `role_grant_offer_create({role})` precedent rather than
|
|
26
31
|
* generating per-role methods.
|
|
27
32
|
*
|
|
28
33
|
* Specs and schemas live in `auth/self_service_role_action_specs.ts` so
|
|
@@ -33,31 +38,31 @@
|
|
|
33
38
|
*/
|
|
34
39
|
import { rpc_action } from '../actions/action_rpc.js';
|
|
35
40
|
import { jsonrpc_errors } from '../http/jsonrpc_errors.js';
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
41
|
+
import { BUILTIN_ROLE_SPECS_BY_NAME, list_roles_with_grant_path, } from './role_schema.js';
|
|
42
|
+
import { GRANT_PATH_SELF_SERVICE } from './grant_path_schema.js';
|
|
43
|
+
import { query_create_role_grant, query_revoke_role_grant } from './role_grant_queries.js';
|
|
44
|
+
import { is_role_grant_active } from './account_schema.js';
|
|
39
45
|
import { has_scoped_role } from './request_context.js';
|
|
40
46
|
import { ERROR_ROLE_NOT_SELF_SERVICE_ELIGIBLE, self_service_role_set_action_spec, } from './self_service_role_action_specs.js';
|
|
41
|
-
const require_request_auth = (auth) => {
|
|
42
|
-
if (!auth)
|
|
43
|
-
throw new Error('unreachable: action auth guard did not enforce authentication');
|
|
44
|
-
return auth;
|
|
45
|
-
};
|
|
46
47
|
/**
|
|
47
48
|
* Build the unified self-service role toggle RPC action.
|
|
48
49
|
*
|
|
49
|
-
* @param deps - `
|
|
50
|
-
*
|
|
50
|
+
* @param deps - `RouteFactoryDeps` (`log`, `audit`, …); `audit.emit` writes
|
|
51
|
+
* audit rows via the captured pool. The bound emitter encapsulates
|
|
52
|
+
* `on_audit_event` fan-out and the optional `AuditLogConfig`.
|
|
53
|
+
* @param options - optional eligible-role override plus optional role schema for default-eligibility derivation
|
|
51
54
|
* @returns the `RpcAction` array to spread into a `create_rpc_endpoint` call
|
|
52
|
-
* @throws Error at factory time if any `eligible_roles` entry is missing from `options.roles.
|
|
55
|
+
* @throws Error at factory time if any `eligible_roles` entry is missing from `options.roles.role_specs`
|
|
53
56
|
*/
|
|
54
|
-
export const create_self_service_role_actions = (deps, options) => {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
export const create_self_service_role_actions = (deps, options = {}) => {
|
|
58
|
+
const role_specs = options.roles?.role_specs ?? BUILTIN_ROLE_SPECS_BY_NAME;
|
|
59
|
+
const eligible = options.eligible_roles
|
|
60
|
+
? new Set(options.eligible_roles)
|
|
61
|
+
: new Set(list_roles_with_grant_path(role_specs, GRANT_PATH_SELF_SERVICE));
|
|
62
|
+
if (options.eligible_roles && options.roles) {
|
|
58
63
|
for (const r of eligible) {
|
|
59
|
-
if (!
|
|
60
|
-
throw new Error(`create_self_service_role_actions: eligible_roles entry "${r}" is not registered in roles.
|
|
64
|
+
if (!role_specs.has(r)) {
|
|
65
|
+
throw new Error(`create_self_service_role_actions: eligible_roles entry "${r}" is not registered in roles.role_specs — typo or missing call to create_role_schema`);
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
68
|
}
|
|
@@ -69,69 +74,84 @@ export const create_self_service_role_actions = (deps, options) => {
|
|
|
69
74
|
}
|
|
70
75
|
};
|
|
71
76
|
const handler = async (input, ctx) => {
|
|
72
|
-
const auth =
|
|
77
|
+
const auth = ctx.auth;
|
|
73
78
|
reject_if_ineligible(input.role);
|
|
74
79
|
if (input.enabled) {
|
|
75
|
-
// Pre-check for idempotent re-grant. `
|
|
76
|
-
// idempotent (returns the existing
|
|
80
|
+
// Pre-check for idempotent re-grant. `query_create_role_grant` is itself
|
|
81
|
+
// idempotent (returns the existing role_grant instead of inserting), but
|
|
77
82
|
// it doesn't signal "already existed" vs "newly inserted" — so we
|
|
78
|
-
// peek first. Reads from the in-memory `auth.
|
|
83
|
+
// peek first. Reads from the in-memory `auth.role_grants` snapshot
|
|
79
84
|
// (no DB roundtrip). The TOCTOU window is benign for self-service:
|
|
80
|
-
// two concurrent grants both observe "no
|
|
81
|
-
// `
|
|
85
|
+
// two concurrent grants both observe "no role_grant", both call
|
|
86
|
+
// `query_create_role_grant`, and one collapses onto the other inside the
|
|
82
87
|
// query's `ON CONFLICT DO NOTHING`. Worst case both responses report
|
|
83
|
-
// `changed: true`; the DB still ends up with exactly one
|
|
88
|
+
// `changed: true`; the DB still ends up with exactly one role_grant.
|
|
84
89
|
if (has_scoped_role(auth, input.role, null)) {
|
|
85
90
|
return { ok: true, enabled: true, changed: false };
|
|
86
91
|
}
|
|
87
|
-
const
|
|
92
|
+
const role_grant = await query_create_role_grant(ctx, {
|
|
88
93
|
actor_id: auth.actor.id,
|
|
89
94
|
role: input.role,
|
|
90
95
|
scope_id: null,
|
|
91
96
|
expires_at: null,
|
|
92
97
|
granted_by: auth.actor.id,
|
|
93
98
|
});
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
// `role_grant_create` is the canonical actor-bound-subject event —
|
|
100
|
+
// populate both target columns even on self-service so the
|
|
101
|
+
// "always populated for role_grant_create" rule holds uniformly
|
|
102
|
+
// regardless of who initiated the grant. On self-service the
|
|
103
|
+
// grantor and grantee are the same identity; admin direct-grant
|
|
104
|
+
// (separate code path) populates the same columns with the
|
|
105
|
+
// grantee actor.
|
|
106
|
+
deps.audit.emit(ctx, {
|
|
107
|
+
event_type: 'role_grant_create',
|
|
96
108
|
actor_id: auth.actor.id,
|
|
97
109
|
account_id: auth.account.id,
|
|
110
|
+
target_account_id: auth.account.id,
|
|
111
|
+
target_actor_id: auth.actor.id,
|
|
98
112
|
ip: ctx.client_ip,
|
|
99
113
|
metadata: {
|
|
100
|
-
role:
|
|
101
|
-
|
|
102
|
-
scope_id:
|
|
114
|
+
role: role_grant.role,
|
|
115
|
+
role_grant_id: role_grant.id,
|
|
116
|
+
scope_id: role_grant.scope_id,
|
|
103
117
|
self_service: true,
|
|
104
118
|
},
|
|
105
|
-
}
|
|
119
|
+
});
|
|
106
120
|
return { ok: true, enabled: true, changed: true };
|
|
107
121
|
}
|
|
108
|
-
// Find an active global
|
|
109
|
-
// `auth.
|
|
122
|
+
// Find an active global role_grant for this (actor, role) in the in-memory
|
|
123
|
+
// `auth.role_grants` snapshot. No DB roundtrip — same correctness-equivalent
|
|
110
124
|
// pattern as `has_scoped_role` above (race window is between predicate
|
|
111
|
-
// and `
|
|
125
|
+
// and `query_revoke_role_grant`'s actual UPDATE, not between predicate and
|
|
112
126
|
// middleware load).
|
|
113
127
|
const now = new Date();
|
|
114
|
-
const target = auth.
|
|
128
|
+
const target = auth.role_grants.find((p) => p.role === input.role && p.scope_id === null && is_role_grant_active(p, now));
|
|
115
129
|
if (!target) {
|
|
116
130
|
return { ok: true, enabled: false, changed: false };
|
|
117
131
|
}
|
|
118
|
-
const result = await
|
|
132
|
+
const result = await query_revoke_role_grant(ctx, target.id, auth.actor.id, auth.actor.id);
|
|
119
133
|
if (!result) {
|
|
120
134
|
// Raced with another revoker — treat as already revoked.
|
|
121
135
|
return { ok: true, enabled: false, changed: false };
|
|
122
136
|
}
|
|
123
|
-
|
|
124
|
-
|
|
137
|
+
// Same actor-bound rule as the grant branch — `role_grant_revoke`
|
|
138
|
+
// always populates both target columns even on self-service so
|
|
139
|
+
// forensic queries that filter on `target_actor_id IS NOT NULL`
|
|
140
|
+
// don't silently miss self-toggled role_grants.
|
|
141
|
+
deps.audit.emit(ctx, {
|
|
142
|
+
event_type: 'role_grant_revoke',
|
|
125
143
|
actor_id: auth.actor.id,
|
|
126
144
|
account_id: auth.account.id,
|
|
145
|
+
target_account_id: auth.account.id,
|
|
146
|
+
target_actor_id: auth.actor.id,
|
|
127
147
|
ip: ctx.client_ip,
|
|
128
148
|
metadata: {
|
|
129
149
|
role: result.role,
|
|
130
|
-
|
|
150
|
+
role_grant_id: result.id,
|
|
131
151
|
scope_id: result.scope_id,
|
|
132
152
|
self_service: true,
|
|
133
153
|
},
|
|
134
|
-
}
|
|
154
|
+
});
|
|
135
155
|
return { ok: true, enabled: false, changed: true };
|
|
136
156
|
};
|
|
137
157
|
return [rpc_action(self_service_role_set_action_spec, handler)];
|
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
import type { Keyring } from './keyring.js';
|
|
13
13
|
/** Cookie max age in seconds (30 days — aligned with AUTH_SESSION_LIFETIME_MS). */
|
|
14
14
|
export declare const SESSION_AGE_MAX: number;
|
|
15
|
+
/**
|
|
16
|
+
* Threshold (seconds) at which `process_session_cookie` re-signs a still-valid
|
|
17
|
+
* cookie to extend its embedded expiration. Mirrors the DB-side
|
|
18
|
+
* `AUTH_SESSION_EXTEND_THRESHOLD_MS` so a continuously-active user's cookie
|
|
19
|
+
* stays in sync with their server-side session lifetime. Set
|
|
20
|
+
* `SessionOptions.refresh_threshold_seconds = 0` to disable.
|
|
21
|
+
*/
|
|
22
|
+
export declare const SESSION_REFRESH_THRESHOLD_S: number;
|
|
15
23
|
/**
|
|
16
24
|
* Cookie options for session cookies.
|
|
17
25
|
*/
|
|
@@ -47,9 +55,9 @@ export declare const SESSION_COOKIE_OPTIONS: SessionCookieOptions;
|
|
|
47
55
|
*
|
|
48
56
|
* @example
|
|
49
57
|
* ```ts
|
|
50
|
-
* //
|
|
51
|
-
* const
|
|
52
|
-
* cookie_name: '
|
|
58
|
+
* // zap: 3-part format (admin:session_id)
|
|
59
|
+
* const zap_config: SessionOptions<string> = {
|
|
60
|
+
* cookie_name: 'zap_session',
|
|
53
61
|
* context_key: 'auth_session_id',
|
|
54
62
|
* encode_identity: (session_id) => `admin:${session_id}`,
|
|
55
63
|
* decode_identity: (payload) => {
|
|
@@ -75,8 +83,23 @@ export interface SessionOptions<TIdentity> {
|
|
|
75
83
|
cookie_name: string;
|
|
76
84
|
/** Hono context variable name for the identity. */
|
|
77
85
|
context_key: string;
|
|
86
|
+
/**
|
|
87
|
+
* Cookie lifetime in seconds. Single source of truth for both the embedded
|
|
88
|
+
* `expires_at` (via `create_session_cookie_value`) and the cookie's HTTP
|
|
89
|
+
* `Max-Age` attribute (via `set_session_cookie`). Defaults to
|
|
90
|
+
* `SESSION_AGE_MAX` (30 days). The `cookie_options` slot intentionally
|
|
91
|
+
* cannot carry `maxAge` so the two values can't drift.
|
|
92
|
+
*/
|
|
78
93
|
max_age?: number;
|
|
79
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Threshold (seconds) for expiration-based cookie refresh. When a parsed
|
|
96
|
+
* cookie's `expires_at - now <= refresh_threshold_seconds`,
|
|
97
|
+
* `process_session_cookie` returns `action: 'refresh'` with a freshly-signed
|
|
98
|
+
* value (extending the embedded expiration by `max_age`). Defaults to
|
|
99
|
+
* `SESSION_REFRESH_THRESHOLD_S` (1 day). Set to `0` to disable.
|
|
100
|
+
*/
|
|
101
|
+
refresh_threshold_seconds?: number;
|
|
102
|
+
cookie_options?: Partial<Omit<SessionCookieOptions, 'maxAge'>>;
|
|
80
103
|
/** Encode identity into the cookie payload (before the `:expires_at` suffix). */
|
|
81
104
|
encode_identity: (identity: TIdentity) => string;
|
|
82
105
|
/** Decode identity from cookie payload. Return null if invalid. */
|
|
@@ -90,6 +113,14 @@ export interface ParsedSession<TIdentity> {
|
|
|
90
113
|
identity: TIdentity;
|
|
91
114
|
/** True if verified with a non-primary key (needs re-signing). */
|
|
92
115
|
should_refresh_signature: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* True if the embedded `expires_at` is within
|
|
118
|
+
* `options.refresh_threshold_seconds` of `now`. Signals that the cookie is
|
|
119
|
+
* valid but should be re-signed to extend its lifetime — mirrors
|
|
120
|
+
* `query_session_touch`'s DB-side extension so the cookie and server
|
|
121
|
+
* session don't drift. Always false when the threshold is `0`.
|
|
122
|
+
*/
|
|
123
|
+
should_refresh_expiration: boolean;
|
|
93
124
|
/** Index of the key that verified the signature. */
|
|
94
125
|
key_index: number;
|
|
95
126
|
}
|
|
@@ -97,7 +128,9 @@ export interface ParsedSession<TIdentity> {
|
|
|
97
128
|
* Parse a signed session cookie value.
|
|
98
129
|
*
|
|
99
130
|
* The signed value format is `${encode(identity)}:${expires_at}`.
|
|
100
|
-
* Tries all keys in order to support key rotation.
|
|
131
|
+
* Tries all keys in order to support key rotation. The result's
|
|
132
|
+
* `should_refresh_expiration` flag fires when the cookie is within
|
|
133
|
+
* `options.refresh_threshold_seconds` of `expires_at`.
|
|
101
134
|
*
|
|
102
135
|
* @param signed_value - the raw cookie value (signed)
|
|
103
136
|
* @param keyring - key ring for verification
|
|
@@ -139,7 +172,7 @@ export interface ProcessSessionResult<TIdentity> {
|
|
|
139
172
|
* server hashes it (blake3) to look up the `auth_session` row.
|
|
140
173
|
* Only the `cookie_name` varies per app.
|
|
141
174
|
*
|
|
142
|
-
* @param cookie_name - cookie name (e.g. `'
|
|
175
|
+
* @param cookie_name - cookie name (e.g. `'zap_session'`, `'visiones_session'`)
|
|
143
176
|
* @returns a `SessionOptions<string>` ready for use with session middleware
|
|
144
177
|
*/
|
|
145
178
|
export declare const create_session_config: (cookie_name: string) => SessionOptions<string>;
|
|
@@ -148,6 +181,10 @@ export declare const fuz_session_config: SessionOptions<string>;
|
|
|
148
181
|
/**
|
|
149
182
|
* Process a session cookie and determine what action to take.
|
|
150
183
|
*
|
|
184
|
+
* `action: 'refresh'` fires on key rotation **or** impending expiration
|
|
185
|
+
* (within `options.refresh_threshold_seconds`); both produce a freshly-signed
|
|
186
|
+
* `new_signed_value`.
|
|
187
|
+
*
|
|
151
188
|
* @param signed_value - the raw cookie value (may be undefined)
|
|
152
189
|
* @param keyring - key ring for verification and signing
|
|
153
190
|
* @param options - session configuration
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session_cookie.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_cookie.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,cAAc,CAAC;AAE1C,mFAAmF;AACnF,eAAO,MAAM,eAAe,QAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"session_cookie.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_cookie.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,cAAc,CAAC;AAE1C,mFAAmF;AACnF,eAAO,MAAM,eAAe,QAAoB,CAAC;AAEjD;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,QAAe,CAAC;AAQxD;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,oBAMpC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,WAAW,cAAc,CAAC,SAAS;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,cAAc,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC/D,iFAAiF;IACjF,eAAe,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,MAAM,CAAC;IACjD,mEAAmE;IACnE,eAAe,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,GAAG,IAAI,CAAC;CACvD;AAED;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,SAAS;IACvC,4BAA4B;IAC5B,QAAQ,EAAE,SAAS,CAAC;IACpB,kEAAkE;IAClE,wBAAwB,EAAE,OAAO,CAAC;IAClC;;;;;;OAMG;IACH,yBAAyB,EAAE,OAAO,CAAC;IACnC,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa,GAAU,SAAS,EAC5C,cAAc,MAAM,GAAG,SAAS,EAChC,SAAS,OAAO,EAChB,SAAS,cAAc,CAAC,SAAS,CAAC,EAClC,cAAc,MAAM,KAClB,OAAO,CAAC,aAAa,CAAC,SAAS,CAAC,GAAG,IAAI,GAAG,SAAS,CAoCrD,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,2BAA2B,GAAU,SAAS,EAC1D,SAAS,OAAO,EAChB,UAAU,SAAS,EACnB,SAAS,cAAc,CAAC,SAAS,CAAC,EAClC,cAAc,MAAM,KAClB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,oBAAoB,CAAC,SAAS;IAC9C,oCAAoC;IACpC,KAAK,EAAE,OAAO,CAAC;IACf,sCAAsC;IACtC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;IACrC,iDAAiD;IACjD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oDAAoD;IACpD,QAAQ,CAAC,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,GAAI,aAAa,MAAM,KAAG,cAAc,CAAC,MAAM,CAK/E,CAAC;AAEH,iDAAiD;AACjD,eAAO,MAAM,kBAAkB,EAAE,cAAc,CAAC,MAAM,CAAwC,CAAC;AAE/F;;;;;;;;;;;;GAYG;AAEH,eAAO,MAAM,sBAAsB,GAAU,SAAS,EACrD,cAAc,MAAM,GAAG,SAAS,EAChC,SAAS,OAAO,EAChB,SAAS,cAAc,CAAC,SAAS,CAAC,EAClC,cAAc,MAAM,KAClB,OAAO,CAAC,oBAAoB,CAAC,SAAS,CAAC,CA8BzC,CAAC"}
|
|
@@ -11,8 +11,18 @@
|
|
|
11
11
|
*/
|
|
12
12
|
/** Cookie max age in seconds (30 days — aligned with AUTH_SESSION_LIFETIME_MS). */
|
|
13
13
|
export const SESSION_AGE_MAX = 60 * 60 * 24 * 30;
|
|
14
|
+
/**
|
|
15
|
+
* Threshold (seconds) at which `process_session_cookie` re-signs a still-valid
|
|
16
|
+
* cookie to extend its embedded expiration. Mirrors the DB-side
|
|
17
|
+
* `AUTH_SESSION_EXTEND_THRESHOLD_MS` so a continuously-active user's cookie
|
|
18
|
+
* stays in sync with their server-side session lifetime. Set
|
|
19
|
+
* `SessionOptions.refresh_threshold_seconds = 0` to disable.
|
|
20
|
+
*/
|
|
21
|
+
export const SESSION_REFRESH_THRESHOLD_S = 60 * 60 * 24;
|
|
14
22
|
/** Separator between identity payload and expires_at in signed value. */
|
|
15
23
|
const VALUE_SEPARATOR = ':';
|
|
24
|
+
/** Strict integer matcher for the embedded `expires_at` field. */
|
|
25
|
+
const EXPIRES_AT_INTEGER_RE = /^\d+$/;
|
|
16
26
|
/**
|
|
17
27
|
* Default cookie options for session cookies.
|
|
18
28
|
*
|
|
@@ -32,7 +42,9 @@ export const SESSION_COOKIE_OPTIONS = {
|
|
|
32
42
|
* Parse a signed session cookie value.
|
|
33
43
|
*
|
|
34
44
|
* The signed value format is `${encode(identity)}:${expires_at}`.
|
|
35
|
-
* Tries all keys in order to support key rotation.
|
|
45
|
+
* Tries all keys in order to support key rotation. The result's
|
|
46
|
+
* `should_refresh_expiration` flag fires when the cookie is within
|
|
47
|
+
* `options.refresh_threshold_seconds` of `expires_at`.
|
|
36
48
|
*
|
|
37
49
|
* @param signed_value - the raw cookie value (signed)
|
|
38
50
|
* @param keyring - key ring for verification
|
|
@@ -55,6 +67,11 @@ export const parse_session = async (signed_value, keyring, options, now_seconds)
|
|
|
55
67
|
const identity = options.decode_identity(identity_payload);
|
|
56
68
|
if (identity === null)
|
|
57
69
|
return null;
|
|
70
|
+
// Strict integer match — `parseInt` would accept `'123abc'` as `123` and
|
|
71
|
+
// `' 123'` as `123`. HMAC integrity makes such a tampered value
|
|
72
|
+
// theoretical, but stricter parsing closes the gap as defense-in-depth.
|
|
73
|
+
if (!EXPIRES_AT_INTEGER_RE.test(expires_at_str))
|
|
74
|
+
return null;
|
|
58
75
|
const expires_at = parseInt(expires_at_str, 10);
|
|
59
76
|
if (!Number.isFinite(expires_at))
|
|
60
77
|
return null;
|
|
@@ -62,9 +79,12 @@ export const parse_session = async (signed_value, keyring, options, now_seconds)
|
|
|
62
79
|
const now = now_seconds ?? Math.floor(Date.now() / 1000);
|
|
63
80
|
if (expires_at <= now)
|
|
64
81
|
return null;
|
|
82
|
+
const refresh_threshold = options.refresh_threshold_seconds ?? SESSION_REFRESH_THRESHOLD_S;
|
|
83
|
+
const should_refresh_expiration = refresh_threshold > 0 && expires_at - now <= refresh_threshold;
|
|
65
84
|
return {
|
|
66
85
|
identity,
|
|
67
86
|
should_refresh_signature: result.key_index > 0,
|
|
87
|
+
should_refresh_expiration,
|
|
68
88
|
key_index: result.key_index,
|
|
69
89
|
};
|
|
70
90
|
};
|
|
@@ -94,7 +114,7 @@ export const create_session_cookie_value = async (keyring, identity, options, no
|
|
|
94
114
|
* server hashes it (blake3) to look up the `auth_session` row.
|
|
95
115
|
* Only the `cookie_name` varies per app.
|
|
96
116
|
*
|
|
97
|
-
* @param cookie_name - cookie name (e.g. `'
|
|
117
|
+
* @param cookie_name - cookie name (e.g. `'zap_session'`, `'visiones_session'`)
|
|
98
118
|
* @returns a `SessionOptions<string>` ready for use with session middleware
|
|
99
119
|
*/
|
|
100
120
|
export const create_session_config = (cookie_name) => ({
|
|
@@ -108,6 +128,10 @@ export const fuz_session_config = create_session_config('fuz_session');
|
|
|
108
128
|
/**
|
|
109
129
|
* Process a session cookie and determine what action to take.
|
|
110
130
|
*
|
|
131
|
+
* `action: 'refresh'` fires on key rotation **or** impending expiration
|
|
132
|
+
* (within `options.refresh_threshold_seconds`); both produce a freshly-signed
|
|
133
|
+
* `new_signed_value`.
|
|
134
|
+
*
|
|
111
135
|
* @param signed_value - the raw cookie value (may be undefined)
|
|
112
136
|
* @param keyring - key ring for verification and signing
|
|
113
137
|
* @param options - session configuration
|
|
@@ -125,9 +149,11 @@ export const process_session_cookie = async (signed_value, keyring, options, now
|
|
|
125
149
|
// Invalid cookie - should be cleared
|
|
126
150
|
return { valid: false, action: 'clear' };
|
|
127
151
|
}
|
|
128
|
-
// Valid session
|
|
129
|
-
|
|
130
|
-
|
|
152
|
+
// Valid session — re-sign if the verifying key isn't primary OR if the
|
|
153
|
+
// embedded expiration is approaching the threshold. The latter mirrors
|
|
154
|
+
// `query_session_touch`'s DB-side extension so a continuously-active user's
|
|
155
|
+
// cookie doesn't hard-expire while their server session is still alive.
|
|
156
|
+
if (parsed.should_refresh_signature || parsed.should_refresh_expiration) {
|
|
131
157
|
const new_signed_value = await create_session_cookie_value(keyring, parsed.identity, options, now);
|
|
132
158
|
return { valid: true, action: 'refresh', new_signed_value, identity: parsed.identity };
|
|
133
159
|
}
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hono session
|
|
3
|
-
*
|
|
4
|
-
* Thin wrapper that gets/sets cookies and delegates to session processing.
|
|
2
|
+
* Hono session boundary — cookie I/O, request-time middleware, and the
|
|
3
|
+
* session-creation helper shared by login / signup / bootstrap.
|
|
5
4
|
*
|
|
6
5
|
* @module
|
|
7
6
|
*/
|
|
8
7
|
import type { Context, MiddlewareHandler } from 'hono';
|
|
9
8
|
import type { Keyring } from './keyring.js';
|
|
10
9
|
import { type SessionOptions } from './session_cookie.js';
|
|
10
|
+
import type { QueryDeps } from '../db/query_deps.js';
|
|
11
11
|
/**
|
|
12
12
|
* Read the session cookie value from a request.
|
|
13
13
|
*/
|
|
14
14
|
export declare const get_session_cookie: <T>(c: Context, options: SessionOptions<T>) => string | undefined;
|
|
15
15
|
/**
|
|
16
16
|
* Set the session cookie on a response.
|
|
17
|
+
*
|
|
18
|
+
* `options.max_age` is the single source of truth for cookie lifetime: it
|
|
19
|
+
* drives both the embedded `expires_at` (via `create_session_cookie_value`)
|
|
20
|
+
* and the cookie's HTTP `Max-Age` attribute set here. Falls back to
|
|
21
|
+
* `SESSION_COOKIE_OPTIONS.maxAge` (= `SESSION_AGE_MAX`) when unset.
|
|
22
|
+
* `options.cookie_options` cannot carry `maxAge` (omitted in the type) so
|
|
23
|
+
* the two values can't drift.
|
|
17
24
|
*/
|
|
18
25
|
export declare const set_session_cookie: <T>(c: Context, value: string, options: SessionOptions<T>) => void;
|
|
19
26
|
/**
|
|
@@ -31,4 +38,31 @@ export declare const clear_session_cookie: <T>(c: Context, options: SessionOptio
|
|
|
31
38
|
* @mutates Hono context - sets `options.context_key` and may refresh or clear the session cookie
|
|
32
39
|
*/
|
|
33
40
|
export declare const create_session_middleware: <TIdentity>(keyring: Keyring, options: SessionOptions<TIdentity>) => MiddlewareHandler;
|
|
41
|
+
/**
|
|
42
|
+
* Options for `create_session_and_set_cookie`.
|
|
43
|
+
*/
|
|
44
|
+
export interface CreateSessionAndSetCookieOptions {
|
|
45
|
+
/** Keyring for cookie signing. */
|
|
46
|
+
keyring: Keyring;
|
|
47
|
+
/** Query deps (needs db for session creation). */
|
|
48
|
+
deps: QueryDeps;
|
|
49
|
+
/** Hono context for setting the cookie. */
|
|
50
|
+
c: Context;
|
|
51
|
+
/** The account to create a session for. */
|
|
52
|
+
account_id: string;
|
|
53
|
+
/** Session cookie configuration. */
|
|
54
|
+
session_options: SessionOptions<string>;
|
|
55
|
+
/** Per-account session cap (`null` to skip enforcement). */
|
|
56
|
+
max_sessions?: number | null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create an auth session and set the session cookie on the response.
|
|
60
|
+
*
|
|
61
|
+
* Shared by login, signup, and bootstrap — generates a token, hashes it,
|
|
62
|
+
* persists the session row, optionally enforces a per-account session limit,
|
|
63
|
+
* and sets the signed cookie.
|
|
64
|
+
*
|
|
65
|
+
* @mutates `auth_session` table - inserts the new session row (and evicts older rows when `max_sessions` is set)
|
|
66
|
+
*/
|
|
67
|
+
export declare const create_session_and_set_cookie: (options: CreateSessionAndSetCookieOptions) => Promise<void>;
|
|
34
68
|
//# sourceMappingURL=session_middleware.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session_middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_middleware.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"session_middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_middleware.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAC,OAAO,EAAE,iBAAiB,EAAC,MAAM,MAAM,CAAC;AAGrD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,cAAc,CAAC;AAC1C,OAAO,EACN,KAAK,cAAc,EAKnB,MAAM,qBAAqB,CAAC;AAQ7B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EACnC,GAAG,OAAO,EACV,SAAS,cAAc,CAAC,CAAC,CAAC,KACxB,MAAM,GAAG,SAEX,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EACnC,GAAG,OAAO,EACV,OAAO,MAAM,EACb,SAAS,cAAc,CAAC,CAAC,CAAC,KACxB,IAOF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,EAAE,GAAG,OAAO,EAAE,SAAS,cAAc,CAAC,CAAC,CAAC,KAAG,IAMhF,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,EAClD,SAAS,OAAO,EAChB,SAAS,cAAc,CAAC,SAAS,CAAC,KAChC,iBAgBF,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,gCAAgC;IAChD,kCAAkC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,kDAAkD;IAClD,IAAI,EAAE,SAAS,CAAC;IAChB,2CAA2C;IAC3C,CAAC,EAAE,OAAO,CAAC;IACX,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,oCAAoC;IACpC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,GACzC,SAAS,gCAAgC,KACvC,OAAO,CAAC,IAAI,CAad,CAAC"}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hono session
|
|
3
|
-
*
|
|
4
|
-
* Thin wrapper that gets/sets cookies and delegates to session processing.
|
|
2
|
+
* Hono session boundary — cookie I/O, request-time middleware, and the
|
|
3
|
+
* session-creation helper shared by login / signup / bootstrap.
|
|
5
4
|
*
|
|
6
5
|
* @module
|
|
7
6
|
*/
|
|
8
7
|
import { getCookie, setCookie, deleteCookie } from 'hono/cookie';
|
|
9
|
-
import { SESSION_COOKIE_OPTIONS, process_session_cookie, } from './session_cookie.js';
|
|
8
|
+
import { SESSION_COOKIE_OPTIONS, process_session_cookie, create_session_cookie_value, } from './session_cookie.js';
|
|
9
|
+
import { generate_session_token, hash_session_token, AUTH_SESSION_LIFETIME_MS, query_create_session, query_session_enforce_limit, } from './session_queries.js';
|
|
10
10
|
/**
|
|
11
11
|
* Read the session cookie value from a request.
|
|
12
12
|
*/
|
|
@@ -15,15 +15,20 @@ export const get_session_cookie = (c, options) => {
|
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
17
|
* Set the session cookie on a response.
|
|
18
|
+
*
|
|
19
|
+
* `options.max_age` is the single source of truth for cookie lifetime: it
|
|
20
|
+
* drives both the embedded `expires_at` (via `create_session_cookie_value`)
|
|
21
|
+
* and the cookie's HTTP `Max-Age` attribute set here. Falls back to
|
|
22
|
+
* `SESSION_COOKIE_OPTIONS.maxAge` (= `SESSION_AGE_MAX`) when unset.
|
|
23
|
+
* `options.cookie_options` cannot carry `maxAge` (omitted in the type) so
|
|
24
|
+
* the two values can't drift.
|
|
18
25
|
*/
|
|
19
26
|
export const set_session_cookie = (c, value, options) => {
|
|
20
27
|
const cookie_options = {
|
|
21
28
|
...SESSION_COOKIE_OPTIONS,
|
|
22
29
|
...options.cookie_options,
|
|
30
|
+
maxAge: options.max_age ?? SESSION_COOKIE_OPTIONS.maxAge,
|
|
23
31
|
};
|
|
24
|
-
if (options.max_age !== undefined) {
|
|
25
|
-
cookie_options.maxAge = options.max_age;
|
|
26
|
-
}
|
|
27
32
|
setCookie(c, options.cookie_name, value, cookie_options);
|
|
28
33
|
};
|
|
29
34
|
/**
|
|
@@ -61,3 +66,24 @@ export const create_session_middleware = (keyring, options) => {
|
|
|
61
66
|
await next();
|
|
62
67
|
};
|
|
63
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* Create an auth session and set the session cookie on the response.
|
|
71
|
+
*
|
|
72
|
+
* Shared by login, signup, and bootstrap — generates a token, hashes it,
|
|
73
|
+
* persists the session row, optionally enforces a per-account session limit,
|
|
74
|
+
* and sets the signed cookie.
|
|
75
|
+
*
|
|
76
|
+
* @mutates `auth_session` table - inserts the new session row (and evicts older rows when `max_sessions` is set)
|
|
77
|
+
*/
|
|
78
|
+
export const create_session_and_set_cookie = async (options) => {
|
|
79
|
+
const { keyring, deps, c, account_id, session_options, max_sessions } = options;
|
|
80
|
+
const session_token = generate_session_token();
|
|
81
|
+
const token_hash = hash_session_token(session_token);
|
|
82
|
+
const expires_at = new Date(Date.now() + AUTH_SESSION_LIFETIME_MS);
|
|
83
|
+
await query_create_session(deps, token_hash, account_id, expires_at);
|
|
84
|
+
if (max_sessions != null) {
|
|
85
|
+
await query_session_enforce_limit(deps, account_id, max_sessions);
|
|
86
|
+
}
|
|
87
|
+
const cookie_value = await create_session_cookie_value(keyring, session_token, session_options);
|
|
88
|
+
set_session_cookie(c, cookie_value, session_options);
|
|
89
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signup_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/signup_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAStB,OAAO,EAAkB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAClF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"signup_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/signup_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAStB,OAAO,EAAkB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAClF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAOhD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AAE1D,OAAO,KAAK,EAAC,uBAAuB,EAAC,MAAM,qBAAqB,CAAC;AAEjE;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,uBAAuB;IAClE,6FAA6F;IAC7F,2BAA2B,EAAE,WAAW,GAAG,IAAI,CAAC;IAChD,yFAAyF;IACzF,YAAY,EAAE,WAAW,CAAC;CAC1B;AAID,0FAA0F;AAC1F,eAAO,MAAM,WAAW;;;;kBAItB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,8EAA8E;AAC9E,eAAO,MAAM,YAAY;;kBAEvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,gBAAgB,EACtB,SAAS,kBAAkB,KACzB,KAAK,CAAC,SAAS,CAmJjB,CAAC"}
|