@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
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role grant database queries.
|
|
3
|
+
*
|
|
4
|
+
* Role grants are time-bounded, revocable grants of a role to an actor.
|
|
5
|
+
* The system is safe by default — no role_grant, no capability.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { assert_row } from '../db/assert_row.js';
|
|
10
|
+
import { ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN, ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID, } from './role_grant_offer_schema.js';
|
|
11
|
+
/**
|
|
12
|
+
* Grant a role_grant to an actor.
|
|
13
|
+
* Idempotent — if an active role_grant already exists for this actor, role, and
|
|
14
|
+
* scope, returns the existing role_grant instead of creating a duplicate.
|
|
15
|
+
*
|
|
16
|
+
* The `ON CONFLICT` target and the fallback `SELECT` both collapse `NULL`
|
|
17
|
+
* scopes via the same sentinel + index-side `'GLOBAL'` token used by the
|
|
18
|
+
* partial unique index (`role_grant_actor_role_scope_active_unique`). The
|
|
19
|
+
* `IS NOT DISTINCT FROM` form on the fallback is deliberate — plain `=`
|
|
20
|
+
* would miss the NULL-scope case where the conflict fired.
|
|
21
|
+
*
|
|
22
|
+
* `scope_kind` is paired-null with `scope_id` per the
|
|
23
|
+
* `role_grant_scope_kind_paired` CHECK; mismatched pairs raise at the DB
|
|
24
|
+
* layer rather than producing silent rows.
|
|
25
|
+
*
|
|
26
|
+
* @param deps - query dependencies
|
|
27
|
+
* @param input - the role_grant fields
|
|
28
|
+
* @returns the created or existing active role_grant
|
|
29
|
+
* @mutates `role_grant` table - inserts a row when no active role_grant matches `(actor_id, role, scope_kind, scope_id)`
|
|
30
|
+
*/
|
|
31
|
+
export const query_create_role_grant = async (deps, input) => {
|
|
32
|
+
const inserted = await deps.db.query_one(`INSERT INTO role_grant (actor_id, role, scope_kind, scope_id, expires_at, granted_by, source_offer_id)
|
|
33
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
34
|
+
ON CONFLICT (
|
|
35
|
+
actor_id,
|
|
36
|
+
role,
|
|
37
|
+
COALESCE(scope_kind, '${ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN}'),
|
|
38
|
+
COALESCE(scope_id, '${ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID}'::uuid)
|
|
39
|
+
)
|
|
40
|
+
WHERE revoked_at IS NULL
|
|
41
|
+
DO NOTHING
|
|
42
|
+
RETURNING *`, [
|
|
43
|
+
input.actor_id,
|
|
44
|
+
input.role,
|
|
45
|
+
input.scope_kind ?? null,
|
|
46
|
+
input.scope_id ?? null,
|
|
47
|
+
input.expires_at?.toISOString() ?? null,
|
|
48
|
+
input.granted_by ?? null,
|
|
49
|
+
input.source_offer_id ?? null,
|
|
50
|
+
]);
|
|
51
|
+
if (inserted)
|
|
52
|
+
return inserted;
|
|
53
|
+
// Active role_grant already exists — return it (idempotent grant).
|
|
54
|
+
const existing = await deps.db.query_one(`SELECT * FROM role_grant
|
|
55
|
+
WHERE actor_id = $1
|
|
56
|
+
AND role = $2
|
|
57
|
+
AND scope_kind IS NOT DISTINCT FROM $3
|
|
58
|
+
AND scope_id IS NOT DISTINCT FROM $4
|
|
59
|
+
AND revoked_at IS NULL`, [input.actor_id, input.role, input.scope_kind ?? null, input.scope_id ?? null]);
|
|
60
|
+
return assert_row(existing, 'idempotent role_grant grant');
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Look up the role of an active role_grant (constrained to a specific
|
|
64
|
+
* actor) plus the actor's `account_id`.
|
|
65
|
+
*
|
|
66
|
+
* Used by admin routes to inspect the role_grant's role before acting
|
|
67
|
+
* (e.g., enforcing the admin-grant-path gate on revoke). The actor constraint
|
|
68
|
+
* mirrors `query_revoke_role_grant` so IDOR protection is consistent:
|
|
69
|
+
* a caller can only see role_grants belonging to the target actor.
|
|
70
|
+
*
|
|
71
|
+
* The JOIN to `actor` collapses what used to be a second
|
|
72
|
+
* `query_actor_by_id` round-trip in the revoke handler into one read,
|
|
73
|
+
* which closes the small TOCTOU window where the actor row could be
|
|
74
|
+
* deleted between the IDOR check and the actor lookup. The `account_id`
|
|
75
|
+
* is needed by the audit envelope's `target_account_id` field and the
|
|
76
|
+
* SSE/WS socket-close fan-out targeting.
|
|
77
|
+
*
|
|
78
|
+
* Returns `null` if the role_grant is not found, already revoked, or
|
|
79
|
+
* belongs to a different actor.
|
|
80
|
+
*
|
|
81
|
+
* @param deps - query dependencies
|
|
82
|
+
* @param role_grant_id - the role_grant id to look up
|
|
83
|
+
* @param actor_id - the actor that must own the role_grant
|
|
84
|
+
* @returns `{role, account_id}` on a match, or `null`
|
|
85
|
+
*/
|
|
86
|
+
export const query_role_grant_find_active_role_for_actor = async (deps, role_grant_id, actor_id) => {
|
|
87
|
+
const row = await deps.db.query_one(`SELECT role_grant.role, actor.account_id
|
|
88
|
+
FROM role_grant
|
|
89
|
+
JOIN actor ON actor.id = role_grant.actor_id
|
|
90
|
+
WHERE role_grant.id = $1 AND role_grant.actor_id = $2 AND role_grant.revoked_at IS NULL`, [role_grant_id, actor_id]);
|
|
91
|
+
return row ?? null;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Revoke a role_grant by id, constrained to a specific actor.
|
|
95
|
+
*
|
|
96
|
+
* Requires `actor_id` to prevent cross-account revocation (IDOR guard).
|
|
97
|
+
* Returns `null` if the role_grant is not found, already revoked, or belongs
|
|
98
|
+
* to a different actor.
|
|
99
|
+
*
|
|
100
|
+
* Supersedes any pending offers for the revoked role_grant's
|
|
101
|
+
* `(to_account, role, scope)` in the same transaction. Prevents the
|
|
102
|
+
* "accept a pre-revoke offer to bypass the revoke" path — any stale
|
|
103
|
+
* offer becomes terminal at revoke time. A fresh post-revoke grant
|
|
104
|
+
* requires the grantor to call `query_role_grant_offer_create` again.
|
|
105
|
+
*
|
|
106
|
+
* @param deps - query dependencies
|
|
107
|
+
* @param role_grant_id - the role_grant to revoke
|
|
108
|
+
* @param actor_id - the actor that must own the role_grant
|
|
109
|
+
* @param revoked_by - the actor who revoked it (for audit trail)
|
|
110
|
+
* @param reason - optional free-form reason, stamped on `role_grant.revoked_reason` and surfaced to the revokee notification.
|
|
111
|
+
* @mutates `role_grant` row - sets `revoked_at`, `revoked_by`, and `revoked_reason`
|
|
112
|
+
* @mutates `role_grant_offer` rows - stamps `superseded_at` on every pending sibling for the same `(account, role, scope)`
|
|
113
|
+
*/
|
|
114
|
+
export const query_revoke_role_grant = async (deps, role_grant_id, actor_id, revoked_by, reason) => {
|
|
115
|
+
const rows = await deps.db.query(`UPDATE role_grant SET revoked_at = NOW(), revoked_by = $3, revoked_reason = $4
|
|
116
|
+
WHERE id = $1 AND actor_id = $2 AND revoked_at IS NULL
|
|
117
|
+
RETURNING id, role, scope_kind, scope_id`, [role_grant_id, actor_id, revoked_by ?? null, reason ?? null]);
|
|
118
|
+
const revoked = rows[0];
|
|
119
|
+
if (!revoked)
|
|
120
|
+
return null;
|
|
121
|
+
// CTE joins `actor` after the UPDATE so each superseded row carries the
|
|
122
|
+
// grantor's `account_id` — callers fan out `role_grant_offer_supersede`
|
|
123
|
+
// notifications to that account without a second round-trip. The match
|
|
124
|
+
// keys on `scope_id` only because the (scope_kind, scope_id) pair-CHECK
|
|
125
|
+
// makes scope_kind a function of scope_id; matching on both adds no
|
|
126
|
+
// new selectivity in v1.
|
|
127
|
+
const superseded_offers = await deps.db.query(`WITH updated AS (
|
|
128
|
+
UPDATE role_grant_offer o
|
|
129
|
+
SET superseded_at = NOW()
|
|
130
|
+
FROM actor a
|
|
131
|
+
WHERE a.id = $1
|
|
132
|
+
AND o.to_account_id = a.account_id
|
|
133
|
+
AND o.role = $2
|
|
134
|
+
AND o.scope_id IS NOT DISTINCT FROM $3
|
|
135
|
+
AND o.accepted_at IS NULL
|
|
136
|
+
AND o.declined_at IS NULL
|
|
137
|
+
AND o.retracted_at IS NULL
|
|
138
|
+
AND o.superseded_at IS NULL
|
|
139
|
+
RETURNING o.*
|
|
140
|
+
)
|
|
141
|
+
SELECT u.*, grantor.account_id AS from_account_id
|
|
142
|
+
FROM updated u
|
|
143
|
+
JOIN actor grantor ON grantor.id = u.from_actor_id`, [actor_id, revoked.role, revoked.scope_id]);
|
|
144
|
+
return {
|
|
145
|
+
id: revoked.id,
|
|
146
|
+
role: revoked.role,
|
|
147
|
+
scope_kind: revoked.scope_kind,
|
|
148
|
+
scope_id: revoked.scope_id,
|
|
149
|
+
superseded_offers,
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Find all active (non-revoked, non-expired) role_grants for an actor.
|
|
154
|
+
*/
|
|
155
|
+
export const query_role_grant_find_active_for_actor = async (deps, actor_id) => {
|
|
156
|
+
return deps.db.query(`SELECT * FROM role_grant
|
|
157
|
+
WHERE actor_id = $1
|
|
158
|
+
AND revoked_at IS NULL
|
|
159
|
+
AND (expires_at IS NULL OR expires_at > NOW())
|
|
160
|
+
ORDER BY created_at`, [actor_id]);
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Check if an actor has an active role_grant for a given role.
|
|
164
|
+
*
|
|
165
|
+
* The `scope_id` parameter selects between global and scoped checks:
|
|
166
|
+
* - Omitted or `null` — matches a global role_grant (`scope_id IS NULL`).
|
|
167
|
+
* Pre-scope callers keep their existing semantics.
|
|
168
|
+
* - A scope uuid — matches a role_grant bound to that exact scope.
|
|
169
|
+
*
|
|
170
|
+
* The `IS NOT DISTINCT FROM` comparison handles the NULL case uniformly.
|
|
171
|
+
*/
|
|
172
|
+
export const query_role_grant_has_role = async (deps, actor_id, role, scope_id) => {
|
|
173
|
+
const row = await deps.db.query_one(`SELECT EXISTS(
|
|
174
|
+
SELECT 1 FROM role_grant
|
|
175
|
+
WHERE actor_id = $1
|
|
176
|
+
AND role = $2
|
|
177
|
+
AND scope_id IS NOT DISTINCT FROM $3
|
|
178
|
+
AND revoked_at IS NULL
|
|
179
|
+
AND (expires_at IS NULL OR expires_at > NOW())
|
|
180
|
+
) AS exists`, [actor_id, role, scope_id ?? null]);
|
|
181
|
+
return row?.exists ?? false;
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* List all role_grants for an actor (including revoked/expired).
|
|
185
|
+
*/
|
|
186
|
+
export const query_role_grant_list_for_actor = async (deps, actor_id) => {
|
|
187
|
+
return deps.db.query(`SELECT * FROM role_grant WHERE actor_id = $1 ORDER BY created_at DESC`, [actor_id]);
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Find the account ID of an account that holds an active role_grant for a given role.
|
|
191
|
+
*
|
|
192
|
+
* Joins role_grant → actor → account. Returns the first match, or `null` if none.
|
|
193
|
+
*
|
|
194
|
+
* @param deps - query dependencies
|
|
195
|
+
* @param role - the role to search for
|
|
196
|
+
* @returns the account ID, or `null`
|
|
197
|
+
*/
|
|
198
|
+
export const query_role_grant_find_account_id_for_role = async (deps, role) => {
|
|
199
|
+
const row = await deps.db.query_one(`SELECT a.id AS account_id
|
|
200
|
+
FROM role_grant p
|
|
201
|
+
JOIN actor act ON act.id = p.actor_id
|
|
202
|
+
JOIN account a ON a.id = act.account_id
|
|
203
|
+
WHERE p.role = $1
|
|
204
|
+
AND p.revoked_at IS NULL
|
|
205
|
+
AND (p.expires_at IS NULL OR p.expires_at > NOW())
|
|
206
|
+
LIMIT 1`, [role]);
|
|
207
|
+
return row?.account_id ?? null;
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* Revoke every active role_grant bound to a scope and supersede every pending
|
|
211
|
+
* offer at the scope, in one cascade.
|
|
212
|
+
*
|
|
213
|
+
* Use this from a consumer's parent-scope delete handler (e.g., classroom
|
|
214
|
+
* deletion) — `role_grant.scope_id` and `role_grant_offer.scope_id` are polymorphic
|
|
215
|
+
* with no FK constraint by design, so a parent row deletion would otherwise
|
|
216
|
+
* orphan role_grants and offers. The cascade is **role-agnostic**: anything
|
|
217
|
+
* attached to the destroyed scope is cleaned up.
|
|
218
|
+
*
|
|
219
|
+
* Both updates run as separate statements inside the caller's transaction
|
|
220
|
+
* (mirrors `query_role_grant_revoke_role`'s shape). The two halves are
|
|
221
|
+
* independent — orphan pending offers can exist at a scope with no active
|
|
222
|
+
* role_grants, so the supersede half always runs even when no role_grant was
|
|
223
|
+
* revoked.
|
|
224
|
+
*
|
|
225
|
+
* @param deps - query dependencies
|
|
226
|
+
* @param scope_id - the scope whose role_grants and offers to terminate
|
|
227
|
+
* @param revoked_by - the actor performing the cascade (audit trail)
|
|
228
|
+
* @param reason - optional free-form reason, stamped on `role_grant.revoked_reason`.
|
|
229
|
+
* @returns the revoked role_grants (with `account_id` for fan-out) and superseded offers (with `from_account_id` for fan-out)
|
|
230
|
+
* @mutates `role_grant` table - sets `revoked_at`/`revoked_by`/`revoked_reason` on every active row at `scope_id`
|
|
231
|
+
* @mutates `role_grant_offer` table - stamps `superseded_at` on every pending row at `scope_id`
|
|
232
|
+
*/
|
|
233
|
+
export const query_role_grant_revoke_for_scope = async (deps, scope_id, revoked_by, reason) => {
|
|
234
|
+
// Revoke every active role_grant at the scope. CTE returns `actor_id` directly
|
|
235
|
+
// from the role_grant row (drives `target_actor_id` audit envelopes); a join
|
|
236
|
+
// against `actor` resolves `account_id` for `target_account_id`
|
|
237
|
+
// + WS/SSE socket-close fan-out, all in one round-trip.
|
|
238
|
+
const revoked = await deps.db.query(`WITH updated AS (
|
|
239
|
+
UPDATE role_grant
|
|
240
|
+
SET revoked_at = NOW(), revoked_by = $2, revoked_reason = $3
|
|
241
|
+
WHERE scope_id = $1 AND revoked_at IS NULL
|
|
242
|
+
RETURNING id, role, scope_kind, scope_id, actor_id
|
|
243
|
+
)
|
|
244
|
+
SELECT u.id AS role_grant_id, u.role, u.scope_kind, u.scope_id, u.actor_id, a.account_id
|
|
245
|
+
FROM updated u
|
|
246
|
+
JOIN actor a ON a.id = u.actor_id`, [scope_id, revoked_by ?? null, reason ?? null]);
|
|
247
|
+
// Supersede every pending offer at the scope — tuple-matched or orphan,
|
|
248
|
+
// no distinction. The cause of every supersede in this cascade is the
|
|
249
|
+
// scope deletion; offers tuple-matched to a revoked role_grant are not
|
|
250
|
+
// tagged separately because the revoke is itself a consequence of the
|
|
251
|
+
// scope going away.
|
|
252
|
+
const superseded_offers = await deps.db.query(`WITH updated AS (
|
|
253
|
+
UPDATE role_grant_offer o
|
|
254
|
+
SET superseded_at = NOW()
|
|
255
|
+
WHERE o.scope_id = $1
|
|
256
|
+
AND o.accepted_at IS NULL
|
|
257
|
+
AND o.declined_at IS NULL
|
|
258
|
+
AND o.retracted_at IS NULL
|
|
259
|
+
AND o.superseded_at IS NULL
|
|
260
|
+
RETURNING o.*
|
|
261
|
+
)
|
|
262
|
+
SELECT u.*, grantor.account_id AS from_account_id
|
|
263
|
+
FROM updated u
|
|
264
|
+
JOIN actor grantor ON grantor.id = u.from_actor_id`, [scope_id]);
|
|
265
|
+
return { revoked, superseded_offers };
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* Revoke every active role_grant an actor holds for a given role.
|
|
269
|
+
*
|
|
270
|
+
* With scoped role_grants a single actor+role tuple can hold several active
|
|
271
|
+
* role_grants (one per scope), so this revokes all of them. Pass
|
|
272
|
+
* `query_revoke_role_grant(role_grant_id, ...)` when a single scoped role_grant
|
|
273
|
+
* is the target.
|
|
274
|
+
*
|
|
275
|
+
* Also supersedes pending offers for the actor's account across every
|
|
276
|
+
* scope of this role (the actor can no longer hold the role, so any
|
|
277
|
+
* pending offer of the same role is a bypass vector).
|
|
278
|
+
*
|
|
279
|
+
* @param deps - query dependencies
|
|
280
|
+
* @param actor_id - the actor whose role_grants to revoke
|
|
281
|
+
* @param role - the role to revoke
|
|
282
|
+
* @param revoked_by - the actor who revoked it (for audit trail)
|
|
283
|
+
* @param reason - optional free-form reason, stamped on `role_grant.revoked_reason`.
|
|
284
|
+
* @returns the list of revoked role_grants (empty if none were active) and superseded pending offers
|
|
285
|
+
* @mutates `role_grant` table - sets `revoked_at`/`revoked_by`/`revoked_reason` on every active row for `(actor, role)`
|
|
286
|
+
* @mutates `role_grant_offer` table - stamps `superseded_at` on every matching pending offer
|
|
287
|
+
*/
|
|
288
|
+
export const query_role_grant_revoke_role = async (deps, actor_id, role, revoked_by, reason) => {
|
|
289
|
+
// CTE pulls the revokee's `account_id` via a join on `actor` so callers
|
|
290
|
+
// can address the revokee without an extra round-trip.
|
|
291
|
+
const revoked = await deps.db.query(`WITH updated AS (
|
|
292
|
+
UPDATE role_grant
|
|
293
|
+
SET revoked_at = NOW(), revoked_by = $3, revoked_reason = $4
|
|
294
|
+
WHERE actor_id = $1 AND role = $2 AND revoked_at IS NULL
|
|
295
|
+
RETURNING id, role, scope_kind, scope_id, actor_id
|
|
296
|
+
)
|
|
297
|
+
SELECT u.id AS role_grant_id, u.role, u.scope_kind, u.scope_id, a.account_id
|
|
298
|
+
FROM updated u
|
|
299
|
+
JOIN actor a ON a.id = u.actor_id`, [actor_id, role, revoked_by ?? null, reason ?? null]);
|
|
300
|
+
if (revoked.length === 0) {
|
|
301
|
+
return { revoked: [], superseded_offers: [] };
|
|
302
|
+
}
|
|
303
|
+
const superseded_offers = await deps.db.query(`WITH updated AS (
|
|
304
|
+
UPDATE role_grant_offer o
|
|
305
|
+
SET superseded_at = NOW()
|
|
306
|
+
FROM actor a
|
|
307
|
+
WHERE a.id = $1
|
|
308
|
+
AND o.to_account_id = a.account_id
|
|
309
|
+
AND o.role = $2
|
|
310
|
+
AND o.accepted_at IS NULL
|
|
311
|
+
AND o.declined_at IS NULL
|
|
312
|
+
AND o.retracted_at IS NULL
|
|
313
|
+
AND o.superseded_at IS NULL
|
|
314
|
+
RETURNING o.*
|
|
315
|
+
)
|
|
316
|
+
SELECT u.*, grantor.account_id AS from_account_id
|
|
317
|
+
FROM updated u
|
|
318
|
+
JOIN actor grantor ON grantor.id = u.from_actor_id`, [actor_id, role]);
|
|
319
|
+
return { revoked, superseded_offers };
|
|
320
|
+
};
|
|
@@ -1,80 +1,190 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Role system — builtin roles, role
|
|
2
|
+
* Role system — builtin roles, role specs, and extensible role schema factory.
|
|
3
3
|
*
|
|
4
|
-
* Defines the authorization policy vocabulary: which roles exist,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Defines the authorization policy vocabulary: which roles exist, their
|
|
5
|
+
* required credential types, the scope kinds each role applies to, and
|
|
6
|
+
* the grant paths through which each role can be granted. Each role
|
|
7
|
+
* gets a structured `RoleSpec`; the factory `create_role_schema` merges
|
|
8
|
+
* builtins with consumer-declared specs and validates every cross-axis
|
|
9
|
+
* field against the corresponding open registries
|
|
10
|
+
* (`create_credential_type_schema`, `create_scope_kind_schema`,
|
|
11
|
+
* `create_grant_path_schema`) at construction time so misconfigurations
|
|
12
|
+
* fire at server startup, not at first call.
|
|
13
|
+
*
|
|
14
|
+
* `RoleSpec` carries the four cross-axis fields that the dispatcher
|
|
15
|
+
* branches on: credential type, scope kind, grant path, and the
|
|
16
|
+
* role-name itself. v1 keeps the cross-axis fields informative-only
|
|
17
|
+
* (registry-membership validation, no INSERT-time enforcement); v2 may
|
|
18
|
+
* add `(role, scope_kind)` enforcement once the shape is clear from
|
|
19
|
+
* real consumer usage.
|
|
7
20
|
*
|
|
8
21
|
* @module
|
|
9
22
|
*/
|
|
10
23
|
import { z } from 'zod';
|
|
24
|
+
import { type CredentialTypeSchemaResult } from './credential_type_schema.js';
|
|
25
|
+
import { type GrantPathSchemaResult } from './grant_path_schema.js';
|
|
26
|
+
import type { ScopeKindSchemaResult } from './scope_kind_schema.js';
|
|
11
27
|
/** Valid role name: lowercase letters and underscores, no leading/trailing underscore. */
|
|
12
28
|
export declare const RoleName: z.ZodString;
|
|
13
29
|
export type RoleName = z.infer<typeof RoleName>;
|
|
14
30
|
/** System-level role. Requires daemon token (filesystem proof). Controls the keep. */
|
|
15
31
|
export declare const ROLE_KEEPER = "keeper";
|
|
16
|
-
/** App-level administrative role.
|
|
32
|
+
/** App-level administrative role. Granted via the admin path. */
|
|
17
33
|
export declare const ROLE_ADMIN = "admin";
|
|
18
34
|
/** The builtin role names as a const tuple. */
|
|
19
35
|
export declare const BUILTIN_ROLES: readonly ["keeper", "admin"];
|
|
20
36
|
/** Zod schema for builtin roles only. */
|
|
21
37
|
export declare const BuiltinRole: z.ZodEnum<{
|
|
22
|
-
keeper: "keeper";
|
|
23
38
|
admin: "admin";
|
|
39
|
+
keeper: "keeper";
|
|
24
40
|
}>;
|
|
25
41
|
export type BuiltinRole = z.infer<typeof BuiltinRole>;
|
|
26
42
|
/**
|
|
27
43
|
* Configuration for a role.
|
|
28
44
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
45
|
+
* Each role declares the credential types its holders must use, the
|
|
46
|
+
* scope kinds it applies to, and the grant paths through which it can
|
|
47
|
+
* be granted. Every cross-axis field is an open-registry string array —
|
|
48
|
+
* `required_credential_types` against `create_credential_type_schema`,
|
|
49
|
+
* `applicable_scope_kinds` against `create_scope_kind_schema`,
|
|
50
|
+
* `grant_paths` against `create_grant_path_schema`. Pass the registry
|
|
51
|
+
* results to `create_role_schema` and every entry is checked at
|
|
52
|
+
* construction time.
|
|
53
|
+
*
|
|
54
|
+
* Empty arrays carry meaning:
|
|
55
|
+
*
|
|
56
|
+
* - `required_credential_types: []` — any authenticated credential type
|
|
57
|
+
* may exercise the role (the default for app-defined roles).
|
|
58
|
+
* - `applicable_scope_kinds: []` — the role applies at the global scope
|
|
59
|
+
* only (no `scope_kind` / `scope_id` set on its role_grants). This is the
|
|
60
|
+
* default for app-defined roles; consumers add scope kinds explicitly.
|
|
61
|
+
* - `grant_paths: []` — the role has no grant path declared in this
|
|
62
|
+
* registry; it is unreachable through admin / self-service / system
|
|
63
|
+
* flows. Only useful for diagnostic snapshotting.
|
|
64
|
+
*
|
|
65
|
+
* Builtins (`keeper`, `admin`) ship preconfigured in
|
|
66
|
+
* `BUILTIN_ROLE_SPECS_BY_NAME`.
|
|
31
67
|
*/
|
|
32
|
-
export interface
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
|
|
68
|
+
export interface RoleSpec {
|
|
69
|
+
/** Unique role name. Must match `RoleName` regex; collisions with builtins throw. */
|
|
70
|
+
name: string;
|
|
71
|
+
/** Admin-UI-facing copy describing the role's intent. */
|
|
72
|
+
description?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Credential types whose holders are permitted to exercise this
|
|
75
|
+
* role. Each entry is checked at construction time against the
|
|
76
|
+
* `credential_types` registry passed to `create_role_schema`. Empty
|
|
77
|
+
* array = any authenticated credential type.
|
|
78
|
+
*/
|
|
79
|
+
required_credential_types?: ReadonlyArray<string>;
|
|
80
|
+
/**
|
|
81
|
+
* Scope kinds at which this role's role_grants may be granted. Each
|
|
82
|
+
* entry is checked at construction time against the `scope_kinds`
|
|
83
|
+
* registry passed to `create_role_schema`. Empty array = global only.
|
|
84
|
+
* v1 keeps this informative-only (no INSERT-time enforcement).
|
|
85
|
+
*/
|
|
86
|
+
applicable_scope_kinds?: ReadonlyArray<string>;
|
|
87
|
+
/**
|
|
88
|
+
* Grant paths through which this role can be granted. Each entry is
|
|
89
|
+
* checked at construction time against the `grant_paths` registry
|
|
90
|
+
* passed to `create_role_schema`. Drives downstream defaults:
|
|
91
|
+
*
|
|
92
|
+
* - `admin_actions.grantable_roles` ⊇ {role : `'admin'` ∈ grant_paths}
|
|
93
|
+
* - `self_service_role_actions` default eligibility ⊇ {role : `'self_service'` ∈ grant_paths}
|
|
94
|
+
*
|
|
95
|
+
* Empty array = role is not granted via any registered path (only
|
|
96
|
+
* exists for diagnostic / future use).
|
|
97
|
+
*/
|
|
98
|
+
grant_paths?: ReadonlyArray<string>;
|
|
37
99
|
}
|
|
38
100
|
/**
|
|
39
|
-
* Builtin role
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* action factories; runtime mutation has no effect on already-built role
|
|
46
|
-
* schemas.
|
|
101
|
+
* Builtin role specs, keyed by role name. Not overridable by consumers
|
|
102
|
+
* — read once at startup by `create_role_schema` and the action
|
|
103
|
+
* factories that fall back to builtins when no consumer `roles` is
|
|
104
|
+
* supplied. `ReadonlyMap` encodes the contract; runtime mutation has
|
|
105
|
+
* no effect on already-built role schemas (the factory copies entries
|
|
106
|
+
* into a fresh `Map`).
|
|
47
107
|
*/
|
|
48
|
-
export declare const
|
|
49
|
-
/**
|
|
108
|
+
export declare const BUILTIN_ROLE_SPECS_BY_NAME: ReadonlyMap<string, RoleSpec>;
|
|
109
|
+
/** Optional registries to validate `RoleSpec` cross-axis fields against at construction time. */
|
|
110
|
+
export interface CreateRoleSchemaOptions {
|
|
111
|
+
/** Pass `create_credential_type_schema()` to validate `RoleSpec.required_credential_types` entries. */
|
|
112
|
+
credential_types?: CredentialTypeSchemaResult;
|
|
113
|
+
/** Pass `create_scope_kind_schema()` to validate `RoleSpec.applicable_scope_kinds` entries. */
|
|
114
|
+
scope_kinds?: ScopeKindSchemaResult;
|
|
115
|
+
/** Pass `create_grant_path_schema()` to validate `RoleSpec.grant_paths` entries. */
|
|
116
|
+
grant_paths?: GrantPathSchemaResult;
|
|
117
|
+
}
|
|
118
|
+
/** The result of `create_role_schema` — a Zod schema and spec map for all roles. */
|
|
50
119
|
export interface RoleSchemaResult {
|
|
51
|
-
/** Zod schema that validates role strings. Use at I/O boundaries (grant endpoint,
|
|
120
|
+
/** Zod schema that validates role strings. Use at I/O boundaries (grant endpoint, role_grant queries). */
|
|
52
121
|
Role: z.ZodType<string>;
|
|
53
|
-
/**
|
|
54
|
-
|
|
122
|
+
/** Specs for every role (builtins + app-defined). Keyed by role name. */
|
|
123
|
+
role_specs: ReadonlyMap<string, RoleSpec>;
|
|
55
124
|
}
|
|
56
125
|
/**
|
|
57
|
-
* Create a role schema and
|
|
126
|
+
* Create a role schema and spec map that extends the builtins with
|
|
127
|
+
* app-defined roles.
|
|
128
|
+
*
|
|
129
|
+
* Call once at server init. The returned `Role` schema validates role
|
|
130
|
+
* strings at I/O boundaries (grant endpoint, role_grant queries). The
|
|
131
|
+
* `role_specs` map is read by middleware for `required_credential_types`
|
|
132
|
+
* checks and by admin / self-service factories to derive their default
|
|
133
|
+
* eligibility filters from `RoleSpec.grant_paths`.
|
|
134
|
+
*
|
|
135
|
+
* Construction-time guards (all fire on misconfiguration):
|
|
58
136
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
137
|
+
* 1. Every `consumer_roles[i].name` matches `RoleName` regex.
|
|
138
|
+
* 2. No two consumer roles share a name.
|
|
139
|
+
* 3. No consumer role collides with a builtin (`keeper` / `admin`).
|
|
140
|
+
* 4. When `options.credential_types` is supplied, every entry in
|
|
141
|
+
* `required_credential_types` is registered in that map.
|
|
142
|
+
* 5. When `options.scope_kinds` is supplied, every entry in
|
|
143
|
+
* `applicable_scope_kinds` is registered in that map. (Builtins
|
|
144
|
+
* declare empty `applicable_scope_kinds`, so they pass any registry.)
|
|
145
|
+
* 6. When `options.grant_paths` is supplied, every entry in
|
|
146
|
+
* `grant_paths` is registered in that map. (Builtins use only
|
|
147
|
+
* `'admin'` and `'bootstrap'`, both of which are builtin grant
|
|
148
|
+
* paths, so they pass the default registry from
|
|
149
|
+
* `create_grant_path_schema()`.)
|
|
63
150
|
*
|
|
64
|
-
* @param
|
|
65
|
-
* @
|
|
151
|
+
* @param consumer_roles - app-defined role specs
|
|
152
|
+
* @param options - optional registries for cross-axis validation
|
|
153
|
+
* @returns `{Role, role_specs}` — Zod schema and full spec map
|
|
66
154
|
*
|
|
67
|
-
* @throws Error if any `
|
|
155
|
+
* @throws Error if any `consumer_roles` entry fails any of the construction-time guards above
|
|
68
156
|
*
|
|
69
157
|
* @example
|
|
70
158
|
* ```ts
|
|
71
|
-
* // visiones
|
|
72
|
-
* const
|
|
73
|
-
*
|
|
159
|
+
* // visiones — opt into all four registries for full construction-time validation
|
|
160
|
+
* const credential_types = create_credential_type_schema();
|
|
161
|
+
* const scope_kinds = create_scope_kind_schema({
|
|
162
|
+
* classroom: {description: 'A classroom — teacher and student role_grants scope here.'},
|
|
74
163
|
* });
|
|
75
|
-
*
|
|
76
|
-
*
|
|
164
|
+
* const grant_paths = create_grant_path_schema();
|
|
165
|
+
*
|
|
166
|
+
* const {Role, role_specs} = create_role_schema(
|
|
167
|
+
* [
|
|
168
|
+
* {
|
|
169
|
+
* name: 'teacher',
|
|
170
|
+
* description: 'Educator role. Web-grantable; applies at classroom scope.',
|
|
171
|
+
* grant_paths: ['admin'],
|
|
172
|
+
* applicable_scope_kinds: ['classroom'],
|
|
173
|
+
* },
|
|
174
|
+
* ],
|
|
175
|
+
* {credential_types, scope_kinds, grant_paths},
|
|
176
|
+
* );
|
|
77
177
|
* ```
|
|
78
178
|
*/
|
|
79
|
-
export declare const create_role_schema:
|
|
179
|
+
export declare const create_role_schema: (consumer_roles: ReadonlyArray<RoleSpec>, options?: CreateRoleSchemaOptions) => RoleSchemaResult;
|
|
180
|
+
/**
|
|
181
|
+
* Predicate over a `RoleSpec` map: does the named role include the given
|
|
182
|
+
* grant path? Returns `false` for unknown roles. Used by
|
|
183
|
+
* `admin_actions.create_admin_actions` (path = `'admin'`) and
|
|
184
|
+
* `self_service_role_actions.create_self_service_role_actions` (path =
|
|
185
|
+
* `'self_service'`) to derive their default eligibility filters.
|
|
186
|
+
*/
|
|
187
|
+
export declare const role_has_grant_path: (role_specs: ReadonlyMap<string, RoleSpec>, role: string, grant_path: string) => boolean;
|
|
188
|
+
/** Filter helper: list every role whose `grant_paths` includes the given path. */
|
|
189
|
+
export declare const list_roles_with_grant_path: (role_specs: ReadonlyMap<string, RoleSpec>, grant_path: string) => Array<string>;
|
|
80
190
|
//# sourceMappingURL=role_schema.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_schema.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"role_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,EAEN,KAAK,0BAA0B,EAC/B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAGN,KAAK,qBAAqB,EAC1B,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,wBAAwB,CAAC;AAElE,0FAA0F;AAC1F,eAAO,MAAM,QAAQ,aAKnB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC;AAIhD,sFAAsF;AACtF,eAAO,MAAM,WAAW,WAAW,CAAC;AAEpC,iEAAiE;AACjE,eAAO,MAAM,UAAU,UAAU,CAAC;AAElC,+CAA+C;AAC/C,eAAO,MAAM,aAAa,8BAAqC,CAAC;AAEhE,yCAAyC;AACzC,eAAO,MAAM,WAAW;;;EAAwB,CAAC;AACjD,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,QAAQ;IACxB,qFAAqF;IACrF,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,yBAAyB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAClD;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/C;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAuBnE,CAAC;AAEH,iGAAiG;AACjG,MAAM,WAAW,uBAAuB;IACvC,uGAAuG;IACvG,gBAAgB,CAAC,EAAE,0BAA0B,CAAC;IAC9C,+FAA+F;IAC/F,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC,oFAAoF;IACpF,WAAW,CAAC,EAAE,qBAAqB,CAAC;CACpC;AAED,oFAAoF;AACpF,MAAM,WAAW,gBAAgB;IAChC,0GAA0G;IAC1G,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACxB,yEAAyE;IACzE,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;CAC1C;AAkBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,eAAO,MAAM,kBAAkB,GAC9B,gBAAgB,aAAa,CAAC,QAAQ,CAAC,EACvC,UAAS,uBAA4B,KACnC,gBA2CF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAC/B,YAAY,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,EACzC,MAAM,MAAM,EACZ,YAAY,MAAM,KAChB,OAGF,CAAC;AAEF,kFAAkF;AAClF,eAAO,MAAM,0BAA0B,GACtC,YAAY,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,EACzC,YAAY,MAAM,KAChB,KAAK,CAAC,MAAM,CAMd,CAAC"}
|