import type { SeededRow } from "../types.js"; import type { Asking, ResourceOracle } from "./oracle.js"; /** * The same four questions, asked of the application instead of the database. * * This is what makes the API plane work where there is no connection string: a * Rails app on MySQL, a Django app on Mongo, a Go service on DynamoDB. An * endpoint probe never cared what the storage was, and with this neither does * the engine. * * Everything here is a read-back *as the persona whose creation request made * the resource*, which is the reason the oracle interface had to learn who is * asking. Against Postgres the run connection sees every row and the question * "is it still there" has one answer; over HTTP the same question asked as the * wrong person answers "no" for a resource that is sitting right there, and a * delete probe would be credited with destroying something it never touched. * * The exactness is unchanged and comes from the same place it always did. We * planted the resource, the marker in it is 128 bits from a CSPRNG that has * never been transmitted before, and every answer below is an exact string * comparison against the identifier the application handed back when it created * it. Nothing here reads a response to decide whether it *looks* like a leak. * * Where the application will not answer, the reply is the one that costs a check * rather than the one that invents a finding: `snapshot` says it could not read * the resource, `fieldEquals` says no, and `exists` says the resource is still * there. In particular a read that comes back 401, 403 or 500 *for the owner* is * never read as absence — the row is either gone or the app is broken, and only * one of those is a fact about a delete. */ /** Where one kind of resource lives in the application's URL space. */ export interface ResourcePlan { /** * The id this resource is known by for the whole run, e.g. `app.notes`. It * takes the place of a table id and is matched against route paths by exactly * the same rule, so `/api/notes/:id` finds it. */ resourceId: string; /** Where a create goes, e.g. `/api/notes`. */ collection: string; /** Where a read-back goes, e.g. `/api/notes/:id`. */ item: string; /** The dynamic segment's name in {@link item}. */ param: string; /** The field of the representation that carries the identifier. */ idField: string; /** The field the marker was planted in, once one has been found. */ markerField: string | null; } export interface HttpOracleOptions { baseUrl: string; /** How to reach each kind of resource, keyed by `resourceId`. */ plans: Map; /** The credential for a persona; an anonymous client for `null`. */ headersFor(as: Asking): Record; timeoutMs?: number; /** * Make a replacement for a resource a probe destroyed, rewriting `row` in * place with whatever identifier the application assigns it. * * Optional, and its absence is honest rather than fatal: without it a * destroyed resource stays destroyed, every later delete check against that * resource reports that it could not be made, and no delete is credited on * the strength of an absence an earlier probe caused. */ recreate?(row: SeededRow, as: Asking): Promise; /** Every request this oracle makes, for the transcript. */ onRequest?(entry: { method: string; path: string; status: number | null; as: string; }): void; } export declare function httpOracle(opts: HttpOracleOptions): ResourceOracle; /** * The object in this response that is the resource we asked for. * * A representation arrives bare, wrapped in `data`, or as a one-element list, * and all three are ordinary. What is never assumed is that whatever came back * *is* the resource: the identifier has to match, exactly and as a whole value. */ export declare function representation(text: string, idField: string, id: string): Record | null; /** * A field of a representation, by the name the row was recorded under. * * An endpoint hands back `createdAt` for a column called `created_at`, and the * two are the same field. Case and separators are folded away and nothing else * is: a name that does not match after that is a field the application does not * show, and the answer is that it could not be read rather than a search for * something similar. */ export declare function pick(record: Record, name: string): unknown;