auth/role_grant_queries.ts

Role grant database queries.

Role grants are time-bounded, revocable grants of a role to an actor. The system is safe by default — no role_grant, no capability.

Declarations
#

13 declarations

view source

query_account_has_global_role
#

auth/role_grant_queries.ts view source

(deps: QueryDeps, account_id: string, role: string): Promise<boolean>

Account-grain check: does any actor on account_id hold an active global role_grant for role?

Symmetric with query_role_grant_has_role but keyed on the account instead of a single actor — for surfaces with auth: actor: 'none' that don't load auth.role_grants and can't use the in-memory has_scoped_role predicate. Joins role_grantactor; matches only global role_grants (scope_id IS NULL) since the use case is "is the caller's account broadly admin", not scope-aware.

Fast under the existing idx_role_grant_actor index — the inner actor_id IN (...) subquery is index-scan, and the outer EXISTS stops at the first match.

deps

query dependencies

account_id

the account to check

type string

role

the role to check for (e.g. ROLE_ADMIN)

type string

returns

Promise<boolean>

true if any actor on the account has an active global role_grant for role

query_create_role_grant
#

auth/role_grant_queries.ts view source

(deps: QueryDeps, input: CreateRoleGrantInput): Promise<RoleGrant>

Grant a role_grant to an actor. Idempotent — if an active role_grant already exists for this actor, role, and scope, returns the existing role_grant instead of creating a duplicate.

The ON CONFLICT target and the fallback SELECT both collapse NULL scopes via the same sentinel + index-side 'GLOBAL' token used by the partial unique index (role_grant_actor_role_scope_active_unique). The IS NOT DISTINCT FROM form on the fallback is deliberate — plain = would miss the NULL-scope case where the conflict fired.

scope_kind is paired-null with scope_id per the role_grant_scope_kind_paired CHECK; mismatched pairs raise at the DB layer rather than producing silent rows.

deps

query dependencies

input

the role_grant fields

returns

Promise<RoleGrant>

the created or existing active role_grant

query_revoke_role_grant
#

auth/role_grant_queries.ts view source

(deps: QueryDeps, role_grant_id: string & $brand<"Uuid">, actor_id: string & $brand<"Uuid">, revoked_by: (string & $brand<"Uuid">) | null, reason?: string | ... 1 more ... | undefined): Promise<...>

Revoke a role_grant by id, constrained to a specific actor.

Requires actor_id to prevent cross-account revocation (IDOR guard). Returns null if the role_grant is not found, already revoked, or belongs to a different actor.

Supersedes any pending offers for the revoked role_grant's (to_account, role, scope) in the same transaction. Prevents the "accept a pre-revoke offer to bypass the revoke" path — any stale offer becomes terminal at revoke time. A fresh post-revoke grant requires the grantor to call query_role_grant_offer_create again.

deps

query dependencies

role_grant_id

the role_grant to revoke

type string & $brand<"Uuid">

actor_id

the actor that must own the role_grant

type string & $brand<"Uuid">

revoked_by

the actor who revoked it (for audit trail)

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

reason?

optional free-form reason, stamped on role_grant.revoked_reason and surfaced to the revokee notification.

type string | null | undefined
optional

returns

Promise<RevokeRoleGrantResult | null>

query_role_grant_find_account_id_for_role
#

auth/role_grant_queries.ts view source

(deps: QueryDeps, role: string): Promise<string | null>

Find the account ID of an account that holds an active role_grant for a given role.

Joins role_grant → actor → account. Returns the first match, or null if none.

deps

query dependencies

role

the role to search for

type string

returns

Promise<string | null>

the account ID, or null

query_role_grant_find_active_for_actor
#

auth/role_grant_queries.ts view source

(deps: QueryDeps, actor_id: string): Promise<RoleGrant[]>

Find all active (non-revoked, non-expired) role_grants for an actor.

deps

actor_id

type string

returns

Promise<RoleGrant[]>

query_role_grant_find_active_role_for_actor
#

auth/role_grant_queries.ts view source

(deps: QueryDeps, role_grant_id: string, actor_id: string): Promise<{ role: string; account_id: string & $brand<"Uuid">; } | null>

Look up the role of an active role_grant (constrained to a specific actor) plus the actor's account_id.

Used by admin routes to inspect the role_grant's role before acting (e.g., enforcing the admin-grant-path gate on revoke). The actor constraint mirrors query_revoke_role_grant so IDOR protection is consistent: a caller can only see role_grants belonging to the target actor.

The JOIN to actor collapses what used to be a second query_actor_by_id round-trip in the revoke handler into one read, which closes the small TOCTOU window where the actor row could be deleted between the IDOR check and the actor lookup. The account_id is needed by the audit envelope's target_account_id field and the SSE/WS socket-close fan-out targeting.

Returns null if the role_grant is not found, already revoked, or belongs to a different actor.

deps

query dependencies

role_grant_id

the role_grant id to look up

type string

actor_id

the actor that must own the role_grant

type string

returns

Promise<{ role: string; account_id: string & $brand<"Uuid">; } | null>

{role, account_id} on a match, or null

query_role_grant_has_role
#

auth/role_grant_queries.ts view source

(deps: QueryDeps, actor_id: string, role: string, scope_id?: string | null | undefined): Promise<boolean>

Check if an actor has an active role_grant for a given role.

The scope_id parameter selects between global and scoped checks: - Omitted or null — matches a global role_grant (scope_id IS NULL). Pre-scope callers keep their existing semantics. - A scope uuid — matches a role_grant bound to that exact scope.

