@fuzdev/fuz_app 0.55.0 → 0.57.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 +72 -39
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +81 -33
- 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 +14 -6
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/surface_invariants.js +119 -43
- 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,71 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Role grant offer RPC action handlers — the consentful-role-grants action surface.
|
|
3
3
|
*
|
|
4
4
|
* Seven actions: six offer-lifecycle methods (create / accept / decline /
|
|
5
|
-
* retract / list / history) plus `
|
|
5
|
+
* retract / list / history) plus `role_grant_revoke` (admin-only). All mount
|
|
6
6
|
* on a consumer's JSON-RPC endpoint via `create_rpc_endpoint`. The action
|
|
7
|
-
* specs themselves live in `auth/
|
|
7
|
+
* specs themselves live in `auth/role_grant_offer_action_specs.ts`. Mutations
|
|
8
8
|
* declare `side_effects: true` so the RPC dispatcher wraps the handler in
|
|
9
|
-
* a DB transaction; `
|
|
9
|
+
* a DB transaction; `role_grant_offer_list` and `role_grant_offer_history` declare
|
|
10
10
|
* `side_effects: false` so they are addressable via GET.
|
|
11
11
|
*
|
|
12
12
|
* Authorization:
|
|
13
|
-
* - `
|
|
14
|
-
* role being offered, and that role must
|
|
15
|
-
* needing a richer policy (e.g., "teacher may offer student in
|
|
16
|
-
* classroom") pass an `authorize` callback that overrides the default.
|
|
17
|
-
* - `
|
|
13
|
+
* - `role_grant_offer_create` — the grantor must hold an active role_grant for the
|
|
14
|
+
* role being offered, and that role's `grant_paths` must include `'admin'`.
|
|
15
|
+
* Consumers needing a richer policy (e.g., "teacher may offer student in
|
|
16
|
+
* *their* classroom") pass an `authorize` callback that overrides the default.
|
|
17
|
+
* - `role_grant_offer_accept` / `role_grant_offer_decline` — keyed to the caller's
|
|
18
18
|
* account; `query_*` helpers enforce the IDOR guard.
|
|
19
|
-
* - `
|
|
20
|
-
* - `
|
|
19
|
+
* - `role_grant_offer_retract` — keyed to the caller's actor.
|
|
20
|
+
* - `role_grant_offer_list` / `role_grant_offer_history` — self by default;
|
|
21
21
|
* `{account_id}` is admin-only.
|
|
22
|
-
* - `
|
|
22
|
+
* - `role_grant_revoke` — spec-level `auth: {role: 'admin'}`; the RPC
|
|
23
23
|
* dispatcher rejects non-admin callers before the handler runs.
|
|
24
|
-
*
|
|
25
|
-
* via this surface. Keys on `actor_id` to survive multi-actor accounts.
|
|
24
|
+
* The admin-grant-path gate prevents revoking keeper / daemon-scoped
|
|
25
|
+
* roles via this surface. Keys on `actor_id` to survive multi-actor accounts.
|
|
26
26
|
*
|
|
27
27
|
* Audit events are emitted in-transaction by the query layer (atomic with
|
|
28
|
-
* the
|
|
29
|
-
* `
|
|
30
|
-
* `
|
|
28
|
+
* the role_grant write on accept/revoke) or by the handler via the bound
|
|
29
|
+
* `deps.audit.emit_role_grant_target` helper for single-event lifecycle
|
|
30
|
+
* transitions. `audit.notify` (SSE/WS broadcast) fires post-commit in both
|
|
31
|
+
* paths.
|
|
31
32
|
*
|
|
32
33
|
* WS notifications fan out post-commit via `emit_after_commit` when a
|
|
33
34
|
* `notification_sender` is wired: offer lifecycle transitions notify the
|
|
34
|
-
* counterparty, `
|
|
35
|
+
* counterparty, `role_grant_revoke` notifies the revokee plus each superseded
|
|
35
36
|
* pending offer's grantor.
|
|
36
37
|
*
|
|
37
38
|
* @module
|
|
38
39
|
*/
|
|
39
|
-
import {
|
|
40
|
+
import { rpc_action, } from '../actions/action_rpc.js';
|
|
40
41
|
import { jsonrpc_errors } from '../http/jsonrpc_errors.js';
|
|
41
42
|
import { emit_after_commit } from '../http/pending_effects.js';
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
43
|
+
import { BUILTIN_ROLE_SPECS_BY_NAME, ROLE_ADMIN, role_has_grant_path, } from './role_schema.js';
|
|
44
|
+
import { GRANT_PATH_ADMIN } from './grant_path_schema.js';
|
|
45
|
+
import { ROLE_GRANT_OFFER_DEFAULT_TTL_MS, to_role_grant_offer_json, } from './role_grant_offer_schema.js';
|
|
46
|
+
import { query_role_grant_offer_create, query_role_grant_offer_decline, query_role_grant_offer_retract, query_role_grant_offer_list, query_role_grant_offer_history_for_account, query_accept_offer, RoleGrantOfferActorAccountMismatchError, RoleGrantOfferActorMismatchError, RoleGrantOfferAlreadyTerminalError, RoleGrantOfferExpiredError, RoleGrantOfferNotFoundError, RoleGrantOfferSelfTargetError, } from './role_grant_offer_queries.js';
|
|
47
|
+
import { query_role_grant_find_active_role_for_actor, query_revoke_role_grant, } from './role_grant_queries.js';
|
|
46
48
|
import { query_actor_by_id } from './account_queries.js';
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
51
|
-
import { ERROR_OFFER_ACTOR_ACCOUNT_MISMATCH, ERROR_OFFER_ACTOR_MISMATCH, ERROR_OFFER_EXPIRED, ERROR_OFFER_NOT_AUTHORIZED, ERROR_OFFER_NOT_FOUND, ERROR_OFFER_ROLE_NOT_GRANTABLE, ERROR_OFFER_SELF_TARGET, ERROR_OFFER_TERMINAL, permit_offer_create_action_spec, permit_offer_accept_action_spec, permit_offer_decline_action_spec, permit_offer_retract_action_spec, permit_offer_list_action_spec, permit_offer_history_action_spec, permit_revoke_action_spec, } from './permit_offer_action_specs.js';
|
|
49
|
+
import { has_scoped_role } from './request_context.js';
|
|
50
|
+
import { build_role_grant_offer_accepted_notification, build_role_grant_offer_declined_notification, build_role_grant_offer_received_notification, build_role_grant_offer_retracted_notification, build_role_grant_offer_supersede_notification, build_role_grant_revoke_notification, } from './role_grant_offer_notifications.js';
|
|
51
|
+
import { ERROR_ROLE_GRANT_NOT_FOUND, ERROR_ROLE_NOT_WEB_GRANTABLE } from '../http/error_schemas.js';
|
|
52
|
+
import { ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH, ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH, ERROR_ROLE_GRANT_OFFER_EXPIRED, ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED, ERROR_ROLE_GRANT_OFFER_NOT_FOUND, ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE, ERROR_ROLE_GRANT_OFFER_SELF_TARGET, ERROR_ROLE_GRANT_OFFER_TERMINAL, role_grant_offer_create_action_spec, role_grant_offer_accept_action_spec, role_grant_offer_decline_action_spec, role_grant_offer_retract_action_spec, role_grant_offer_list_action_spec, role_grant_offer_history_action_spec, role_grant_revoke_action_spec, } from './role_grant_offer_action_specs.js';
|
|
52
53
|
// -- Helpers ----------------------------------------------------------------
|
|
53
|
-
/**
|
|
54
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Fan out a batch of pre-written audit rows to the bound emitter's
|
|
56
|
+
* `notify` listener chain. Used by accept, whose events were written
|
|
57
|
+
* in-transaction by `query_accept_offer` — the rows are already in the DB,
|
|
58
|
+
* we just need SSE/WS subscribers to see them.
|
|
59
|
+
*
|
|
60
|
+
* Per-listener exceptions are isolated inside `audit.notify`; one failing
|
|
61
|
+
* subscriber does not starve siblings, and a failure on the first event
|
|
62
|
+
* does not skip the rest.
|
|
63
|
+
*/
|
|
64
|
+
const fan_out_audit_events = (events, audit) => {
|
|
55
65
|
for (const event of events) {
|
|
56
|
-
|
|
57
|
-
on_audit_event(event);
|
|
58
|
-
}
|
|
59
|
-
catch (err) {
|
|
60
|
-
log.error('on_audit_event callback failed:', err);
|
|
61
|
-
}
|
|
66
|
+
audit.notify(event);
|
|
62
67
|
}
|
|
63
68
|
};
|
|
64
69
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
65
70
|
const default_authorize = async (auth, input, _deps, _ctx) => {
|
|
66
|
-
// Caller must hold an active
|
|
71
|
+
// Caller must hold an active role_grant for the offered role. Global (no scope)
|
|
67
72
|
// check — the scope-aware "only in this classroom" policy is consumer-level.
|
|
68
|
-
// Reads from the in-memory `auth.
|
|
73
|
+
// Reads from the in-memory `auth.role_grants` snapshot loaded once per request
|
|
69
74
|
// by `create_request_context_middleware`; no DB roundtrip needed.
|
|
70
75
|
return has_scoped_role(auth, input.role, null);
|
|
71
76
|
};
|
|
@@ -73,9 +78,10 @@ const default_authorize = async (auth, input, _deps, _ctx) => {
|
|
|
73
78
|
* Authorization callback that admits any admin and otherwise falls back to
|
|
74
79
|
* the symmetric default (caller must hold the offered role globally).
|
|
75
80
|
*
|
|
76
|
-
* The
|
|
77
|
-
* `authorize` callback, so this never sees
|
|
78
|
-
*
|
|
81
|
+
* The admin-grant-path filter in `create_handler` runs **before** the
|
|
82
|
+
* `authorize` callback, so this never sees roles whose `grant_paths`
|
|
83
|
+
* omits `'admin'`. Drop into
|
|
84
|
+
* `create_role_grant_offer_actions({authorize: authorize_admin_or_holder})`
|
|
79
85
|
* (or any factory that forwards `authorize`, e.g. `create_standard_rpc_actions`)
|
|
80
86
|
* for the common "admins offer anything; users offer what they hold"
|
|
81
87
|
* pattern. Scope-aware policies (e.g. classroom_teacher offering
|
|
@@ -83,31 +89,36 @@ const default_authorize = async (auth, input, _deps, _ctx) => {
|
|
|
83
89
|
* before delegating.
|
|
84
90
|
*/
|
|
85
91
|
export const authorize_admin_or_holder = async (auth, input, _deps, _ctx) => {
|
|
86
|
-
|
|
92
|
+
// Admin bypass keys on **global** admin role_grants only — `has_scoped_role(_, _, null)`
|
|
93
|
+
// rejects scoped admin role_grants. Without this, a `{role: 'admin', scope_id: scope_X}`
|
|
94
|
+
// role_grant would let the holder offer any admin-grant-path role without holding it
|
|
95
|
+
// themselves, escalating scoped admin to global authority over the offer surface.
|
|
96
|
+
if (has_scoped_role(auth, ROLE_ADMIN, null))
|
|
87
97
|
return true;
|
|
88
98
|
return has_scoped_role(auth, input.role, null);
|
|
89
99
|
};
|
|
100
|
+
// -- Action factory ---------------------------------------------------------
|
|
90
101
|
/**
|
|
91
|
-
* Create the seven
|
|
92
|
-
* plus `
|
|
102
|
+
* Create the seven role-grant-offer RPC actions (six offer-lifecycle methods
|
|
103
|
+
* plus `role_grant_revoke`).
|
|
93
104
|
*
|
|
94
|
-
* @param deps - `
|
|
105
|
+
* @param deps - `RouteFactoryDeps` (`log`, `audit`, …) plus optional `notification_sender` for WS fan-out — when absent, WS fan-out is silently skipped (DB-only side effects still happen). Consumers wiring `BackendWebsocketTransport` assign its instance directly (the transport's `send_to_account` signature accepts the broader `JsonrpcMessageFromServerToClient`, which is contravariantly compatible)
|
|
95
106
|
* @param options - role schema, default TTL, authorization override
|
|
96
107
|
* @returns the `RpcAction` array to spread into a `create_rpc_endpoint` call
|
|
97
108
|
*/
|
|
98
|
-
export const
|
|
99
|
-
const {
|
|
100
|
-
const
|
|
101
|
-
const default_ttl_ms = options.default_ttl_ms ??
|
|
109
|
+
export const create_role_grant_offer_actions = (deps, options = {}) => {
|
|
110
|
+
const { log, audit, notification_sender = null } = deps;
|
|
111
|
+
const role_specs = options.roles?.role_specs ?? BUILTIN_ROLE_SPECS_BY_NAME;
|
|
112
|
+
const default_ttl_ms = options.default_ttl_ms ?? ROLE_GRANT_OFFER_DEFAULT_TTL_MS;
|
|
102
113
|
const authorize = options.authorize ?? default_authorize;
|
|
103
|
-
// Four denial paths (
|
|
114
|
+
// Four denial paths (admin-grant-path gate, authorize, self-target,
|
|
104
115
|
// actor-account mismatch) all emit the same failure-outcome audit
|
|
105
116
|
// event. `target_actor_id` is populated when the caller supplied a
|
|
106
117
|
// `to_actor_id` so failure rows match the success-shape envelope of
|
|
107
118
|
// actor-targeted offers.
|
|
108
119
|
const emit_create_failure_audit = (ctx, auth, input) => {
|
|
109
|
-
|
|
110
|
-
event_type: '
|
|
120
|
+
audit.emit_role_grant_target(ctx, auth, {
|
|
121
|
+
event_type: 'role_grant_offer_create',
|
|
111
122
|
outcome: 'failure',
|
|
112
123
|
target_account_id: input.to_account_id,
|
|
113
124
|
target_actor_id: input.to_actor_id ?? null,
|
|
@@ -119,16 +130,15 @@ export const create_permit_offer_actions = (deps, options = {}) => {
|
|
|
119
130
|
});
|
|
120
131
|
};
|
|
121
132
|
// Returns {offer} only — no auto-accept. Recipient must call
|
|
122
|
-
//
|
|
133
|
+
// role_grant_offer_accept; admin tests materialize role_grants via
|
|
123
134
|
// query_accept_offer (see testing/admin_integration.ts `offer_and_accept`).
|
|
124
135
|
const create_handler = async (input, ctx) => {
|
|
125
136
|
const auth = ctx.auth;
|
|
126
|
-
// Role must
|
|
127
|
-
|
|
128
|
-
if (!rc?.web_grantable) {
|
|
137
|
+
// Role must include the admin grant path — same gate as admin direct-grant.
|
|
138
|
+
if (!role_has_grant_path(role_specs, input.role, GRANT_PATH_ADMIN)) {
|
|
129
139
|
emit_create_failure_audit(ctx, auth, input);
|
|
130
140
|
throw jsonrpc_errors.forbidden('role not grantable', {
|
|
131
|
-
reason:
|
|
141
|
+
reason: ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE,
|
|
132
142
|
});
|
|
133
143
|
}
|
|
134
144
|
const authorized = await authorize(auth, {
|
|
@@ -139,32 +149,33 @@ export const create_permit_offer_actions = (deps, options = {}) => {
|
|
|
139
149
|
if (!authorized) {
|
|
140
150
|
emit_create_failure_audit(ctx, auth, input);
|
|
141
151
|
throw jsonrpc_errors.forbidden('not authorized to offer this role', {
|
|
142
|
-
reason:
|
|
152
|
+
reason: ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED,
|
|
143
153
|
});
|
|
144
154
|
}
|
|
145
155
|
let offer;
|
|
146
156
|
try {
|
|
147
|
-
offer = await
|
|
157
|
+
offer = await query_role_grant_offer_create(ctx, {
|
|
148
158
|
from_actor_id: auth.actor.id,
|
|
149
159
|
to_account_id: input.to_account_id,
|
|
150
160
|
to_actor_id: input.to_actor_id ?? null,
|
|
151
161
|
role: input.role,
|
|
162
|
+
scope_kind: input.scope_kind ?? null,
|
|
152
163
|
scope_id: input.scope_id ?? null,
|
|
153
164
|
message: input.message ?? null,
|
|
154
165
|
expires_at: new Date(Date.now() + default_ttl_ms),
|
|
155
166
|
});
|
|
156
167
|
}
|
|
157
168
|
catch (err) {
|
|
158
|
-
if (err instanceof
|
|
169
|
+
if (err instanceof RoleGrantOfferSelfTargetError) {
|
|
159
170
|
emit_create_failure_audit(ctx, auth, input);
|
|
160
171
|
throw jsonrpc_errors.invalid_params('cannot offer to self', {
|
|
161
|
-
reason:
|
|
172
|
+
reason: ERROR_ROLE_GRANT_OFFER_SELF_TARGET,
|
|
162
173
|
});
|
|
163
174
|
}
|
|
164
|
-
if (err instanceof
|
|
175
|
+
if (err instanceof RoleGrantOfferActorAccountMismatchError) {
|
|
165
176
|
emit_create_failure_audit(ctx, auth, input);
|
|
166
177
|
throw jsonrpc_errors.invalid_params('to_actor_id does not belong to to_account_id', {
|
|
167
|
-
reason:
|
|
178
|
+
reason: ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH,
|
|
168
179
|
});
|
|
169
180
|
}
|
|
170
181
|
throw err;
|
|
@@ -173,8 +184,8 @@ export const create_permit_offer_actions = (deps, options = {}) => {
|
|
|
173
184
|
// (per the offer's `to_actor_id`), null for account-grain offers
|
|
174
185
|
// — closes the audit hole where offer-shape events used to leave
|
|
175
186
|
// actor-grain forensics blank even when the binding was known.
|
|
176
|
-
|
|
177
|
-
event_type: '
|
|
187
|
+
audit.emit_role_grant_target(ctx, auth, {
|
|
188
|
+
event_type: 'role_grant_offer_create',
|
|
178
189
|
target_account_id: input.to_account_id,
|
|
179
190
|
target_actor_id: offer.to_actor_id,
|
|
180
191
|
metadata: {
|
|
@@ -184,10 +195,10 @@ export const create_permit_offer_actions = (deps, options = {}) => {
|
|
|
184
195
|
to_account_id: offer.to_account_id,
|
|
185
196
|
},
|
|
186
197
|
});
|
|
187
|
-
const offer_json =
|
|
198
|
+
const offer_json = to_role_grant_offer_json(offer);
|
|
188
199
|
if (notification_sender) {
|
|
189
200
|
emit_after_commit(ctx, () => {
|
|
190
|
-
notification_sender.send_to_account(offer.to_account_id,
|
|
201
|
+
notification_sender.send_to_account(offer.to_account_id, build_role_grant_offer_received_notification({ offer: offer_json }));
|
|
191
202
|
});
|
|
192
203
|
}
|
|
193
204
|
return { offer: offer_json };
|
|
@@ -204,18 +215,18 @@ export const create_permit_offer_actions = (deps, options = {}) => {
|
|
|
204
215
|
});
|
|
205
216
|
}
|
|
206
217
|
catch (err) {
|
|
207
|
-
if (err instanceof
|
|
208
|
-
throw jsonrpc_errors.not_found('offer', { reason:
|
|
218
|
+
if (err instanceof RoleGrantOfferNotFoundError) {
|
|
219
|
+
throw jsonrpc_errors.not_found('offer', { reason: ERROR_ROLE_GRANT_OFFER_NOT_FOUND });
|
|
209
220
|
}
|
|
210
|
-
if (err instanceof
|
|
211
|
-
throw jsonrpc_errors.invalid_request({ reason:
|
|
221
|
+
if (err instanceof RoleGrantOfferAlreadyTerminalError) {
|
|
222
|
+
throw jsonrpc_errors.invalid_request({ reason: ERROR_ROLE_GRANT_OFFER_TERMINAL });
|
|
212
223
|
}
|
|
213
|
-
if (err instanceof
|
|
214
|
-
throw jsonrpc_errors.invalid_request({ reason:
|
|
224
|
+
if (err instanceof RoleGrantOfferExpiredError) {
|
|
225
|
+
throw jsonrpc_errors.invalid_request({ reason: ERROR_ROLE_GRANT_OFFER_EXPIRED });
|
|
215
226
|
}
|
|
216
|
-
if (err instanceof
|
|
227
|
+
if (err instanceof RoleGrantOfferActorMismatchError) {
|
|
217
228
|
throw jsonrpc_errors.forbidden('offer is targeted to a different actor', {
|
|
218
|
-
reason:
|
|
229
|
+
reason: ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH,
|
|
219
230
|
});
|
|
220
231
|
}
|
|
221
232
|
throw err;
|
|
@@ -228,24 +239,24 @@ export const create_permit_offer_actions = (deps, options = {}) => {
|
|
|
228
239
|
? await query_actor_by_id(ctx, result.offer.from_actor_id)
|
|
229
240
|
: null;
|
|
230
241
|
const grantor_account_id = grantor_actor?.account_id ?? null;
|
|
231
|
-
const offer_json =
|
|
242
|
+
const offer_json = to_role_grant_offer_json(result.offer);
|
|
232
243
|
const supersede_payloads = result.superseded_offers.map((sib) => ({
|
|
233
|
-
offer:
|
|
244
|
+
offer: to_role_grant_offer_json(sib),
|
|
234
245
|
from_account_id: sib.from_account_id,
|
|
235
246
|
}));
|
|
236
247
|
// Audit events are written in-transaction by query_accept_offer; wire
|
|
237
|
-
// them through
|
|
248
|
+
// them through `audit.notify` post-commit so SSE/WS broadcasts fire.
|
|
238
249
|
// WS notifications piggyback on the same post-commit microtask so the
|
|
239
250
|
// grantor sees "accepted" and each superseded grantor sees
|
|
240
251
|
// "supersede" only once the accept has durably committed.
|
|
241
252
|
emit_after_commit(ctx, () => {
|
|
242
|
-
fan_out_audit_events(result.audit_events,
|
|
253
|
+
fan_out_audit_events(result.audit_events, audit);
|
|
243
254
|
if (notification_sender && grantor_account_id) {
|
|
244
|
-
notification_sender.send_to_account(grantor_account_id,
|
|
255
|
+
notification_sender.send_to_account(grantor_account_id, build_role_grant_offer_accepted_notification({ offer: offer_json }));
|
|
245
256
|
}
|
|
246
257
|
if (notification_sender) {
|
|
247
258
|
for (const sib of supersede_payloads) {
|
|
248
|
-
notification_sender.send_to_account(sib.from_account_id,
|
|
259
|
+
notification_sender.send_to_account(sib.from_account_id, build_role_grant_offer_supersede_notification({
|
|
249
260
|
offer: sib.offer,
|
|
250
261
|
reason: 'sibling_accepted',
|
|
251
262
|
cause_id: result.offer.id,
|
|
@@ -254,7 +265,7 @@ export const create_permit_offer_actions = (deps, options = {}) => {
|
|
|
254
265
|
}
|
|
255
266
|
});
|
|
256
267
|
return {
|
|
257
|
-
|
|
268
|
+
role_grant_id: result.role_grant.id,
|
|
258
269
|
offer: offer_json,
|
|
259
270
|
superseded_offer_ids: result.superseded_offers.map((o) => o.id),
|
|
260
271
|
};
|
|
@@ -263,24 +274,24 @@ export const create_permit_offer_actions = (deps, options = {}) => {
|
|
|
263
274
|
const auth = ctx.auth;
|
|
264
275
|
let declined;
|
|
265
276
|
try {
|
|
266
|
-
declined = await
|
|
277
|
+
declined = await query_role_grant_offer_decline(ctx, input.offer_id, auth.account.id, input.reason ?? null);
|
|
267
278
|
}
|
|
268
279
|
catch (err) {
|
|
269
|
-
if (err instanceof
|
|
270
|
-
throw jsonrpc_errors.invalid_request({ reason:
|
|
280
|
+
if (err instanceof RoleGrantOfferAlreadyTerminalError) {
|
|
281
|
+
throw jsonrpc_errors.invalid_request({ reason: ERROR_ROLE_GRANT_OFFER_TERMINAL });
|
|
271
282
|
}
|
|
272
283
|
throw err;
|
|
273
284
|
}
|
|
274
285
|
if (!declined) {
|
|
275
|
-
throw jsonrpc_errors.not_found('offer', { reason:
|
|
286
|
+
throw jsonrpc_errors.not_found('offer', { reason: ERROR_ROLE_GRANT_OFFER_NOT_FOUND });
|
|
276
287
|
}
|
|
277
|
-
// `
|
|
288
|
+
// `role_grant_offer_decline` is *to* the offering actor — populate both
|
|
278
289
|
// `target_actor_id` (the grantor actor) and `target_account_id`
|
|
279
290
|
// (the grantor account, joined in the decline RETURNING via CTE).
|
|
280
291
|
// The "both populated → same account" invariant holds: the
|
|
281
292
|
// grantor's actor↔account binding is 1:1 by definition of `actor`.
|
|
282
|
-
|
|
283
|
-
event_type: '
|
|
293
|
+
audit.emit_role_grant_target(ctx, auth, {
|
|
294
|
+
event_type: 'role_grant_offer_decline',
|
|
284
295
|
target_account_id: declined.from_account_id,
|
|
285
296
|
target_actor_id: declined.from_actor_id,
|
|
286
297
|
metadata: {
|
|
@@ -295,9 +306,9 @@ export const create_permit_offer_actions = (deps, options = {}) => {
|
|
|
295
306
|
// the decline RETURNING — no second SELECT needed. The decline
|
|
296
307
|
// reason rides along on `offer.decline_reason` — the DB set it
|
|
297
308
|
// in the RETURNING above.
|
|
298
|
-
const offer_json =
|
|
309
|
+
const offer_json = to_role_grant_offer_json(declined);
|
|
299
310
|
emit_after_commit(ctx, () => {
|
|
300
|
-
notification_sender.send_to_account(declined.from_account_id,
|
|
311
|
+
notification_sender.send_to_account(declined.from_account_id, build_role_grant_offer_declined_notification({ offer: offer_json }));
|
|
301
312
|
});
|
|
302
313
|
}
|
|
303
314
|
return { ok: true };
|
|
@@ -306,23 +317,23 @@ export const create_permit_offer_actions = (deps, options = {}) => {
|
|
|
306
317
|
const auth = ctx.auth;
|
|
307
318
|
let retracted;
|
|
308
319
|
try {
|
|
309
|
-
retracted = await
|
|
320
|
+
retracted = await query_role_grant_offer_retract(ctx, input.offer_id, auth.actor.id);
|
|
310
321
|
}
|
|
311
322
|
catch (err) {
|
|
312
|
-
if (err instanceof
|
|
313
|
-
throw jsonrpc_errors.invalid_request({ reason:
|
|
323
|
+
if (err instanceof RoleGrantOfferAlreadyTerminalError) {
|
|
324
|
+
throw jsonrpc_errors.invalid_request({ reason: ERROR_ROLE_GRANT_OFFER_TERMINAL });
|
|
314
325
|
}
|
|
315
326
|
throw err;
|
|
316
327
|
}
|
|
317
328
|
if (!retracted) {
|
|
318
|
-
throw jsonrpc_errors.not_found('offer', { reason:
|
|
329
|
+
throw jsonrpc_errors.not_found('offer', { reason: ERROR_ROLE_GRANT_OFFER_NOT_FOUND });
|
|
319
330
|
}
|
|
320
|
-
// `
|
|
331
|
+
// `role_grant_offer_retract` is *from* the recipient inbox —
|
|
321
332
|
// `target_account_id` is the recipient account; `target_actor_id`
|
|
322
333
|
// inherits the offer's `to_actor_id` (set on actor-targeted
|
|
323
334
|
// offers, null on account-grain offers).
|
|
324
|
-
|
|
325
|
-
event_type: '
|
|
335
|
+
audit.emit_role_grant_target(ctx, auth, {
|
|
336
|
+
event_type: 'role_grant_offer_retract',
|
|
326
337
|
target_account_id: retracted.to_account_id,
|
|
327
338
|
target_actor_id: retracted.to_actor_id,
|
|
328
339
|
metadata: {
|
|
@@ -332,9 +343,9 @@ export const create_permit_offer_actions = (deps, options = {}) => {
|
|
|
332
343
|
},
|
|
333
344
|
});
|
|
334
345
|
if (notification_sender) {
|
|
335
|
-
const offer_json =
|
|
346
|
+
const offer_json = to_role_grant_offer_json(retracted);
|
|
336
347
|
emit_after_commit(ctx, () => {
|
|
337
|
-
notification_sender.send_to_account(retracted.to_account_id,
|
|
348
|
+
notification_sender.send_to_account(retracted.to_account_id, build_role_grant_offer_retracted_notification({ offer: offer_json }));
|
|
338
349
|
});
|
|
339
350
|
}
|
|
340
351
|
return { ok: true };
|
|
@@ -342,65 +353,67 @@ export const create_permit_offer_actions = (deps, options = {}) => {
|
|
|
342
353
|
const list_handler = async (input, ctx) => {
|
|
343
354
|
const auth = ctx.auth;
|
|
344
355
|
const target = input.account_id ?? auth.account.id;
|
|
345
|
-
|
|
356
|
+
// Cross-account inspection requires **global** admin — a scoped admin
|
|
357
|
+
// role_grant must not be able to read another account's offer list.
|
|
358
|
+
if (target !== auth.account.id && !has_scoped_role(auth, ROLE_ADMIN, null)) {
|
|
346
359
|
throw jsonrpc_errors.forbidden('admin required to inspect another account');
|
|
347
360
|
}
|
|
348
|
-
const offers = await
|
|
349
|
-
return { offers: offers.map(
|
|
361
|
+
const offers = await query_role_grant_offer_list(ctx, target);
|
|
362
|
+
return { offers: offers.map(to_role_grant_offer_json) };
|
|
350
363
|
};
|
|
351
364
|
const history_handler = async (input, ctx) => {
|
|
352
365
|
const auth = ctx.auth;
|
|
353
366
|
const target = input.account_id ?? auth.account.id;
|
|
354
|
-
if (target !== auth.account.id && !
|
|
367
|
+
if (target !== auth.account.id && !has_scoped_role(auth, ROLE_ADMIN, null)) {
|
|
355
368
|
throw jsonrpc_errors.forbidden('admin required to inspect another account');
|
|
356
369
|
}
|
|
357
|
-
const offers = await
|
|
358
|
-
return { offers: offers.map(
|
|
370
|
+
const offers = await query_role_grant_offer_history_for_account(ctx, target, input.limit ?? undefined, input.offset ?? undefined);
|
|
371
|
+
return { offers: offers.map(to_role_grant_offer_json) };
|
|
359
372
|
};
|
|
360
373
|
const revoke_handler = async (input, ctx) => {
|
|
361
374
|
const auth = ctx.auth;
|
|
362
375
|
// IDOR guard + role lookup + actor → account JOIN. One SELECT —
|
|
363
|
-
// returns null when the
|
|
376
|
+
// returns null when the role_grant is revoked, missing, or belongs
|
|
364
377
|
// to a different actor. The JOIN supplies `account_id` for the
|
|
365
378
|
// audit envelope's `target_account_id` and the post-commit
|
|
366
|
-
// SSE/WS socket-close fan-out target. `
|
|
379
|
+
// SSE/WS socket-close fan-out target. `role_grant_revoke` is the
|
|
367
380
|
// canonical actor-bound-subject event: `target_actor_id` is the
|
|
368
|
-
//
|
|
381
|
+
// role_grant's grantee (input.actor_id); `target_account_id` is the
|
|
369
382
|
// account hosting that actor (sessions remain account-grain
|
|
370
383
|
// after multi-actor lands).
|
|
371
|
-
const
|
|
372
|
-
if (!
|
|
373
|
-
throw jsonrpc_errors.not_found('
|
|
384
|
+
const role_grant_row = await query_role_grant_find_active_role_for_actor(ctx, input.role_grant_id, input.actor_id);
|
|
385
|
+
if (!role_grant_row) {
|
|
386
|
+
throw jsonrpc_errors.not_found('role_grant', { reason: ERROR_ROLE_GRANT_NOT_FOUND });
|
|
374
387
|
}
|
|
375
|
-
const target_account_id =
|
|
388
|
+
const target_account_id = role_grant_row.account_id;
|
|
376
389
|
const target_actor_id = input.actor_id;
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
if (!
|
|
380
|
-
|
|
381
|
-
event_type: '
|
|
390
|
+
// Admin-grant-path gate — keeper / daemon-scoped roles stay CLI-only
|
|
391
|
+
// (their `grant_paths` does not include `'admin'`).
|
|
392
|
+
if (!role_has_grant_path(role_specs, role_grant_row.role, GRANT_PATH_ADMIN)) {
|
|
393
|
+
audit.emit_role_grant_target(ctx, auth, {
|
|
394
|
+
event_type: 'role_grant_revoke',
|
|
382
395
|
outcome: 'failure',
|
|
383
396
|
target_account_id,
|
|
384
397
|
target_actor_id,
|
|
385
|
-
metadata: { role:
|
|
398
|
+
metadata: { role: role_grant_row.role, role_grant_id: input.role_grant_id },
|
|
386
399
|
});
|
|
387
400
|
throw jsonrpc_errors.forbidden('role not web-grantable', {
|
|
388
401
|
reason: ERROR_ROLE_NOT_WEB_GRANTABLE,
|
|
389
402
|
});
|
|
390
403
|
}
|
|
391
|
-
const result = await
|
|
404
|
+
const result = await query_revoke_role_grant(ctx, input.role_grant_id, input.actor_id, auth.actor.id, input.reason ?? null);
|
|
392
405
|
if (!result) {
|
|
393
|
-
// Raced with another revoker or the
|
|
406
|
+
// Raced with another revoker or the role_grant was revoked between
|
|
394
407
|
// the IDOR check and the UPDATE.
|
|
395
|
-
throw jsonrpc_errors.not_found('
|
|
408
|
+
throw jsonrpc_errors.not_found('role_grant', { reason: ERROR_ROLE_GRANT_NOT_FOUND });
|
|
396
409
|
}
|
|
397
|
-
|
|
398
|
-
event_type: '
|
|
410
|
+
audit.emit_role_grant_target(ctx, auth, {
|
|
411
|
+
event_type: 'role_grant_revoke',
|
|
399
412
|
target_account_id,
|
|
400
413
|
target_actor_id,
|
|
401
414
|
metadata: {
|
|
402
415
|
role: result.role,
|
|
403
|
-
|
|
416
|
+
role_grant_id: result.id,
|
|
404
417
|
scope_id: result.scope_id,
|
|
405
418
|
reason: input.reason ?? undefined,
|
|
406
419
|
},
|
|
@@ -410,37 +423,37 @@ export const create_permit_offer_actions = (deps, options = {}) => {
|
|
|
410
423
|
// `target_actor_id` inherits the offer's `to_actor_id` (actor-grain
|
|
411
424
|
// when the superseded offer was actor-targeted, null otherwise).
|
|
412
425
|
for (const offer of result.superseded_offers) {
|
|
413
|
-
|
|
414
|
-
event_type: '
|
|
426
|
+
audit.emit_role_grant_target(ctx, auth, {
|
|
427
|
+
event_type: 'role_grant_offer_supersede',
|
|
415
428
|
target_account_id: offer.to_account_id,
|
|
416
429
|
target_actor_id: offer.to_actor_id,
|
|
417
430
|
metadata: {
|
|
418
431
|
offer_id: offer.id,
|
|
419
432
|
role: offer.role,
|
|
420
433
|
scope_id: offer.scope_id,
|
|
421
|
-
reason: '
|
|
434
|
+
reason: 'role_grant_revoked',
|
|
422
435
|
cause_id: result.id,
|
|
423
436
|
},
|
|
424
437
|
});
|
|
425
438
|
}
|
|
426
439
|
if (notification_sender) {
|
|
427
440
|
const superseded = result.superseded_offers.map((o) => ({
|
|
428
|
-
offer:
|
|
441
|
+
offer: to_role_grant_offer_json(o),
|
|
429
442
|
from_account_id: o.from_account_id,
|
|
430
443
|
}));
|
|
431
444
|
const cause_id = result.id;
|
|
432
445
|
const reason = input.reason ?? null;
|
|
433
446
|
emit_after_commit(ctx, () => {
|
|
434
|
-
notification_sender.send_to_account(target_account_id,
|
|
435
|
-
|
|
447
|
+
notification_sender.send_to_account(target_account_id, build_role_grant_revoke_notification({
|
|
448
|
+
role_grant_id: result.id,
|
|
436
449
|
role: result.role,
|
|
437
450
|
scope_id: result.scope_id,
|
|
438
451
|
reason,
|
|
439
452
|
}));
|
|
440
453
|
for (const sib of superseded) {
|
|
441
|
-
notification_sender.send_to_account(sib.from_account_id,
|
|
454
|
+
notification_sender.send_to_account(sib.from_account_id, build_role_grant_offer_supersede_notification({
|
|
442
455
|
offer: sib.offer,
|
|
443
|
-
reason: '
|
|
456
|
+
reason: 'role_grant_revoked',
|
|
444
457
|
cause_id,
|
|
445
458
|
}));
|
|
446
459
|
}
|
|
@@ -449,12 +462,12 @@ export const create_permit_offer_actions = (deps, options = {}) => {
|
|
|
449
462
|
return { ok: true, revoked: true };
|
|
450
463
|
};
|
|
451
464
|
return [
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
465
|
+
rpc_action(role_grant_offer_create_action_spec, create_handler),
|
|
466
|
+
rpc_action(role_grant_offer_accept_action_spec, accept_handler),
|
|
467
|
+
rpc_action(role_grant_offer_decline_action_spec, decline_handler),
|
|
468
|
+
rpc_action(role_grant_offer_retract_action_spec, retract_handler),
|
|
469
|
+
rpc_action(role_grant_offer_list_action_spec, list_handler),
|
|
470
|
+
rpc_action(role_grant_offer_history_action_spec, history_handler),
|
|
471
|
+
rpc_action(role_grant_revoke_action_spec, revoke_handler),
|
|
459
472
|
];
|
|
460
473
|
};
|