/** * Peers (#1706) — the kernel's half of one vertical calling another's operations, written once * so both adapters (and the platform's resolver) cannot disagree about any of it. * * What lives here, and why each is one function: * * - **`collectPeers`** — the union of every registered module's `peers` declaration. A vertical * is several modules on one host; the door reads them as one declaration per peer vertical. * - **`peerSeats`** — the `vertical:` grants provisioning seats on a scope. The ONE place * that decides "a declared peer holds its keys once installed" (decision F2 on #1706): if that * decision flips to "a tenant admin enables each edge", this call moves from provisioning to * the enabling verb and nothing else changes. * - **`admitPeer`** — the door's admission, run inside the scope's own serialized task on every * call: the peer is declared, its kill switch is on, and the operation is on its allowlist. * None of those refusals is a K-35 denial (no key was checked), exactly as the capability * door's are. `operation: null` admits a DELIVERY rather than an invocation (#1705's * cross-vertical events) — the same declaration and the same switch, and no allowlist, since * a delivery names no operation. * - **`switchPeer`** — the per-(scope, peer) kill switch, which is #1666's switch applied to a * `vertical:` subject (`switchSubjectGrants`): OFF tombstones every grant the peer holds on the * scope and makes an OFF marker live. The checker reads only live `granted:`/`role:` tuples, * so a switched-off peer holds nothing to ANY check — `covers` included — without the checker * knowing the marker exists; and `seatScopeTuple` seats nothing for a subject whose marker is * live, so no re-provision can hand the grants back. `restoreToPeer` is the only way back. * The switch is also recorded in the directory (#2029, `system-switch-record.ts`, kind * `peer`), so a scope whose storage loses the marker — a wipe, a restore, a PITR rewind — gets * it back from every carry's re-assert, exactly as a module's does. * - **`peerGrantsStatus`** — the read half of that switch: every peer this scope holds or has * held grants for, and where each stands. The SAME predicate `admitPeer` gates on, so the * status a tenant reads and the answer a call gets cannot disagree. * - **`resolveVerticalInstanceFrom`** — "the instance of vertical Y in tenant T": one rule for * every directory that is asked. * * What is deliberately NOT here: who is calling. The door is handed a `VerticalCaller` by the * platform (the router on the hosted path, the local broker on the pure host), and trusts it the * way `getConnectorScope` trusts the connection id the control plane hands it. The door's own * guarantees are the ones above; the caller's liveness and tenancy are the platform's gate. */ import { type CheckSubject, type PeerSpec, type Scope, type TenantId, type ScopeId, type PeerInstanceResolution, type VerticalCaller, type VerticalResolution } from '@substrat-run/contracts'; import { PEER_SUBJECT_PREFIX, peerSubjectRef, type SwitchOutcome, type SwitchSql } from './system-switch.js'; export { PEER_SUBJECT_PREFIX, peerSubjectRef }; /** Directory-owned, on both adapters. The row survives a target's lifecycle changes. */ export declare const PEER_BINDINGS_DDL = "\n CREATE TABLE IF NOT EXISTS peer_bindings (\n tenant_id TEXT NOT NULL,\n caller_scope_id TEXT NOT NULL,\n vertical TEXT NOT NULL,\n target_scope_id TEXT NOT NULL,\n invalidated INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (tenant_id, caller_scope_id, vertical)\n );\n CREATE INDEX IF NOT EXISTS peer_bindings_target ON peer_bindings (target_scope_id);\n"; /** One peer vertical as every registered module declares it, together. */ export interface PeerDeclaration { readonly vertical: string; /** The door's allowlist. Empty for a receive-only peer. */ readonly operations: ReadonlySet; /** What `vertical:` is seated with at provisioning. */ readonly permissions: ReadonlySet; } export type PeerDeclarations = ReadonlyMap; /** * The union of every module's `peers` declaration, keyed by peer slug. Two modules naming one * peer contribute both of their allowlists and both of their key sets: each module speaks for * its own operations, and a reviewer reads each entry in its own module's permission section. */ export declare function collectPeers(manifests: Iterable<{ peers?: readonly PeerSpec[]; }>): Map; /** One grant provisioning seats — handed to `seatScopeTuple` by each adapter. */ export interface PeerSeat { readonly subject: string; readonly relation: string; readonly object: string; } /** * The grants a scope holds for its declared peers: `vertical:` / `granted:` / * `scope:`, one per declared key, in a stable order. Seated with `seatScopeTuple` — so a * grant an operator's switch tombstoned stays tombstoned (#1659), and a peer whose switch is off * gets nothing seated at all. */ export declare function peerSeats(peers: PeerDeclarations, scopeId: string): PeerSeat[]; /** * The door's admission (#1706), run inside the scope's own serialized task on every call, so a * switch pulled between two calls refuses the second. Throws `forbidden` — a door refusal, never * a K-35 denial: no permission was checked, exactly as for a capability's allowlist. Returns the * check subject the operation then runs as, with every ordinary `ctx.check` inside it. * * `operation: null` is a delivery (#1705), which names no operation and so meets no allowlist. */ export declare function admitPeer(db: SwitchSql, peers: PeerDeclarations, caller: VerticalCaller, operation: string | null): CheckSubject; /** * Move one peer's kill switch on one scope. Idempotent, and `held: false` with nothing written * when the peer has no authority reaching the scope: no grant and no marker here, and no live * TENANT-level `vertical:` grant (`tenantHeld`, which the caller reads from the directory). * A typo'd slug, or a peer this scope was never provisioned with, must not write a marker that * silently blocks it the day a version declaring it lands. A peer whose only authority here is a * tenant-level grant IS held (#2030): OFF writes the marker, which the door and the evaluator * both refuse it on, and tombstones nothing. Run inside one transaction. */ export declare function switchPeer(db: SwitchSql, input: { vertical: string; scopeId: string; to: 'on' | 'off'; at: string; tenantHeld?: boolean; fence?: string; }): SwitchOutcome; /** One peer's position on one scope, as `peerGrantsStatus` enumerates them. */ export interface PeerGrantsRow { vertical: string; calls: 'on' | 'off' | 'ungranted'; } /** * Every peer this scope holds or has held grants for, and where each stands (#1706) — the * read half of the kill switch, and `systemGrantsStatus`'s shape with the subject swapped. * * One SELECT enumerating the `vertical:` subjects this scope's storage has a `granted:` * tuple or an OFF marker for — revoked rows included, deliberately: a peer whose grants were * tombstoned must keep appearing, as `ungranted`, rather than vanishing from the view that * exists to say where it stands. A peer the scope never had any row for is absent, which is * the honest answer to "what does this scope know about peers" — "not installed here" is a * fact the DIRECTORY holds, not this storage, and the surfaces above join the two. * * Then `subjectGrantState` per subject: the SAME predicate `admitPeer` refuses on, so the * status a tenant reads and the answer the next call gets cannot disagree. * * `substr` rather than `LIKE`, as everywhere on this table: `LIKE` is case-insensitive in * SQLite and a Durable Object caps its patterns (#1655). */ export declare function peerGrantsStatus(db: SwitchSql, now: string): PeerGrantsRow[]; /** * The tenant a peer name is read in: its id, and its directory slug (`tenants.slug`), which is * the one prefix a push pinned to that tenant registers under. Both come from the directory * row of the tenant the platform authenticated the caller in, never from the name being * matched. `slug: null` reads names exactly. */ export interface PeerTenant { readonly id: TenantId; readonly slug: string | null; } /** * The registered names a declared peer name reaches in one tenant (#2252) — the one rule, as * the list a directory query binds (`vertical IN (…)`). * * A push pinned to a tenant registers `/`, while `substrat.calls` and * `peers` name the bare ``. So a bare declared name reaches its exact spelling and THIS * tenant's pinned one, and nothing else: never another tenant's `/` (a listed * vertical somebody else owns), and never the reverse — a declared `acme/crm` does not reach a * bare `crm`. An empty name reaches nothing. */ export declare function peerVerticalSpellings(declared: string, tenantSlug: string | null): string[]; /** `registered` is one of `peerVerticalSpellings(declared, tenantSlug)`. */ export declare function samePeerVertical(registered: string | null | undefined, declared: string, tenantSlug: string | null): boolean; /** * The caller as THIS vertical's `peers` names it (#2252), run at the door before anything else * reads the caller: a declaration naming the caller's registered spelling wins; otherwise a * pinned `/` caller is the bare `` declared, when that is declared. * `tenantSlug` is the platform's (`verticalCaller`), so another tenant's `/` stays * itself and is refused as undeclared. The result carries no slug: `admitPeer`, the door's pin * and the recorded subject all read the declared name. */ export declare function peerCallerAt(peers: PeerDeclarations, caller: VerticalCaller): VerticalCaller; /** A directory row, as much of it as the resolution reads. */ export type VerticalInstanceCandidate = Pick; /** * A live instance of the peer name `vertical` in `tenant`: of THAT tenant, registered under a * spelling of THAT vertical, primary (`isPrimaryScope`: not a fork, not a preview — a preview * must never receive another app's production calls), and `active` (a suspended or archived * instance answers nothing). What a call, a binding and the dashboard's picker all count. */ export declare function isPeerInstanceOf(scope: VerticalInstanceCandidate, tenant: PeerTenant, vertical: string): boolean; /** * "The instance of vertical Y in tenant T" (#1706) — the one rule every directory applies. * * A candidate is a live instance of that vertical in that tenant (`isPeerInstanceOf`). An exact * spelling wins over the * tenant-pinned one, so a tenant running both `crm` and `/crm` still resolves. * Exactly one → `resolved`; none → `not-installed`; more than one → `ambiguous`, refused * rather than guessed. * * The tenant is a parameter, and it is the ONLY tenant searched: nothing in a caller's request * names a tenant, so a cross-tenant answer is not reachable from here. */ export declare function resolveVerticalInstanceFrom(scopes: Iterable, tenant: PeerTenant, vertical: string): VerticalResolution; /** * Resolve a peer call using the tenant's explicit choice, if any. Every caller of this rule * supplies only scopes read from its own tenant; the predicate checks tenant and vertical again * so a corrupt or forged binding cannot turn a foreign scope into a destination. */ export declare function resolvePeerInstanceFrom(scopes: Iterable, tenant: PeerTenant, vertical: string, boundScopeId: ScopeId | null, invalidated?: boolean): PeerInstanceResolution; //# sourceMappingURL=peer.d.ts.map