@fuzdev/fuz_app 0.29.0 → 0.31.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 +630 -0
- package/dist/actions/action_rpc.d.ts +29 -0
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +42 -6
- package/dist/actions/action_types.d.ts +2 -2
- package/dist/actions/cancel.d.ts +12 -13
- package/dist/actions/cancel.d.ts.map +1 -1
- package/dist/actions/cancel.js +10 -13
- package/dist/actions/heartbeat.d.ts +8 -13
- package/dist/actions/heartbeat.d.ts.map +1 -1
- package/dist/actions/heartbeat.js +5 -8
- package/dist/actions/register_action_ws.d.ts +3 -3
- package/dist/actions/register_action_ws.js +2 -2
- package/dist/actions/register_ws_endpoint.d.ts +4 -4
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +3 -3
- package/dist/actions/socket.svelte.d.ts +16 -16
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +15 -15
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.d.ts +15 -0
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +17 -0
- package/dist/auth/CLAUDE.md +923 -0
- package/dist/auth/account_action_specs.d.ts +216 -0
- package/dist/auth/account_action_specs.d.ts.map +1 -0
- package/dist/auth/account_action_specs.js +159 -0
- package/dist/auth/account_actions.d.ts +51 -0
- package/dist/auth/account_actions.d.ts.map +1 -0
- package/dist/auth/account_actions.js +119 -0
- package/dist/auth/account_queries.d.ts +6 -2
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +40 -4
- package/dist/auth/account_routes.d.ts +94 -16
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +108 -180
- package/dist/auth/account_schema.d.ts +85 -30
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +40 -8
- package/dist/auth/admin_action_specs.d.ts +674 -0
- package/dist/auth/admin_action_specs.d.ts.map +1 -0
- package/dist/auth/admin_action_specs.js +287 -0
- package/dist/auth/admin_actions.d.ts +69 -0
- package/dist/auth/admin_actions.d.ts.map +1 -0
- package/dist/auth/admin_actions.js +256 -0
- package/dist/auth/api_token.d.ts +10 -0
- package/dist/auth/api_token.d.ts.map +1 -1
- package/dist/auth/api_token.js +9 -0
- package/dist/auth/api_token_queries.d.ts +3 -3
- package/dist/auth/api_token_queries.js +3 -3
- package/dist/auth/app_settings_schema.d.ts +4 -3
- package/dist/auth/app_settings_schema.d.ts.map +1 -1
- package/dist/auth/app_settings_schema.js +2 -1
- package/dist/auth/audit_log_routes.d.ts +14 -6
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +22 -79
- package/dist/auth/audit_log_schema.d.ts +100 -29
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +83 -11
- package/dist/auth/bootstrap_routes.d.ts +14 -0
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +10 -3
- package/dist/auth/cleanup.d.ts +63 -0
- package/dist/auth/cleanup.d.ts.map +1 -0
- package/dist/auth/cleanup.js +80 -0
- package/dist/auth/invite_schema.d.ts +11 -10
- package/dist/auth/invite_schema.d.ts.map +1 -1
- package/dist/auth/invite_schema.js +4 -3
- package/dist/auth/migrations.d.ts +6 -0
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +28 -0
- package/dist/auth/permit_offer_action_specs.d.ts +364 -0
- package/dist/auth/permit_offer_action_specs.d.ts.map +1 -0
- package/dist/auth/permit_offer_action_specs.js +216 -0
- package/dist/auth/permit_offer_actions.d.ts +96 -0
- package/dist/auth/permit_offer_actions.d.ts.map +1 -0
- package/dist/auth/permit_offer_actions.js +428 -0
- package/dist/auth/permit_offer_notifications.d.ts +361 -0
- package/dist/auth/permit_offer_notifications.d.ts.map +1 -0
- package/dist/auth/permit_offer_notifications.js +179 -0
- package/dist/auth/permit_offer_queries.d.ts +165 -0
- package/dist/auth/permit_offer_queries.d.ts.map +1 -0
- package/dist/auth/permit_offer_queries.js +390 -0
- package/dist/auth/permit_offer_schema.d.ts +103 -0
- package/dist/auth/permit_offer_schema.d.ts.map +1 -0
- package/dist/auth/permit_offer_schema.js +142 -0
- package/dist/auth/permit_queries.d.ts +77 -14
- package/dist/auth/permit_queries.d.ts.map +1 -1
- package/dist/auth/permit_queries.js +119 -24
- package/dist/auth/session_queries.d.ts +4 -2
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +4 -2
- package/dist/auth/signup_routes.d.ts +13 -0
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +14 -7
- package/dist/http/CLAUDE.md +584 -0
- package/dist/http/pending_effects.d.ts +29 -0
- package/dist/http/pending_effects.d.ts.map +1 -0
- package/dist/http/pending_effects.js +31 -0
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/route_spec.js +4 -3
- package/dist/rate_limiter.d.ts +30 -0
- package/dist/rate_limiter.d.ts.map +1 -1
- package/dist/rate_limiter.js +25 -2
- package/dist/realtime/sse_auth_guard.d.ts +2 -0
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +5 -3
- package/dist/testing/CLAUDE.md +668 -1
- package/dist/testing/admin_integration.d.ts +10 -7
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +382 -482
- package/dist/testing/app_server.d.ts +7 -6
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/attack_surface.d.ts +9 -3
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +4 -4
- package/dist/testing/audit_completeness.d.ts +6 -0
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +158 -134
- package/dist/testing/auth_apps.d.ts.map +1 -1
- package/dist/testing/auth_apps.js +4 -33
- package/dist/testing/db.d.ts +1 -1
- package/dist/testing/db.d.ts.map +1 -1
- package/dist/testing/db.js +2 -0
- package/dist/testing/entities.d.ts +35 -13
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +17 -0
- package/dist/testing/integration.d.ts +10 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +352 -340
- package/dist/testing/integration_helpers.d.ts +16 -5
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +24 -4
- package/dist/testing/rate_limiting.d.ts +7 -0
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +41 -10
- package/dist/testing/rpc_helpers.d.ts +153 -1
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +184 -8
- package/dist/testing/sse_round_trip.d.ts +8 -0
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +10 -3
- package/dist/testing/standard.d.ts +9 -1
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +6 -2
- package/dist/testing/surface_invariants.d.ts +7 -3
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/surface_invariants.js +5 -4
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +9 -38
- package/dist/ui/AccountSessions.svelte +8 -4
- package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
- package/dist/ui/AdminAccounts.svelte +61 -33
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminAuditLog.svelte +3 -2
- package/dist/ui/AdminAuditLog.svelte.d.ts.map +1 -1
- package/dist/ui/AdminInvites.svelte +3 -2
- package/dist/ui/AdminInvites.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +14 -9
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/AdminPermitHistory.svelte +3 -2
- package/dist/ui/AdminPermitHistory.svelte.d.ts.map +1 -1
- package/dist/ui/AdminSessions.svelte +29 -25
- package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +351 -0
- package/dist/ui/OpenSignupToggle.svelte +6 -3
- package/dist/ui/OpenSignupToggle.svelte.d.ts.map +1 -1
- package/dist/ui/PermitOfferForm.svelte +141 -0
- package/dist/ui/PermitOfferForm.svelte.d.ts +14 -0
- package/dist/ui/PermitOfferForm.svelte.d.ts.map +1 -0
- package/dist/ui/PermitOfferHistory.svelte +109 -0
- package/dist/ui/PermitOfferHistory.svelte.d.ts +11 -0
- package/dist/ui/PermitOfferHistory.svelte.d.ts.map +1 -0
- package/dist/ui/PermitOfferInbox.svelte +121 -0
- package/dist/ui/PermitOfferInbox.svelte.d.ts +12 -0
- package/dist/ui/PermitOfferInbox.svelte.d.ts.map +1 -0
- package/dist/ui/account_sessions_state.svelte.d.ts +53 -3
- package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.js +39 -16
- package/dist/ui/admin_accounts_state.svelte.d.ts +118 -2
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +99 -23
- package/dist/ui/admin_invites_state.svelte.d.ts +47 -1
- package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_invites_state.svelte.js +38 -26
- package/dist/ui/admin_sessions_state.svelte.d.ts +26 -0
- package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_sessions_state.svelte.js +35 -21
- package/dist/ui/app_settings_state.svelte.d.ts +39 -0
- package/dist/ui/app_settings_state.svelte.d.ts.map +1 -1
- package/dist/ui/app_settings_state.svelte.js +34 -18
- package/dist/ui/audit_log_state.svelte.d.ts +40 -3
- package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
- package/dist/ui/audit_log_state.svelte.js +36 -42
- package/dist/ui/auth_state.svelte.d.ts +4 -3
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +4 -1
- package/dist/ui/permit_offers_state.svelte.d.ts +125 -0
- package/dist/ui/permit_offers_state.svelte.d.ts.map +1 -0
- package/dist/ui/permit_offers_state.svelte.js +197 -0
- package/package.json +3 -3
- package/dist/auth/admin_routes.d.ts +0 -29
- package/dist/auth/admin_routes.d.ts.map +0 -1
- package/dist/auth/admin_routes.js +0 -226
- package/dist/auth/app_settings_routes.d.ts +0 -27
- package/dist/auth/app_settings_routes.d.ts.map +0 -1
- package/dist/auth/app_settings_routes.js +0 -66
- package/dist/auth/invite_routes.d.ts +0 -18
- package/dist/auth/invite_routes.d.ts.map +0 -1
- package/dist/auth/invite_routes.js +0 -129
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permit offer RPC action handlers — the consentful-permits action surface.
|
|
3
|
+
*
|
|
4
|
+
* Seven actions: six offer-lifecycle methods (create / accept / decline /
|
|
5
|
+
* retract / list / history) plus `permit_revoke` (admin-only). All mount
|
|
6
|
+
* on a consumer's JSON-RPC endpoint via `create_rpc_endpoint`. The action
|
|
7
|
+
* specs themselves live in `./permit_offer_action_specs.js`. Mutations
|
|
8
|
+
* declare `side_effects: true` so the RPC dispatcher wraps the handler in
|
|
9
|
+
* a DB transaction; `permit_offer_list` and `permit_offer_history` declare
|
|
10
|
+
* `side_effects: false` so they are addressable via GET.
|
|
11
|
+
*
|
|
12
|
+
* Authorization:
|
|
13
|
+
* - `permit_offer_create` — the grantor must hold an active permit for the
|
|
14
|
+
* role being offered, and that role must be `web_grantable`. Consumers
|
|
15
|
+
* needing a richer policy (e.g., "teacher may offer student in *their*
|
|
16
|
+
* classroom") pass an `authorize` callback that overrides the default.
|
|
17
|
+
* - `permit_offer_accept` / `permit_offer_decline` — keyed to the caller's
|
|
18
|
+
* account; `query_*` helpers enforce the IDOR guard.
|
|
19
|
+
* - `permit_offer_retract` — keyed to the caller's actor.
|
|
20
|
+
* - `permit_offer_list` / `permit_offer_history` — self by default;
|
|
21
|
+
* `{account_id}` is admin-only.
|
|
22
|
+
* - `permit_revoke` — spec-level `auth: {role: 'admin'}`; the RPC
|
|
23
|
+
* dispatcher rejects non-admin callers before the handler runs.
|
|
24
|
+
* `web_grantable` gate prevents revoking keeper/daemon-scoped roles
|
|
25
|
+
* via this surface. Keys on `actor_id` to survive multi-actor accounts.
|
|
26
|
+
*
|
|
27
|
+
* Audit events are emitted in-transaction by the query layer (atomic with
|
|
28
|
+
* the permit write on accept/revoke) or by the handler via
|
|
29
|
+
* `audit_log_fire_and_forget` for single-event lifecycle transitions.
|
|
30
|
+
* `on_audit_event` (SSE broadcast) fires post-commit in both paths.
|
|
31
|
+
*
|
|
32
|
+
* WS notifications fan out post-commit via `emit_after_commit` when a
|
|
33
|
+
* `notification_sender` is wired: offer lifecycle transitions notify the
|
|
34
|
+
* counterparty, `permit_revoke` notifies the revokee plus each superseded
|
|
35
|
+
* pending offer's grantor.
|
|
36
|
+
*
|
|
37
|
+
* @module
|
|
38
|
+
*/
|
|
39
|
+
import { type ActionContext, type RpcAction } from '../actions/action_rpc.js';
|
|
40
|
+
import { type RoleSchemaResult } from './role_schema.js';
|
|
41
|
+
import { type RequestContext } from './request_context.js';
|
|
42
|
+
import type { RouteFactoryDeps } from './deps.js';
|
|
43
|
+
import { type NotificationSender } from './permit_offer_notifications.js';
|
|
44
|
+
/**
|
|
45
|
+
* Authorization callback for `permit_offer_create`. Returns `true` to allow,
|
|
46
|
+
* `false` to reject (handler converts to `forbidden`).
|
|
47
|
+
*
|
|
48
|
+
* Provided with the fully-resolved request context and the parsed input
|
|
49
|
+
* (pre-TTL, pre-normalization). Consumers override the default to implement
|
|
50
|
+
* policies like "teacher may offer classroom_student only in classrooms they
|
|
51
|
+
* teach".
|
|
52
|
+
*/
|
|
53
|
+
export type PermitOfferCreateAuthorize = (auth: RequestContext, input: {
|
|
54
|
+
to_account_id: string;
|
|
55
|
+
role: string;
|
|
56
|
+
scope_id: string | null;
|
|
57
|
+
}, deps: Pick<RouteFactoryDeps, 'log'>, ctx: ActionContext) => boolean | Promise<boolean>;
|
|
58
|
+
/** Options for `create_permit_offer_actions`. */
|
|
59
|
+
export interface PermitOfferActionOptions {
|
|
60
|
+
/**
|
|
61
|
+
* Role schema result from `create_role_schema()`. Defaults to builtin roles only.
|
|
62
|
+
* The `role_options` map is read for `web_grantable` lookups.
|
|
63
|
+
*/
|
|
64
|
+
roles?: RoleSchemaResult;
|
|
65
|
+
/** TTL applied to newly-created offers. Defaults to `PERMIT_OFFER_DEFAULT_TTL_MS`. */
|
|
66
|
+
default_ttl_ms?: number;
|
|
67
|
+
/**
|
|
68
|
+
* Custom authorization for `permit_offer_create`. The default requires the
|
|
69
|
+
* caller to hold an active permit for the offered role *and* the role to
|
|
70
|
+
* be `web_grantable`. Consumers with richer policies (scope-aware, chained
|
|
71
|
+
* roles) override this.
|
|
72
|
+
*/
|
|
73
|
+
authorize?: PermitOfferCreateAuthorize;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Dependencies for `create_permit_offer_actions`.
|
|
77
|
+
*
|
|
78
|
+
* `notification_sender` is optional — when absent, WS fan-out is silently
|
|
79
|
+
* skipped. Consumers wiring `BackendWebsocketTransport` assign its instance
|
|
80
|
+
* directly (the transport's `send_to_account` signature accepts the broader
|
|
81
|
+
* `JsonrpcMessageFromServerToClient`, which is contravariantly compatible).
|
|
82
|
+
*/
|
|
83
|
+
export interface PermitOfferActionDeps extends Pick<RouteFactoryDeps, 'log' | 'on_audit_event'> {
|
|
84
|
+
/** Optional WS fan-out primitive. `null` or absent → notifications skipped. */
|
|
85
|
+
notification_sender?: NotificationSender | null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Create the seven permit-offer RPC actions (six offer-lifecycle methods
|
|
89
|
+
* plus `permit_revoke`).
|
|
90
|
+
*
|
|
91
|
+
* @param deps - stateless capabilities; needs `log` and `on_audit_event`; optional `notification_sender` for WS fan-out
|
|
92
|
+
* @param options - role schema, default TTL, authorization override
|
|
93
|
+
* @returns the `RpcAction` array to spread into a `create_rpc_endpoint` call
|
|
94
|
+
*/
|
|
95
|
+
export declare const create_permit_offer_actions: (deps: PermitOfferActionDeps, options?: PermitOfferActionOptions) => Array<RpcAction>;
|
|
96
|
+
//# sourceMappingURL=permit_offer_actions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permit_offer_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/permit_offer_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAAa,KAAK,aAAa,EAAE,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAGxF,OAAO,EAAmC,KAAK,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAsBzF,OAAO,EAAW,KAAK,cAAc,EAAC,MAAM,sBAAsB,CAAC;AACnE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAChD,OAAO,EAON,KAAK,kBAAkB,EACvB,MAAM,iCAAiC,CAAC;AAmCzC;;;;;;;;GAQG;AACH,MAAM,MAAM,0BAA0B,GAAG,CACxC,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAC,EACrE,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,EACnC,GAAG,EAAE,aAAa,KACd,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,iDAAiD;AACjD,MAAM,WAAW,wBAAwB;IACxC;;;OAGG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,sFAAsF;IACtF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,0BAA0B,CAAC;CACvC;AAqCD;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,gBAAgB,CAAC;IAC9F,+EAA+E;IAC/E,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CAChD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,qBAAqB,EAC3B,UAAS,wBAA6B,KACpC,KAAK,CAAC,SAAS,CA2djB,CAAC"}
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permit offer RPC action handlers — the consentful-permits action surface.
|
|
3
|
+
*
|
|
4
|
+
* Seven actions: six offer-lifecycle methods (create / accept / decline /
|
|
5
|
+
* retract / list / history) plus `permit_revoke` (admin-only). All mount
|
|
6
|
+
* on a consumer's JSON-RPC endpoint via `create_rpc_endpoint`. The action
|
|
7
|
+
* specs themselves live in `./permit_offer_action_specs.js`. Mutations
|
|
8
|
+
* declare `side_effects: true` so the RPC dispatcher wraps the handler in
|
|
9
|
+
* a DB transaction; `permit_offer_list` and `permit_offer_history` declare
|
|
10
|
+
* `side_effects: false` so they are addressable via GET.
|
|
11
|
+
*
|
|
12
|
+
* Authorization:
|
|
13
|
+
* - `permit_offer_create` — the grantor must hold an active permit for the
|
|
14
|
+
* role being offered, and that role must be `web_grantable`. Consumers
|
|
15
|
+
* needing a richer policy (e.g., "teacher may offer student in *their*
|
|
16
|
+
* classroom") pass an `authorize` callback that overrides the default.
|
|
17
|
+
* - `permit_offer_accept` / `permit_offer_decline` — keyed to the caller's
|
|
18
|
+
* account; `query_*` helpers enforce the IDOR guard.
|
|
19
|
+
* - `permit_offer_retract` — keyed to the caller's actor.
|
|
20
|
+
* - `permit_offer_list` / `permit_offer_history` — self by default;
|
|
21
|
+
* `{account_id}` is admin-only.
|
|
22
|
+
* - `permit_revoke` — spec-level `auth: {role: 'admin'}`; the RPC
|
|
23
|
+
* dispatcher rejects non-admin callers before the handler runs.
|
|
24
|
+
* `web_grantable` gate prevents revoking keeper/daemon-scoped roles
|
|
25
|
+
* via this surface. Keys on `actor_id` to survive multi-actor accounts.
|
|
26
|
+
*
|
|
27
|
+
* Audit events are emitted in-transaction by the query layer (atomic with
|
|
28
|
+
* the permit write on accept/revoke) or by the handler via
|
|
29
|
+
* `audit_log_fire_and_forget` for single-event lifecycle transitions.
|
|
30
|
+
* `on_audit_event` (SSE broadcast) fires post-commit in both paths.
|
|
31
|
+
*
|
|
32
|
+
* WS notifications fan out post-commit via `emit_after_commit` when a
|
|
33
|
+
* `notification_sender` is wired: offer lifecycle transitions notify the
|
|
34
|
+
* counterparty, `permit_revoke` notifies the revokee plus each superseded
|
|
35
|
+
* pending offer's grantor.
|
|
36
|
+
*
|
|
37
|
+
* @module
|
|
38
|
+
*/
|
|
39
|
+
import { rpc_action } from '../actions/action_rpc.js';
|
|
40
|
+
import { jsonrpc_errors } from '../http/jsonrpc_errors.js';
|
|
41
|
+
import { emit_after_commit } from '../http/pending_effects.js';
|
|
42
|
+
import { BUILTIN_ROLE_OPTIONS, ROLE_ADMIN } from './role_schema.js';
|
|
43
|
+
import { PERMIT_OFFER_DEFAULT_TTL_MS, to_permit_offer_json } from './permit_offer_schema.js';
|
|
44
|
+
import { query_permit_offer_create, query_permit_offer_decline, query_permit_offer_retract, query_permit_offer_list, query_permit_offer_history_for_account, query_accept_offer, PermitOfferAlreadyTerminalError, PermitOfferExpiredError, PermitOfferNotFoundError, PermitOfferSelfTargetError, } from './permit_offer_queries.js';
|
|
45
|
+
import { query_permit_find_active_role_for_actor, query_permit_has_role, query_revoke_permit, } from './permit_queries.js';
|
|
46
|
+
import { query_actor_by_id } from './account_queries.js';
|
|
47
|
+
import { audit_log_fire_and_forget } from './audit_log_queries.js';
|
|
48
|
+
import { has_role } from './request_context.js';
|
|
49
|
+
import { build_permit_offer_accepted_notification, build_permit_offer_declined_notification, build_permit_offer_received_notification, build_permit_offer_retracted_notification, build_permit_offer_supersede_notification, build_permit_revoke_notification, } from './permit_offer_notifications.js';
|
|
50
|
+
import { ERROR_ACCOUNT_NOT_FOUND, ERROR_PERMIT_NOT_FOUND, ERROR_ROLE_NOT_WEB_GRANTABLE, } from '../http/error_schemas.js';
|
|
51
|
+
import { 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';
|
|
52
|
+
// -- Helpers ----------------------------------------------------------------
|
|
53
|
+
/** Fire `on_audit_event` for each event — used by accept, whose events were written in-transaction. */
|
|
54
|
+
const fan_out_audit_events = (events, on_audit_event, log) => {
|
|
55
|
+
for (const event of events) {
|
|
56
|
+
try {
|
|
57
|
+
on_audit_event(event);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
log.error('on_audit_event callback failed:', err);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const default_authorize = async (auth, input, _deps, ctx) => {
|
|
65
|
+
// Caller must hold an active permit for the offered role. Global (no scope)
|
|
66
|
+
// check — the scope-aware "only in this classroom" policy is consumer-level.
|
|
67
|
+
return query_permit_has_role(ctx, auth.actor.id, input.role);
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Narrow `ctx.auth` to non-null. The RPC dispatcher has already enforced
|
|
71
|
+
* `auth: 'authenticated'` before the handler runs — this is a type narrow,
|
|
72
|
+
* not a runtime check that would otherwise fail.
|
|
73
|
+
*/
|
|
74
|
+
const require_request_auth = (auth) => {
|
|
75
|
+
if (!auth)
|
|
76
|
+
throw new Error('unreachable: action auth guard did not enforce authentication');
|
|
77
|
+
return auth;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Create the seven permit-offer RPC actions (six offer-lifecycle methods
|
|
81
|
+
* plus `permit_revoke`).
|
|
82
|
+
*
|
|
83
|
+
* @param deps - stateless capabilities; needs `log` and `on_audit_event`; optional `notification_sender` for WS fan-out
|
|
84
|
+
* @param options - role schema, default TTL, authorization override
|
|
85
|
+
* @returns the `RpcAction` array to spread into a `create_rpc_endpoint` call
|
|
86
|
+
*/
|
|
87
|
+
export const create_permit_offer_actions = (deps, options = {}) => {
|
|
88
|
+
const { on_audit_event, log, notification_sender = null } = deps;
|
|
89
|
+
const role_options = options.roles?.role_options ?? BUILTIN_ROLE_OPTIONS;
|
|
90
|
+
const default_ttl_ms = options.default_ttl_ms ?? PERMIT_OFFER_DEFAULT_TTL_MS;
|
|
91
|
+
const authorize = options.authorize ?? default_authorize;
|
|
92
|
+
// Three denial paths (web_grantable, authorize, self-target) all emit the
|
|
93
|
+
// same failure-outcome audit event. Local closure over `log` + `on_audit_event`.
|
|
94
|
+
const emit_create_failure_audit = (ctx, auth, input) => {
|
|
95
|
+
void audit_log_fire_and_forget(ctx, {
|
|
96
|
+
event_type: 'permit_offer_create',
|
|
97
|
+
outcome: 'failure',
|
|
98
|
+
actor_id: auth.actor.id,
|
|
99
|
+
account_id: auth.account.id,
|
|
100
|
+
target_account_id: input.to_account_id,
|
|
101
|
+
ip: ctx.client_ip,
|
|
102
|
+
metadata: {
|
|
103
|
+
role: input.role,
|
|
104
|
+
scope_id: input.scope_id ?? null,
|
|
105
|
+
to_account_id: input.to_account_id,
|
|
106
|
+
},
|
|
107
|
+
}, log, on_audit_event);
|
|
108
|
+
};
|
|
109
|
+
const create_handler = async (input, ctx) => {
|
|
110
|
+
const auth = require_request_auth(ctx.auth);
|
|
111
|
+
// Role must be web_grantable — same gate as admin direct-grant.
|
|
112
|
+
const rc = role_options.get(input.role);
|
|
113
|
+
if (!rc?.web_grantable) {
|
|
114
|
+
emit_create_failure_audit(ctx, auth, input);
|
|
115
|
+
throw jsonrpc_errors.forbidden('role not grantable', {
|
|
116
|
+
reason: ERROR_OFFER_ROLE_NOT_GRANTABLE,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const authorized = await authorize(auth, {
|
|
120
|
+
to_account_id: input.to_account_id,
|
|
121
|
+
role: input.role,
|
|
122
|
+
scope_id: input.scope_id ?? null,
|
|
123
|
+
}, { log }, ctx);
|
|
124
|
+
if (!authorized) {
|
|
125
|
+
emit_create_failure_audit(ctx, auth, input);
|
|
126
|
+
throw jsonrpc_errors.forbidden('not authorized to offer this role', {
|
|
127
|
+
reason: ERROR_OFFER_NOT_AUTHORIZED,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
let offer;
|
|
131
|
+
try {
|
|
132
|
+
offer = await query_permit_offer_create(ctx, {
|
|
133
|
+
from_actor_id: auth.actor.id,
|
|
134
|
+
to_account_id: input.to_account_id,
|
|
135
|
+
role: input.role,
|
|
136
|
+
scope_id: input.scope_id ?? null,
|
|
137
|
+
message: input.message ?? null,
|
|
138
|
+
expires_at: new Date(Date.now() + default_ttl_ms),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
if (err instanceof PermitOfferSelfTargetError) {
|
|
143
|
+
emit_create_failure_audit(ctx, auth, input);
|
|
144
|
+
throw jsonrpc_errors.invalid_params('cannot offer to self', {
|
|
145
|
+
reason: ERROR_OFFER_SELF_TARGET,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
void audit_log_fire_and_forget(ctx, {
|
|
151
|
+
event_type: 'permit_offer_create',
|
|
152
|
+
actor_id: auth.actor.id,
|
|
153
|
+
account_id: auth.account.id,
|
|
154
|
+
target_account_id: input.to_account_id,
|
|
155
|
+
ip: ctx.client_ip,
|
|
156
|
+
metadata: {
|
|
157
|
+
offer_id: offer.id,
|
|
158
|
+
role: offer.role,
|
|
159
|
+
scope_id: offer.scope_id,
|
|
160
|
+
to_account_id: offer.to_account_id,
|
|
161
|
+
},
|
|
162
|
+
}, log, on_audit_event);
|
|
163
|
+
const offer_json = to_permit_offer_json(offer);
|
|
164
|
+
if (notification_sender) {
|
|
165
|
+
emit_after_commit(ctx, () => {
|
|
166
|
+
notification_sender.send_to_account(offer.to_account_id, build_permit_offer_received_notification({ offer: offer_json }));
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return { offer: offer_json };
|
|
170
|
+
};
|
|
171
|
+
const accept_handler = async (input, ctx) => {
|
|
172
|
+
const auth = require_request_auth(ctx.auth);
|
|
173
|
+
let result;
|
|
174
|
+
try {
|
|
175
|
+
result = await query_accept_offer(ctx, {
|
|
176
|
+
offer_id: input.offer_id,
|
|
177
|
+
to_account_id: auth.account.id,
|
|
178
|
+
ip: ctx.client_ip,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
if (err instanceof PermitOfferNotFoundError) {
|
|
183
|
+
throw jsonrpc_errors.not_found('offer', { reason: ERROR_OFFER_NOT_FOUND });
|
|
184
|
+
}
|
|
185
|
+
if (err instanceof PermitOfferAlreadyTerminalError) {
|
|
186
|
+
throw jsonrpc_errors.invalid_request({ reason: ERROR_OFFER_TERMINAL });
|
|
187
|
+
}
|
|
188
|
+
if (err instanceof PermitOfferExpiredError) {
|
|
189
|
+
throw jsonrpc_errors.invalid_request({ reason: ERROR_OFFER_EXPIRED });
|
|
190
|
+
}
|
|
191
|
+
throw err;
|
|
192
|
+
}
|
|
193
|
+
// Look up the grantor's account_id inside the transaction so the
|
|
194
|
+
// post-commit notification has a valid target. One cheap SELECT by
|
|
195
|
+
// PK — the alternative (widening `query_accept_offer` again) would
|
|
196
|
+
// bleed transport concerns into the query layer.
|
|
197
|
+
const grantor_actor = notification_sender
|
|
198
|
+
? await query_actor_by_id(ctx, result.offer.from_actor_id)
|
|
199
|
+
: null;
|
|
200
|
+
const grantor_account_id = grantor_actor?.account_id ?? null;
|
|
201
|
+
const offer_json = to_permit_offer_json(result.offer);
|
|
202
|
+
const supersede_payloads = result.superseded_offers.map((sib) => ({
|
|
203
|
+
offer: to_permit_offer_json(sib),
|
|
204
|
+
from_account_id: sib.from_account_id,
|
|
205
|
+
}));
|
|
206
|
+
// Audit events are written in-transaction by query_accept_offer; wire
|
|
207
|
+
// them through on_audit_event post-commit so SSE broadcasts fire.
|
|
208
|
+
// WS notifications piggyback on the same post-commit microtask so the
|
|
209
|
+
// grantor sees "accepted" and each superseded grantor sees
|
|
210
|
+
// "supersede" only once the accept has durably committed.
|
|
211
|
+
emit_after_commit(ctx, () => {
|
|
212
|
+
fan_out_audit_events(result.audit_events, on_audit_event, ctx.log);
|
|
213
|
+
if (notification_sender && grantor_account_id) {
|
|
214
|
+
notification_sender.send_to_account(grantor_account_id, build_permit_offer_accepted_notification({ offer: offer_json }));
|
|
215
|
+
}
|
|
216
|
+
if (notification_sender) {
|
|
217
|
+
for (const sib of supersede_payloads) {
|
|
218
|
+
notification_sender.send_to_account(sib.from_account_id, build_permit_offer_supersede_notification({
|
|
219
|
+
offer: sib.offer,
|
|
220
|
+
reason: 'sibling_accepted',
|
|
221
|
+
cause_id: result.offer.id,
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
permit_id: result.permit.id,
|
|
228
|
+
offer: offer_json,
|
|
229
|
+
superseded_offer_ids: result.superseded_offers.map((o) => o.id),
|
|
230
|
+
};
|
|
231
|
+
};
|
|
232
|
+
const decline_handler = async (input, ctx) => {
|
|
233
|
+
const auth = require_request_auth(ctx.auth);
|
|
234
|
+
let declined;
|
|
235
|
+
try {
|
|
236
|
+
declined = await query_permit_offer_decline(ctx, input.offer_id, auth.account.id, input.reason ?? null);
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
if (err instanceof PermitOfferAlreadyTerminalError) {
|
|
240
|
+
throw jsonrpc_errors.invalid_request({ reason: ERROR_OFFER_TERMINAL });
|
|
241
|
+
}
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
if (!declined) {
|
|
245
|
+
throw jsonrpc_errors.not_found('offer', { reason: ERROR_OFFER_NOT_FOUND });
|
|
246
|
+
}
|
|
247
|
+
void audit_log_fire_and_forget(ctx, {
|
|
248
|
+
event_type: 'permit_offer_decline',
|
|
249
|
+
actor_id: auth.actor.id,
|
|
250
|
+
account_id: auth.account.id,
|
|
251
|
+
ip: ctx.client_ip,
|
|
252
|
+
metadata: {
|
|
253
|
+
offer_id: declined.id,
|
|
254
|
+
role: declined.role,
|
|
255
|
+
scope_id: declined.scope_id,
|
|
256
|
+
reason: input.reason ?? undefined,
|
|
257
|
+
},
|
|
258
|
+
}, log, on_audit_event);
|
|
259
|
+
if (notification_sender) {
|
|
260
|
+
// Look up the grantor's account (SELECT by PK, same tx) for the
|
|
261
|
+
// notification target. The decline reason rides along on
|
|
262
|
+
// `offer.decline_reason` — the DB set it in the RETURNING above.
|
|
263
|
+
const grantor_actor = await query_actor_by_id(ctx, declined.from_actor_id);
|
|
264
|
+
const grantor_account_id = grantor_actor?.account_id ?? null;
|
|
265
|
+
if (grantor_account_id) {
|
|
266
|
+
const offer_json = to_permit_offer_json(declined);
|
|
267
|
+
emit_after_commit(ctx, () => {
|
|
268
|
+
notification_sender.send_to_account(grantor_account_id, build_permit_offer_declined_notification({ offer: offer_json }));
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return { ok: true };
|
|
273
|
+
};
|
|
274
|
+
const retract_handler = async (input, ctx) => {
|
|
275
|
+
const auth = require_request_auth(ctx.auth);
|
|
276
|
+
let retracted;
|
|
277
|
+
try {
|
|
278
|
+
retracted = await query_permit_offer_retract(ctx, input.offer_id, auth.actor.id);
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
if (err instanceof PermitOfferAlreadyTerminalError) {
|
|
282
|
+
throw jsonrpc_errors.invalid_request({ reason: ERROR_OFFER_TERMINAL });
|
|
283
|
+
}
|
|
284
|
+
throw err;
|
|
285
|
+
}
|
|
286
|
+
if (!retracted) {
|
|
287
|
+
throw jsonrpc_errors.not_found('offer', { reason: ERROR_OFFER_NOT_FOUND });
|
|
288
|
+
}
|
|
289
|
+
void audit_log_fire_and_forget(ctx, {
|
|
290
|
+
event_type: 'permit_offer_retract',
|
|
291
|
+
actor_id: auth.actor.id,
|
|
292
|
+
account_id: auth.account.id,
|
|
293
|
+
ip: ctx.client_ip,
|
|
294
|
+
metadata: {
|
|
295
|
+
offer_id: retracted.id,
|
|
296
|
+
role: retracted.role,
|
|
297
|
+
scope_id: retracted.scope_id,
|
|
298
|
+
},
|
|
299
|
+
}, log, on_audit_event);
|
|
300
|
+
if (notification_sender) {
|
|
301
|
+
const offer_json = to_permit_offer_json(retracted);
|
|
302
|
+
emit_after_commit(ctx, () => {
|
|
303
|
+
notification_sender.send_to_account(retracted.to_account_id, build_permit_offer_retracted_notification({ offer: offer_json }));
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return { ok: true };
|
|
307
|
+
};
|
|
308
|
+
const list_handler = async (input, ctx) => {
|
|
309
|
+
const auth = require_request_auth(ctx.auth);
|
|
310
|
+
const target = input.account_id ?? auth.account.id;
|
|
311
|
+
if (target !== auth.account.id && !has_role(auth, ROLE_ADMIN)) {
|
|
312
|
+
throw jsonrpc_errors.forbidden('admin required to inspect another account');
|
|
313
|
+
}
|
|
314
|
+
const offers = await query_permit_offer_list(ctx, target);
|
|
315
|
+
return { offers: offers.map(to_permit_offer_json) };
|
|
316
|
+
};
|
|
317
|
+
const history_handler = async (input, ctx) => {
|
|
318
|
+
const auth = require_request_auth(ctx.auth);
|
|
319
|
+
const target = input.account_id ?? auth.account.id;
|
|
320
|
+
if (target !== auth.account.id && !has_role(auth, ROLE_ADMIN)) {
|
|
321
|
+
throw jsonrpc_errors.forbidden('admin required to inspect another account');
|
|
322
|
+
}
|
|
323
|
+
const offers = await query_permit_offer_history_for_account(ctx, target, input.limit ?? undefined, input.offset ?? undefined);
|
|
324
|
+
return { offers: offers.map(to_permit_offer_json) };
|
|
325
|
+
};
|
|
326
|
+
const revoke_handler = async (input, ctx) => {
|
|
327
|
+
const auth = require_request_auth(ctx.auth);
|
|
328
|
+
// IDOR guard + role lookup. One SELECT — returns null when the
|
|
329
|
+
// permit is revoked, missing, or belongs to a different actor.
|
|
330
|
+
const permit_row = await query_permit_find_active_role_for_actor(ctx, input.permit_id, input.actor_id);
|
|
331
|
+
if (!permit_row) {
|
|
332
|
+
throw jsonrpc_errors.not_found('permit', { reason: ERROR_PERMIT_NOT_FOUND });
|
|
333
|
+
}
|
|
334
|
+
// Resolve the target actor's account once — drives both the audit
|
|
335
|
+
// `target_account_id` and the post-commit notification target.
|
|
336
|
+
const target_actor = await query_actor_by_id(ctx, input.actor_id);
|
|
337
|
+
if (!target_actor) {
|
|
338
|
+
// The IDOR guard above already matched, so a missing actor here
|
|
339
|
+
// indicates a race (account deleted between the two SELECTs).
|
|
340
|
+
// Treat as account-not-found for the caller.
|
|
341
|
+
throw jsonrpc_errors.not_found('account', { reason: ERROR_ACCOUNT_NOT_FOUND });
|
|
342
|
+
}
|
|
343
|
+
const target_account_id = target_actor.account_id;
|
|
344
|
+
// web_grantable gate — keeper/daemon-scoped roles stay CLI-only.
|
|
345
|
+
const rc = role_options.get(permit_row.role);
|
|
346
|
+
if (!rc?.web_grantable) {
|
|
347
|
+
void audit_log_fire_and_forget(ctx, {
|
|
348
|
+
event_type: 'permit_revoke',
|
|
349
|
+
outcome: 'failure',
|
|
350
|
+
actor_id: auth.actor.id,
|
|
351
|
+
account_id: auth.account.id,
|
|
352
|
+
target_account_id,
|
|
353
|
+
ip: ctx.client_ip,
|
|
354
|
+
metadata: { role: permit_row.role, permit_id: input.permit_id },
|
|
355
|
+
}, log, on_audit_event);
|
|
356
|
+
throw jsonrpc_errors.forbidden('role not web-grantable', {
|
|
357
|
+
reason: ERROR_ROLE_NOT_WEB_GRANTABLE,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
const result = await query_revoke_permit(ctx, input.permit_id, input.actor_id, auth.actor.id, input.reason ?? null);
|
|
361
|
+
if (!result) {
|
|
362
|
+
// Raced with another revoker or the permit was revoked between
|
|
363
|
+
// the IDOR check and the UPDATE.
|
|
364
|
+
throw jsonrpc_errors.not_found('permit', { reason: ERROR_PERMIT_NOT_FOUND });
|
|
365
|
+
}
|
|
366
|
+
void audit_log_fire_and_forget(ctx, {
|
|
367
|
+
event_type: 'permit_revoke',
|
|
368
|
+
actor_id: auth.actor.id,
|
|
369
|
+
account_id: auth.account.id,
|
|
370
|
+
target_account_id,
|
|
371
|
+
ip: ctx.client_ip,
|
|
372
|
+
metadata: {
|
|
373
|
+
role: result.role,
|
|
374
|
+
permit_id: result.id,
|
|
375
|
+
scope_id: result.scope_id,
|
|
376
|
+
reason: input.reason ?? undefined,
|
|
377
|
+
},
|
|
378
|
+
}, log, on_audit_event);
|
|
379
|
+
for (const offer of result.superseded_offers) {
|
|
380
|
+
void audit_log_fire_and_forget(ctx, {
|
|
381
|
+
event_type: 'permit_offer_supersede',
|
|
382
|
+
actor_id: auth.actor.id,
|
|
383
|
+
account_id: offer.to_account_id,
|
|
384
|
+
ip: ctx.client_ip,
|
|
385
|
+
metadata: {
|
|
386
|
+
offer_id: offer.id,
|
|
387
|
+
role: offer.role,
|
|
388
|
+
scope_id: offer.scope_id,
|
|
389
|
+
reason: 'permit_revoked',
|
|
390
|
+
cause_id: result.id,
|
|
391
|
+
},
|
|
392
|
+
}, log, on_audit_event);
|
|
393
|
+
}
|
|
394
|
+
if (notification_sender) {
|
|
395
|
+
const superseded = result.superseded_offers.map((o) => ({
|
|
396
|
+
offer: to_permit_offer_json(o),
|
|
397
|
+
from_account_id: o.from_account_id,
|
|
398
|
+
}));
|
|
399
|
+
const cause_id = result.id;
|
|
400
|
+
const reason = input.reason ?? null;
|
|
401
|
+
emit_after_commit(ctx, () => {
|
|
402
|
+
notification_sender.send_to_account(target_account_id, build_permit_revoke_notification({
|
|
403
|
+
permit_id: result.id,
|
|
404
|
+
role: result.role,
|
|
405
|
+
scope_id: result.scope_id,
|
|
406
|
+
reason,
|
|
407
|
+
}));
|
|
408
|
+
for (const sib of superseded) {
|
|
409
|
+
notification_sender.send_to_account(sib.from_account_id, build_permit_offer_supersede_notification({
|
|
410
|
+
offer: sib.offer,
|
|
411
|
+
reason: 'permit_revoked',
|
|
412
|
+
cause_id,
|
|
413
|
+
}));
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
return { ok: true, revoked: true };
|
|
418
|
+
};
|
|
419
|
+
return [
|
|
420
|
+
rpc_action(permit_offer_create_action_spec, create_handler),
|
|
421
|
+
rpc_action(permit_offer_accept_action_spec, accept_handler),
|
|
422
|
+
rpc_action(permit_offer_decline_action_spec, decline_handler),
|
|
423
|
+
rpc_action(permit_offer_retract_action_spec, retract_handler),
|
|
424
|
+
rpc_action(permit_offer_list_action_spec, list_handler),
|
|
425
|
+
rpc_action(permit_offer_history_action_spec, history_handler),
|
|
426
|
+
rpc_action(permit_revoke_action_spec, revoke_handler),
|
|
427
|
+
];
|
|
428
|
+
};
|