@fuzdev/fuz_app 0.54.0 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/CLAUDE.md +214 -103
- package/dist/actions/action_bridge.d.ts +8 -5
- package/dist/actions/action_bridge.d.ts.map +1 -1
- package/dist/actions/action_bridge.js +1 -11
- package/dist/actions/action_codegen.d.ts +32 -0
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +35 -15
- package/dist/actions/action_registry.d.ts.map +1 -1
- package/dist/actions/action_registry.js +5 -2
- package/dist/actions/action_rpc.d.ts +141 -22
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +106 -187
- package/dist/actions/action_spec.d.ts +55 -16
- package/dist/actions/action_spec.d.ts.map +1 -1
- package/dist/actions/action_spec.js +16 -11
- package/dist/actions/action_types.d.ts +28 -60
- package/dist/actions/action_types.d.ts.map +1 -1
- package/dist/actions/action_types.js +13 -5
- package/dist/actions/broadcast_api.d.ts +2 -2
- package/dist/actions/broadcast_api.js +2 -2
- package/dist/actions/compile_action_registry.d.ts +50 -0
- package/dist/actions/compile_action_registry.d.ts.map +1 -0
- package/dist/actions/compile_action_registry.js +69 -0
- package/dist/actions/heartbeat.d.ts +8 -4
- package/dist/actions/heartbeat.d.ts.map +1 -1
- package/dist/actions/heartbeat.js +5 -4
- package/dist/actions/perform_action.d.ts +145 -0
- package/dist/actions/perform_action.d.ts.map +1 -0
- package/dist/actions/perform_action.js +258 -0
- package/dist/actions/register_action_ws.d.ts +46 -40
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +101 -159
- package/dist/actions/register_ws_endpoint.d.ts +15 -10
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +54 -7
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +0 -4
- package/dist/actions/transports_ws_auth_guard.d.ts +1 -1
- package/dist/actions/transports_ws_auth_guard.js +1 -1
- package/dist/actions/transports_ws_backend.d.ts +1 -1
- package/dist/actions/transports_ws_backend.js +1 -1
- package/dist/auth/CLAUDE.md +794 -410
- package/dist/auth/account_action_specs.d.ts +28 -7
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +7 -7
- package/dist/auth/account_actions.d.ts +7 -13
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +26 -35
- package/dist/auth/account_queries.d.ts +52 -16
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +87 -38
- package/dist/auth/account_routes.d.ts +9 -11
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +118 -46
- package/dist/auth/account_schema.d.ts +46 -35
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +21 -28
- package/dist/auth/admin_action_specs.d.ts +100 -32
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +64 -33
- package/dist/auth/admin_actions.d.ts +13 -19
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +37 -41
- package/dist/auth/audit_emitter.d.ts +160 -0
- package/dist/auth/audit_emitter.d.ts.map +1 -0
- package/dist/auth/audit_emitter.js +83 -0
- package/dist/auth/audit_log_queries.d.ts +17 -48
- package/dist/auth/audit_log_queries.d.ts.map +1 -1
- package/dist/auth/audit_log_queries.js +20 -56
- package/dist/auth/audit_log_routes.d.ts +1 -1
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +7 -3
- package/dist/auth/audit_log_schema.d.ts +92 -32
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +75 -46
- package/dist/auth/auth_guard_resolver.d.ts +44 -0
- package/dist/auth/auth_guard_resolver.d.ts.map +1 -0
- package/dist/auth/auth_guard_resolver.js +56 -0
- package/dist/auth/bearer_auth.d.ts +9 -7
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +13 -21
- package/dist/auth/bootstrap_account.d.ts +7 -7
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +7 -7
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +11 -10
- package/dist/auth/cleanup.d.ts +20 -26
- package/dist/auth/cleanup.d.ts.map +1 -1
- package/dist/auth/cleanup.js +33 -42
- package/dist/auth/credential_type_schema.d.ts +115 -0
- package/dist/auth/credential_type_schema.d.ts.map +1 -0
- package/dist/auth/credential_type_schema.js +127 -0
- package/dist/auth/daemon_token_middleware.d.ts +23 -11
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +28 -22
- package/dist/auth/ddl.d.ts +2 -2
- package/dist/auth/ddl.d.ts.map +1 -1
- package/dist/auth/ddl.js +6 -6
- package/dist/auth/deps.d.ts +7 -18
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/grant_path_schema.d.ts +117 -0
- package/dist/auth/grant_path_schema.d.ts.map +1 -0
- package/dist/auth/grant_path_schema.js +137 -0
- package/dist/auth/invite_queries.d.ts +12 -1
- package/dist/auth/invite_queries.d.ts.map +1 -1
- package/dist/auth/invite_queries.js +12 -1
- package/dist/auth/invite_schema.d.ts +1 -1
- package/dist/auth/invite_schema.d.ts.map +1 -1
- package/dist/auth/invite_schema.js +1 -1
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +9 -4
- package/dist/auth/migrations.d.ts +37 -14
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +79 -32
- package/dist/auth/request_context.d.ts +331 -61
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +378 -95
- package/dist/auth/{permit_offer_action_specs.d.ts → role_grant_offer_action_specs.d.ts} +163 -94
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -0
- package/dist/auth/role_grant_offer_action_specs.js +262 -0
- package/dist/auth/role_grant_offer_actions.d.ts +104 -0
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -0
- package/dist/auth/role_grant_offer_actions.js +473 -0
- package/dist/auth/{permit_offer_notifications.d.ts → role_grant_offer_notifications.d.ts} +90 -70
- package/dist/auth/role_grant_offer_notifications.d.ts.map +1 -0
- package/dist/auth/role_grant_offer_notifications.js +182 -0
- package/dist/auth/role_grant_offer_queries.d.ts +242 -0
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -0
- package/dist/auth/role_grant_offer_queries.js +533 -0
- package/dist/auth/role_grant_offer_schema.d.ts +150 -0
- package/dist/auth/role_grant_offer_schema.d.ts.map +1 -0
- package/dist/auth/{permit_offer_schema.js → role_grant_offer_schema.js} +60 -36
- package/dist/auth/role_grant_queries.d.ts +231 -0
- package/dist/auth/role_grant_queries.d.ts.map +1 -0
- package/dist/auth/role_grant_queries.js +320 -0
- package/dist/auth/role_schema.d.ts +150 -40
- package/dist/auth/role_schema.d.ts.map +1 -1
- package/dist/auth/role_schema.js +144 -45
- package/dist/auth/scope_kind_schema.d.ts +96 -0
- package/dist/auth/scope_kind_schema.d.ts.map +1 -0
- package/dist/auth/scope_kind_schema.js +94 -0
- package/dist/auth/self_service_role_action_specs.d.ts +6 -1
- package/dist/auth/self_service_role_action_specs.d.ts.map +1 -1
- package/dist/auth/self_service_role_action_specs.js +3 -1
- package/dist/auth/self_service_role_actions.d.ts +34 -27
- package/dist/auth/self_service_role_actions.d.ts.map +1 -1
- package/dist/auth/self_service_role_actions.js +68 -48
- package/dist/auth/session_cookie.d.ts +43 -6
- package/dist/auth/session_cookie.d.ts.map +1 -1
- package/dist/auth/session_cookie.js +31 -5
- package/dist/auth/session_middleware.d.ts +37 -3
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +33 -7
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +48 -19
- package/dist/auth/standard_action_specs.d.ts +2 -2
- package/dist/auth/standard_action_specs.js +4 -4
- package/dist/auth/standard_rpc_actions.d.ts +23 -19
- package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
- package/dist/auth/standard_rpc_actions.js +12 -12
- package/dist/db/migrate.d.ts +12 -8
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +10 -7
- package/dist/dev/setup.d.ts +2 -2
- package/dist/dev/setup.d.ts.map +1 -1
- package/dist/dev/setup.js +9 -7
- package/dist/env/load.d.ts +1 -1
- package/dist/env/load.js +1 -1
- package/dist/hono_context.d.ts +64 -5
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/hono_context.js +38 -2
- package/dist/http/CLAUDE.md +264 -87
- package/dist/http/auth_shape.d.ts +191 -0
- package/dist/http/auth_shape.d.ts.map +1 -0
- package/dist/http/auth_shape.js +237 -0
- package/dist/http/common_routes.js +3 -3
- package/dist/http/db_routes.d.ts +4 -0
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +44 -7
- package/dist/http/error_schemas.d.ts +132 -19
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +132 -40
- package/dist/http/jsonrpc_errors.d.ts +27 -2
- package/dist/http/jsonrpc_errors.d.ts.map +1 -1
- package/dist/http/jsonrpc_errors.js +26 -2
- package/dist/http/pending_effects.d.ts +71 -18
- package/dist/http/pending_effects.d.ts.map +1 -1
- package/dist/http/pending_effects.js +87 -18
- package/dist/http/proxy.d.ts +52 -5
- package/dist/http/proxy.d.ts.map +1 -1
- package/dist/http/proxy.js +92 -14
- package/dist/http/route_spec.d.ts +113 -41
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/route_spec.js +130 -52
- package/dist/http/schema_helpers.d.ts +3 -2
- package/dist/http/schema_helpers.d.ts.map +1 -1
- package/dist/http/schema_helpers.js +9 -2
- package/dist/http/surface.d.ts +2 -1
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +1 -2
- package/dist/http/surface_query.d.ts +39 -35
- package/dist/http/surface_query.d.ts.map +1 -1
- package/dist/http/surface_query.js +79 -36
- package/dist/primitive_schemas.d.ts +39 -0
- package/dist/primitive_schemas.d.ts.map +1 -0
- package/dist/primitive_schemas.js +40 -0
- package/dist/realtime/sse_auth_guard.d.ts +5 -5
- package/dist/realtime/sse_auth_guard.js +9 -9
- package/dist/runtime/mock.d.ts +1 -1
- package/dist/runtime/mock.js +1 -1
- package/dist/server/app_backend.d.ts +14 -11
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +12 -8
- package/dist/server/app_server.d.ts +7 -7
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +36 -31
- package/dist/server/validate_nginx.d.ts +1 -1
- package/dist/server/validate_nginx.js +1 -1
- package/dist/testing/CLAUDE.md +73 -55
- package/dist/testing/admin_integration.d.ts +5 -6
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +100 -96
- package/dist/testing/adversarial_headers.js +1 -1
- package/dist/testing/app_server.d.ts +11 -14
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +18 -17
- package/dist/testing/assertions.d.ts.map +1 -1
- package/dist/testing/assertions.js +2 -1
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +15 -9
- package/dist/testing/audit_completeness.d.ts +2 -2
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +53 -39
- package/dist/testing/auth_apps.d.ts +5 -4
- package/dist/testing/auth_apps.d.ts.map +1 -1
- package/dist/testing/auth_apps.js +28 -22
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +5 -5
- package/dist/testing/db.d.ts +1 -1
- package/dist/testing/db.d.ts.map +1 -1
- package/dist/testing/db.js +4 -4
- package/dist/testing/db_entities.d.ts +22 -0
- package/dist/testing/db_entities.d.ts.map +1 -0
- package/dist/testing/db_entities.js +28 -0
- package/dist/testing/entities.d.ts +10 -8
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +22 -18
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +13 -14
- package/dist/testing/integration_helpers.d.ts +8 -6
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +29 -23
- package/dist/testing/middleware.d.ts +15 -11
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +75 -32
- package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
- package/dist/testing/rpc_attack_surface.js +40 -24
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +3 -1
- package/dist/testing/rpc_round_trip.d.ts +1 -1
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +14 -13
- package/dist/testing/sse_round_trip.d.ts +3 -4
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +7 -11
- package/dist/testing/standard.d.ts +1 -1
- package/dist/testing/stubs.d.ts +25 -0
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +43 -2
- package/dist/testing/surface_invariants.d.ts +2 -2
- package/dist/testing/ws_round_trip.d.ts +12 -13
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +24 -12
- package/dist/ui/AdminAccounts.svelte +23 -20
- package/dist/ui/AdminOverview.svelte +15 -13
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/{AdminPermitHistory.svelte → AdminRoleGrantHistory.svelte} +12 -12
- package/dist/ui/AdminRoleGrantHistory.svelte.d.ts +4 -0
- package/dist/ui/AdminRoleGrantHistory.svelte.d.ts.map +1 -0
- package/dist/ui/BootstrapForm.svelte +1 -1
- package/dist/ui/CLAUDE.md +65 -59
- package/dist/ui/{PermitOfferForm.svelte → RoleGrantOfferForm.svelte} +37 -22
- package/dist/ui/RoleGrantOfferForm.svelte.d.ts +20 -0
- package/dist/ui/RoleGrantOfferForm.svelte.d.ts.map +1 -0
- package/dist/ui/{PermitOfferHistory.svelte → RoleGrantOfferHistory.svelte} +12 -12
- package/dist/ui/{PermitOfferHistory.svelte.d.ts → RoleGrantOfferHistory.svelte.d.ts} +4 -4
- package/dist/ui/RoleGrantOfferHistory.svelte.d.ts.map +1 -0
- package/dist/ui/{PermitOfferInbox.svelte → RoleGrantOfferInbox.svelte} +14 -14
- package/dist/ui/{PermitOfferInbox.svelte.d.ts → RoleGrantOfferInbox.svelte.d.ts} +4 -4
- package/dist/ui/RoleGrantOfferInbox.svelte.d.ts.map +1 -0
- package/dist/ui/SignupForm.svelte +1 -1
- package/dist/ui/SurfaceExplorer.svelte +35 -15
- package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.d.ts +2 -3
- package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.js +2 -3
- package/dist/ui/admin_accounts_state.svelte.d.ts +25 -18
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +28 -17
- package/dist/ui/admin_rpc_adapters.d.ts +20 -20
- package/dist/ui/admin_rpc_adapters.d.ts.map +1 -1
- package/dist/ui/admin_rpc_adapters.js +17 -17
- package/dist/ui/admin_sessions_state.svelte.d.ts +2 -2
- package/dist/ui/admin_sessions_state.svelte.js +2 -2
- package/dist/ui/audit_log_state.svelte.d.ts +7 -7
- package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
- package/dist/ui/audit_log_state.svelte.js +6 -6
- package/dist/ui/auth_state.svelte.d.ts +3 -3
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +6 -6
- package/dist/ui/format_scope.d.ts +2 -2
- package/dist/ui/format_scope.js +2 -2
- package/dist/ui/{permit_offers_state.svelte.d.ts → role_grant_offers_state.svelte.d.ts} +39 -31
- package/dist/ui/role_grant_offers_state.svelte.d.ts.map +1 -0
- package/dist/ui/{permit_offers_state.svelte.js → role_grant_offers_state.svelte.js} +25 -19
- package/dist/ui/ui_format.js +2 -2
- package/package.json +3 -3
- package/dist/auth/permit_offer_action_specs.d.ts.map +0 -1
- package/dist/auth/permit_offer_action_specs.js +0 -227
- package/dist/auth/permit_offer_actions.d.ts +0 -110
- package/dist/auth/permit_offer_actions.d.ts.map +0 -1
- package/dist/auth/permit_offer_actions.js +0 -452
- package/dist/auth/permit_offer_notifications.d.ts.map +0 -1
- package/dist/auth/permit_offer_notifications.js +0 -182
- package/dist/auth/permit_offer_queries.d.ts +0 -183
- package/dist/auth/permit_offer_queries.d.ts.map +0 -1
- package/dist/auth/permit_offer_queries.js +0 -408
- package/dist/auth/permit_offer_schema.d.ts +0 -103
- package/dist/auth/permit_offer_schema.d.ts.map +0 -1
- package/dist/auth/permit_queries.d.ts +0 -210
- package/dist/auth/permit_queries.d.ts.map +0 -1
- package/dist/auth/permit_queries.js +0 -294
- package/dist/auth/require_keeper.d.ts +0 -20
- package/dist/auth/require_keeper.d.ts.map +0 -1
- package/dist/auth/require_keeper.js +0 -35
- package/dist/auth/route_guards.d.ts +0 -21
- package/dist/auth/route_guards.d.ts.map +0 -1
- package/dist/auth/route_guards.js +0 -32
- package/dist/auth/session_lifecycle.d.ts +0 -37
- package/dist/auth/session_lifecycle.d.ts.map +0 -1
- package/dist/auth/session_lifecycle.js +0 -29
- package/dist/ui/AdminPermitHistory.svelte.d.ts +0 -4
- package/dist/ui/AdminPermitHistory.svelte.d.ts.map +0 -1
- package/dist/ui/PermitOfferForm.svelte.d.ts +0 -14
- package/dist/ui/PermitOfferForm.svelte.d.ts.map +0 -1
- package/dist/ui/PermitOfferHistory.svelte.d.ts.map +0 -1
- package/dist/ui/PermitOfferInbox.svelte.d.ts.map +0 -1
- package/dist/ui/permit_offers_state.svelte.d.ts.map +0 -1
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Permit offer database queries.
|
|
3
|
-
*
|
|
4
|
-
* Covers the offer side of the consentful-permits flow: create (with
|
|
5
|
-
* re-offer upsert), decline, retract, list, find-pending, sweep-expired,
|
|
6
|
-
* and the atomic `query_accept_offer` that bridges offer → permit.
|
|
7
|
-
*
|
|
8
|
-
* IDOR guards are expressed in each helper's signature — decline/accept
|
|
9
|
-
* require the recipient's `to_account_id`, retract requires the grantor's
|
|
10
|
-
* `from_actor_id`.
|
|
11
|
-
*
|
|
12
|
-
* @module
|
|
13
|
-
*/
|
|
14
|
-
import type { Uuid } from '@fuzdev/fuz_util/id.js';
|
|
15
|
-
import type { QueryDeps } from '../db/query_deps.js';
|
|
16
|
-
import type { Permit } from './account_schema.js';
|
|
17
|
-
import { type CreatePermitOfferInput, type PermitOffer, type SupersededOffer } from './permit_offer_schema.js';
|
|
18
|
-
import type { AuditLogEvent } from './audit_log_schema.js';
|
|
19
|
-
/**
|
|
20
|
-
* Error thrown by offer-lifecycle queries when the offer is in a non-pending
|
|
21
|
-
* state (accepted / declined / retracted / superseded) and therefore not
|
|
22
|
-
* actionable. Distinct from `PermitOfferExpiredError` — expiry has its own
|
|
23
|
-
* user-facing story ("ask the grantor to re-send") so it travels separately.
|
|
24
|
-
*/
|
|
25
|
-
export declare class PermitOfferAlreadyTerminalError extends Error {
|
|
26
|
-
constructor(offer_id: string);
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Error thrown when an offer's `expires_at` has passed. The accept path
|
|
30
|
-
* enforces this independently of the sweep — a stale offer past its expiry
|
|
31
|
-
* must not be accepted, even in the race window between expiry and the
|
|
32
|
-
* sweep stamping the audit event.
|
|
33
|
-
*/
|
|
34
|
-
export declare class PermitOfferExpiredError extends Error {
|
|
35
|
-
constructor(offer_id: string);
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Error thrown when an offer cannot be located for the caller. Covers both
|
|
39
|
-
* "offer does not exist" and "offer belongs to a different recipient"
|
|
40
|
-
* (IDOR guard) — the standard 404-over-403 pattern that avoids disclosing
|
|
41
|
-
* whether an offer id exists.
|
|
42
|
-
*/
|
|
43
|
-
export declare class PermitOfferNotFoundError extends Error {
|
|
44
|
-
constructor(offer_id: string);
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Error thrown when a grantor attempts to offer a permit to their own account.
|
|
48
|
-
*
|
|
49
|
-
* Enforced here (rather than via a CHECK constraint) so the constraint can
|
|
50
|
-
* be expressed as a cross-row JOIN on `actor.account_id` without requiring
|
|
51
|
-
* denormalized columns.
|
|
52
|
-
*/
|
|
53
|
-
export declare class PermitOfferSelfTargetError extends Error {
|
|
54
|
-
constructor();
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Create a new permit offer, or refresh an existing pending offer for the
|
|
58
|
-
* same `(to_account_id, role, scope_id, from_actor_id)` tuple.
|
|
59
|
-
*
|
|
60
|
-
* Re-offer semantics: a second call by the same grantor with the same
|
|
61
|
-
* `(to_account, role, scope)` while pending upserts the existing row,
|
|
62
|
-
* refreshing `message` and `expires_at`. A different grantor offering the
|
|
63
|
-
* same `(to_account, role, scope)` creates a distinct row — multiple
|
|
64
|
-
* pending grantors coexist. After a terminal state, a re-offer is a fresh
|
|
65
|
-
* INSERT.
|
|
66
|
-
*
|
|
67
|
-
* Self-offer rejection: throws `PermitOfferSelfTargetError` if the offering
|
|
68
|
-
* actor belongs to the recipient account.
|
|
69
|
-
*
|
|
70
|
-
* @mutates `permit_offer` table - inserts a new offer or upserts the matching pending row
|
|
71
|
-
* @throws PermitOfferSelfTargetError if the offering actor belongs to `to_account_id`
|
|
72
|
-
*/
|
|
73
|
-
export declare const query_permit_offer_create: (deps: QueryDeps, input: CreatePermitOfferInput) => Promise<PermitOffer>;
|
|
74
|
-
/**
|
|
75
|
-
* Mark an offer declined.
|
|
76
|
-
*
|
|
77
|
-
* Guarded by `to_account_id` (IDOR). Returns `null` if the offer does not
|
|
78
|
-
* exist or belongs to a different account. Throws
|
|
79
|
-
* `PermitOfferAlreadyTerminalError` if the offer exists for the caller but
|
|
80
|
-
* is already in a terminal state.
|
|
81
|
-
*
|
|
82
|
-
* @mutates `permit_offer` row - sets `declined_at` and `decline_reason`
|
|
83
|
-
* @throws PermitOfferAlreadyTerminalError if the offer is already accepted, declined, retracted, or superseded
|
|
84
|
-
*/
|
|
85
|
-
export declare const query_permit_offer_decline: (deps: QueryDeps, offer_id: string, to_account_id: string, reason: string | null) => Promise<PermitOffer | null>;
|
|
86
|
-
/**
|
|
87
|
-
* Mark an offer retracted by the grantor.
|
|
88
|
-
*
|
|
89
|
-
* Guarded by `from_actor_id` (IDOR). Returns `null` if the offer does not
|
|
90
|
-
* exist or was issued by a different actor. Throws
|
|
91
|
-
* `PermitOfferAlreadyTerminalError` if the offer exists for this grantor
|
|
92
|
-
* but is already in a terminal state.
|
|
93
|
-
*
|
|
94
|
-
* @mutates `permit_offer` row - sets `retracted_at`
|
|
95
|
-
* @throws PermitOfferAlreadyTerminalError if the offer is already accepted, declined, retracted, or superseded
|
|
96
|
-
*/
|
|
97
|
-
export declare const query_permit_offer_retract: (deps: QueryDeps, offer_id: string, from_actor_id: string) => Promise<PermitOffer | null>;
|
|
98
|
-
/**
|
|
99
|
-
* List pending, non-expired offers for an account, soonest expiry first.
|
|
100
|
-
*
|
|
101
|
-
* Expired offers are filtered server-side (`expires_at > NOW()`) so the
|
|
102
|
-
* inbox never surfaces a row that can no longer be accepted. The periodic
|
|
103
|
-
* sweep (`query_permit_offer_sweep_expired`) handles audit tombstoning.
|
|
104
|
-
*/
|
|
105
|
-
export declare const query_permit_offer_list: (deps: QueryDeps, to_account_id: string) => Promise<Array<PermitOffer>>;
|
|
106
|
-
/**
|
|
107
|
-
* List every offer involving an account (either direction), newest first.
|
|
108
|
-
*
|
|
109
|
-
* Includes terminal offers — used by the grantor-side admin / history view.
|
|
110
|
-
*/
|
|
111
|
-
export declare const query_permit_offer_history_for_account: (deps: QueryDeps, account_id: string, limit?: number, offset?: number) => Promise<Array<PermitOffer>>;
|
|
112
|
-
/**
|
|
113
|
-
* Look up a pending offer by id. Returns `null` if the offer is terminal,
|
|
114
|
-
* expired (server-side filter), or missing.
|
|
115
|
-
*/
|
|
116
|
-
export declare const query_permit_offer_find_pending: (deps: QueryDeps, offer_id: string) => Promise<PermitOffer | null>;
|
|
117
|
-
/**
|
|
118
|
-
* Return pending offers whose `expires_at` has passed.
|
|
119
|
-
*
|
|
120
|
-
* Callers fire `permit_offer_expire` audit events for each row. The schema
|
|
121
|
-
* does not tombstone the row, so callers are responsible for their own
|
|
122
|
-
* idempotency (e.g. check whether a `permit_offer_expire` audit event
|
|
123
|
-
* already exists for the offer id).
|
|
124
|
-
*/
|
|
125
|
-
export declare const query_permit_offer_sweep_expired: (deps: QueryDeps) => Promise<Array<PermitOffer>>;
|
|
126
|
-
/** Input for `query_accept_offer`. */
|
|
127
|
-
export interface AcceptOfferInput {
|
|
128
|
-
offer_id: Uuid;
|
|
129
|
-
/** Account of the accepting recipient — IDOR guard against another account accepting the offer. */
|
|
130
|
-
to_account_id: Uuid;
|
|
131
|
-
/** Optional IP to stamp on the audit events. */
|
|
132
|
-
ip?: string | null;
|
|
133
|
-
}
|
|
134
|
-
/** Result of `query_accept_offer` — the permit produced (new or pre-existing on race), plus the (now-accepted) offer. */
|
|
135
|
-
export interface AcceptOfferResult {
|
|
136
|
-
permit: Permit;
|
|
137
|
-
offer: PermitOffer;
|
|
138
|
-
/** `true` if this call is the one that accepted the offer (new permit inserted); `false` on a race returning the already-created permit. */
|
|
139
|
-
created: boolean;
|
|
140
|
-
/**
|
|
141
|
-
* Sibling offers superseded by this accept — empty on the race-loser path.
|
|
142
|
-
* Each entry carries its grantor's `from_account_id` so the caller can
|
|
143
|
-
* fan out `permit_offer_supersede` notifications without a second
|
|
144
|
-
* round-trip.
|
|
145
|
-
*/
|
|
146
|
-
superseded_offers: Array<SupersededOffer>;
|
|
147
|
-
/** Audit events emitted in-transaction — fed back through the normal `on_audit_event` broadcast chain by the caller. Includes one `permit_offer_supersede` per superseded sibling. */
|
|
148
|
-
audit_events: Array<AuditLogEvent>;
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Accept an offer atomically: mark accepted, insert the permit, stamp
|
|
152
|
-
* `resulting_permit_id`, supersede sibling pending offers for the same
|
|
153
|
-
* `(to_account, role, scope)`, and emit `permit_offer_accept` +
|
|
154
|
-
* `permit_grant` + one `permit_offer_supersede` per sibling. Must run
|
|
155
|
-
* inside a transaction — the caller's route spec should declare
|
|
156
|
-
* `transaction: true` (or wrap explicitly).
|
|
157
|
-
*
|
|
158
|
-
* Idempotent on race: if a second concurrent call observes the offer
|
|
159
|
-
* already accepted, returns the existing permit rather than creating a
|
|
160
|
-
* duplicate or throwing.
|
|
161
|
-
*
|
|
162
|
-
* Error map:
|
|
163
|
-
* - `PermitOfferNotFoundError` — offer does not exist, or belongs to a
|
|
164
|
-
* different recipient (IDOR guard). The offer row is untouched.
|
|
165
|
-
* - `PermitOfferAlreadyTerminalError` — offer is declined, retracted, or
|
|
166
|
-
* superseded.
|
|
167
|
-
* - `PermitOfferExpiredError` — offer is pending but past `expires_at`.
|
|
168
|
-
*
|
|
169
|
-
* Sibling supersede is what closes the "accept a pre-revoke sibling offer
|
|
170
|
-
* to bypass a revoke" path: once A is accepted, B/C/... can no longer be
|
|
171
|
-
* accepted even if the resulting permit is later revoked.
|
|
172
|
-
*
|
|
173
|
-
* @mutates `permit_offer` row - stamps `accepted_at` and `resulting_permit_id`
|
|
174
|
-
* @mutates `permit` table - inserts the resulting permit (idempotent on race)
|
|
175
|
-
* @mutates `permit_offer` siblings - stamps `superseded_at` on every other pending offer for the tuple
|
|
176
|
-
* @mutates `audit_log` table - emits `permit_offer_accept` + `permit_grant` + one `permit_offer_supersede` per sibling
|
|
177
|
-
* @throws PermitOfferNotFoundError if the offer is missing or belongs to another recipient
|
|
178
|
-
* @throws PermitOfferAlreadyTerminalError if the offer is declined, retracted, or superseded
|
|
179
|
-
* @throws PermitOfferExpiredError if the offer is pending but past `expires_at`
|
|
180
|
-
* @throws Error if the accepting account has no actor (1:1 invariant) or invariant assertions fail
|
|
181
|
-
*/
|
|
182
|
-
export declare const query_accept_offer: (deps: QueryDeps, input: AcceptOfferInput) => Promise<AcceptOfferResult>;
|
|
183
|
-
//# sourceMappingURL=permit_offer_queries.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"permit_offer_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/permit_offer_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAEjD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,qBAAqB,CAAC;AAEhD,OAAO,EAEN,KAAK,sBAAsB,EAC3B,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,uBAAuB,CAAC;AAEzD;;;;;GAKG;AACH,qBAAa,+BAAgC,SAAQ,KAAK;gBAC7C,QAAQ,EAAE,MAAM;CAI5B;AAED;;;;;GAKG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;gBACrC,QAAQ,EAAE,MAAM;CAI5B;AAED;;;;;GAKG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;gBACtC,QAAQ,EAAE,MAAM;CAI5B;AAED;;;;;;GAMG;AACH,qBAAa,0BAA2B,SAAQ,KAAK;;CAKpD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,SAAS,EACf,OAAO,sBAAsB,KAC3B,OAAO,CAAC,WAAW,CAyBrB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,0BAA0B,GACtC,MAAM,SAAS,EACf,UAAU,MAAM,EAChB,eAAe,MAAM,EACrB,QAAQ,MAAM,GAAG,IAAI,KACnB,OAAO,CAAC,WAAW,GAAG,IAAI,CAe5B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,0BAA0B,GACtC,MAAM,SAAS,EACf,UAAU,MAAM,EAChB,eAAe,MAAM,KACnB,OAAO,CAAC,WAAW,GAAG,IAAI,CAe5B,CAAC;AA8BF;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,eAAe,MAAM,KACnB,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAY5B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sCAAsC,GAClD,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAW,EACX,eAAU,KACR,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAS5B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,+BAA+B,GAC3C,MAAM,SAAS,EACf,UAAU,MAAM,KACd,OAAO,CAAC,WAAW,GAAG,IAAI,CAY5B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,KACb,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAU5B,CAAC;AAEF,sCAAsC;AACtC,MAAM,WAAW,gBAAgB;IAChC,QAAQ,EAAE,IAAI,CAAC;IACf,mGAAmG;IACnG,aAAa,EAAE,IAAI,CAAC;IACpB,gDAAgD;IAChD,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnB;AAED,yHAAyH;AACzH,MAAM,WAAW,iBAAiB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,WAAW,CAAC;IACnB,4IAA4I;IAC5I,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,iBAAiB,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;IAC1C,sLAAsL;IACtL,YAAY,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,kBAAkB,GAC9B,MAAM,SAAS,EACf,OAAO,gBAAgB,KACrB,OAAO,CAAC,iBAAiB,CAqK3B,CAAC"}
|
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Permit offer database queries.
|
|
3
|
-
*
|
|
4
|
-
* Covers the offer side of the consentful-permits flow: create (with
|
|
5
|
-
* re-offer upsert), decline, retract, list, find-pending, sweep-expired,
|
|
6
|
-
* and the atomic `query_accept_offer` that bridges offer → permit.
|
|
7
|
-
*
|
|
8
|
-
* IDOR guards are expressed in each helper's signature — decline/accept
|
|
9
|
-
* require the recipient's `to_account_id`, retract requires the grantor's
|
|
10
|
-
* `from_actor_id`.
|
|
11
|
-
*
|
|
12
|
-
* @module
|
|
13
|
-
*/
|
|
14
|
-
import { assert_row } from '../db/assert_row.js';
|
|
15
|
-
import { query_actor_by_account } from './account_queries.js';
|
|
16
|
-
import { PERMIT_OFFER_SCOPE_SENTINEL_UUID, } from './permit_offer_schema.js';
|
|
17
|
-
import { query_audit_log } from './audit_log_queries.js';
|
|
18
|
-
/**
|
|
19
|
-
* Error thrown by offer-lifecycle queries when the offer is in a non-pending
|
|
20
|
-
* state (accepted / declined / retracted / superseded) and therefore not
|
|
21
|
-
* actionable. Distinct from `PermitOfferExpiredError` — expiry has its own
|
|
22
|
-
* user-facing story ("ask the grantor to re-send") so it travels separately.
|
|
23
|
-
*/
|
|
24
|
-
export class PermitOfferAlreadyTerminalError extends Error {
|
|
25
|
-
constructor(offer_id) {
|
|
26
|
-
super(`Offer ${offer_id} is already in a terminal state`);
|
|
27
|
-
this.name = 'PermitOfferAlreadyTerminalError';
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Error thrown when an offer's `expires_at` has passed. The accept path
|
|
32
|
-
* enforces this independently of the sweep — a stale offer past its expiry
|
|
33
|
-
* must not be accepted, even in the race window between expiry and the
|
|
34
|
-
* sweep stamping the audit event.
|
|
35
|
-
*/
|
|
36
|
-
export class PermitOfferExpiredError extends Error {
|
|
37
|
-
constructor(offer_id) {
|
|
38
|
-
super(`Offer ${offer_id} has expired`);
|
|
39
|
-
this.name = 'PermitOfferExpiredError';
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Error thrown when an offer cannot be located for the caller. Covers both
|
|
44
|
-
* "offer does not exist" and "offer belongs to a different recipient"
|
|
45
|
-
* (IDOR guard) — the standard 404-over-403 pattern that avoids disclosing
|
|
46
|
-
* whether an offer id exists.
|
|
47
|
-
*/
|
|
48
|
-
export class PermitOfferNotFoundError extends Error {
|
|
49
|
-
constructor(offer_id) {
|
|
50
|
-
super(`Offer ${offer_id} not found`);
|
|
51
|
-
this.name = 'PermitOfferNotFoundError';
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Error thrown when a grantor attempts to offer a permit to their own account.
|
|
56
|
-
*
|
|
57
|
-
* Enforced here (rather than via a CHECK constraint) so the constraint can
|
|
58
|
-
* be expressed as a cross-row JOIN on `actor.account_id` without requiring
|
|
59
|
-
* denormalized columns.
|
|
60
|
-
*/
|
|
61
|
-
export class PermitOfferSelfTargetError extends Error {
|
|
62
|
-
constructor() {
|
|
63
|
-
super('Cannot offer a permit to your own account');
|
|
64
|
-
this.name = 'PermitOfferSelfTargetError';
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Create a new permit offer, or refresh an existing pending offer for the
|
|
69
|
-
* same `(to_account_id, role, scope_id, from_actor_id)` tuple.
|
|
70
|
-
*
|
|
71
|
-
* Re-offer semantics: a second call by the same grantor with the same
|
|
72
|
-
* `(to_account, role, scope)` while pending upserts the existing row,
|
|
73
|
-
* refreshing `message` and `expires_at`. A different grantor offering the
|
|
74
|
-
* same `(to_account, role, scope)` creates a distinct row — multiple
|
|
75
|
-
* pending grantors coexist. After a terminal state, a re-offer is a fresh
|
|
76
|
-
* INSERT.
|
|
77
|
-
*
|
|
78
|
-
* Self-offer rejection: throws `PermitOfferSelfTargetError` if the offering
|
|
79
|
-
* actor belongs to the recipient account.
|
|
80
|
-
*
|
|
81
|
-
* @mutates `permit_offer` table - inserts a new offer or upserts the matching pending row
|
|
82
|
-
* @throws PermitOfferSelfTargetError if the offering actor belongs to `to_account_id`
|
|
83
|
-
*/
|
|
84
|
-
export const query_permit_offer_create = async (deps, input) => {
|
|
85
|
-
const actor = await query_actor_by_account(deps, input.to_account_id);
|
|
86
|
-
if (actor && actor.id === input.from_actor_id) {
|
|
87
|
-
throw new PermitOfferSelfTargetError();
|
|
88
|
-
}
|
|
89
|
-
const row = await deps.db.query_one(`INSERT INTO permit_offer
|
|
90
|
-
(from_actor_id, to_account_id, role, scope_id, message, expires_at)
|
|
91
|
-
VALUES ($1, $2, $3, $4, $5, $6)
|
|
92
|
-
ON CONFLICT (to_account_id, role, COALESCE(scope_id, '${PERMIT_OFFER_SCOPE_SENTINEL_UUID}'::uuid), from_actor_id)
|
|
93
|
-
WHERE accepted_at IS NULL AND declined_at IS NULL AND retracted_at IS NULL AND superseded_at IS NULL
|
|
94
|
-
DO UPDATE SET
|
|
95
|
-
message = EXCLUDED.message,
|
|
96
|
-
expires_at = EXCLUDED.expires_at
|
|
97
|
-
RETURNING *`, [
|
|
98
|
-
input.from_actor_id,
|
|
99
|
-
input.to_account_id,
|
|
100
|
-
input.role,
|
|
101
|
-
input.scope_id ?? null,
|
|
102
|
-
input.message ?? null,
|
|
103
|
-
input.expires_at.toISOString(),
|
|
104
|
-
]);
|
|
105
|
-
return assert_row(row, 'INSERT INTO permit_offer');
|
|
106
|
-
};
|
|
107
|
-
/**
|
|
108
|
-
* Mark an offer declined.
|
|
109
|
-
*
|
|
110
|
-
* Guarded by `to_account_id` (IDOR). Returns `null` if the offer does not
|
|
111
|
-
* exist or belongs to a different account. Throws
|
|
112
|
-
* `PermitOfferAlreadyTerminalError` if the offer exists for the caller but
|
|
113
|
-
* is already in a terminal state.
|
|
114
|
-
*
|
|
115
|
-
* @mutates `permit_offer` row - sets `declined_at` and `decline_reason`
|
|
116
|
-
* @throws PermitOfferAlreadyTerminalError if the offer is already accepted, declined, retracted, or superseded
|
|
117
|
-
*/
|
|
118
|
-
export const query_permit_offer_decline = async (deps, offer_id, to_account_id, reason) => {
|
|
119
|
-
const updated = await deps.db.query_one(`UPDATE permit_offer
|
|
120
|
-
SET declined_at = NOW(), decline_reason = $3
|
|
121
|
-
WHERE id = $1
|
|
122
|
-
AND to_account_id = $2
|
|
123
|
-
AND accepted_at IS NULL
|
|
124
|
-
AND declined_at IS NULL
|
|
125
|
-
AND retracted_at IS NULL
|
|
126
|
-
AND superseded_at IS NULL
|
|
127
|
-
RETURNING *`, [offer_id, to_account_id, reason ?? null]);
|
|
128
|
-
if (updated)
|
|
129
|
-
return updated;
|
|
130
|
-
return resolve_terminal_or_missing(deps, offer_id, { to_account_id });
|
|
131
|
-
};
|
|
132
|
-
/**
|
|
133
|
-
* Mark an offer retracted by the grantor.
|
|
134
|
-
*
|
|
135
|
-
* Guarded by `from_actor_id` (IDOR). Returns `null` if the offer does not
|
|
136
|
-
* exist or was issued by a different actor. Throws
|
|
137
|
-
* `PermitOfferAlreadyTerminalError` if the offer exists for this grantor
|
|
138
|
-
* but is already in a terminal state.
|
|
139
|
-
*
|
|
140
|
-
* @mutates `permit_offer` row - sets `retracted_at`
|
|
141
|
-
* @throws PermitOfferAlreadyTerminalError if the offer is already accepted, declined, retracted, or superseded
|
|
142
|
-
*/
|
|
143
|
-
export const query_permit_offer_retract = async (deps, offer_id, from_actor_id) => {
|
|
144
|
-
const updated = await deps.db.query_one(`UPDATE permit_offer
|
|
145
|
-
SET retracted_at = NOW()
|
|
146
|
-
WHERE id = $1
|
|
147
|
-
AND from_actor_id = $2
|
|
148
|
-
AND accepted_at IS NULL
|
|
149
|
-
AND declined_at IS NULL
|
|
150
|
-
AND retracted_at IS NULL
|
|
151
|
-
AND superseded_at IS NULL
|
|
152
|
-
RETURNING *`, [offer_id, from_actor_id]);
|
|
153
|
-
if (updated)
|
|
154
|
-
return updated;
|
|
155
|
-
return resolve_terminal_or_missing(deps, offer_id, { from_actor_id });
|
|
156
|
-
};
|
|
157
|
-
/** Helper: distinguish "not found / different owner" from "already terminal". */
|
|
158
|
-
const resolve_terminal_or_missing = async (deps, offer_id, scope) => {
|
|
159
|
-
const conditions = ['id = $1'];
|
|
160
|
-
const params = [offer_id];
|
|
161
|
-
let idx = 2;
|
|
162
|
-
if (scope.to_account_id) {
|
|
163
|
-
conditions.push(`to_account_id = $${idx++}`);
|
|
164
|
-
params.push(scope.to_account_id);
|
|
165
|
-
}
|
|
166
|
-
if (scope.from_actor_id) {
|
|
167
|
-
conditions.push(`from_actor_id = $${idx++}`);
|
|
168
|
-
params.push(scope.from_actor_id);
|
|
169
|
-
}
|
|
170
|
-
const row = await deps.db.query_one(`SELECT * FROM permit_offer WHERE ${conditions.join(' AND ')}`, params);
|
|
171
|
-
if (!row)
|
|
172
|
-
return null;
|
|
173
|
-
if (row.accepted_at || row.declined_at || row.retracted_at || row.superseded_at) {
|
|
174
|
-
throw new PermitOfferAlreadyTerminalError(offer_id);
|
|
175
|
-
}
|
|
176
|
-
return null;
|
|
177
|
-
};
|
|
178
|
-
/**
|
|
179
|
-
* List pending, non-expired offers for an account, soonest expiry first.
|
|
180
|
-
*
|
|
181
|
-
* Expired offers are filtered server-side (`expires_at > NOW()`) so the
|
|
182
|
-
* inbox never surfaces a row that can no longer be accepted. The periodic
|
|
183
|
-
* sweep (`query_permit_offer_sweep_expired`) handles audit tombstoning.
|
|
184
|
-
*/
|
|
185
|
-
export const query_permit_offer_list = async (deps, to_account_id) => {
|
|
186
|
-
return deps.db.query(`SELECT * FROM permit_offer
|
|
187
|
-
WHERE to_account_id = $1
|
|
188
|
-
AND accepted_at IS NULL
|
|
189
|
-
AND declined_at IS NULL
|
|
190
|
-
AND retracted_at IS NULL
|
|
191
|
-
AND superseded_at IS NULL
|
|
192
|
-
AND expires_at > NOW()
|
|
193
|
-
ORDER BY expires_at ASC`, [to_account_id]);
|
|
194
|
-
};
|
|
195
|
-
/**
|
|
196
|
-
* List every offer involving an account (either direction), newest first.
|
|
197
|
-
*
|
|
198
|
-
* Includes terminal offers — used by the grantor-side admin / history view.
|
|
199
|
-
*/
|
|
200
|
-
export const query_permit_offer_history_for_account = async (deps, account_id, limit = 100, offset = 0) => {
|
|
201
|
-
return deps.db.query(`SELECT o.* FROM permit_offer o
|
|
202
|
-
LEFT JOIN actor a ON a.id = o.from_actor_id
|
|
203
|
-
WHERE o.to_account_id = $1 OR a.account_id = $1
|
|
204
|
-
ORDER BY o.created_at DESC
|
|
205
|
-
LIMIT $2 OFFSET $3`, [account_id, limit, offset]);
|
|
206
|
-
};
|
|
207
|
-
/**
|
|
208
|
-
* Look up a pending offer by id. Returns `null` if the offer is terminal,
|
|
209
|
-
* expired (server-side filter), or missing.
|
|
210
|
-
*/
|
|
211
|
-
export const query_permit_offer_find_pending = async (deps, offer_id) => {
|
|
212
|
-
const row = await deps.db.query_one(`SELECT * FROM permit_offer
|
|
213
|
-
WHERE id = $1
|
|
214
|
-
AND accepted_at IS NULL
|
|
215
|
-
AND declined_at IS NULL
|
|
216
|
-
AND retracted_at IS NULL
|
|
217
|
-
AND superseded_at IS NULL
|
|
218
|
-
AND expires_at > NOW()`, [offer_id]);
|
|
219
|
-
return row ?? null;
|
|
220
|
-
};
|
|
221
|
-
/**
|
|
222
|
-
* Return pending offers whose `expires_at` has passed.
|
|
223
|
-
*
|
|
224
|
-
* Callers fire `permit_offer_expire` audit events for each row. The schema
|
|
225
|
-
* does not tombstone the row, so callers are responsible for their own
|
|
226
|
-
* idempotency (e.g. check whether a `permit_offer_expire` audit event
|
|
227
|
-
* already exists for the offer id).
|
|
228
|
-
*/
|
|
229
|
-
export const query_permit_offer_sweep_expired = async (deps) => {
|
|
230
|
-
return deps.db.query(`SELECT * FROM permit_offer
|
|
231
|
-
WHERE accepted_at IS NULL
|
|
232
|
-
AND declined_at IS NULL
|
|
233
|
-
AND retracted_at IS NULL
|
|
234
|
-
AND superseded_at IS NULL
|
|
235
|
-
AND expires_at <= NOW()
|
|
236
|
-
ORDER BY expires_at ASC`);
|
|
237
|
-
};
|
|
238
|
-
/**
|
|
239
|
-
* Accept an offer atomically: mark accepted, insert the permit, stamp
|
|
240
|
-
* `resulting_permit_id`, supersede sibling pending offers for the same
|
|
241
|
-
* `(to_account, role, scope)`, and emit `permit_offer_accept` +
|
|
242
|
-
* `permit_grant` + one `permit_offer_supersede` per sibling. Must run
|
|
243
|
-
* inside a transaction — the caller's route spec should declare
|
|
244
|
-
* `transaction: true` (or wrap explicitly).
|
|
245
|
-
*
|
|
246
|
-
* Idempotent on race: if a second concurrent call observes the offer
|
|
247
|
-
* already accepted, returns the existing permit rather than creating a
|
|
248
|
-
* duplicate or throwing.
|
|
249
|
-
*
|
|
250
|
-
* Error map:
|
|
251
|
-
* - `PermitOfferNotFoundError` — offer does not exist, or belongs to a
|
|
252
|
-
* different recipient (IDOR guard). The offer row is untouched.
|
|
253
|
-
* - `PermitOfferAlreadyTerminalError` — offer is declined, retracted, or
|
|
254
|
-
* superseded.
|
|
255
|
-
* - `PermitOfferExpiredError` — offer is pending but past `expires_at`.
|
|
256
|
-
*
|
|
257
|
-
* Sibling supersede is what closes the "accept a pre-revoke sibling offer
|
|
258
|
-
* to bypass a revoke" path: once A is accepted, B/C/... can no longer be
|
|
259
|
-
* accepted even if the resulting permit is later revoked.
|
|
260
|
-
*
|
|
261
|
-
* @mutates `permit_offer` row - stamps `accepted_at` and `resulting_permit_id`
|
|
262
|
-
* @mutates `permit` table - inserts the resulting permit (idempotent on race)
|
|
263
|
-
* @mutates `permit_offer` siblings - stamps `superseded_at` on every other pending offer for the tuple
|
|
264
|
-
* @mutates `audit_log` table - emits `permit_offer_accept` + `permit_grant` + one `permit_offer_supersede` per sibling
|
|
265
|
-
* @throws PermitOfferNotFoundError if the offer is missing or belongs to another recipient
|
|
266
|
-
* @throws PermitOfferAlreadyTerminalError if the offer is declined, retracted, or superseded
|
|
267
|
-
* @throws PermitOfferExpiredError if the offer is pending but past `expires_at`
|
|
268
|
-
* @throws Error if the accepting account has no actor (1:1 invariant) or invariant assertions fail
|
|
269
|
-
*/
|
|
270
|
-
export const query_accept_offer = async (deps, input) => {
|
|
271
|
-
const { offer_id, to_account_id, ip } = input;
|
|
272
|
-
// Claim the offer with a row-level lock. Subsequent concurrent callers
|
|
273
|
-
// block on the lock until this transaction commits/rolls back; after commit
|
|
274
|
-
// they see the new state (accepted or terminal) and branch idempotently.
|
|
275
|
-
// We defer writing `accepted_at` until the permit row exists — the
|
|
276
|
-
// `permit_offer_permit_iff_accepted` CHECK constraint demands both be set
|
|
277
|
-
// (or neither) at row-visibility time.
|
|
278
|
-
const locked = await deps.db.query_one(`SELECT * FROM permit_offer
|
|
279
|
-
WHERE id = $1 AND to_account_id = $2
|
|
280
|
-
FOR UPDATE`, [offer_id, to_account_id]);
|
|
281
|
-
if (!locked) {
|
|
282
|
-
throw new PermitOfferNotFoundError(offer_id);
|
|
283
|
-
}
|
|
284
|
-
if (locked.accepted_at) {
|
|
285
|
-
// Race winner already committed; return the pre-existing permit.
|
|
286
|
-
// `permit_offer_permit_iff_accepted` CHECK guarantees resulting_permit_id is non-null.
|
|
287
|
-
const permit = await deps.db.query_one(`SELECT * FROM permit WHERE id = $1`, [
|
|
288
|
-
locked.resulting_permit_id,
|
|
289
|
-
]);
|
|
290
|
-
return {
|
|
291
|
-
permit: assert_row(permit, 'resulting_permit lookup'),
|
|
292
|
-
offer: locked,
|
|
293
|
-
created: false,
|
|
294
|
-
superseded_offers: [],
|
|
295
|
-
audit_events: [],
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
if (locked.declined_at || locked.retracted_at || locked.superseded_at) {
|
|
299
|
-
throw new PermitOfferAlreadyTerminalError(offer_id);
|
|
300
|
-
}
|
|
301
|
-
// Expiry check AFTER the accepted-path: a validly-accepted offer past its
|
|
302
|
-
// expires_at still returns the permit idempotently. Only pending offers
|
|
303
|
-
// past expiry reach this branch.
|
|
304
|
-
if (new Date(locked.expires_at) <= new Date()) {
|
|
305
|
-
throw new PermitOfferExpiredError(offer_id);
|
|
306
|
-
}
|
|
307
|
-
// Resolve the accepting actor (1:1 account→actor in v1).
|
|
308
|
-
const actor = await query_actor_by_account(deps, to_account_id);
|
|
309
|
-
if (!actor) {
|
|
310
|
-
throw new Error(`No actor for account ${to_account_id} accepting offer ${offer_id}`);
|
|
311
|
-
}
|
|
312
|
-
// Insert the permit. Uses the normal grant idempotency — if another
|
|
313
|
-
// code path already granted the same (actor, role, scope), reuse it.
|
|
314
|
-
const granted_permit = await deps.db.query_one(`INSERT INTO permit (actor_id, role, scope_id, granted_by, source_offer_id)
|
|
315
|
-
VALUES ($1, $2, $3, $4, $5)
|
|
316
|
-
ON CONFLICT (actor_id, role, COALESCE(scope_id, '${PERMIT_OFFER_SCOPE_SENTINEL_UUID}'::uuid))
|
|
317
|
-
WHERE revoked_at IS NULL
|
|
318
|
-
DO NOTHING
|
|
319
|
-
RETURNING *`, [actor.id, locked.role, locked.scope_id, locked.from_actor_id, locked.id]);
|
|
320
|
-
let permit;
|
|
321
|
-
if (granted_permit) {
|
|
322
|
-
permit = granted_permit;
|
|
323
|
-
}
|
|
324
|
-
else {
|
|
325
|
-
const existing = await deps.db.query_one(`SELECT * FROM permit
|
|
326
|
-
WHERE actor_id = $1
|
|
327
|
-
AND role = $2
|
|
328
|
-
AND scope_id IS NOT DISTINCT FROM $3
|
|
329
|
-
AND revoked_at IS NULL`, [actor.id, locked.role, locked.scope_id]);
|
|
330
|
-
permit = assert_row(existing, 'query_accept_offer idempotent permit lookup');
|
|
331
|
-
}
|
|
332
|
-
// Single UPDATE sets both sides of the CHECK constraint at once.
|
|
333
|
-
const offer_accepted = await deps.db.query_one(`UPDATE permit_offer
|
|
334
|
-
SET accepted_at = NOW(), resulting_permit_id = $2
|
|
335
|
-
WHERE id = $1
|
|
336
|
-
RETURNING *`, [locked.id, permit.id]);
|
|
337
|
-
const offer = assert_row(offer_accepted, 'mark offer accepted');
|
|
338
|
-
// Supersede sibling pending offers for the same (to_account, role, scope).
|
|
339
|
-
// Forecloses the "accept this other sibling later to get the role back
|
|
340
|
-
// after a revoke" path — any pending offer for this tuple at accept time
|
|
341
|
-
// is obsoleted by the accept. CTE joins `actor` to surface each sibling's
|
|
342
|
-
// grantor `account_id` for the caller's notification fan-out.
|
|
343
|
-
const superseded = await deps.db.query(`WITH updated AS (
|
|
344
|
-
UPDATE permit_offer
|
|
345
|
-
SET superseded_at = NOW()
|
|
346
|
-
WHERE to_account_id = $1
|
|
347
|
-
AND role = $2
|
|
348
|
-
AND scope_id IS NOT DISTINCT FROM $3
|
|
349
|
-
AND id <> $4
|
|
350
|
-
AND accepted_at IS NULL
|
|
351
|
-
AND declined_at IS NULL
|
|
352
|
-
AND retracted_at IS NULL
|
|
353
|
-
AND superseded_at IS NULL
|
|
354
|
-
RETURNING *
|
|
355
|
-
)
|
|
356
|
-
SELECT u.*, grantor.account_id AS from_account_id
|
|
357
|
-
FROM updated u
|
|
358
|
-
JOIN actor grantor ON grantor.id = u.from_actor_id`, [to_account_id, offer.role, offer.scope_id, offer.id]);
|
|
359
|
-
// Emit audit events in-transaction (atomic with the permit insert).
|
|
360
|
-
// `RETURNING *` after the SET guarantees `offer.resulting_permit_id === permit.id`.
|
|
361
|
-
const offer_accept_event = await query_audit_log(deps, {
|
|
362
|
-
event_type: 'permit_offer_accept',
|
|
363
|
-
actor_id: actor.id,
|
|
364
|
-
account_id: to_account_id,
|
|
365
|
-
ip: ip ?? null,
|
|
366
|
-
metadata: {
|
|
367
|
-
offer_id: offer.id,
|
|
368
|
-
permit_id: permit.id,
|
|
369
|
-
role: offer.role,
|
|
370
|
-
scope_id: offer.scope_id,
|
|
371
|
-
},
|
|
372
|
-
});
|
|
373
|
-
const permit_grant_event = await query_audit_log(deps, {
|
|
374
|
-
event_type: 'permit_grant',
|
|
375
|
-
actor_id: actor.id,
|
|
376
|
-
account_id: to_account_id,
|
|
377
|
-
ip: ip ?? null,
|
|
378
|
-
metadata: {
|
|
379
|
-
role: offer.role,
|
|
380
|
-
permit_id: permit.id,
|
|
381
|
-
scope_id: offer.scope_id,
|
|
382
|
-
source_offer_id: offer.id,
|
|
383
|
-
},
|
|
384
|
-
});
|
|
385
|
-
const supersede_events = [];
|
|
386
|
-
for (const sibling of superseded) {
|
|
387
|
-
supersede_events.push(await query_audit_log(deps, {
|
|
388
|
-
event_type: 'permit_offer_supersede',
|
|
389
|
-
actor_id: actor.id,
|
|
390
|
-
account_id: to_account_id,
|
|
391
|
-
ip: ip ?? null,
|
|
392
|
-
metadata: {
|
|
393
|
-
offer_id: sibling.id,
|
|
394
|
-
role: sibling.role,
|
|
395
|
-
scope_id: sibling.scope_id,
|
|
396
|
-
reason: 'sibling_accepted',
|
|
397
|
-
cause_id: offer.id,
|
|
398
|
-
},
|
|
399
|
-
}));
|
|
400
|
-
}
|
|
401
|
-
return {
|
|
402
|
-
permit,
|
|
403
|
-
offer,
|
|
404
|
-
created: true,
|
|
405
|
-
superseded_offers: superseded,
|
|
406
|
-
audit_events: [offer_accept_event, permit_grant_event, ...supersede_events],
|
|
407
|
-
};
|
|
408
|
-
};
|