import { type EntityGrantShape, type EntityRef, type PrincipalId } from '@substrat-run/contracts'; import type { SwitchSql } from './system-switch.js'; /** * A declared entity-grant SHAPE, reconciled (#2071). * * A vertical declares the keys a person is given on their own record (`ENTITY_GRANTS`: * meridian's `EMPLOYEE_SELF` on `employee`, todo's owner grant on `owner`). They used to be * minted key by key, once, when the person arrived, so a key added to the shape by a later * release reached only the people who arrived after it, while `PERMISSIONS.md` said everyone * held it. * * Three pieces, each one statement shared by both adapters: * * - {@link grantEntityShapeIn}: the grant. The shape's keys plus a MARKER tuple * (`ENTITY_SHAPE_MARKER_RELATION`) on the same entity. The marker is what makes a person a * holder of the shape, which no set of keys can: someone `ctx.grant`ed one of those keys * holds part of the shape and must never be topped up to all of it. * - {@link topUpEntityGrantShapes}: one bounded pass of the reconcile, writing each top-up's * `entity.grants-topped-up` event beside it. Each live marker whose * entity lacks a key of the CURRENT shape gets that key. A key with any row at all is * skipped, and that includes a tombstone (K-21): a key someone took back from that person * stays taken back. A key merely dropped from the shape is left where it is, because silently * revoking authority on a deploy is the riskier mistake. * - The retirement (#2082), in the same pass and budget, between the backfill and the top-up: a * key the shape declares `retired` is tombstoned for every live marker that still holds it * live, with one `entity.grants-retired` event per (person, entity). The tombstones are the * progress; a record tuple (`shape:`, `shape-retired:`, `scope:`) ends it once * a batch comes back short, so a key granted to a holder again afterwards is left alone. A key * back in the shape's `permissions` tombstones that record, so a later release may retire it * again. One tuple is one authority: a direct grant of the key on the same entity to the same * person is that row, and goes with it. * - The backfill, inside the same budget, until it is done once per (scope, entity type): how * people granted before markers existed become holders. For own-record holders it reads * provenance, never key sets alone. * The shape's declared `holder` says whose record each entity is (`'self'`: the entity id is * the principal; a table column naming the principal), and a person is marked only on their * own record, and only when they hold a row there (live or tombstoned) for some key of the * shape — evidence they were given it. Someone `ctx.grant`ed even the whole shape on another * person's record is never marked. A shape with no `holder` gets no backfill. It is read in * windows like the walks (#2154): each pass reads at most its read budget of rows, in an index * order, marks at most its write budget, and hands back the last row it read as the cursor's * `row`, so the next pass seeks past it rather than re-reading from the start. A record tuple * (`shape:`, `shape-backfilled`, `scope:`) ends it once a read reaches the end of the * range, and never before. A table holder's read is keyed on `idColumn` and then the row's own * identity (its `rowid`, or a `WITHOUT ROWID` table's primary key), because nothing makes * `idColumn` unique or the principal non-NULL, and a key that ties lets a window's edge skip a row. * * The cut-off is the cursor. Evidence that lands behind it while the backfill runs (a direct * grant of a shape key to the person on their own record, for `'self'` or a table holder) is not * marked, and once the record is written nothing is: the same accepted gap as a grant landing * after the record. There is deliberately no confirming walk, as a retirement has: it would * double every first backfill's reads to catch a grant that a shape grant should have made. For * `'grantee'` nothing can land at all, since the first pass records the grantee keys and the * tuple writer refuses them to every other grant from then on. * - `holder: 'grantee'` (#2083), for a record that names no principal (a portal customer, a * contact): whoever holds a live current or retired key of the shape on such an entity was given it. * Each reconcile records current and retired keys (`shape-grantee-key`), and the explicit tuple * writer makes non-shape grants refuse them on that entity type. A tuple from before the * first such pass cannot be told apart. A marker already written by a shape grant persists * through retirement, so its holder receives the new keys even if their old key was K alone. * * Only a shape declared `bootstrap: true` is reconciled: one a person is GIVEN on their own * record. A sharing shape, which people reach through `ctx.grant` (todo's `list`), is skipped * whole, backfill included, so a sharee is never marked a holder. */ /** * The index every marker walk reads (#2083): markers only, in `(object, subject)` order, so one * shape's markers are one range (`:` up to `;`) and a pass's cursor is a seek into * it. Without it a pass scanned the whole `_substrat_tuples` table, even when it had nothing to * do. Partial, so it costs a row per marker and nothing for any other tuple. SQLite uses a * partial index only where it can prove the index's WHERE from the query's, so the walks write the * relation as this same literal: proved from the text alone, with no dependence on SQLite * re-planning once a parameter is bound. In KERNEL_DDL on both adapters, which every wake re-runs, * so an existing scope builds it on its next wake. */ export declare const SHAPE_MARKER_INDEX_DDL = "CREATE INDEX IF NOT EXISTS _substrat_tuples_shape_marker ON _substrat_tuples (object, subject) WHERE relation = 'bootstrap'"; /** Holders topped up per pass — one transaction each, so a large scope never holds one long. */ export declare const SHAPE_TOP_UP_BATCH = 500; /** * Rows a pass may READ, per row of work it may write: markers on a walk, and the tuples or table * rows the backfill reads for candidates (#2154). The budget bounds writes, but finding * the holders who need one means reading past the ones who do not, and on a scope where nobody * does (the common reconcile: nothing changed) that is every marker. Ten per row keeps a pass at * 5000 index rows by default, short on any scope, while a rollout still spends most of each pass * writing. A pass that reads its window without filling its budget hands back a cursor, and the * next pass reads on from there. */ export declare const SHAPE_MARKER_READS_PER_ROW = 10; /** * Confirming walks that may take keys, per shape per reconcile, before its retirement is left open * for the next reconcile. Each one that takes keys starts another, so a steady stream of grants of * a retired key landing ahead of the walk would otherwise keep a single reconcile walking forever. * Left open, the retirement keeps no record, so the next reconcile simply runs it again. */ export declare const SHAPE_RETIRE_CONFIRMS_MAX = 3; /** The largest batch a caller may ask for: past this a pass is the long transaction it exists to avoid. */ export declare const SHAPE_TOP_UP_BATCH_MAX = 5000; /** `batch`, or `validation_failed`: a pass size is a positive integer no larger than the max. */ export declare function shapeTopUpBatch(batch?: number): number; /** * The shape's grant to one person on one entity: the marker and every key, each an explicit * write, so a re-grant brings back what a revoke tombstoned — as `ctx.grant` does. * `entityObjectRef` refuses a ref the walk could not read back (#1856). Run it in ONE * transaction, so the marker and its keys land together or not at all. */ export declare function grantEntityShapeIn(db: SwitchSql, principal: PrincipalId, entity: EntityRef, permissions: readonly string[]): void; /** * Where a pass stopped, so the next pass of the same reconcile starts there instead of at the * first marker (#2083): the shape (its index in `shapes`), the step of it, and for a marker walk * the last marker it read. Without it every pass re-read the holders earlier passes had already * finished, and a rollout cost passes × markers. * * Skipping what lies behind it is safe because a walk leaves every marker it passes finished: * topped up (each key now has a row) or with its retired keys tombstoned. A marker written behind * it mid-run is complete when a shape grant writes it, and the backfill finishes before its * shape's walks begin. The exception is a shape grant by an OLDER deployment, carrying the shape * as it was, landing behind the cursor while the reconcile runs: * * - Missing a key the shape gained: the top-up keeps no record, so the next reconcile's top-up * reaches that holder. * - Holding a key the shape retired: a retirement ends in a run-once record, so a holder it never * saw would never be reached. The first walk therefore never writes that record. Once it reaches * the last marker, a CONFIRMING walk starts again from the first (`confirm`), and the record is * written only when a confirming walk reaches the last marker having taken nothing; one that * took keys starts another, up to `SHAPE_RETIRE_CONFIRMS_MAX`; past that the retirement is left * open for the next reconcile. A grant landing behind the confirming walk itself is the gap * left, the same as one landing after the retirement finished. */ export interface ShapeCursor { shape: number; step: 'backfill' | 'retire' | 'topUp'; /** The last marker a walk read; `null` resumes the step from its start. */ marker: { object: string; subject: string; } | null; /** * The backfill only (#2154): the sort key of the last row its read reached, `[object, relation, * subject]` of a tuple for a `'self'` or `'grantee'` holder, `[id, rowid]` for a table holder * (`[id, …primary key]` for a `WITHOUT ROWID` table). Absent * or `null` reads the backfill from its start, which is always safe: a pair it already marked * is no longer a candidate, so a cursor from before this field (or one a rollback ignores) only * costs a re-read. */ row?: readonly (string | number)[] | null; /** * A confirming retire walk only: whether it has taken nothing so far, and how many confirming * walks before it, on this shape in this reconcile, took keys. */ confirm?: { clean: boolean; count: number; }; } /** Where a pass runs and how its events are stamped — the facts only the adapter holds. */ export interface ShapePass { tenantId: string; scopeId: string; shapes: readonly EntityGrantShape[]; /** The pass's one instant: marker liveness and every event's `occurredAt`. */ now: string; /** Rows of work at most: a backfill mark or a holder topped up each count one. */ limit: number; /** The previous pass's `next`; absent or `null` starts the reconcile from the beginning. */ after?: ShapeCursor | null; /** The adapter's monotonic event-id mint, given the instant in ms. */ mintEventId: (ms: number) => string; /** The deploy writing the events, for the outbox `version` column. */ version: string | null; } /** * One pass of the reconcile over one scope, at most `limit` rows of work and * `limit × SHAPE_MARKER_READS_PER_ROW` rows read (markers, and the backfill's tuples or table * rows): backfill marks for any shape whose backfill * is not done here, then holders whose retired keys are taken back, each with its * `entity.grants-retired` event, then holders topped up, each with its `entity.grants-topped-up` * event. `next` is `null` once the scope is done; otherwise the caller runs another pass with it * as `after`. `retirementsLeftOpen` counts the shapes whose retirement this pass gave up on at * `SHAPE_RETIRE_CONFIRMS_MAX`, for the next reconcile to run again. The last pass (`next: null`) * also answers `boundEver` (#2184): every type this scope has ever bound (`shape-bound`), read * after this reconcile recorded its facts, for the platform's binding-shape gate. Run it inside ONE * transaction, so the keys and their events commit together. Re-running a finished scope writes * nothing. */ export declare function topUpEntityGrantShapes(db: SwitchSql, pass: ShapePass): { toppedUp: number; retired: number; retirementsLeftOpen: number; next: ShapeCursor | null; boundEver?: string[]; }; /** * A BINDING shape's record (#1686), on the scope's tuple spine beside the grantee keys: which * table column names the record's person (`..`), the key that * lets someone bind or unbind them, and the shape's current keys — what a binding invite's claim * gives. The claim and an unbind read these, never the vertical's code: the shape is the reviewed * registry the platform last reconciled this scope with. Each identifier is a SQL identifier, so * `.` cannot occur inside one. */ export declare const BIND_HOLDER_RELATION = "shape-bind-holder"; export declare const BIND_PERMISSION_RELATION = "shape-bind-permission"; export declare const BIND_KEY_RELATION = "shape-bind-key"; /** * `(shape:, shape-bound, bound)`: this scope has declared a binding for `` (#1686). * Written by the first reconcile that records the binding (and by every claim, to the same effect) * and STICKY — no reconcile removes it, whatever later * versions declare — so an erasure keeps releasing that type's holders after a version drops the * shape, drops its `bind` or moves its holder column. A spine tuple like the others here, so * module code can never write one (`ctx.sql` refuses every `_substrat_*` write). */ export declare const BOUND_EVER_RELATION = "shape-bound"; //# sourceMappingURL=entity-grant-shape.d.ts.map