import { z } from 'zod'; /** * The read side of the K-35 denial log — a scope-local record of every ENFORCED * permission refusal (`assertAllowed`), written on the deny path as a fresh * autocommit AFTER the rollback it is evidence of. * * This is the *other* kind of evidence from a conformance receipt. A receipt says * "we attempted the attack in CI at commit X"; these rows say "on your data, in * production, here is every refusal, by whom, against which key". K-35 made the * case for the row itself: a denial is the one event where an actor's intent and * the permission model visibly disagree. * * Two properties of the log shape everything below, and both come straight from * K-35's own reasoning about why denials are NOT admin-log entries: * * 1. **The volume is attacker-influenceable.** A probing client mints unlimited * rows, so a newest-first page of raw rows is the wrong default view — 200 rows * from one prober hide everyone else. That is why `denialSummary` exists beside * the row list, and why K-35 called rate-bucketing sanctionable up front. * 2. **The window is a storage bound, not a retention policy.** Rows `drain` rather * than expire (K-24's split). Until a Tier-2 sink exists, what is here is simply * what has not been pruned — so the summary reports the window's own floor * (`windowOldestAt`) rather than letting a caller read absence as "never happened". */ /** How many denial rows an unbounded read returns — a screenful, newest-first. */ export declare const DEFAULT_DENIAL_LIMIT = 50; /** The hard ceiling on one page of denial rows, and on one page of buckets. */ export declare const DENIAL_LIMIT_MAX = 200; /** * One recorded refusal. `scopeId` is null for a tenant-node check (one that named no * scope); `operation` is null when the denial unwound something that was not an * operation invocation. `drainedAt` marks a row already shipped to a Tier-2 sink and * therefore eligible to be pruned — bookkeeping, not a judgement about the denial. */ export declare const permissionDenial: z.ZodObject<{ id: z.ZodString; actor: z.ZodUnion, z.ZodObject<{ system: z.core.$ZodBranded; }, z.core.$strip>, z.ZodObject<{ connection: z.ZodString; }, z.core.$strip>]>; permission: z.core.$ZodBranded; tenantId: z.core.$ZodBranded; scopeId: z.ZodNullable>; operation: z.ZodNullable; impersonation: z.ZodNullable; by: z.core.$ZodBranded; }, z.core.$strip>>; invocationId: z.ZodNullable; at: z.ZodString; drainedAt: z.ZodNullable; }, z.core.$strip>; export type PermissionDenial = z.infer; /** * What narrows a denial read. Every field is an exact match except the `since`/`until` * bounds on `at` (inclusive lower, exclusive upper) — enough to answer "who probed * this key", "what did this actor try", and "what happened during the incident window" * without a SQL console. * * `actor` takes the LOGICAL actor — a bare principal ULID, or the object form for a * system/connection actor (`{"system":"invoicing"}`). The writer persists * `JSON.stringify(actor)`, so a principal is stored with its quotes; normalizing to that * encoding is the reader's job (`storedActor` in the kernel's query builder), not every * caller's. * * `groupBy` is the one field that narrows nothing: it picks which bucketing the SUMMARY * answers with (#1456), and the row read ignores it. It lives on the shared filter all * the same, because both reads travel through one encoder, one decoder per door and one * drift test — a second filter type would be a second copy of all three. */ export declare const denialGroupBy: z.ZodEnum<{ "actor-permission": "actor-permission"; operation: "operation"; }>; export type DenialGroupBy = z.infer; export declare const denialFilter: z.ZodObject<{ actor: z.ZodOptional; permission: z.ZodOptional; operation: z.ZodOptional; since: z.ZodOptional; until: z.ZodOptional; limit: z.ZodOptional; groupBy: z.ZodOptional>; }, z.core.$strip>; export type DenialFilter = z.infer; /** * The filter's wire form — the ONE encoder every denial-log caller uses (#971). * * Both denial reads (`/denials` and `/denials/summary`) take the same filter, on both * branches of the route (a co-located scope answered locally, a hosted one asked * through its vertical), and every client that reaches them has to turn the filter * into a query string. That spelling was copy-pasted verbatim into each one, which is * how the fields and the route's decoder drift apart without anything going red: the * decoder still accepts `until`, the caller that never got the copy simply stops * sending it, and the screen silently reads an unbounded window. * * It lives here, beside `denialFilter`, rather than in the control-plane API package * — the callers include a BROWSER console, and a serializer is not worth pulling a * Hono server's module graph into a SPA bundle for. The schema is already the thing * both sides share; its encoding belongs with it. * * This is the form for a caller that has more to say than the filter — the platform's * vertical client adds `scopeId`, since the internal route is not scope-addressed in * its path. Callers that send the filter alone want `denialQuery` below. */ export declare function denialFilterParams(filter?: DenialFilter): URLSearchParams; /** * `denialFilterParams` as a URL suffix: `?`-prefixed, or `''` when nothing is * narrowed — so a call site is `` `${path}${denialQuery(filter)}` `` and an * unnarrowed read never grows a dangling `?`. */ export declare function denialQuery(filter?: DenialFilter): string; /** * One (actor, permission) bucket — K-35's "first occurrence + count per actor/key/ * window", which is the shape that survives a flood. `operations` is the number of * DISTINCT operations the actor was refused this key on, and it is the discriminator * worth the extra aggregate: one operation refused four hundred times is a broken * screen or a misconfigured role, while the same count spread across a dozen * operations is someone walking the surface. */ export declare const denialBucket: z.ZodObject<{ actor: z.ZodUnion, z.ZodObject<{ system: z.core.$ZodBranded; }, z.core.$strip>, z.ZodObject<{ connection: z.ZodString; }, z.core.$strip>]>; permission: z.core.$ZodBranded; count: z.ZodNumber; operations: z.ZodNumber; firstAt: z.ZodString; lastAt: z.ZodString; }, z.core.$strip>; export type DenialBucket = z.infer; /** * One per-operation bucket (#1456) — "which operation keeps getting refused", the * question a per-operation health panel asks. It is a different question from the * (actor, permission) one and not a refinement of it: the same operation refused for * a dozen actors is one row here and a dozen there. `operation` is null for the * refusals that unwound something other than an operation invocation; they are still * rows the log holds, so they get a bucket rather than vanishing from the sum. */ export declare const denialOperationBucket: z.ZodObject<{ operation: z.ZodNullable; count: z.ZodNumber; firstAt: z.ZodString; lastAt: z.ZodString; }, z.core.$strip>; export type DenialOperationBucket = z.infer; /** The summary as K-35 asked for it: one bucket per (actor, permission). */ export declare const denialActorSummary: z.ZodObject<{ total: z.ZodNumber; actors: z.ZodNumber; windowOldestAt: z.ZodNullable; windowNewestAt: z.ZodNullable; drained: z.ZodNumber; groupBy: z.ZodLiteral<"actor-permission">; buckets: z.ZodArray, z.ZodObject<{ system: z.core.$ZodBranded; }, z.core.$strip>, z.ZodObject<{ connection: z.ZodString; }, z.core.$strip>]>; permission: z.core.$ZodBranded; count: z.ZodNumber; operations: z.ZodNumber; firstAt: z.ZodString; lastAt: z.ZodString; }, z.core.$strip>>; }, z.core.$strip>; export type DenialActorSummary = z.infer; /** The summary grouped by operation (#1456) — the same facts, a different bucket. */ export declare const denialOperationSummary: z.ZodObject<{ total: z.ZodNumber; actors: z.ZodNumber; windowOldestAt: z.ZodNullable; windowNewestAt: z.ZodNullable; drained: z.ZodNumber; groupBy: z.ZodLiteral<"operation">; buckets: z.ZodArray; count: z.ZodNumber; firstAt: z.ZodString; lastAt: z.ZodString; }, z.core.$strip>>; }, z.core.$strip>; export type DenialOperationSummary = z.infer; /** * The bucketed view of a scope's denial log, plus the facts that keep it honest. * * One read, two groupings, chosen by the filter's `groupBy` and ECHOED on the answer: * the caller learns which question was answered rather than inferring it from a * bucket's fields — which matters across a version skew, since the route lives inside * each vertical's deploy and an older one that never heard of `groupBy` answers the * default grouping to a caller that asked for the other. */ export declare const denialSummary: z.ZodDiscriminatedUnion<[z.ZodObject<{ total: z.ZodNumber; actors: z.ZodNumber; windowOldestAt: z.ZodNullable; windowNewestAt: z.ZodNullable; drained: z.ZodNumber; groupBy: z.ZodLiteral<"actor-permission">; buckets: z.ZodArray, z.ZodObject<{ system: z.core.$ZodBranded; }, z.core.$strip>, z.ZodObject<{ connection: z.ZodString; }, z.core.$strip>]>; permission: z.core.$ZodBranded; count: z.ZodNumber; operations: z.ZodNumber; firstAt: z.ZodString; lastAt: z.ZodString; }, z.core.$strip>>; }, z.core.$strip>, z.ZodObject<{ total: z.ZodNumber; actors: z.ZodNumber; windowOldestAt: z.ZodNullable; windowNewestAt: z.ZodNullable; drained: z.ZodNumber; groupBy: z.ZodLiteral<"operation">; buckets: z.ZodArray; count: z.ZodNumber; firstAt: z.ZodString; lastAt: z.ZodString; }, z.core.$strip>>; }, z.core.$strip>], "groupBy">; export type DenialSummary = z.infer; //# sourceMappingURL=denial.d.ts.map