import { type DenialActorSummary, type DenialBucket, type DenialFilter, type DenialGroupBy, type DenialOperationBucket, type DenialOperationSummary, type PermissionDenial } from '@substrat-run/contracts'; /** * The SELECTs behind every read of a scope's denial log (#867, K-35's stated tail). * * Four surfaces answer questions from `_substrat_denials` — the pure adapter's * `HostAdmin`, the Durable Object's RPC, the vertical's `/internal/denials` seam and * the control-plane route above them — and they must not drift on what "newest" * means, what a bucket groups by, or which rows a window bound includes. Ids are * ULIDs, so `ORDER BY id DESC` IS newest-first with no second index. * * Filters are re-parsed here rather than trusted: this builds SQL and every field can * arrive from an HTTP query string. Values are bound, never interpolated. */ /** Every column of `_substrat_denials`, in the order `mapDenialRow` expects. */ export declare const DENIAL_COLUMNS = "id, actor, permission, tenant_id, scope_id, operation, impersonation, invocation_id, at, drained_at"; /** The raw row shape, as either adapter hands it back. */ export interface DenialRow { id: string; actor: string; permission: string; tenant_id: string; scope_id: string | null; operation: string | null; /** K-42: the staff actor + session, as JSON, when the refusal was under one. */ impersonation: string | null; /** #1525: the invocation the refusal happened during. NULL = none was carried. */ invocation_id: string | null; at: string; drained_at: string | null; } /** * The stored spelling of an actor. The writer persists `JSON.stringify(actor)`, so a * principal is stored WITH its quotes (`"01J…"`) while a system or connection actor is * stored as an object (`{"system":"invoicing"}`). A caller filtering by actor holds the * logical form, not that encoding, so normalize rather than making every call site know: * text that already parses as JSON is passed through, anything else is a bare id and is * stringified. Round-trips exactly what `recordDenial` wrote in both adapters. */ export declare function storedActor(input: string): string; /** Turn a stored row into the contract shape. */ export declare function mapDenialRow(row: DenialRow): PermissionDenial; /** A bounded page of raw denial rows, newest first. */ export declare function denialListQuery(filter?: DenialFilter): { sql: string; params: (string | number)[]; }; /** * K-35's rate-buckets: one row per (actor, permission), busiest first — or, with * `groupBy: 'operation'` (#1456), one row per operation, ordered the same way. * * Busiest-first rather than newest-first on purpose — this view exists BECAUSE the * volume is attacker-influenceable, and ordering by recency would let whoever wrote * the last hundred rows push everyone else off the page, which is the exact failure * the bucketing is there to prevent. Ties break on `MAX(id)` so the order is total. * * The grouping is returned beside the SQL so the adapter maps the rows it gets back * with the matching mapper (`mapDenialSummaryBuckets`) rather than re-deriving which * query it ran from the filter. */ export declare function denialSummaryQuery(filter?: DenialFilter): { sql: string; params: (string | number)[]; groupBy: DenialGroupBy; }; export interface DenialBucketRow { actor: string; permission: string; count: number; operations: number; first_at: string; last_at: string; } export declare function mapDenialBucketRow(row: DenialBucketRow): DenialBucket; export interface DenialOperationBucketRow { operation: string | null; count: number; first_at: string; last_at: string; } export declare function mapDenialOperationBucketRow(row: DenialOperationBucketRow): DenialOperationBucket; /** The half of a `DenialSummary` the grouped query answers; the facts are the other half. */ export type DenialSummaryBuckets = Pick | Pick; /** * Map the rows `denialSummaryQuery` came back with, under the grouping it reported — * and carry that grouping onto the answer, so a caller who asked one question can see * which one was answered. Both adapters spread this into the summary they return. */ export declare function mapDenialSummaryBuckets(groupBy: DenialGroupBy, rows: unknown[]): DenialSummaryBuckets; /** Totals for the FILTERED set — what the capped bucket list is a page of. */ export declare function denialTotalsQuery(filter?: DenialFilter): { sql: string; params: (string | number)[]; }; /** * Facts about the WINDOW, filter ignored — deliberately. * * These describe the log, not the query. A caller reading an empty filtered result * needs to know whether the log reaches back past the interval it asked about, because * rows here drain rather than expire (K-24's split) and until a Tier-2 sink exists the * window simply IS the retention. Reporting the floor is what stops absence being read * as "this never happened" — K-35 calls that a stated limitation, so the surface states it. */ export declare const DENIAL_WINDOW_QUERY: string; export interface DenialWindowRow { oldest_at: string | null; newest_at: string | null; drained: number | null; } //# sourceMappingURL=denial-query.d.ts.map