import type { ConnectionProbe, EntitlementGrant, EntitlementGrantInput, QueryScopeInput, ReadScopeTableInput, Scope, ScopeId, ScopeQueryResult, DenialFilter, DenialSummary, PermissionDenial, ScopeTable, ScopeTablePage, Tenant, TenantId } from '@substrat-run/contracts'; import { type IdentityTenant } from './identity-tenants.js'; /** * A typed HTTP client for the control-plane API — the vertical side of the * connect seam (first-flow.md slice 4). * * A vertical that runs against a *separately deployed* shared control plane uses * this to (a) register its tenant, entitlements, and scope on boot, and (b) gate * each request on the directory's authoritative lifecycle — `assertScopeActive` * fails closed exactly as the kernel's own `getScope` does, so a suspend in the * console bites the vertical's next operation even across process and deployment * boundaries. * * What it deliberately does NOT do: write roles or grants. Those are not on the * control-plane HTTP surface (api.ts §4.5 — permission writes are the human * checkpoint, D-22/D-29), so a connected vertical keeps its permission model * local and treats the shared plane as the authority for tenant/scope lifecycle * and entitlements only. * * `fetch` is injectable: pass a Worker service-binding's fetch, or the router's * own `app.fetch` for an in-process test, instead of the global. */ export interface ControlPlaneClientOptions { /** Base URL of the control-plane API, e.g. `https://cp.example.com` or `http://127.0.0.1:8788`. */ baseUrl: string; /** * The platform actor id stamped as the audit subject on every write — sent as the * dev-only `x-platform-actor` header, and ONLY when no `serviceToken` is set. With a * token the control plane resolves the subject from the token (its fixed service * actor), so the header is never consulted there and is not sent (#980). */ actor: string; /** * A service credential (`x-service-token`) proving the caller is an authorized * vertical, not just anyone with an actor id. Required when the control plane * has real auth — the dev-actor header alone does not authenticate there. When * set, it is the request's only credential: `actor` is not sent. */ serviceToken?: string; /** Defaults to the global `fetch`. */ fetch?: typeof globalThis.fetch; } export interface ClientProvisionScopeInput { tenantId: TenantId; scopeId: ScopeId; slug?: string; kind?: string; name?: string; vertical?: string | null; jurisdiction?: 'eu' | 'us' | 'global'; } /** A non-2xx (or unreachable) control-plane response. `status` is 0 on a transport error. */ export declare class ControlPlaneError extends Error { readonly status: number; /** * The provider's own answer, when the plane refused a connect because the credential * was rejected upstream (#605, 422). Carried so a console can show WHY — "Scrive: * No valid access credentials were provided" — instead of a generic save failure. */ readonly probe?: ConnectionProbe | undefined; constructor(status: number, message: string, /** * The provider's own answer, when the plane refused a connect because the credential * was rejected upstream (#605, 422). Carried so a console can show WHY — "Scrive: * No valid access credentials were provided" — instead of a generic save failure. */ probe?: ConnectionProbe | undefined); } export declare class ControlPlaneClient { private readonly baseUrl; private readonly actor; private readonly serviceToken?; private readonly fetchImpl; constructor(options: ControlPlaneClientOptions); /** * One request, and the one place a failure is read: a transport error or a non-2xx answer * throws `ControlPlaneError`, so what comes back is always a successful `Response`. A 404 * is handed back as-is when the caller allows it. */ private send; private call; createTenant(input: { id: TenantId; slug: string; name: string; }): Promise; grantEntitlement(tenantId: TenantId, key: string, plan?: EntitlementGrantInput): Promise; provisionScope(input: ClientProvisionScopeInput): Promise; /** * Confirm the scope exists here, moving the directory row provisioning → active. * * In this (push) direction the vertical has already built the scope locally, so * registering and confirming are the same moment — but they stay two calls so * `provisionScope` means one thing everywhere, and so the directory is never the * one deciding a scope is ready (K-31). */ activateScope(tenantId: TenantId, scopeId: ScopeId): Promise; getTenant(tenantId: TenantId): Promise; getScopeRecord(tenantId: TenantId, scopeId: ScopeId): Promise; listEntitlements(tenantId: TenantId): Promise; /** * The tenants a login builds for, each flagged with whether it holds the `builder` * entitlement — the builder studio's membership read (builder-plane.md §4). `externalId` * is the login's OIDC subject. Service-token gated: an unset token is refused by the * plane, never bypassed. * * The answer is PARSED, not asserted: a plane that renamed `entitled`, or answered an * `{ error }` body with a 200, would hand a cast `entitled: undefined` — falsy, so the * studio would lock the tenant out with the ordinary "not enabled" page and nothing * anywhere would say the directory had changed shape. A refused parse throws instead. * The message names the shape that was wrong, never the body — the body is directory * facts about a person's tenants. */ identityTenants(externalId: string): Promise; /** Every table in the scope's own database, with row counts (Data view). */ listScopeTables(tenantId: TenantId, scopeId: ScopeId): Promise; /** A bounded page of one table of the scope's database. */ readScopeTable(tenantId: TenantId, scopeId: ScopeId, input: ReadScopeTableInput): Promise; /** One read-only SQL statement against the scope's database — the console (#219). */ queryScope(tenantId: TenantId, scopeId: ScopeId, input: QueryScopeInput): Promise; /** * The scope's recorded permission refusals, bucketed per (actor, permission) with the * window's own facts beside them — the view to open first, because the raw log's * volume is attacker-influenceable and a prober can flood a newest-first page. */ summarizeDenials(tenantId: TenantId, scopeId: ScopeId, filter?: DenialFilter): Promise; /** The raw rows behind a bucket, newest first. */ listDenials(tenantId: TenantId, scopeId: ScopeId, filter?: DenialFilter): Promise; /** * The gate. Throws unless the tenant is active AND the scope exists and is * active — the same fail-closed logic the kernel's `validateScopeAccess` * applies locally, so a tenant-level cascade suspend bites too, not just a * per-scope one. Call it before handing a request to the local scope host. */ assertScopeActive(tenantId: TenantId, scopeId: ScopeId): Promise; } //# sourceMappingURL=client.d.ts.map