import { type Coverage, type Decision, type EntityRef, type Node, type PermissionKey, type CheckSubject, type RoleDefinition } from '@substrat-run/contracts'; import type { PermissionChecker } from './permission-checker.js'; import { type CapabilityRow } from './capability.js'; import { type GrantedEntitiesPage, type GrantWalkRow } from './grant-scoped-read.js'; /** The one result selector both adapter contexts call, using their own `ctx.check` path. */ export declare function grantedEntitiesForContext(checker: PermissionChecker, subject: CheckSubject, permission: PermissionKey, node: Node, entityType: string, options: { limit?: number; cursor?: string; } | undefined, runCheck: (permission: PermissionKey, entity?: EntityRef) => Promise, withheld?: ReadonlySet, nodeOverride?: boolean): Promise; type MaybePromise = T | Promise; /** * One tuple row as the spine stores it — snake_case, because that is what both adapters * `SELECT`. `expires_at`/`revoked_at` are what `live()` judges; a revoked row is still * here and still readable as evidence (K-21), it just stops granting. */ export interface PermissionTupleRow { subject: string; relation: string; object: string; expires_at: string | null; revoked_at: string | null; } /** * The scope-local half of the read: scope-level assignments/grants, entity-narrowed grants, * and the declared `parent` edges the entity walk follows. Entity tuples are scope-local by * construction, so all three live together. */ export interface ScopeTupleReader { /** Keyset grant root and reverse parent edge reads for grant-scoped enumeration. */ nextGrant?(subject: string, relation: string, after: string): GrantWalkRow | undefined; nextChild?(parent: string, after: string): GrantWalkRow | undefined; /** * Scope-level tuples for `subject` whose relation starts with `relationPrefix`. A * pre-filter only: the adapters answer it with SQL `LIKE`, which ignores ASCII case and * reads `_` as a wildcard, so the evaluator re-judges every row's relation exactly (#1869). */ tuples(subject: string, relationPrefix: string): MaybePromise; /** The one grant tuple (subject, relation, object), if it exists. */ grant(subject: string, relation: string, object: string): MaybePromise; /** The declared `parent` edges out of `object`. */ parents(object: string): MaybePromise; /** * The entity types whose declared shape protects `relation` (`granted:`) in this scope — * `shapeProtectedTypesQuery`, the spelling the explicit writer refuses by (#2083, #1686). * The entity walk stops climbing at a record of such a type for that key (#2177). Required, so * no reader can leave it out and let a grant on a parent reach through the record. */ shapeProtectedTypes(relation: string): MaybePromise; /** * The capability row a `{ kind: 'capability' }` subject names (#1672), read with * `capabilityByIdQuery` — capabilities live in the scope's own spine beside the entities * they reach. Optional so a reader written before capabilities still compiles; ABSENT * means every capability check DENIES, never that one is waved through. */ capability?(id: string): MaybePromise; /** * Is a kill switch holding `subject` (`system:`, `vertical:`) off on this * scope (#1666, #1706)? While it answers true the evaluator denies that subject here * whatever grants it holds, scope- or tenant-level (#1823). Both adapters answer it today * from the scope's own OFF marker, with `SYSTEM_SWITCH_OFF_QUERY` — the spelling the * schedule gate and the grant refusal share. Required, so no reader can leave it out and * fail open. */ switchedOff(subject: string): MaybePromise; } /** * Everything the evaluator needs to know, and nothing about where it comes from. * * `scopeFor` takes the whole `Node` rather than a scope id so an adapter decides for itself * when a scope store is reachable: the pure adapter resolves `node.scopeId` against its open * databases and answers `undefined` when there is none, while a ScopeDO simply *is* one * scope and always answers itself. */ export interface PermissionTupleReader { /** What "now" means when a tuple's `expires_at` is judged (#956). */ now(): string; /** Tenant-level tuples for `subject` whose relation starts with `relationPrefix`. */ tenantTuples(tenantId: string, subject: string, relationPrefix: string): MaybePromise; /** A role definition, or `undefined` — which is a deny, so an absent projection fails closed. */ getRole(tenantId: string, key: string): MaybePromise; /** The scope store this node's check reads, if there is one. */ scopeFor(node: Node): ScopeTupleReader | undefined; } /** * `live`, as a SQL predicate — for a query that must agree with the walk about which edges * exist (`ctx.relink`, #1864). Binds ONE parameter: the same `now`. */ export declare const liveTupleSql: (alias?: string) => string; /** * Is `entity` the `root`, or does it lie beneath it along live declared `parent` edges? * * The walk a capability check makes to its own root (`checkCapability` above), with no * subject and no grant: a live read narrowed `within` an entity (#1853) asks exactly this * of every frame, so it travels the same edges to the same depth, skipping the same * revoked ones. A row linked to two parents (`ctx.link` twice) reaches either root; a row * `ctx.relink`ed away from one reaches only the other (#1864). Given the `key` a root is checked * by, it stops where that key's check stops (`keyedStop`, #2177). */ export declare function reachesWithin(scope: WithinReader, entity: EntityRef, root: EntityRef, now: string, key?: PermissionKey): Promise; /** * Every root `reachesWithin` would answer `true` for, as `entityType:entityId` refs — the * entity itself included. The same walk, run to its full depth once, for a caller asking * about many roots: a live fan-out with one socket per widget session (#1853). */ export declare function ancestorsWithin(scope: WithinReader, entity: EntityRef, now: string, key?: PermissionKey): Promise>; /** * The reader `reachesWithin` / `ancestorsWithin` walk: the parent edges, and — for a walk on * behalf of a key — the types whose shape protects it. */ export type WithinReader = Pick & Partial>; /** * Build the evaluator over one adapter's reader. Stateless per call: everything it knows it * reads at check time, which is what makes check-after-write consistent (the "no zookies" * property) and what lets `reader.now()` decide expiry rather than the wall clock. */ export declare function createTupleEvaluator(reader: PermissionTupleReader): PermissionChecker; /** * A directory read that never yields (#1184): what a directory UNIT — one SQLite transaction, * one synchronous ControlPlaneDO method — can consult without letting another write in. The * tenant-level half of `PermissionTupleReader`, with every answer in hand. */ export interface TenantDirectoryReader { now(): string; tenantTuples(tenantId: string, subject: string, relationPrefix: string): PermissionTupleRow[]; getRole(tenantId: string, key: string): RoleDefinition | undefined; } /** * `covers` at the TENANT node, synchronously (#1184): the K-21 set comparison over the * principal and every org it is a live member of, each one's live tenant-level `role:` and * `granted:` tuples, roles expanded. The membership executor's bound, asked again inside the * unit that writes the role, so a grant, role definition or demotion landing between its * early check and its write cannot be written past. * * The same answer as `createTupleEvaluator(reader).covers(principal, required, tenantNode)` — * a tenant node has no scope store, no entity walk and no switch — which * `permission-eval.test.ts` pins over the cases where the two could part. */ export declare function tenantCoverage(reader: TenantDirectoryReader, tenantId: string, principal: string, required: readonly PermissionKey[]): Coverage; /** * `principal`'s live membership of `orgId` — its tuple, or `undefined` (#2047). Synchronous, for * the org bound a directory unit asks: joining someone to an org, or taking them out of it, * needs the inviter or remover to be a live member themselves. * * That is the exact K-21 bound for an org. Membership confers the same thing on every member, * at every node — the org's tenant grants, its grants in each scope's own store, its * entity-narrowed grants, and whatever it is granted later — so a member holds everything a * join would confer, and a non-member does not. No scope store is read, because none needs to * be: equality of membership is the comparison. */ export declare function liveOrgMembership(reader: TenantDirectoryReader, tenantId: string, principal: string, orgId: string): PermissionTupleRow | undefined; /** * The org half of a membership change's unit (#2047), decided once for both adapters: whether * `boundedBy` may join `principal` to the org or take them out of it, and — for a join — when * the membership it writes expires. Reads only; the adapter writes. */ export declare function orgChangeBound(reader: TenantDirectoryReader, tenantId: string, change: { op: 'add' | 'remove'; principal: string; boundedBy: string; orgId: string; }): { bounded: false; } | { bounded: true; expiresAt: string | null; }; /** The `after` of an `addMember` audit row: the expiry only when there is one, as before #2047. */ export declare const memberAddedAudit: (principal: string, orgId: string, expiresAt: string | null) => { principal: string; orgId: string; expiresAt?: string | undefined; }; /** * When a membership written by a join expires (#2047): never later than the membership of the * principal whose authority bounded it, so a temporary member cannot confer a permanent one, * and never earlier than a live membership the joiner already holds, which a re-invitation must * not cut short. `null` is "never". */ export declare function joinedMembershipExpiry(bound: Pick, existing: Pick | undefined): string | null; export {}; //# sourceMappingURL=permission-eval.d.ts.map