/**
 * 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`.
 *
 * @module
 */
import type { Uuid } from '@fuzdev/fuz_util/id.js';
import type { QueryDeps } from '../db/query_deps.js';
import type { RoleGrant } from './account_schema.js';
import type { CreateRoleGrantOfferInput, RoleGrantOffer, SupersededOffer } from './role_grant_offer_schema.js';
import type { AuditLogEvent } from './audit_log_schema.js';
/**
 * 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.
 */
export declare class RoleGrantOfferAlreadyTerminalError extends Error {
    constructor(offer_id: string);
}
/**
 * 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.
 */
export declare class RoleGrantOfferExpiredError extends Error {
    constructor(offer_id: string);
}
/**
 * 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.
 */
export declare class RoleGrantOfferNotFoundError extends Error {
    constructor(offer_id: string);
}
/**
 * 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`.
 */
export declare class RoleGrantOfferSelfTargetError extends Error {
    constructor();
}
/**
 * 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".
 */
export declare class RoleGrantOfferActorMismatchError extends Error {
    constructor(offer_id: string);
}
/**
 * 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.
 */
export declare class RoleGrantOfferActorAccountMismatchError extends Error {
    constructor();
}
/**
 * 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.
 *
 * @mutates `role_grant_offer` table - inserts a new offer or upserts the matching pending row
 * @throws RoleGrantOfferSelfTargetError if the offering actor belongs to `to_account_id`
 * @throws RoleGrantOfferActorAccountMismatchError if `to_actor_id` is set but does not belong to `to_account_id`
 */
export declare const query_role_grant_offer_create: (deps: QueryDeps, input: CreateRoleGrantOfferInput) => Promise<RoleGrantOffer>;
/** Result of `query_role_grant_offer_decline` — the declined offer plus the grantor's `account_id`. */
export interface DeclinedOffer extends RoleGrantOffer {
    /**
     * Grantor's `account_id`, resolved via a join on `actor` so the audit
     * envelope's `target_account_id` (decline is *to* the grantor) and the
     * post-commit notification target are both addressable without a
     * second round-trip.
     */
    from_account_id: Uuid;
}
/**
 * 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.
 *
 * @mutates `role_grant_offer` row - sets `declined_at` and `decline_reason`
 * @throws RoleGrantOfferAlreadyTerminalError if the offer is already accepted, declined, retracted, or superseded
 */
export declare const query_role_grant_offer_decline: (deps: QueryDeps, offer_id: string, to_account_id: string, reason: string | null) => Promise<DeclinedOffer | 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.
 *
 * @mutates `role_grant_offer` row - sets `retracted_at`
 * @throws RoleGrantOfferAlreadyTerminalError if the offer is already accepted, declined, retracted, or superseded
 */
export declare const query_role_grant_offer_retract: (deps: QueryDeps, offer_id: string, from_actor_id: string) => Promise<RoleGrantOffer | null>;
/**
 * 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.
 */
export declare const query_role_grant_offer_list: (deps: QueryDeps, to_account_id: string) => Promise<Array<RoleGrantOffer>>;
/**
 * List every offer involving an account (either direction), newest first.
 *
 * Includes terminal offers — used by the grantor-side admin / history view.
 */
export declare const query_role_grant_offer_history_for_account: (deps: QueryDeps, account_id: string, limit?: number, offset?: number) => Promise<Array<RoleGrantOffer>>;
/**
 * Look up a pending offer by id. Returns `null` if the offer is terminal,
 * expired (server-side filter), or missing.
 */
export declare const query_role_grant_offer_find_pending: (deps: QueryDeps, offer_id: string) => Promise<RoleGrantOffer | null>;
/**
 * 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).
 */
export declare const query_role_grant_offer_sweep_expired: (deps: QueryDeps) => Promise<Array<RoleGrantOffer>>;
/** Input for `query_accept_offer`. */
export interface AcceptOfferInput {
    offer_id: Uuid;
    /** Account of the accepting recipient — IDOR guard against another account accepting the offer. */
    to_account_id: Uuid;
    /**
     * 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.
     */
    actor_id: Uuid;
    /** Optional IP to stamp on the audit events. */
    ip?: string | null;
}
/** Result of `query_accept_offer` — the role_grant produced (new or pre-existing on race), plus the (now-accepted) offer. */
export interface AcceptOfferResult {
    role_grant: RoleGrant;
    offer: RoleGrantOffer;
    /** `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. */
    created: boolean;
    /**
     * 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.
     */
    superseded_offers: Array<SupersededOffer>;
    /** 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. */
    audit_events: Array<AuditLogEvent>;
}
/**
 * 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.
 *
 * @mutates `role_grant_offer` row - stamps `accepted_at` and `resulting_role_grant_id`
 * @mutates `role_grant` table - inserts the resulting role_grant (idempotent on race)
 * @mutates `role_grant_offer` siblings - stamps `superseded_at` on every other pending offer for the tuple
 * @mutates `audit_log` table - emits `role_grant_offer_accept` + `role_grant_create` + one `role_grant_offer_supersede` per sibling
 * @throws RoleGrantOfferNotFoundError if the offer is missing or belongs to another recipient
 * @throws RoleGrantOfferAlreadyTerminalError if the offer is declined, retracted, or superseded
 * @throws RoleGrantOfferExpiredError if the offer is pending but past `expires_at`
 * @throws Error if the accepting `actor_id` does not belong to `to_account_id`, or invariant assertions fail
 */
export declare const query_accept_offer: (deps: QueryDeps, input: AcceptOfferInput) => Promise<AcceptOfferResult>;
//# sourceMappingURL=role_grant_offer_queries.d.ts.map