/**
 * 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.
 *
 * @module
 */
import type { Uuid } from '@fuzdev/fuz_util/id.js';
import type { QueryDeps } from '../db/query_deps.js';
import type { RoleGrant, CreateRoleGrantInput } from './account_schema.js';
import type { SupersededOffer } from './role_grant_offer_schema.js';
/**
 * 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.
 *
 * @param deps - query dependencies
 * @param input - the role_grant fields
 * @returns the created or existing active role_grant
 * @mutates `role_grant` table - inserts a row when no active role_grant matches `(actor_id, role, scope_kind, scope_id)`
 */
export declare const query_create_role_grant: (deps: QueryDeps, input: CreateRoleGrantInput) => Promise<RoleGrant>;
/**
 * 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.
 *
 * @param deps - query dependencies
 * @param role_grant_id - the role_grant id to look up
 * @param actor_id - the actor that must own the role_grant
 * @returns `{role, account_id}` on a match, or `null`
 */
export declare const query_role_grant_find_active_role_for_actor: (deps: QueryDeps, role_grant_id: string, actor_id: string) => Promise<{
    role: string;
    account_id: Uuid;
} | null>;
/** Result of `query_revoke_role_grant` — the revoked role_grant plus any pending offers superseded by the revoke. */
export interface RevokeRoleGrantResult {
    id: Uuid;
    role: string;
    scope_kind: string | null;
    scope_id: Uuid | null;
    /**
     * 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>`).
     */
    superseded_offers: Array<SupersededOffer>;
}
/**
 * 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.
 *
 * @param deps - query dependencies
 * @param role_grant_id - the role_grant to revoke
 * @param actor_id - the actor that must own the role_grant
 * @param revoked_by - the actor who revoked it (for audit trail)
 * @param reason - optional free-form reason, stamped on `role_grant.revoked_reason` and surfaced to the revokee notification.
 * @mutates `role_grant` row - sets `revoked_at`, `revoked_by`, and `revoked_reason`
 * @mutates `role_grant_offer` rows - stamps `superseded_at` on every pending sibling for the same `(account, role, scope)`
 */
export declare const query_revoke_role_grant: (deps: QueryDeps, role_grant_id: Uuid, actor_id: Uuid, revoked_by: Uuid | null, reason?: string | null) => Promise<RevokeRoleGrantResult | null>;
/**
 * Find all active (non-revoked, non-expired) role_grants for an actor.
 */
export declare const query_role_grant_find_active_for_actor: (deps: QueryDeps, actor_id: string) => Promise<Array<RoleGrant>>;
/**
 * 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.
 */