The IS NOT DISTINCT FROM comparison handles the NULL case uniformly.

deps

actor_id

type string

role

type string

scope_id?

type string | null | undefined
optional

returns

Promise<boolean>

query_role_grant_list_for_actor
#

auth/role_grant_queries.ts view source

(deps: QueryDeps, actor_id: string): Promise<RoleGrant[]>

List all role_grants for an actor (including revoked/expired).

deps

actor_id

type string

returns

Promise<RoleGrant[]>

query_role_grant_revoke_for_scope
#

auth/role_grant_queries.ts view source

(deps: QueryDeps, scope_id: string & $brand<"Uuid">, revoked_by: (string & $brand<"Uuid">) | null, reason?: string | null | undefined): Promise<RevokeForScopeResult>

Revoke every active role_grant bound to a scope and supersede every pending offer at the scope, in one cascade.

Use this from a consumer's parent-scope delete handler (e.g., classroom deletion) — role_grant.scope_id and role_grant_offer.scope_id are polymorphic with no FK constraint by design, so a parent row deletion would otherwise orphan role_grants and offers. The cascade is role-agnostic: anything attached to the destroyed scope is cleaned up.

Both updates run as separate statements inside the caller's transaction (mirrors query_role_grant_revoke_role's shape). The two halves are independent — orphan pending offers can exist at a scope with no active role_grants, so the supersede half always runs even when no role_grant was revoked.

deps

query dependencies

scope_id

the scope whose role_grants and offers to terminate

type string & $brand<"Uuid">

revoked_by

the actor performing the cascade (audit trail)

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

reason?

optional free-form reason, stamped on role_grant.revoked_reason.

type string | null | undefined
optional

returns

Promise<RevokeForScopeResult>

the revoked role_grants (with account_id for fan-out) and superseded offers (with from_account_id for fan-out)

query_role_grant_revoke_role
#

auth/role_grant_queries.ts view source

(deps: QueryDeps, actor_id: string, role: string, revoked_by: string | null, reason?: string | null | undefined): Promise<RevokeRoleResult>

Revoke every active role_grant an actor holds for a given role.

With scoped role_grants a single actor+role tuple can hold several active role_grants (one per scope), so this revokes all of them. Pass query_revoke_role_grant(role_grant_id, ...) when a single scoped role_grant is the target.

Also supersedes pending offers for the actor's account across every scope of this role (the actor can no longer hold the role, so any pending offer of the same role is a bypass vector).

deps

query dependencies

actor_id

the actor whose role_grants to revoke

type string

role

the role to revoke

type string

revoked_by

the actor who revoked it (for audit trail)

type string | null

reason?

optional free-form reason, stamped on role_grant.revoked_reason.

type string | null | undefined
optional

returns

Promise<RevokeRoleResult>

the list of revoked role_grants (empty if none were active) and superseded pending offers

RevokeForScopeResult
#

auth/role_grant_queries.ts view source

RevokeForScopeResult

Result of query_role_grant_revoke_for_scope — every role_grant revoked plus every pending offer superseded by the scope-wide cascade.

revoked

One entry per role_grant revoked by this call. Carries both the revokee's actor_id (the role_grant's grantee — drives target_actor_id audit envelopes) and account_id (the actor's account — drives target_account_id for SSE/WS socket-close fan-out). Empty array means no active role_grant was bound to the scope. scope_kind is surfaced for forensic completeness; the cascade itself keys on scope_id regardless of kind.

type Array<{ role_grant_id: Uuid; role: string; scope_kind: string | null; scope_id: Uuid; actor_id: Uuid; account_id: Uuid; }>

superseded_offers

Every pending offer at the scope — tuple-matched and orphan, undifferentiated — superseded in the same cascade. Each entry carries its grantor's from_account_id for role_grant_offer_supersede notification fan-out.

The caller is responsible for emitting role_grant_offer_supersede audit events with reason: 'scope_destroyed' and cause_id: <destroyed scope row id> per entry — the cause of every supersede here is the scope deletion, not any individual role_grant revoke (the revokes are themselves consequences of the scope going away).

type Array<SupersededOffer>

RevokeRoleGrantResult
#

auth/role_grant_queries.ts view source

RevokeRoleGrantResult

Result of query_revoke_role_grant — the revoked role_grant plus any pending offers superseded by the revoke.

id

type Uuid

role

type string

scope_kind

type string | null

scope_id

type Uuid | null

superseded_offers

Pending offers for the revoked role_grant's (account, role, scope) that were marked superseded as a side effect. Each entry carries its grantor's from_account_id so callers can fan out role_grant_offer_supersede notifications without a second round-trip. The caller is responsible for emitting a role_grant_offer_supersede audit event per entry (with reason: 'role_grant_revoked' and cause_id: <revoked role_grant id>).

type Array<SupersededOffer>

RevokeRoleResult
#

auth/role_grant_queries.ts view source

RevokeRoleResult

Result of query_role_grant_revoke_role — every role_grant revoked plus the pending offers superseded by the bulk revoke.

revoked

One entry per role_grant revoked by this call. Carries the revokee's account_id so callers can fan out a role_grant_revoke notification per scope-instance. Empty array means nothing was active for (actor, role).

type Array<{ role_grant_id: string; role: string; scope_kind: string | null; scope_id: string | null; account_id: string; }>

superseded_offers

Pending offers for the actor's account+role (all scopes) superseded by the bulk revoke. Each entry carries its grantor's from_account_id so callers can fan out role_grant_offer_supersede notifications without a second round-trip.

type Array<SupersededOffer>

Depends on
#

Imported by
#