auth/role_grant_offer_action_specs.ts

Role grant offer RPC action specs — declarative contract for the consentful-role-grants surface (offer lifecycle + admin revoke + the immediate admin-only role_grant_assign).

Import this module for the specs, Input/Output schemas, ERROR_ROLE_GRANT_OFFER_* reason constants, and the all_role_grant_offer_action_specs registry. Handlers live in auth/role_grant_offer_actions.ts.

Authorization enforcement: offer-lifecycle specs declare account+actor required (no roles) and rely on query_* IDOR guards or in-handler policy checks (e.g. role_grant_offer_list/_history elevate to admin only when inspecting another account — an input-dependent check that can't be expressed at the spec level). role_grant_revoke and role_grant_assign add roles: ['admin'] — the RPC dispatcher's per-spec post-authorization auth gate (check_action_auth_post_authorization) rejects non-admin callers before the handler runs even though the endpoint hosts non-admin methods alongside.

view source

Declarations
#

35 declarations

all_role_grant_offer_action_specs
#

auth/role_grant_offer_action_specs.ts view source

{ method: string; initiator: "frontend" | "backend" | "both"; side_effects: boolean; input: ZodType<unknown, unknown, $ZodTypeInternals<unknown, unknown>>; output: ZodType<unknown, unknown, $ZodTypeInternals<unknown, unknown>>; ... 6 more ...; rate_limit?: "both" | ... 2 more ... | undefined; }[]

type RequestResponseActionSpec[]

import {all_role_grant_offer_action_specs} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

All role-grant-offer action specs — a codegen-ready registry. Consumers spread this into their own action-spec array to include offer lifecycle + revoke + assign methods in a typed client surface.

ERROR_ROLE_GRANT_BUILTIN_SCOPED
#

auth/role_grant_offer_action_specs.ts view source

"role_grant_builtin_scoped" import {ERROR_ROLE_GRANT_BUILTIN_SCOPED} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Error reason — a grant path was asked to bind a builtin role to a scope (builtin roles are global-only, see is_builtin_role). Refused by role_grant_assign, role_grant_offer_create, and role_grant_offer_accept.

ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH
#

auth/role_grant_offer_action_specs.ts view source

"role_grant_offer_actor_account_mismatch" import {ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Error reason — role_grant_offer_create was called with a to_actor_id that does not belong to to_account_id.

ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH
#

auth/role_grant_offer_action_specs.ts view source

"role_grant_offer_actor_mismatch" import {ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Error reason — actor-targeted offer was accepted by an actor other than to_actor_id.

ERROR_ROLE_GRANT_OFFER_EXPIRED
#

ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED
#

auth/role_grant_offer_action_specs.ts view source

"role_grant_offer_not_authorized" import {ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Error reason — caller is not authorized to offer this role (default policy: caller lacks the role; consumer authorize callback may add further policy).

ERROR_ROLE_GRANT_OFFER_NOT_FOUND
#

auth/role_grant_offer_action_specs.ts view source

"role_grant_offer_not_found" import {ERROR_ROLE_GRANT_OFFER_NOT_FOUND} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Error reason — offer does not exist or belongs to a different recipient (404-over-403 IDOR mask).

ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE
#

auth/role_grant_offer_action_specs.ts view source

"role_grant_offer_role_not_grantable" import {ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Error reason — the offered role does not include 'admin' in its RoleSpec.grant_paths (nobody may offer it via this surface).

ERROR_ROLE_GRANT_OFFER_SELF_TARGET
#

auth/role_grant_offer_action_specs.ts view source

"role_grant_offer_self_target" import {ERROR_ROLE_GRANT_OFFER_SELF_TARGET} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Error reason — caller tried to offer themselves a role_grant.

ERROR_ROLE_GRANT_OFFER_TERMINAL
#

auth/role_grant_offer_action_specs.ts view source

"role_grant_offer_terminal" import {ERROR_ROLE_GRANT_OFFER_TERMINAL} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Error reason — offer is declined, retracted, or superseded.

GRANT_SCOPE_PAIRED_MESSAGE
#

auth/role_grant_offer_action_specs.ts view source

"'scope_kind' and 'scope_id' must be given together (both for a scoped grant, neither for a global one)" import {GRANT_SCOPE_PAIRED_MESSAGE} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

The message a half-scoped grant input fails with — the grant inputs' refine, and the UI's pre-RPC guard.

is_grant_scope_paired
#

auth/role_grant_offer_action_specs.ts view source

(input: { scope_kind?: string | null | undefined; scope_id?: string | null | undefined; }): boolean import {is_grant_scope_paired} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Whether a grant input's scope_kind + scope_id are paired — both absent (a global grant) or both present (a scoped one), the role_grant_scope_kind_paired / role_grant_offer_scope_kind_paired CHECKs. The grant inputs refine on it so a half-scoped call is a -32602 rather than a constraint-violation 500.

input

the grant input's scope pair

type { scope_kind?: string | null | undefined; scope_id?: string | null | undefined; }

returns

boolean

true when both are set or both are absent

role_grant_assign_action_spec
#

auth/role_grant_offer_action_specs.ts view source

{ method: string; kind: "request_response"; initiator: "frontend"; auth: { account: "required"; actor: "required"; roles: string[]; }; side_effects: true; input: ZodObject<{ to_account_id: $ZodBranded<ZodUUID, "Uuid", "out">; ... 4 more ...; acting: ZodOptional<...>; }, $strict>; ... 4 more ...; rate_limit: "account... import {role_grant_assign_action_spec} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

rate_limit: 'account' bounds admin-side burn of role_grant_assign — the action is admin-gated and audit-trailed, but the per-account cap matches role_grant_revoke so a single admin script can't churn grants in a loop.

role_grant_offer_accept_action_spec
#

auth/role_grant_offer_action_specs.ts view source

{ method: string; kind: "request_response"; initiator: "frontend"; auth: { account: "required"; actor: "required"; credential_types: string[]; }; side_effects: true; input: ZodObject<{ offer_id: $ZodBranded<ZodUUID, "Uuid", "out">; acting: ZodOptional<...>; }, $strict>; output: ZodObject<...>; async: true; descripti... import {role_grant_offer_accept_action_spec} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

credential_types: ['session'] — see docs/security.md §Credential-channel gating. The only offer verb that moves the *caller's own* authority: accepting a pending admin offer over a leaked API token is a privilege pivot in the same family the credential-lifecycle mutations are gated to prevent (the token is account-wide and non-expiring, and the resulting role_grant outlives revoking it). Consent inherently belongs to a human at a browser, and no automation accepts an offer on its own behalf. The sibling verbs stay ungated deliberately — _create is the offering side, and _decline / _retract destroy a pending offer and confer nothing.

role_grant_offer_create_action_spec
#

auth/role_grant_offer_action_specs.ts view source

{ method: string; kind: "request_response"; initiator: "frontend"; auth: { account: "required"; actor: "required"; }; side_effects: true; input: ZodObject<{ to_account_id: $ZodBranded<ZodUUID, "Uuid", "out">; ... 5 more ...; acting: ZodOptional<...>; }, $strict>; ... 4 more ...; rate_limit: "account"; } import {role_grant_offer_create_action_spec} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

rate_limit: 'account' throttles offer-spam at the authenticated grantor and bounds the account-existence oracle on to_account_id — the same shape as invite_create_action_spec upstream addresses, where a hostile authed caller iterates recipients to probe ERROR_ACCOUNT_NOT_FOUND (and the actor-binding via ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH) as an enumeration vector. Failure-outcome audit rows preserve the forensic trail; the rate cap closes the budget.

role_grant_offer_decline_action_spec
#

auth/role_grant_offer_action_specs.ts view source

{ method: string; kind: "request_response"; initiator: "frontend"; auth: { account: "required"; actor: "required"; }; side_effects: true; input: ZodObject<{ offer_id: $ZodBranded<ZodUUID, "Uuid", "out">; reason: ZodOptional<...>; acting: ZodOptional<...>; }, $strict>; output: ZodObject<...>; async: true; description... import {role_grant_offer_decline_action_spec} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

role_grant_offer_history_action_spec
#

auth/role_grant_offer_action_specs.ts view source

{ method: string; kind: "request_response"; initiator: "frontend"; auth: { account: "required"; actor: "required"; }; side_effects: false; input: ZodDefault<ZodObject<{ account_id: ZodOptional<ZodNullable<$ZodBranded<ZodUUID, "Uuid", "out">>>; limit: ZodOptional<...>; offset: ZodOptional<...>; acting: ZodOptional<..... import {role_grant_offer_history_action_spec} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

role_grant_offer_list_action_spec
#

auth/role_grant_offer_action_specs.ts view source

{ method: string; kind: "request_response"; initiator: "frontend"; auth: { account: "required"; actor: "required"; }; side_effects: false; input: ZodDefault<ZodObject<{ account_id: ZodOptional<ZodNullable<$ZodBranded<ZodUUID, "Uuid", "out">>>; acting: ZodOptional<...>; }, $strict>>; output: ZodObject<...>; async: tr... import {role_grant_offer_list_action_spec} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

role_grant_offer_retract_action_spec
#

auth/role_grant_offer_action_specs.ts view source

{ method: string; kind: "request_response"; initiator: "frontend"; auth: { account: "required"; actor: "required"; }; side_effects: true; input: ZodObject<{ offer_id: $ZodBranded<ZodUUID, "Uuid", "out">; acting: ZodOptional<...>; }, $strict>; output: ZodObject<...>; async: true; description: string; error_reasons: (... import {role_grant_offer_retract_action_spec} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

role_grant_revoke_action_spec
#

auth/role_grant_offer_action_specs.ts view source

{ method: string; kind: "request_response"; initiator: "frontend"; auth: { account: "required"; actor: "required"; roles: string[]; }; side_effects: true; input: ZodObject<{ actor_id: $ZodBranded<ZodUUID, "Uuid", "out">; role_grant_id: $ZodBranded<...>; reason: ZodOptional<...>; acting: ZodOptional<...>; }, $strict>... import {role_grant_revoke_action_spec} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

rate_limit: 'account' bounds admin-side burn of role_grant_revoke — the action is admin-gated and audit-trailed, but the per-account cap keeps a single admin script from churning role_grants in a loop and obscuring audit context for unrelated activity.

RoleGrantAssignInput
#

auth/role_grant_offer_action_specs.ts view source

value + type

{ to_account_id: string & $brand<"Uuid">; role: string; to_actor_id?: (string & $brand<"Uuid">) | null | undefined; scope_kind?: string | null | undefined; scope_id?: (string & $brand<...>) | ... 1 more ... | undefined; acting?: (string & $brand<...>) | undefined; } import {RoleGrantAssignInput} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Input for role_grant_assign — the immediate admin-only conferral path, the consent-free sibling of role_grant_offer_create. An admin assigns a role_grant straight onto the target actor; there is no offer for a grantee to accept. No message (no offer to carry it on). scope_kind pairs with scope_id exactly as on role_grant_offer_create — both or neither — and the grant row carries the pair.

to_account_id

type string & $brand<"Uuid">

role

type string

to_actor_id?

type (string & $brand<"Uuid">) | null

scope_kind?

type string | null

scope_id?

type (string & $brand<"Uuid">) | null

acting?

type string & $brand<"Uuid">

RoleGrantAssignOutput
#

auth/role_grant_offer_action_specs.ts view source

value + type

{ ok: true; role_grant_id: string & $brand<"Uuid">; } import {RoleGrantAssignOutput} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Output for role_grant_assign.

ok

type true

role_grant_id

type string & $brand<"Uuid">

RoleGrantOfferAcceptInput
#

auth/role_grant_offer_action_specs.ts view source

value + type

{ offer_id: string & $brand<"Uuid">; acting?: (string & $brand<"Uuid">) | undefined; } import {RoleGrantOfferAcceptInput} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Input for role_grant_offer_accept.

offer_id

type string & $brand<"Uuid">

acting?

type string & $brand<"Uuid">

RoleGrantOfferAcceptOutput
#

auth/role_grant_offer_action_specs.ts view source

value + type

{ role_grant_id: string & $brand<"Uuid">; offer: { id: string & $brand<"Uuid">; from_actor_id: string & $brand<"Uuid">; to_account_id: string & $brand<"Uuid">; ... 12 more ...; resulting_role_grant_id: (string & $brand<...>) | null; }; superseded_offer_ids: (string & $brand<...>)[]; } import {RoleGrantOfferAcceptOutput} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Output for role_grant_offer_accept.

role_grant_id

type string & $brand<"Uuid">

offer

type RoleGrantOfferJson

superseded_offer_ids

type (string & $brand<"Uuid">)[]

RoleGrantOfferCreateInput
#

auth/role_grant_offer_action_specs.ts view source

value + type

{ to_account_id: string & $brand<"Uuid">; role: string; to_actor_id?: (string & $brand<"Uuid">) | null | undefined; scope_kind?: string | null | undefined; scope_id?: (string & $brand<...>) | ... 1 more ... | undefined; message?: string | ... 1 more ... | undefined; acting?: (string & $brand<...>) | undefined; } import {RoleGrantOfferCreateInput} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Input for role_grant_offer_create.

to_actor_id (optional) narrows the offer to a specific actor on the recipient account. When supplied, role_grant_offer_accept will only admit the named actor — wrong-actor accepts reject with role_grant_offer_actor_mismatch. The audit envelope's target_actor_id is stamped from this column on the create / supersede / expire / retract events. Omit (or pass null) for the account-grain default — any actor on to_account_id may accept.

to_account_id

type string & $brand<"Uuid">

role

type string

to_actor_id?

type (string & $brand<"Uuid">) | null

scope_kind?

type string | null

scope_id?

type (string & $brand<"Uuid">) | null

message?

type string | null

acting?

type string & $brand<"Uuid">

RoleGrantOfferCreateOutput
#

auth/role_grant_offer_action_specs.ts view source

value + type

{ offer: { id: string & $brand<"Uuid">; from_actor_id: string & $brand<"Uuid">; to_account_id: string & $brand<"Uuid">; to_actor_id: (string & $brand<"Uuid">) | null; ... 11 more ...; resulting_role_grant_id: (string & $brand<...>) | null; }; } import {RoleGrantOfferCreateOutput} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Output for role_grant_offer_create.

offer

type RoleGrantOfferJson

RoleGrantOfferDeclineInput
#

auth/role_grant_offer_action_specs.ts view source

value + type

{ offer_id: string & $brand<"Uuid">; reason?: string | null | undefined; acting?: (string & $brand<"Uuid">) | undefined; } import {RoleGrantOfferDeclineInput} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Input for role_grant_offer_decline.

offer_id

type string & $brand<"Uuid">

reason?

type string | null

acting?

type string & $brand<"Uuid">

RoleGrantOfferHistoryInput
#

auth/role_grant_offer_action_specs.ts view source

value + type

{ account_id?: (string & $brand<"Uuid">) | null | undefined; limit?: number | null | undefined; offset?: number | null | undefined; acting?: (string & $brand<"Uuid">) | undefined; } import {RoleGrantOfferHistoryInput} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Input for role_grant_offer_history. Returns every offer involving the account in either direction (recipient or grantor), including terminal rows, newest first. account_id is admin-only.

account_id?

type (string & $brand<"Uuid">) | null

limit?

type number | null

offset?

type number | null

acting?

type string & $brand<"Uuid">

RoleGrantOfferHistoryOutput
#

auth/role_grant_offer_action_specs.ts view source

value + type

{ offers: { id: string & $brand<"Uuid">; from_actor_id: string & $brand<"Uuid">; to_account_id: string & $brand<"Uuid">; to_actor_id: (string & $brand<"Uuid">) | null; ... 11 more ...; resulting_role_grant_id: (string & $brand<...>) | null; }[]; } import {RoleGrantOfferHistoryOutput} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Output for role_grant_offer_history.

offers

type RoleGrantOfferJson[]

RoleGrantOfferListInput
#

auth/role_grant_offer_action_specs.ts view source

value + type

{ account_id?: (string & $brand<"Uuid">) | null | undefined; acting?: (string & $brand<"Uuid">) | undefined; } import {RoleGrantOfferListInput} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Input for role_grant_offer_list. account_id is admin-only (inspect another account's inbox).

account_id?

type (string & $brand<"Uuid">) | null

acting?

type string & $brand<"Uuid">

RoleGrantOfferListOutput
#

auth/role_grant_offer_action_specs.ts view source

value + type

{ offers: { id: string & $brand<"Uuid">; from_actor_id: string & $brand<"Uuid">; to_account_id: string & $brand<"Uuid">; to_actor_id: (string & $brand<"Uuid">) | null; ... 11 more ...; resulting_role_grant_id: (string & $brand<...>) | null; }[]; } import {RoleGrantOfferListOutput} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Output for role_grant_offer_list.

offers

type RoleGrantOfferJson[]

RoleGrantOfferOkOutput
#

auth/role_grant_offer_action_specs.ts view source

value + type

{ ok: true; } import {RoleGrantOfferOkOutput} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Output for role_grant_offer_decline / role_grant_offer_retract.

ok

type true

RoleGrantOfferRetractInput
#

auth/role_grant_offer_action_specs.ts view source

value + type

{ offer_id: string & $brand<"Uuid">; acting?: (string & $brand<"Uuid">) | undefined; } import {RoleGrantOfferRetractInput} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Input for role_grant_offer_retract.

offer_id

type string & $brand<"Uuid">

acting?

type string & $brand<"Uuid">

RoleGrantRevokeInput
#

auth/role_grant_offer_action_specs.ts view source

value + type

{ actor_id: string & $brand<"Uuid">; role_grant_id: string & $brand<"Uuid">; reason?: string | null | undefined; acting?: (string & $brand<"Uuid">) | undefined; } import {RoleGrantRevokeInput} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Input for role_grant_revoke. Admin-only mutation that revokes an active role_grant on a target actor. actor_id is the natural key — role_grants are actor-scoped, and the admin UI reads row.actor.id straight from the listing. Deriving actor_id from account_id would collapse under multi-actor accounts.

actor_id

type string & $brand<"Uuid">

role_grant_id

type string & $brand<"Uuid">

reason?

type string | null

acting?

type string & $brand<"Uuid">

RoleGrantRevokeOutput
#

auth/role_grant_offer_action_specs.ts view source

value + type

{ ok: true; revoked: true; } import {RoleGrantRevokeOutput} from '@fuzdev/fuz_app/auth/role_grant_offer_action_specs.js';

Output for role_grant_revoke.

ok

type true

revoked

type true

Depends on
#

Imported by
#