auth/role_grant_offer_queries.ts

Role grant offer database queries.

Covers the offer side of the consentful-role-grants flow: create (with re-offer upsert), decline, retract, list, find-pending, sweep-expired, and the atomic query_accept_offer that bridges offer β†’ role_grant.

IDOR guards are expressed in each helper's signature β€” decline/accept require the recipient's to_account_id, retract requires the grantor's from_actor_id.

Declarations
#

17 declarations

view source

AcceptOfferInput
#

auth/role_grant_offer_queries.ts view source

AcceptOfferInput

offer_id

type Uuid

to_account_id

Account of the accepting recipient β€” IDOR guard against another account accepting the offer.

type Uuid

actor_id

Accepting actor β€” the actor that will hold the resulting role_grant. Must belong to to_account_id; the query verifies and throws if not (defense-in-depth β€” the action handler passes auth.actor.id which is session-bound, but the query enforces the invariant for all callers including tests and future direct consumers).

Required because under multi-actor an account may host many actors; the resulting role_grant must bind to the actor that actually accepted, not "an" actor on the account picked by query order.

type Uuid

ip

Optional IP to stamp on the audit events.

type string | null

AcceptOfferResult
#

auth/role_grant_offer_queries.ts view source

AcceptOfferResult

Result of query_accept_offer β€” the role_grant produced (new or pre-existing on race), plus the (now-accepted) offer.

role_grant

offer

created

true if this call is the one that accepted the offer (new role_grant inserted); false on a race returning the already-created role_grant.

type boolean

superseded_offers

Sibling offers superseded by this accept β€” empty on the race-loser path. Each entry carries its grantor's from_account_id so the caller can fan out role_grant_offer_supersede notifications without a second round-trip.

type Array<SupersededOffer>

audit_events

Audit events emitted in-transaction β€” fed back through audit.notify by the caller, which fans out to audit.on_event_chain. Includes one role_grant_offer_supersede per superseded sibling.

type Array<AuditLogEvent>

DeclinedOffer
#

query_accept_offer
#

auth/role_grant_offer_queries.ts view source

(deps: QueryDeps, input: AcceptOfferInput): Promise<AcceptOfferResult>

Accept an offer atomically: mark accepted, insert the role_grant, stamp resulting_role_grant_id, supersede sibling pending offers for the same (to_account, role, scope), and emit role_grant_offer_accept + role_grant_create + one role_grant_offer_supersede per sibling. Must run inside a transaction β€” the caller's route spec should declare transaction: true (or wrap explicitly).

Idempotent on race: if a second concurrent call observes the offer already accepted, returns the existing role_grant rather than creating a duplicate or throwing.

Error map: - RoleGrantOfferNotFoundError β€” offer does not exist, or belongs to a different recipient (IDOR guard). The offer row is untouched. - RoleGrantOfferAlreadyTerminalError β€” offer is declined, retracted, or superseded. - RoleGrantOfferExpiredError β€” offer is pending but past expires_at.

Sibling supersede is what closes the "accept a pre-revoke sibling offer to bypass a revoke" path: once A is accepted, B/C/... can no longer be accepted even if the resulting role_grant is later revoked.

deps

input

returns

Promise<AcceptOfferResult>

throws

  • RoleGrantOfferNotFoundError - if the offer is missing or belongs to another recipient
  • RoleGrantOfferAlreadyTerminalError - if the offer is declined, retracted, or superseded
  • RoleGrantOfferExpiredError - if the offer is pending but past `expires_at`
  • Error - if the accepting `actor_id` does not belong to `to_account_id`, or invariant assertions fail

query_role_grant_offer_create
#

auth/role_grant_offer_queries.ts view source

(deps: QueryDeps, input: CreateRoleGrantOfferInput): Promise<RoleGrantOffer>

Create a new role_grant offer, or refresh an existing pending offer for the same (to_account_id, role, scope_id, from_actor_id) tuple.

Re-offer semantics: a second call by the same grantor with the same (to_account, role, scope) while pending upserts the existing row, refreshing message and expires_at (and to_actor_id β€” supplying a different to_actor_id on re-offer narrows the existing row to the named actor; supplying null widens it back to account-grain). A different grantor offering the same (to_account, role, scope) creates a distinct row β€” multiple pending grantors coexist. After a terminal state, a re-offer is a fresh INSERT.

Self-offer rejection: throws RoleGrantOfferSelfTargetError if the offering actor belongs to the recipient account.

Actor-targeted offers: when to_actor_id is supplied, query_accept_offer rejects any actor other than the named one. Closes the audit hole where offer-shape events would otherwise leave target_actor_id null even when the recipient binding is known at offer time. The actor↔account binding is verified here in one SELECT.

deps

input

returns

Promise<RoleGrantOffer>

throws

  • RoleGrantOfferSelfTargetError - if the offering actor belongs to `to_account_id`
  • RoleGrantOfferActorAccountMismatchError - if `to_actor_id` is set but does not belong to `to_account_id`

query_role_grant_offer_decline
#

auth/role_grant_offer_queries.ts view source

(deps: QueryDeps, offer_id: string, to_account_id: string, reason: string | null): Promise<DeclinedOffer | null>

Mark an offer declined.

Guarded by to_account_id (IDOR). Returns null if the offer does not exist or belongs to a different account. Throws RoleGrantOfferAlreadyTerminalError if the offer exists for the caller but is already in a terminal state.

Returns the declined offer with the grantor's from_account_id joined in via CTE β€” the decline audit envelope populates both target_actor_id (the grantor actor) and target_account_id (the grantor account), satisfying the "both populated β†’ same account" invariant the audit-log column comments describe.