export declare const query_role_grant_has_role: (deps: QueryDeps, actor_id: string, role: string, scope_id?: string | null) => 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_grant` → `actor`; 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.
 *
 * @param deps - query dependencies
 * @param account_id - the account to check
 * @param role - the role to check for (e.g. `ROLE_ADMIN`)
 * @returns `true` if any actor on the account has an active global role_grant for `role`
 */
export declare const query_account_has_global_role: (deps: QueryDeps, account_id: string, role: string) => Promise<boolean>;
/**
 * Like `query_account_has_global_role`, but only counts the grant when the
 * **account itself is active** (`deleted_at IS NULL`). Used by the last-admin
 * branch of the removability guard: a soft-deleted admin can't log in and is
 * excluded from `query_count_active_accounts_with_global_role`, so the guard
 * must use the same active-account predicate when testing whether the *target*
 * is an admin — otherwise removing an already-tombstoned admin is falsely
 * blocked as `cannot_delete_last_admin` even though it can't lower the active
 * count. The keeper branch deliberately uses the unconditional
 * `query_account_has_global_role` (a keeper is never removable regardless of
 * tombstone state).
 *
 * @param deps - query dependencies
 * @param account_id - the account to check
 * @param role - the role to check for (e.g. `ROLE_ADMIN`)
 * @returns `true` if the account is active and any of its actors holds an active global `role` grant
 */
export declare const query_account_has_active_global_role: (deps: QueryDeps, account_id: string, role: string) => Promise<boolean>;
/**
 * Count **active** accounts (`deleted_at IS NULL`) holding an active global
 * role_grant for `role`. Used by the last-admin guard on `account_delete` /
 * `account_purge`: a soft-deleted account's admin grant isn't revoked, so a
 * plain grant count would include tombstoned (unusable) admins — this joins
 * `account` and excludes them, counting only admins that can actually log in.
 *
 * @param deps - query dependencies
 * @param role - the role to count (e.g. `ROLE_ADMIN`)
 * @returns the number of distinct active accounts with an active global `role` grant
 */
export declare const query_count_active_accounts_with_global_role: (deps: QueryDeps, role: string) => Promise<number>;
/**
 * List all role_grants for an actor (including revoked/expired).
 */
export declare const query_role_grant_list_for_actor: (deps: QueryDeps, actor_id: string) => Promise<Array<RoleGrant>>;
/**
 * 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.
 *
 * @param deps - query dependencies
 * @param role - the role to search for
 * @returns the account ID, or `null`
 */
export declare const query_role_grant_find_account_id_for_role: (deps: QueryDeps, role: string) => Promise<string | null>;
/** Result of `query_role_grant_revoke_for_scope` — every role_grant revoked plus every pending offer superseded by the scope-wide cascade. */
export interface RevokeForScopeResult {
    /**
     * 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.
     */
    revoked: Array<{
        role_grant_id: Uuid;
        role: string;
        scope_kind: string | null;
        scope_id: Uuid;
        actor_id: Uuid;
        account_id: Uuid;
    }>;
    /**
     * 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).
     */
    superseded_offers: Array<SupersededOffer>;
}
/**
 * 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.
 *
 * @param deps - query dependencies
 * @param scope_id - the scope whose role_grants and offers to terminate
 * @param revoked_by - the actor performing the cascade (audit trail)
 * @param reason - optional free-form reason, stamped on `role_grant.revoked_reason`.
 * @returns the revoked role_grants (with `account_id` for fan-out) and superseded offers (with `from_account_id` for fan-out)
 * @mutates `role_grant` table - sets `revoked_at`/`revoked_by`/`revoked_reason` on every active row at `scope_id`
 * @mutates `role_grant_offer` table - stamps `superseded_at` on every pending row at `scope_id`
 */
export declare const query_role_grant_revoke_for_scope: (deps: QueryDeps, scope_id: Uuid, revoked_by: Uuid | null, reason?: string | null) => Promise<RevokeForScopeResult>;
/** Result of `query_role_grant_revoke_role` — every role_grant revoked plus the pending offers superseded by the bulk revoke. */
export interface RevokeRoleResult {
    /**
     * 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)`.
     */
    revoked: Array<{
        role_grant_id: string;
        role: string;
        scope_kind: string | null;
        scope_id: string | null;
        account_id: string;
    }>;
    /**
     * 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.
     */
    superseded_offers: Array<SupersededOffer>;
}
/**
 * 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).
 *
 * @param deps - query dependencies
 * @param actor_id - the actor whose role_grants to revoke
 * @param role - the role to revoke
 * @param revoked_by - the actor who revoked it (for audit trail)
 * @param reason - optional free-form reason, stamped on `role_grant.revoked_reason`.
 * @returns the list of revoked role_grants (empty if none were active) and superseded pending offers
 * @mutates `role_grant` table - sets `revoked_at`/`revoked_by`/`revoked_reason` on every active row for `(actor, role)`
 * @mutates `role_grant_offer` table - stamps `superseded_at` on every matching pending offer
 */
export declare const query_role_grant_revoke_role: (deps: QueryDeps, actor_id: string, role: string, revoked_by: string | null, reason?: string | null) => Promise<RevokeRoleResult>;
//# sourceMappingURL=role_grant_queries.d.ts.map