@fuzdev/fuz_app 0.55.0 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/CLAUDE.md +211 -155
- package/dist/actions/action_bridge.d.ts +8 -5
- package/dist/actions/action_bridge.d.ts.map +1 -1
- package/dist/actions/action_bridge.js +1 -11
- package/dist/actions/action_codegen.d.ts +19 -0
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +20 -14
- package/dist/actions/action_registry.d.ts.map +1 -1
- package/dist/actions/action_registry.js +5 -2
- package/dist/actions/action_rpc.d.ts +110 -44
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +92 -287
- package/dist/actions/action_spec.d.ts +55 -16
- package/dist/actions/action_spec.d.ts.map +1 -1
- package/dist/actions/action_spec.js +16 -11
- package/dist/actions/action_types.d.ts +28 -60
- package/dist/actions/action_types.d.ts.map +1 -1
- package/dist/actions/action_types.js +13 -5
- package/dist/actions/broadcast_api.d.ts +2 -2
- package/dist/actions/broadcast_api.js +2 -2
- package/dist/actions/compile_action_registry.d.ts +50 -0
- package/dist/actions/compile_action_registry.d.ts.map +1 -0
- package/dist/actions/compile_action_registry.js +69 -0
- package/dist/actions/heartbeat.d.ts +8 -4
- package/dist/actions/heartbeat.d.ts.map +1 -1
- package/dist/actions/heartbeat.js +5 -4
- package/dist/actions/perform_action.d.ts +145 -0
- package/dist/actions/perform_action.d.ts.map +1 -0
- package/dist/actions/perform_action.js +258 -0
- package/dist/actions/register_action_ws.d.ts +44 -38
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +101 -159
- package/dist/actions/register_ws_endpoint.d.ts +2 -10
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +32 -10
- package/dist/actions/transports_ws_auth_guard.d.ts +1 -1
- package/dist/actions/transports_ws_auth_guard.js +1 -1
- package/dist/actions/transports_ws_backend.d.ts +1 -1
- package/dist/actions/transports_ws_backend.js +1 -1
- package/dist/auth/CLAUDE.md +673 -442
- package/dist/auth/account_action_specs.d.ts +28 -7
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +7 -7
- package/dist/auth/account_actions.d.ts +8 -14
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +26 -32
- package/dist/auth/account_queries.d.ts +46 -13
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +73 -33
- package/dist/auth/account_routes.d.ts +4 -3
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +58 -33
- package/dist/auth/account_schema.d.ts +46 -54
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +21 -48
- package/dist/auth/admin_action_specs.d.ts +55 -21
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +42 -26
- package/dist/auth/admin_actions.d.ts +14 -21
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +47 -44
- package/dist/auth/audit_emitter.d.ts +160 -0
- package/dist/auth/audit_emitter.d.ts.map +1 -0
- package/dist/auth/audit_emitter.js +83 -0
- package/dist/auth/audit_log_queries.d.ts +17 -87
- package/dist/auth/audit_log_queries.d.ts.map +1 -1
- package/dist/auth/audit_log_queries.js +17 -96
- package/dist/auth/audit_log_routes.d.ts +1 -1
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +7 -3
- package/dist/auth/audit_log_schema.d.ts +48 -42
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +56 -43
- package/dist/auth/auth_guard_resolver.d.ts +44 -0
- package/dist/auth/auth_guard_resolver.d.ts.map +1 -0
- package/dist/auth/auth_guard_resolver.js +56 -0
- package/dist/auth/bootstrap_account.d.ts +7 -7
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +7 -7
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +11 -10
- package/dist/auth/cleanup.d.ts +20 -26
- package/dist/auth/cleanup.d.ts.map +1 -1
- package/dist/auth/cleanup.js +33 -47
- package/dist/auth/credential_type_schema.d.ts +115 -0
- package/dist/auth/credential_type_schema.d.ts.map +1 -0
- package/dist/auth/credential_type_schema.js +127 -0
- package/dist/auth/daemon_token_middleware.d.ts +1 -1
- package/dist/auth/daemon_token_middleware.js +3 -3
- package/dist/auth/ddl.d.ts +2 -2
- package/dist/auth/ddl.d.ts.map +1 -1
- package/dist/auth/ddl.js +6 -6
- package/dist/auth/deps.d.ts +7 -32
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/grant_path_schema.d.ts +117 -0
- package/dist/auth/grant_path_schema.d.ts.map +1 -0
- package/dist/auth/grant_path_schema.js +137 -0
- package/dist/auth/invite_queries.d.ts +12 -1
- package/dist/auth/invite_queries.d.ts.map +1 -1
- package/dist/auth/invite_queries.js +12 -1
- package/dist/auth/invite_schema.d.ts +1 -1
- package/dist/auth/invite_schema.d.ts.map +1 -1
- package/dist/auth/invite_schema.js +1 -1
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +5 -2
- package/dist/auth/migrations.d.ts +22 -7
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +64 -25
- package/dist/auth/request_context.d.ts +157 -170
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +224 -268
- package/dist/auth/{permit_offer_action_specs.d.ts → role_grant_offer_action_specs.d.ts} +130 -100
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -0
- package/dist/auth/role_grant_offer_action_specs.js +262 -0
- package/dist/auth/role_grant_offer_actions.d.ts +104 -0
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -0
- package/dist/auth/{permit_offer_actions.js → role_grant_offer_actions.js} +153 -140
- package/dist/auth/{permit_offer_notifications.d.ts → role_grant_offer_notifications.d.ts} +80 -70
- package/dist/auth/role_grant_offer_notifications.d.ts.map +1 -0
- package/dist/auth/role_grant_offer_notifications.js +182 -0
- package/dist/auth/{permit_offer_queries.d.ts → role_grant_offer_queries.d.ts} +64 -64
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -0
- package/dist/auth/{permit_offer_queries.js → role_grant_offer_queries.js} +136 -123
- package/dist/auth/role_grant_offer_schema.d.ts +150 -0
- package/dist/auth/role_grant_offer_schema.d.ts.map +1 -0
- package/dist/auth/{permit_offer_schema.js → role_grant_offer_schema.js} +55 -36
- package/dist/auth/role_grant_queries.d.ts +231 -0
- package/dist/auth/role_grant_queries.d.ts.map +1 -0
- package/dist/auth/role_grant_queries.js +320 -0
- package/dist/auth/role_schema.d.ts +150 -40
- package/dist/auth/role_schema.d.ts.map +1 -1
- package/dist/auth/role_schema.js +144 -45
- package/dist/auth/scope_kind_schema.d.ts +96 -0
- package/dist/auth/scope_kind_schema.d.ts.map +1 -0
- package/dist/auth/scope_kind_schema.js +94 -0
- package/dist/auth/self_service_role_action_specs.d.ts +4 -1
- package/dist/auth/self_service_role_action_specs.d.ts.map +1 -1
- package/dist/auth/self_service_role_action_specs.js +2 -2
- package/dist/auth/self_service_role_actions.d.ts +35 -29
- package/dist/auth/self_service_role_actions.d.ts.map +1 -1
- package/dist/auth/self_service_role_actions.js +58 -48
- package/dist/auth/session_cookie.d.ts +43 -6
- package/dist/auth/session_cookie.d.ts.map +1 -1
- package/dist/auth/session_cookie.js +31 -5
- package/dist/auth/session_middleware.d.ts +37 -3
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +33 -7
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +48 -19
- package/dist/auth/standard_action_specs.d.ts +2 -2
- package/dist/auth/standard_action_specs.js +4 -4
- package/dist/auth/standard_rpc_actions.d.ts +23 -19
- package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
- package/dist/auth/standard_rpc_actions.js +12 -12
- package/dist/db/migrate.d.ts +1 -1
- package/dist/db/migrate.js +1 -1
- package/dist/dev/setup.d.ts +2 -2
- package/dist/dev/setup.d.ts.map +1 -1
- package/dist/dev/setup.js +4 -4
- package/dist/env/load.d.ts +1 -1
- package/dist/env/load.js +1 -1
- package/dist/hono_context.d.ts +27 -45
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/hono_context.js +14 -28
- package/dist/http/CLAUDE.md +235 -121
- package/dist/http/auth_shape.d.ts +191 -0
- package/dist/http/auth_shape.d.ts.map +1 -0
- package/dist/http/auth_shape.js +237 -0
- package/dist/http/common_routes.js +3 -3
- package/dist/http/db_routes.d.ts +4 -0
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +44 -7
- package/dist/http/error_schemas.d.ts +56 -34
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +63 -28
- package/dist/http/pending_effects.d.ts +71 -18
- package/dist/http/pending_effects.d.ts.map +1 -1
- package/dist/http/pending_effects.js +87 -18
- package/dist/http/proxy.d.ts +52 -5
- package/dist/http/proxy.d.ts.map +1 -1
- package/dist/http/proxy.js +92 -14
- package/dist/http/route_spec.d.ts +89 -75
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/route_spec.js +54 -72
- package/dist/http/schema_helpers.d.ts +3 -14
- package/dist/http/schema_helpers.d.ts.map +1 -1
- package/dist/http/schema_helpers.js +2 -14
- package/dist/http/surface.d.ts +2 -10
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +3 -4
- package/dist/http/surface_query.d.ts +39 -35
- package/dist/http/surface_query.d.ts.map +1 -1
- package/dist/http/surface_query.js +79 -36
- package/dist/primitive_schemas.d.ts +39 -0
- package/dist/primitive_schemas.d.ts.map +1 -0
- package/dist/primitive_schemas.js +40 -0
- package/dist/realtime/sse_auth_guard.d.ts +5 -5
- package/dist/realtime/sse_auth_guard.js +9 -9
- package/dist/runtime/mock.d.ts +1 -1
- package/dist/runtime/mock.js +1 -1
- package/dist/server/app_backend.d.ts +14 -11
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +12 -8
- package/dist/server/app_server.d.ts +7 -7
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +35 -40
- package/dist/server/validate_nginx.d.ts +1 -1
- package/dist/server/validate_nginx.js +1 -1
- package/dist/testing/CLAUDE.md +50 -38
- package/dist/testing/admin_integration.d.ts +5 -6
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +87 -85
- package/dist/testing/app_server.d.ts +11 -14
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +16 -15
- package/dist/testing/assertions.d.ts.map +1 -1
- package/dist/testing/assertions.js +2 -1
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +15 -9
- package/dist/testing/audit_completeness.d.ts +2 -2
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +36 -36
- package/dist/testing/auth_apps.d.ts +5 -4
- package/dist/testing/auth_apps.d.ts.map +1 -1
- package/dist/testing/auth_apps.js +22 -19
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +5 -5
- package/dist/testing/db.d.ts +1 -1
- package/dist/testing/db.d.ts.map +1 -1
- package/dist/testing/db.js +4 -4
- package/dist/testing/db_entities.d.ts +22 -0
- package/dist/testing/db_entities.d.ts.map +1 -0
- package/dist/testing/db_entities.js +28 -0
- package/dist/testing/entities.d.ts +8 -7
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +21 -18
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +13 -14
- package/dist/testing/integration_helpers.d.ts +4 -4
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +20 -18
- package/dist/testing/middleware.d.ts +4 -4
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +12 -11
- package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
- package/dist/testing/rpc_attack_surface.js +40 -24
- package/dist/testing/rpc_round_trip.d.ts +1 -1
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +14 -13
- package/dist/testing/sse_round_trip.d.ts +3 -4
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +7 -11
- package/dist/testing/standard.d.ts +1 -1
- package/dist/testing/stubs.d.ts +25 -0
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +43 -2
- package/dist/testing/surface_invariants.d.ts +2 -2
- package/dist/testing/ws_round_trip.d.ts +12 -13
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +19 -11
- package/dist/ui/AdminAccounts.svelte +23 -20
- package/dist/ui/AdminOverview.svelte +15 -13
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/{AdminPermitHistory.svelte → AdminRoleGrantHistory.svelte} +12 -12
- package/dist/ui/AdminRoleGrantHistory.svelte.d.ts +4 -0
- package/dist/ui/AdminRoleGrantHistory.svelte.d.ts.map +1 -0
- package/dist/ui/BootstrapForm.svelte +1 -1
- package/dist/ui/CLAUDE.md +60 -60
- package/dist/ui/{PermitOfferForm.svelte → RoleGrantOfferForm.svelte} +27 -26
- package/dist/ui/{PermitOfferForm.svelte.d.ts → RoleGrantOfferForm.svelte.d.ts} +7 -7
- package/dist/ui/RoleGrantOfferForm.svelte.d.ts.map +1 -0
- package/dist/ui/{PermitOfferHistory.svelte → RoleGrantOfferHistory.svelte} +12 -12
- package/dist/ui/{PermitOfferHistory.svelte.d.ts → RoleGrantOfferHistory.svelte.d.ts} +4 -4
- package/dist/ui/RoleGrantOfferHistory.svelte.d.ts.map +1 -0
- package/dist/ui/{PermitOfferInbox.svelte → RoleGrantOfferInbox.svelte} +14 -14
- package/dist/ui/{PermitOfferInbox.svelte.d.ts → RoleGrantOfferInbox.svelte.d.ts} +4 -4
- package/dist/ui/RoleGrantOfferInbox.svelte.d.ts.map +1 -0
- package/dist/ui/SignupForm.svelte +1 -1
- package/dist/ui/SurfaceExplorer.svelte +35 -15
- package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.d.ts +2 -3
- package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.js +2 -3
- package/dist/ui/admin_accounts_state.svelte.d.ts +18 -18
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +16 -16
- package/dist/ui/admin_rpc_adapters.d.ts +20 -20
- package/dist/ui/admin_rpc_adapters.d.ts.map +1 -1
- package/dist/ui/admin_rpc_adapters.js +17 -17
- package/dist/ui/admin_sessions_state.svelte.d.ts +2 -2
- package/dist/ui/admin_sessions_state.svelte.js +2 -2
- package/dist/ui/audit_log_state.svelte.d.ts +7 -7
- package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
- package/dist/ui/audit_log_state.svelte.js +6 -6
- package/dist/ui/auth_state.svelte.d.ts +3 -3
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +6 -6
- package/dist/ui/format_scope.d.ts +2 -2
- package/dist/ui/format_scope.js +2 -2
- package/dist/ui/{permit_offers_state.svelte.d.ts → role_grant_offers_state.svelte.d.ts} +30 -30
- package/dist/ui/role_grant_offers_state.svelte.d.ts.map +1 -0
- package/dist/ui/{permit_offers_state.svelte.js → role_grant_offers_state.svelte.js} +18 -18
- package/dist/ui/ui_format.js +2 -2
- package/package.json +3 -3
- package/dist/auth/permit_offer_action_specs.d.ts.map +0 -1
- package/dist/auth/permit_offer_action_specs.js +0 -258
- package/dist/auth/permit_offer_actions.d.ts +0 -110
- package/dist/auth/permit_offer_actions.d.ts.map +0 -1
- package/dist/auth/permit_offer_notifications.d.ts.map +0 -1
- package/dist/auth/permit_offer_notifications.js +0 -182
- package/dist/auth/permit_offer_queries.d.ts.map +0 -1
- package/dist/auth/permit_offer_schema.d.ts +0 -125
- package/dist/auth/permit_offer_schema.d.ts.map +0 -1
- package/dist/auth/permit_queries.d.ts +0 -222
- package/dist/auth/permit_queries.d.ts.map +0 -1
- package/dist/auth/permit_queries.js +0 -305
- package/dist/auth/require_keeper.d.ts +0 -20
- package/dist/auth/require_keeper.d.ts.map +0 -1
- package/dist/auth/require_keeper.js +0 -35
- package/dist/auth/route_guards.d.ts +0 -27
- package/dist/auth/route_guards.d.ts.map +0 -1
- package/dist/auth/route_guards.js +0 -38
- package/dist/auth/session_lifecycle.d.ts +0 -37
- package/dist/auth/session_lifecycle.d.ts.map +0 -1
- package/dist/auth/session_lifecycle.js +0 -29
- package/dist/ui/AdminPermitHistory.svelte.d.ts +0 -4
- package/dist/ui/AdminPermitHistory.svelte.d.ts.map +0 -1
- package/dist/ui/PermitOfferForm.svelte.d.ts.map +0 -1
- package/dist/ui/PermitOfferHistory.svelte.d.ts.map +0 -1
- package/dist/ui/PermitOfferInbox.svelte.d.ts.map +0 -1
- package/dist/ui/permit_offers_state.svelte.d.ts.map +0 -1
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Permit database queries.
|
|
3
|
-
*
|
|
4
|
-
* Permits are time-bounded, revocable grants of a role to an actor.
|
|
5
|
-
* The system is safe by default — no permit, no capability.
|
|
6
|
-
*
|
|
7
|
-
* @module
|
|
8
|
-
*/
|
|
9
|
-
import type { Uuid } from '@fuzdev/fuz_util/id.js';
|
|
10
|
-
import type { QueryDeps } from '../db/query_deps.js';
|
|
11
|
-
import type { Permit, GrantPermitInput } from './account_schema.js';
|
|
12
|
-
import { type SupersededOffer } from './permit_offer_schema.js';
|
|
13
|
-
/**
|
|
14
|
-
* Grant a permit to an actor.
|
|
15
|
-
* Idempotent — if an active permit already exists for this actor, role, and
|
|
16
|
-
* scope, returns the existing permit instead of creating a duplicate.
|
|
17
|
-
*
|
|
18
|
-
* The `ON CONFLICT` target and the fallback `SELECT` both collapse `NULL`
|
|
19
|
-
* scopes via the same sentinel used by the partial unique index
|
|
20
|
-
* (`permit_actor_role_scope_active_unique`). The `IS NOT DISTINCT FROM`
|
|
21
|
-
* form on the fallback is deliberate — plain `=` would miss the
|
|
22
|
-
* NULL-scope case where the conflict fired.
|
|
23
|
-
*
|
|
24
|
-
* @param deps - query dependencies
|
|
25
|
-
* @param input - the permit fields
|
|
26
|
-
* @returns the created or existing active permit
|
|
27
|
-
* @mutates `permit` table - inserts a row when no active permit matches `(actor_id, role, scope_id)`
|
|
28
|
-
*/
|
|
29
|
-
export declare const query_grant_permit: (deps: QueryDeps, input: GrantPermitInput) => Promise<Permit>;
|
|
30
|
-
/**
|
|
31
|
-
* Look up the role of an active permit (constrained to a specific
|
|
32
|
-
* actor) plus the actor's `account_id`.
|
|
33
|
-
*
|
|
34
|
-
* Used by admin routes to inspect the permit's role before acting
|
|
35
|
-
* (e.g., enforcing `web_grantable` on revoke). The actor constraint
|
|
36
|
-
* mirrors `query_revoke_permit` so IDOR protection is consistent:
|
|
37
|
-
* a caller can only see permits belonging to the target actor.
|
|
38
|
-
*
|
|
39
|
-
* The JOIN to `actor` collapses what used to be a second
|
|
40
|
-
* `query_actor_by_id` round-trip in the revoke handler into one read,
|
|
41
|
-
* which closes the small TOCTOU window where the actor row could be
|
|
42
|
-
* deleted between the IDOR check and the actor lookup. The `account_id`
|
|
43
|
-
* is needed by the audit envelope's `target_account_id` field and the
|
|
44
|
-
* SSE/WS socket-close fan-out targeting.
|
|
45
|
-
*
|
|
46
|
-
* Returns `null` if the permit is not found, already revoked, or
|
|
47
|
-
* belongs to a different actor.
|
|
48
|
-
*
|
|
49
|
-
* @param deps - query dependencies
|
|
50
|
-
* @param permit_id - the permit id to look up
|
|
51
|
-
* @param actor_id - the actor that must own the permit
|
|
52
|
-
* @returns `{role, account_id}` on a match, or `null`
|
|
53
|
-
*/
|
|
54
|
-
export declare const query_permit_find_active_role_for_actor: (deps: QueryDeps, permit_id: string, actor_id: string) => Promise<{
|
|
55
|
-
role: string;
|
|
56
|
-
account_id: Uuid;
|
|
57
|
-
} | null>;
|
|
58
|
-
/** Result of `query_revoke_permit` — the revoked permit plus any pending offers superseded by the revoke. */
|
|
59
|
-
export interface RevokePermitResult {
|
|
60
|
-
id: Uuid;
|
|
61
|
-
role: string;
|
|
62
|
-
scope_id: Uuid | null;
|
|
63
|
-
/**
|
|
64
|
-
* Pending offers for the revoked permit's `(account, role, scope)` that
|
|
65
|
-
* were marked superseded as a side effect. Each entry carries its
|
|
66
|
-
* grantor's `from_account_id` so callers can fan out
|
|
67
|
-
* `permit_offer_supersede` notifications without a second round-trip.
|
|
68
|
-
* The caller is responsible for emitting a `permit_offer_supersede`
|
|
69
|
-
* audit event per entry (with `reason: 'permit_revoked'` and
|
|
70
|
-
* `cause_id: <revoked permit id>`).
|
|
71
|
-
*/
|
|
72
|
-
superseded_offers: Array<SupersededOffer>;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Revoke a permit by id, constrained to a specific actor.
|
|
76
|
-
*
|
|
77
|
-
* Requires `actor_id` to prevent cross-account revocation (IDOR guard).
|
|
78
|
-
* Returns `null` if the permit is not found, already revoked, or belongs
|
|
79
|
-
* to a different actor.
|
|
80
|
-
*
|
|
81
|
-
* Supersedes any pending offers for the revoked permit's
|
|
82
|
-
* `(to_account, role, scope)` in the same transaction. Prevents the
|
|
83
|
-
* "accept a pre-revoke offer to bypass the revoke" path — any stale
|
|
84
|
-
* offer becomes terminal at revoke time. A fresh post-revoke grant
|
|
85
|
-
* requires the grantor to call `query_permit_offer_create` again.
|
|
86
|
-
*
|
|
87
|
-
* @param deps - query dependencies
|
|
88
|
-
* @param permit_id - the permit to revoke
|
|
89
|
-
* @param actor_id - the actor that must own the permit
|
|
90
|
-
* @param revoked_by - the actor who revoked it (for audit trail)
|
|
91
|
-
* @param reason - optional free-form reason, stamped on `permit.revoked_reason` and surfaced to the revokee notification.
|
|
92
|
-
* @mutates `permit` row - sets `revoked_at`, `revoked_by`, and `revoked_reason`
|
|
93
|
-
* @mutates `permit_offer` rows - stamps `superseded_at` on every pending sibling for the same `(account, role, scope)`
|
|
94
|
-
*/
|
|
95
|
-
export declare const query_revoke_permit: (deps: QueryDeps, permit_id: Uuid, actor_id: Uuid, revoked_by: Uuid | null, reason?: string | null) => Promise<RevokePermitResult | null>;
|
|
96
|
-
/**
|
|
97
|
-
* Find all active (non-revoked, non-expired) permits for an actor.
|
|
98
|
-
*/
|
|
99
|
-
export declare const query_permit_find_active_for_actor: (deps: QueryDeps, actor_id: string) => Promise<Array<Permit>>;
|
|
100
|
-
/**
|
|
101
|
-
* Check if an actor has an active permit for a given role.
|
|
102
|
-
*
|
|
103
|
-
* The `scope_id` parameter selects between global and scoped checks:
|
|
104
|
-
* - Omitted or `null` — matches a global permit (`scope_id IS NULL`).
|
|
105
|
-
* Pre-scope callers keep their existing semantics.
|
|
106
|
-
* - A scope uuid — matches a permit bound to that exact scope.
|
|
107
|
-
*
|
|
108
|
-
* The `IS NOT DISTINCT FROM` comparison handles the NULL case uniformly.
|
|
109
|
-
*/
|
|
110
|
-
export declare const query_permit_has_role: (deps: QueryDeps, actor_id: string, role: string, scope_id?: string | null) => Promise<boolean>;
|
|
111
|
-
/**
|
|
112
|
-
* List all permits for an actor (including revoked/expired).
|
|
113
|
-
*/
|
|
114
|
-
export declare const query_permit_list_for_actor: (deps: QueryDeps, actor_id: string) => Promise<Array<Permit>>;
|
|
115
|
-
/**
|
|
116
|
-
* Find the account ID of an account that holds an active permit for a given role.
|
|
117
|
-
*
|
|
118
|
-
* Joins permit → actor → account. Returns the first match, or `null` if none.
|
|
119
|
-
*
|
|
120
|
-
* @param deps - query dependencies
|
|
121
|
-
* @param role - the role to search for
|
|
122
|
-
* @returns the account ID, or `null`
|
|
123
|
-
*/
|
|
124
|
-
export declare const query_permit_find_account_id_for_role: (deps: QueryDeps, role: string) => Promise<string | null>;
|
|
125
|
-
/** Result of `query_permit_revoke_for_scope` — every permit revoked plus every pending offer superseded by the scope-wide cascade. */
|
|
126
|
-
export interface RevokeForScopeResult {
|
|
127
|
-
/**
|
|
128
|
-
* One entry per permit revoked by this call. Carries both the revokee's
|
|
129
|
-
* `actor_id` (the permit's grantee — drives `target_actor_id` audit
|
|
130
|
-
* envelopes) and `account_id` (the actor's account — drives
|
|
131
|
-
* `target_account_id` for SSE/WS socket-close fan-out). Empty array
|
|
132
|
-
* means no active permit was bound to the scope.
|
|
133
|
-
*/
|
|
134
|
-
revoked: Array<{
|
|
135
|
-
permit_id: Uuid;
|
|
136
|
-
role: string;
|
|
137
|
-
scope_id: Uuid;
|
|
138
|
-
actor_id: Uuid;
|
|
139
|
-
account_id: Uuid;
|
|
140
|
-
}>;
|
|
141
|
-
/**
|
|
142
|
-
* Every pending offer at the scope — tuple-matched and orphan, undifferentiated
|
|
143
|
-
* — superseded in the same cascade. Each entry carries its grantor's
|
|
144
|
-
* `from_account_id` for `permit_offer_supersede` notification fan-out.
|
|
145
|
-
*
|
|
146
|
-
* The caller is responsible for emitting `permit_offer_supersede` audit
|
|
147
|
-
* events with `reason: 'scope_destroyed'` and `cause_id: <destroyed scope row id>`
|
|
148
|
-
* per entry — the cause of every supersede here is the scope deletion,
|
|
149
|
-
* not any individual permit revoke (the revokes are themselves
|
|
150
|
-
* consequences of the scope going away).
|
|
151
|
-
*/
|
|
152
|
-
superseded_offers: Array<SupersededOffer>;
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Revoke every active permit bound to a scope and supersede every pending
|
|
156
|
-
* offer at the scope, in one cascade.
|
|
157
|
-
*
|
|
158
|
-
* Use this from a consumer's parent-scope delete handler (e.g., classroom
|
|
159
|
-
* deletion) — `permit.scope_id` and `permit_offer.scope_id` are polymorphic
|
|
160
|
-
* with no FK constraint by design, so a parent row deletion would otherwise
|
|
161
|
-
* orphan permits and offers. The cascade is **role-agnostic**: anything
|
|
162
|
-
* attached to the destroyed scope is cleaned up.
|
|
163
|
-
*
|
|
164
|
-
* Both updates run as separate statements inside the caller's transaction
|
|
165
|
-
* (mirrors `query_permit_revoke_role`'s shape). The two halves are
|
|
166
|
-
* independent — orphan pending offers can exist at a scope with no active
|
|
167
|
-
* permits, so the supersede half always runs even when no permit was
|
|
168
|
-
* revoked.
|
|
169
|
-
*
|
|
170
|
-
* @param deps - query dependencies
|
|
171
|
-
* @param scope_id - the scope whose permits and offers to terminate
|
|
172
|
-
* @param revoked_by - the actor performing the cascade (audit trail)
|
|
173
|
-
* @param reason - optional free-form reason, stamped on `permit.revoked_reason`.
|
|
174
|
-
* @returns the revoked permits (with `account_id` for fan-out) and superseded offers (with `from_account_id` for fan-out)
|
|
175
|
-
* @mutates `permit` table - sets `revoked_at`/`revoked_by`/`revoked_reason` on every active row at `scope_id`
|
|
176
|
-
* @mutates `permit_offer` table - stamps `superseded_at` on every pending row at `scope_id`
|
|
177
|
-
*/
|
|
178
|
-
export declare const query_permit_revoke_for_scope: (deps: QueryDeps, scope_id: Uuid, revoked_by: Uuid | null, reason?: string | null) => Promise<RevokeForScopeResult>;
|
|
179
|
-
/** Result of `query_permit_revoke_role` — every permit revoked plus the pending offers superseded by the bulk revoke. */
|
|
180
|
-
export interface RevokeRoleResult {
|
|
181
|
-
/**
|
|
182
|
-
* One entry per permit revoked by this call. Carries the revokee's
|
|
183
|
-
* `account_id` so callers can fan out a `permit_revoke` notification per
|
|
184
|
-
* scope-instance. Empty array means nothing was active for `(actor, role)`.
|
|
185
|
-
*/
|
|
186
|
-
revoked: Array<{
|
|
187
|
-
permit_id: string;
|
|
188
|
-
role: string;
|
|
189
|
-
scope_id: string | null;
|
|
190
|
-
account_id: string;
|
|
191
|
-
}>;
|
|
192
|
-
/**
|
|
193
|
-
* Pending offers for the actor's account+role (all scopes) superseded by
|
|
194
|
-
* the bulk revoke. Each entry carries its grantor's `from_account_id` so
|
|
195
|
-
* callers can fan out `permit_offer_supersede` notifications without a
|
|
196
|
-
* second round-trip.
|
|
197
|
-
*/
|
|
198
|
-
superseded_offers: Array<SupersededOffer>;
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Revoke every active permit an actor holds for a given role.
|
|
202
|
-
*
|
|
203
|
-
* With scoped permits a single actor+role tuple can hold several active
|
|
204
|
-
* permits (one per scope), so this revokes all of them. Pass
|
|
205
|
-
* `query_revoke_permit(permit_id, ...)` when a single scoped permit
|
|
206
|
-
* is the target.
|
|
207
|
-
*
|
|
208
|
-
* Also supersedes pending offers for the actor's account across every
|
|
209
|
-
* scope of this role (the actor can no longer hold the role, so any
|
|
210
|
-
* pending offer of the same role is a bypass vector).
|
|
211
|
-
*
|
|
212
|
-
* @param deps - query dependencies
|
|
213
|
-
* @param actor_id - the actor whose permits to revoke
|
|
214
|
-
* @param role - the role to revoke
|
|
215
|
-
* @param revoked_by - the actor who revoked it (for audit trail)
|
|
216
|
-
* @param reason - optional free-form reason, stamped on `permit.revoked_reason`.
|
|
217
|
-
* @returns the list of revoked permits (empty if none were active) and superseded pending offers
|
|
218
|
-
* @mutates `permit` table - sets `revoked_at`/`revoked_by`/`revoked_reason` on every active row for `(actor, role)`
|
|
219
|
-
* @mutates `permit_offer` table - stamps `superseded_at` on every matching pending offer
|
|
220
|
-
*/
|
|
221
|
-
export declare const query_permit_revoke_role: (deps: QueryDeps, actor_id: string, role: string, revoked_by: string | null, reason?: string | null) => Promise<RevokeRoleResult>;
|
|
222
|
-
//# sourceMappingURL=permit_queries.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"permit_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/permit_queries.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Permit database queries.
|
|
3
|
-
*
|
|
4
|
-
* Permits are time-bounded, revocable grants of a role to an actor.
|
|
5
|
-
* The system is safe by default — no permit, no capability.
|
|
6
|
-
*
|
|
7
|
-
* @module
|
|
8
|
-
*/
|
|
9
|
-
import { assert_row } from '../db/assert_row.js';
|
|
10
|
-
import { PERMIT_OFFER_SCOPE_SENTINEL_UUID } from './permit_offer_schema.js';
|
|
11
|
-
/**
|
|
12
|
-
* Grant a permit to an actor.
|
|
13
|
-
* Idempotent — if an active permit already exists for this actor, role, and
|
|
14
|
-
* scope, returns the existing permit instead of creating a duplicate.
|
|
15
|
-
*
|
|
16
|
-
* The `ON CONFLICT` target and the fallback `SELECT` both collapse `NULL`
|
|
17
|
-
* scopes via the same sentinel used by the partial unique index
|
|
18
|
-
* (`permit_actor_role_scope_active_unique`). The `IS NOT DISTINCT FROM`
|
|
19
|
-
* form on the fallback is deliberate — plain `=` would miss the
|
|
20
|
-
* NULL-scope case where the conflict fired.
|
|
21
|
-
*
|
|
22
|
-
* @param deps - query dependencies
|
|
23
|
-
* @param input - the permit fields
|
|
24
|
-
* @returns the created or existing active permit
|
|
25
|
-
* @mutates `permit` table - inserts a row when no active permit matches `(actor_id, role, scope_id)`
|
|
26
|
-
*/
|
|
27
|
-
export const query_grant_permit = async (deps, input) => {
|
|
28
|
-
const inserted = await deps.db.query_one(`INSERT INTO permit (actor_id, role, scope_id, expires_at, granted_by, source_offer_id)
|
|
29
|
-
VALUES ($1, $2, $3, $4, $5, $6)
|
|
30
|
-
ON CONFLICT (actor_id, role, COALESCE(scope_id, '${PERMIT_OFFER_SCOPE_SENTINEL_UUID}'::uuid))
|
|
31
|
-
WHERE revoked_at IS NULL
|
|
32
|
-
DO NOTHING
|
|
33
|
-
RETURNING *`, [
|
|
34
|
-
input.actor_id,
|
|
35
|
-
input.role,
|
|
36
|
-
input.scope_id ?? null,
|
|
37
|
-
input.expires_at?.toISOString() ?? null,
|
|
38
|
-
input.granted_by ?? null,
|
|
39
|
-
input.source_offer_id ?? null,
|
|
40
|
-
]);
|
|
41
|
-
if (inserted)
|
|
42
|
-
return inserted;
|
|
43
|
-
// Active permit already exists — return it (idempotent grant).
|
|
44
|
-
const existing = await deps.db.query_one(`SELECT * FROM permit
|
|
45
|
-
WHERE actor_id = $1
|
|
46
|
-
AND role = $2
|
|
47
|
-
AND scope_id IS NOT DISTINCT FROM $3
|
|
48
|
-
AND revoked_at IS NULL`, [input.actor_id, input.role, input.scope_id ?? null]);
|
|
49
|
-
return assert_row(existing, 'idempotent permit grant');
|
|
50
|
-
};
|
|
51
|
-
/**
|
|
52
|
-
* Look up the role of an active permit (constrained to a specific
|
|
53
|
-
* actor) plus the actor's `account_id`.
|
|
54
|
-
*
|
|
55
|
-
* Used by admin routes to inspect the permit's role before acting
|
|
56
|
-
* (e.g., enforcing `web_grantable` on revoke). The actor constraint
|
|
57
|
-
* mirrors `query_revoke_permit` so IDOR protection is consistent:
|
|
58
|
-
* a caller can only see permits belonging to the target actor.
|
|
59
|
-
*
|
|
60
|
-
* The JOIN to `actor` collapses what used to be a second
|
|
61
|
-
* `query_actor_by_id` round-trip in the revoke handler into one read,
|
|
62
|
-
* which closes the small TOCTOU window where the actor row could be
|
|
63
|
-
* deleted between the IDOR check and the actor lookup. The `account_id`
|
|
64
|
-
* is needed by the audit envelope's `target_account_id` field and the
|
|
65
|
-
* SSE/WS socket-close fan-out targeting.
|
|
66
|
-
*
|
|
67
|
-
* Returns `null` if the permit is not found, already revoked, or
|
|
68
|
-
* belongs to a different actor.
|
|
69
|
-
*
|
|
70
|
-
* @param deps - query dependencies
|
|
71
|
-
* @param permit_id - the permit id to look up
|
|
72
|
-
* @param actor_id - the actor that must own the permit
|
|
73
|
-
* @returns `{role, account_id}` on a match, or `null`
|
|
74
|
-
*/
|
|
75
|
-
export const query_permit_find_active_role_for_actor = async (deps, permit_id, actor_id) => {
|
|
76
|
-
const row = await deps.db.query_one(`SELECT permit.role, actor.account_id
|
|
77
|
-
FROM permit
|
|
78
|
-
JOIN actor ON actor.id = permit.actor_id
|
|
79
|
-
WHERE permit.id = $1 AND permit.actor_id = $2 AND permit.revoked_at IS NULL`, [permit_id, actor_id]);
|
|
80
|
-
return row ?? null;
|
|
81
|
-
};
|
|
82
|
-
/**
|
|
83
|
-
* Revoke a permit by id, constrained to a specific actor.
|
|
84
|
-
*
|
|
85
|
-
* Requires `actor_id` to prevent cross-account revocation (IDOR guard).
|
|
86
|
-
* Returns `null` if the permit is not found, already revoked, or belongs
|
|
87
|
-
* to a different actor.
|
|
88
|
-
*
|
|
89
|
-
* Supersedes any pending offers for the revoked permit's
|
|
90
|
-
* `(to_account, role, scope)` in the same transaction. Prevents the
|
|
91
|
-
* "accept a pre-revoke offer to bypass the revoke" path — any stale
|
|
92
|
-
* offer becomes terminal at revoke time. A fresh post-revoke grant
|
|
93
|
-
* requires the grantor to call `query_permit_offer_create` again.
|
|
94
|
-
*
|
|
95
|
-
* @param deps - query dependencies
|
|
96
|
-
* @param permit_id - the permit to revoke
|
|
97
|
-
* @param actor_id - the actor that must own the permit
|
|
98
|
-
* @param revoked_by - the actor who revoked it (for audit trail)
|
|
99
|
-
* @param reason - optional free-form reason, stamped on `permit.revoked_reason` and surfaced to the revokee notification.
|
|
100
|
-
* @mutates `permit` row - sets `revoked_at`, `revoked_by`, and `revoked_reason`
|
|
101
|
-
* @mutates `permit_offer` rows - stamps `superseded_at` on every pending sibling for the same `(account, role, scope)`
|
|
102
|
-
*/
|
|
103
|
-
export const query_revoke_permit = async (deps, permit_id, actor_id, revoked_by, reason) => {
|
|
104
|
-
const rows = await deps.db.query(`UPDATE permit SET revoked_at = NOW(), revoked_by = $3, revoked_reason = $4
|
|
105
|
-
WHERE id = $1 AND actor_id = $2 AND revoked_at IS NULL
|
|
106
|
-
RETURNING id, role, scope_id`, [permit_id, actor_id, revoked_by ?? null, reason ?? null]);
|
|
107
|
-
const revoked = rows[0];
|
|
108
|
-
if (!revoked)
|
|
109
|
-
return null;
|
|
110
|
-
// CTE joins `actor` after the UPDATE so each superseded row carries the
|
|
111
|
-
// grantor's `account_id` — callers fan out `permit_offer_supersede`
|
|
112
|
-
// notifications to that account without a second round-trip.
|
|
113
|
-
const superseded_offers = await deps.db.query(`WITH updated AS (
|
|
114
|
-
UPDATE permit_offer o
|
|
115
|
-
SET superseded_at = NOW()
|
|
116
|
-
FROM actor a
|
|
117
|
-
WHERE a.id = $1
|
|
118
|
-
AND o.to_account_id = a.account_id
|
|
119
|
-
AND o.role = $2
|
|
120
|
-
AND o.scope_id IS NOT DISTINCT FROM $3
|
|
121
|
-
AND o.accepted_at IS NULL
|
|
122
|
-
AND o.declined_at IS NULL
|
|
123
|
-
AND o.retracted_at IS NULL
|
|
124
|
-
AND o.superseded_at IS NULL
|
|
125
|
-
RETURNING o.*
|
|
126
|
-
)
|
|
127
|
-
SELECT u.*, grantor.account_id AS from_account_id
|
|
128
|
-
FROM updated u
|
|
129
|
-
JOIN actor grantor ON grantor.id = u.from_actor_id`, [actor_id, revoked.role, revoked.scope_id]);
|
|
130
|
-
return {
|
|
131
|
-
id: revoked.id,
|
|
132
|
-
role: revoked.role,
|
|
133
|
-
scope_id: revoked.scope_id,
|
|
134
|
-
superseded_offers,
|
|
135
|
-
};
|
|
136
|
-
};
|
|
137
|
-
/**
|
|
138
|
-
* Find all active (non-revoked, non-expired) permits for an actor.
|
|
139
|
-
*/
|
|
140
|
-
export const query_permit_find_active_for_actor = async (deps, actor_id) => {
|
|
141
|
-
return deps.db.query(`SELECT * FROM permit
|
|
142
|
-
WHERE actor_id = $1
|
|
143
|
-
AND revoked_at IS NULL
|
|
144
|
-
AND (expires_at IS NULL OR expires_at > NOW())
|
|
145
|
-
ORDER BY created_at`, [actor_id]);
|
|
146
|
-
};
|
|
147
|
-
/**
|
|
148
|
-
* Check if an actor has an active permit for a given role.
|
|
149
|
-
*
|
|
150
|
-
* The `scope_id` parameter selects between global and scoped checks:
|
|
151
|
-
* - Omitted or `null` — matches a global permit (`scope_id IS NULL`).
|
|
152
|
-
* Pre-scope callers keep their existing semantics.
|
|
153
|
-
* - A scope uuid — matches a permit bound to that exact scope.
|
|
154
|
-
*
|
|
155
|
-
* The `IS NOT DISTINCT FROM` comparison handles the NULL case uniformly.
|
|
156
|
-
*/
|
|
157
|
-
export const query_permit_has_role = async (deps, actor_id, role, scope_id) => {
|
|
158
|
-
const row = await deps.db.query_one(`SELECT EXISTS(
|
|
159
|
-
SELECT 1 FROM permit
|
|
160
|
-
WHERE actor_id = $1
|
|
161
|
-
AND role = $2
|
|
162
|
-
AND scope_id IS NOT DISTINCT FROM $3
|
|
163
|
-
AND revoked_at IS NULL
|
|
164
|
-
AND (expires_at IS NULL OR expires_at > NOW())
|
|
165
|
-
) AS exists`, [actor_id, role, scope_id ?? null]);
|
|
166
|
-
return row?.exists ?? false;
|
|
167
|
-
};
|
|
168
|
-
/**
|
|
169
|
-
* List all permits for an actor (including revoked/expired).
|
|
170
|
-
*/
|
|
171
|
-
export const query_permit_list_for_actor = async (deps, actor_id) => {
|
|
172
|
-
return deps.db.query(`SELECT * FROM permit WHERE actor_id = $1 ORDER BY created_at DESC`, [actor_id]);
|
|
173
|
-
};
|
|
174
|
-
/**
|
|
175
|
-
* Find the account ID of an account that holds an active permit for a given role.
|
|
176
|
-
*
|
|
177
|
-
* Joins permit → actor → account. Returns the first match, or `null` if none.
|
|
178
|
-
*
|
|
179
|
-
* @param deps - query dependencies
|
|
180
|
-
* @param role - the role to search for
|
|
181
|
-
* @returns the account ID, or `null`
|
|
182
|
-
*/
|
|
183
|
-
export const query_permit_find_account_id_for_role = async (deps, role) => {
|
|
184
|
-
const row = await deps.db.query_one(`SELECT a.id AS account_id
|
|
185
|
-
FROM permit p
|
|
186
|
-
JOIN actor act ON act.id = p.actor_id
|
|
187
|
-
JOIN account a ON a.id = act.account_id
|
|
188
|
-
WHERE p.role = $1
|
|
189
|
-
AND p.revoked_at IS NULL
|
|
190
|
-
AND (p.expires_at IS NULL OR p.expires_at > NOW())
|
|
191
|
-
LIMIT 1`, [role]);
|
|
192
|
-
return row?.account_id ?? null;
|
|
193
|
-
};
|
|
194
|
-
/**
|
|
195
|
-
* Revoke every active permit bound to a scope and supersede every pending
|
|
196
|
-
* offer at the scope, in one cascade.
|
|
197
|
-
*
|
|
198
|
-
* Use this from a consumer's parent-scope delete handler (e.g., classroom
|
|
199
|
-
* deletion) — `permit.scope_id` and `permit_offer.scope_id` are polymorphic
|
|
200
|
-
* with no FK constraint by design, so a parent row deletion would otherwise
|
|
201
|
-
* orphan permits and offers. The cascade is **role-agnostic**: anything
|
|
202
|
-
* attached to the destroyed scope is cleaned up.
|
|
203
|
-
*
|
|
204
|
-
* Both updates run as separate statements inside the caller's transaction
|
|
205
|
-
* (mirrors `query_permit_revoke_role`'s shape). The two halves are
|
|
206
|
-
* independent — orphan pending offers can exist at a scope with no active
|
|
207
|
-
* permits, so the supersede half always runs even when no permit was
|
|
208
|
-
* revoked.
|
|
209
|
-
*
|
|
210
|
-
* @param deps - query dependencies
|
|
211
|
-
* @param scope_id - the scope whose permits and offers to terminate
|
|
212
|
-
* @param revoked_by - the actor performing the cascade (audit trail)
|
|
213
|
-
* @param reason - optional free-form reason, stamped on `permit.revoked_reason`.
|
|
214
|
-
* @returns the revoked permits (with `account_id` for fan-out) and superseded offers (with `from_account_id` for fan-out)
|
|
215
|
-
* @mutates `permit` table - sets `revoked_at`/`revoked_by`/`revoked_reason` on every active row at `scope_id`
|
|
216
|
-
* @mutates `permit_offer` table - stamps `superseded_at` on every pending row at `scope_id`
|
|
217
|
-
*/
|
|
218
|
-
export const query_permit_revoke_for_scope = async (deps, scope_id, revoked_by, reason) => {
|
|
219
|
-
// Revoke every active permit at the scope. CTE returns `actor_id` directly
|
|
220
|
-
// from the permit row (drives `target_actor_id` audit envelopes); a join
|
|
221
|
-
// against `actor` resolves `account_id` for `target_account_id`
|
|
222
|
-
// + WS/SSE socket-close fan-out, all in one round-trip.
|
|
223
|
-
const revoked = await deps.db.query(`WITH updated AS (
|
|
224
|
-
UPDATE permit
|
|
225
|
-
SET revoked_at = NOW(), revoked_by = $2, revoked_reason = $3
|
|
226
|
-
WHERE scope_id = $1 AND revoked_at IS NULL
|
|
227
|
-
RETURNING id, role, scope_id, actor_id
|
|
228
|
-
)
|
|
229
|
-
SELECT u.id AS permit_id, u.role, u.scope_id, u.actor_id, a.account_id
|
|
230
|
-
FROM updated u
|
|
231
|
-
JOIN actor a ON a.id = u.actor_id`, [scope_id, revoked_by ?? null, reason ?? null]);
|
|
232
|
-
// Supersede every pending offer at the scope — tuple-matched or orphan,
|
|
233
|
-
// no distinction. The cause of every supersede in this cascade is the
|
|
234
|
-
// scope deletion; offers tuple-matched to a revoked permit are not
|
|
235
|
-
// tagged separately because the revoke is itself a consequence of the
|
|
236
|
-
// scope going away.
|
|
237
|
-
const superseded_offers = await deps.db.query(`WITH updated AS (
|
|
238
|
-
UPDATE permit_offer o
|
|
239
|
-
SET superseded_at = NOW()
|
|
240
|
-
WHERE o.scope_id = $1
|
|
241
|
-
AND o.accepted_at IS NULL
|
|
242
|
-
AND o.declined_at IS NULL
|
|
243
|
-
AND o.retracted_at IS NULL
|
|
244
|
-
AND o.superseded_at IS NULL
|
|
245
|
-
RETURNING o.*
|
|
246
|
-
)
|
|
247
|
-
SELECT u.*, grantor.account_id AS from_account_id
|
|
248
|
-
FROM updated u
|
|
249
|
-
JOIN actor grantor ON grantor.id = u.from_actor_id`, [scope_id]);
|
|
250
|
-
return { revoked, superseded_offers };
|
|
251
|
-
};
|
|
252
|
-
/**
|
|
253
|
-
* Revoke every active permit an actor holds for a given role.
|
|
254
|
-
*
|
|
255
|
-
* With scoped permits a single actor+role tuple can hold several active
|
|
256
|
-
* permits (one per scope), so this revokes all of them. Pass
|
|
257
|
-
* `query_revoke_permit(permit_id, ...)` when a single scoped permit
|
|
258
|
-
* is the target.
|
|
259
|
-
*
|
|
260
|
-
* Also supersedes pending offers for the actor's account across every
|
|
261
|
-
* scope of this role (the actor can no longer hold the role, so any
|
|
262
|
-
* pending offer of the same role is a bypass vector).
|
|
263
|
-
*
|
|
264
|
-
* @param deps - query dependencies
|
|
265
|
-
* @param actor_id - the actor whose permits to revoke
|
|
266
|
-
* @param role - the role to revoke
|
|
267
|
-
* @param revoked_by - the actor who revoked it (for audit trail)
|
|
268
|
-
* @param reason - optional free-form reason, stamped on `permit.revoked_reason`.
|
|
269
|
-
* @returns the list of revoked permits (empty if none were active) and superseded pending offers
|
|
270
|
-
* @mutates `permit` table - sets `revoked_at`/`revoked_by`/`revoked_reason` on every active row for `(actor, role)`
|
|
271
|
-
* @mutates `permit_offer` table - stamps `superseded_at` on every matching pending offer
|
|
272
|
-
*/
|
|
273
|
-
export const query_permit_revoke_role = async (deps, actor_id, role, revoked_by, reason) => {
|
|
274
|
-
// CTE pulls the revokee's `account_id` via a join on `actor` so callers
|
|
275
|
-
// can address the revokee without an extra round-trip.
|
|
276
|
-
const revoked = await deps.db.query(`WITH updated AS (
|
|
277
|
-
UPDATE permit
|
|
278
|
-
SET revoked_at = NOW(), revoked_by = $3, revoked_reason = $4
|
|
279
|
-
WHERE actor_id = $1 AND role = $2 AND revoked_at IS NULL
|
|
280
|
-
RETURNING id, role, scope_id, actor_id
|
|
281
|
-
)
|
|
282
|
-
SELECT u.id AS permit_id, u.role, u.scope_id, a.account_id
|
|
283
|
-
FROM updated u
|
|
284
|
-
JOIN actor a ON a.id = u.actor_id`, [actor_id, role, revoked_by ?? null, reason ?? null]);
|
|
285
|
-
if (revoked.length === 0) {
|
|
286
|
-
return { revoked: [], superseded_offers: [] };
|
|
287
|
-
}
|
|
288
|
-
const superseded_offers = await deps.db.query(`WITH updated AS (
|
|
289
|
-
UPDATE permit_offer o
|
|
290
|
-
SET superseded_at = NOW()
|
|
291
|
-
FROM actor a
|
|
292
|
-
WHERE a.id = $1
|
|
293
|
-
AND o.to_account_id = a.account_id
|
|
294
|
-
AND o.role = $2
|
|
295
|
-
AND o.accepted_at IS NULL
|
|
296
|
-
AND o.declined_at IS NULL
|
|
297
|
-
AND o.retracted_at IS NULL
|
|
298
|
-
AND o.superseded_at IS NULL
|
|
299
|
-
RETURNING o.*
|
|
300
|
-
)
|
|
301
|
-
SELECT u.*, grantor.account_id AS from_account_id
|
|
302
|
-
FROM updated u
|
|
303
|
-
JOIN actor grantor ON grantor.id = u.from_actor_id`, [actor_id, role]);
|
|
304
|
-
return { revoked, superseded_offers };
|
|
305
|
-
};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Keeper credential type guard.
|
|
3
|
-
*
|
|
4
|
-
* Two-part check:
|
|
5
|
-
* 1. Credential type must be `daemon_token` (not session cookie, not API token).
|
|
6
|
-
* 2. Account must hold active keeper permit.
|
|
7
|
-
*
|
|
8
|
-
* Both must pass. A session cookie from the bootstrap account still fails check #1.
|
|
9
|
-
*
|
|
10
|
-
* @module
|
|
11
|
-
*/
|
|
12
|
-
import type { MiddlewareHandler } from 'hono';
|
|
13
|
-
/**
|
|
14
|
-
* Middleware that requires keeper credentials.
|
|
15
|
-
*
|
|
16
|
-
* Returns 401 if unauthenticated, 403 if credential type is not
|
|
17
|
-
* `daemon_token` or if the keeper role is missing.
|
|
18
|
-
*/
|
|
19
|
-
export declare const require_keeper: MiddlewareHandler;
|
|
20
|
-
//# sourceMappingURL=require_keeper.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"require_keeper.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/require_keeper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,MAAM,CAAC;AAW5C;;;;;GAKG;AACH,eAAO,MAAM,cAAc,EAAE,iBAmB5B,CAAC"}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Keeper credential type guard.
|
|
3
|
-
*
|
|
4
|
-
* Two-part check:
|
|
5
|
-
* 1. Credential type must be `daemon_token` (not session cookie, not API token).
|
|
6
|
-
* 2. Account must hold active keeper permit.
|
|
7
|
-
*
|
|
8
|
-
* Both must pass. A session cookie from the bootstrap account still fails check #1.
|
|
9
|
-
*
|
|
10
|
-
* @module
|
|
11
|
-
*/
|
|
12
|
-
import { get_request_context, has_role } from './request_context.js';
|
|
13
|
-
import { CREDENTIAL_TYPE_KEY } from '../hono_context.js';
|
|
14
|
-
import { ROLE_KEEPER } from './role_schema.js';
|
|
15
|
-
import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INSUFFICIENT_PERMISSIONS, ERROR_KEEPER_REQUIRES_DAEMON_TOKEN, } from '../http/error_schemas.js';
|
|
16
|
-
/**
|
|
17
|
-
* Middleware that requires keeper credentials.
|
|
18
|
-
*
|
|
19
|
-
* Returns 401 if unauthenticated, 403 if credential type is not
|
|
20
|
-
* `daemon_token` or if the keeper role is missing.
|
|
21
|
-
*/
|
|
22
|
-
export const require_keeper = async (c, next) => {
|
|
23
|
-
const ctx = get_request_context(c);
|
|
24
|
-
if (!ctx) {
|
|
25
|
-
return c.json({ error: ERROR_AUTHENTICATION_REQUIRED }, 401);
|
|
26
|
-
}
|
|
27
|
-
const credential_type = c.get(CREDENTIAL_TYPE_KEY);
|
|
28
|
-
if (credential_type !== 'daemon_token') {
|
|
29
|
-
return c.json({ error: ERROR_KEEPER_REQUIRES_DAEMON_TOKEN, credential_type: credential_type ?? 'none' }, 403);
|
|
30
|
-
}
|
|
31
|
-
if (!has_role(ctx, ROLE_KEEPER)) {
|
|
32
|
-
return c.json({ error: ERROR_INSUFFICIENT_PERMISSIONS, required_role: ROLE_KEEPER }, 403);
|
|
33
|
-
}
|
|
34
|
-
await next();
|
|
35
|
-
};
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auth guard resolver for the route spec system.
|
|
3
|
-
*
|
|
4
|
-
* Maps `RouteAuth` discriminants to two-phase auth middleware sets.
|
|
5
|
-
* `pre_validation` carries the 401 check (`require_auth`) so
|
|
6
|
-
* unauthenticated callers never see route-shape information from input
|
|
7
|
-
* parse failures. `post_authorization` carries the 403 role / keeper
|
|
8
|
-
* checks because they read the `RequestContext` populated by the
|
|
9
|
-
* dispatcher's authorization phase.
|
|
10
|
-
*
|
|
11
|
-
* Injected into `apply_route_specs` to decouple the generic HTTP
|
|
12
|
-
* framework (`http/route_spec.ts`) from auth-specific middleware.
|
|
13
|
-
*
|
|
14
|
-
* @module
|
|
15
|
-
*/
|
|
16
|
-
import type { AuthGuardResolver } from '../http/route_spec.js';
|
|
17
|
-
/**
|
|
18
|
-
* Standard auth guard resolver for fuz_app.
|
|
19
|
-
*
|
|
20
|
-
* Maps `RouteAuth` to middleware:
|
|
21
|
-
* - `none` → no guards
|
|
22
|
-
* - `authenticated` → pre-validation `require_auth`
|
|
23
|
-
* - `role` → pre-validation `require_auth` + post-authorization `require_role(role)`
|
|
24
|
-
* - `keeper` → pre-validation `require_auth` + post-authorization `require_keeper`
|
|
25
|
-
*/
|
|
26
|
-
export declare const fuz_auth_guard_resolver: AuthGuardResolver;
|
|
27
|
-
//# sourceMappingURL=route_guards.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"route_guards.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/route_guards.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,uBAAuB,CAAC;AAE7D;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,EAAE,iBAWrC,CAAC"}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auth guard resolver for the route spec system.
|
|
3
|
-
*
|
|
4
|
-
* Maps `RouteAuth` discriminants to two-phase auth middleware sets.
|
|
5
|
-
* `pre_validation` carries the 401 check (`require_auth`) so
|
|
6
|
-
* unauthenticated callers never see route-shape information from input
|
|
7
|
-
* parse failures. `post_authorization` carries the 403 role / keeper
|
|
8
|
-
* checks because they read the `RequestContext` populated by the
|
|
9
|
-
* dispatcher's authorization phase.
|
|
10
|
-
*
|
|
11
|
-
* Injected into `apply_route_specs` to decouple the generic HTTP
|
|
12
|
-
* framework (`http/route_spec.ts`) from auth-specific middleware.
|
|
13
|
-
*
|
|
14
|
-
* @module
|
|
15
|
-
*/
|
|
16
|
-
import { require_auth, require_role } from './request_context.js';
|
|
17
|
-
import { require_keeper } from './require_keeper.js';
|
|
18
|
-
/**
|
|
19
|
-
* Standard auth guard resolver for fuz_app.
|
|
20
|
-
*
|
|
21
|
-
* Maps `RouteAuth` to middleware:
|
|
22
|
-
* - `none` → no guards
|
|
23
|
-
* - `authenticated` → pre-validation `require_auth`
|
|
24
|
-
* - `role` → pre-validation `require_auth` + post-authorization `require_role(role)`
|
|
25
|
-
* - `keeper` → pre-validation `require_auth` + post-authorization `require_keeper`
|
|
26
|
-
*/
|
|
27
|
-
export const fuz_auth_guard_resolver = (auth) => {
|
|
28
|
-
switch (auth.type) {
|
|
29
|
-
case 'none':
|
|
30
|
-
return { pre_validation: [], post_authorization: [] };
|
|
31
|
-
case 'authenticated':
|
|
32
|
-
return { pre_validation: [require_auth], post_authorization: [] };
|
|
33
|
-
case 'role':
|
|
34
|
-
return { pre_validation: [require_auth], post_authorization: [require_role(auth.role)] };
|
|
35
|
-
case 'keeper':
|
|
36
|
-
return { pre_validation: [require_auth], post_authorization: [require_keeper] };
|
|
37
|
-
}
|
|
38
|
-
};
|