deps

offer_id

type string

to_account_id

type string

reason

type string | null

returns

Promise<DeclinedOffer | null>

throws

  • RoleGrantOfferAlreadyTerminalError - if the offer is already accepted, declined, retracted, or superseded

query_role_grant_offer_find_pending
#

auth/role_grant_offer_queries.ts view source

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

Look up a pending offer by id. Returns null if the offer is terminal, expired (server-side filter), or missing.

deps

offer_id

type string

returns

Promise<RoleGrantOffer | null>

query_role_grant_offer_history_for_account
#

auth/role_grant_offer_queries.ts view source

(deps: QueryDeps, account_id: string, limit?: number, offset?: number): Promise<RoleGrantOffer[]>

List every offer involving an account (either direction), newest first.

Includes terminal offers β€” used by the grantor-side admin / history view.

deps

account_id

type string

limit

type number
default 100

offset

type number
default 0

returns

Promise<RoleGrantOffer[]>

query_role_grant_offer_list
#

auth/role_grant_offer_queries.ts view source

(deps: QueryDeps, to_account_id: string): Promise<RoleGrantOffer[]>

List pending, non-expired offers for an account, soonest expiry first.

Expired offers are filtered server-side (expires_at > NOW()) so the inbox never surfaces a row that can no longer be accepted. The periodic sweep (query_role_grant_offer_sweep_expired) handles audit tombstoning.

deps

to_account_id

type string

returns

Promise<RoleGrantOffer[]>

query_role_grant_offer_retract
#

auth/role_grant_offer_queries.ts view source

(deps: QueryDeps, offer_id: string, from_actor_id: string): Promise<RoleGrantOffer | null>

Mark an offer retracted by the grantor.

Guarded by from_actor_id (IDOR). Returns null if the offer does not exist or was issued by a different actor. Throws RoleGrantOfferAlreadyTerminalError if the offer exists for this grantor but is already in a terminal state.

deps

offer_id

type string

from_actor_id

type string

returns

Promise<RoleGrantOffer | null>

throws

  • RoleGrantOfferAlreadyTerminalError - if the offer is already accepted, declined, retracted, or superseded

query_role_grant_offer_sweep_expired
#

auth/role_grant_offer_queries.ts view source

(deps: QueryDeps): Promise<RoleGrantOffer[]>

Return pending offers whose expires_at has passed.

Callers fire role_grant_offer_expire audit events for each row. The schema does not tombstone the row, so callers are responsible for their own idempotency (e.g. check whether a role_grant_offer_expire audit event already exists for the offer id).

deps

returns

Promise<RoleGrantOffer[]>

RoleGrantOfferActorAccountMismatchError
#

auth/role_grant_offer_queries.ts view source

Error thrown when query_role_grant_offer_create is called with a to_actor_id that does not exist or does not belong to to_account_id. Surfaces the actor↔account binding mismatch at the boundary instead of letting the FK silently disagree with the recipient field.

inheritance

extends:
  • Error

constructor

type new (): RoleGrantOfferActorAccountMismatchError

RoleGrantOfferActorMismatchError
#

auth/role_grant_offer_queries.ts view source

Error thrown when an actor-targeted offer is being accepted by an actor other than offer.to_actor_id. Distinct from RoleGrantOfferNotFoundError (the IDOR mask): once an offer has been resolved to the recipient account, a wrong-actor accept on a same-account actor is a contract violation, not a privacy boundary β€” surface a specific error so the client UI can distinguish "this offer isn't for you" from "no such offer".

inheritance

extends:
  • Error

constructor

type new (offer_id: string): RoleGrantOfferActorMismatchError

offer_id
type string

RoleGrantOfferAlreadyTerminalError
#

auth/role_grant_offer_queries.ts view source

Error thrown by offer-lifecycle queries when the offer is in a non-pending state (accepted / declined / retracted / superseded) and therefore not actionable. Distinct from RoleGrantOfferExpiredError β€” expiry has its own user-facing story ("ask the grantor to re-send") so it travels separately.

inheritance

extends:
  • Error

constructor

type new (offer_id: string): RoleGrantOfferAlreadyTerminalError

offer_id
type string

RoleGrantOfferExpiredError
#

auth/role_grant_offer_queries.ts view source

Error thrown when an offer's expires_at has passed. The accept path enforces this independently of the sweep β€” a stale offer past its expiry must not be accepted, even in the race window between expiry and the sweep stamping the audit event.

inheritance

extends:
  • Error

constructor

type new (offer_id: string): RoleGrantOfferExpiredError

offer_id
type string

RoleGrantOfferNotFoundError
#

auth/role_grant_offer_queries.ts view source

Error thrown when an offer cannot be located for the caller. Covers both "offer does not exist" and "offer belongs to a different recipient" (IDOR guard) β€” the standard 404-over-403 pattern that avoids disclosing whether an offer id exists.

inheritance

extends:
  • Error

constructor

type new (offer_id: string): RoleGrantOfferNotFoundError

offer_id
type string

RoleGrantOfferSelfTargetError
#

auth/role_grant_offer_queries.ts view source

Error thrown when a grantor attempts to offer a role_grant to their own account.

Enforced via a single SELECT on the grantor's actor.account_id (rather than via a CHECK constraint or a denormalized column). Resolving from the grantor side keeps the check multi-actor-correct: under multi-actor the recipient account may host many actors, but the grantor β†’ account binding remains 1:1 by definition of actor.

inheritance

extends:
  • Error

constructor

type new (): RoleGrantOfferSelfTargetError

Depends on
#

Imported by
#