import type { OwnershipModel, Persona, PersonaName, ProbeResult } from "../types.js"; import { type AppIdentities, type LoginCredential } from "./identity.js"; import { type CreatedResource, type PlantOutcome } from "./plant.js"; import { type ApiProbeOutcome } from "./probe.js"; import { type Route } from "./routes.js"; /** * The API plane with no database anywhere in it. * * Every load-bearing input the prober used to take came from Postgres: the * seeded rows, the ownership classification, the settlement of a write and of a * delete. So a Rails app on MySQL, a Django app on Mongo and a Go service on * DynamoDB got nothing from this engine however leaky they were — not because * an endpoint probe cares what the storage is, but because the engine did. * * This is the same probe, driven entirely through the application: * * identities two accounts created through the app's own signup, each with * its credential confirmed by the app naming it back. * resources created by POSTing to each collection, with the body corrected * from the application's own validation errors until it is * accepted, and a 128-bit marker in a field it asked for. * the oracle read-back as the persona who created the resource. * * What it will and will not say is narrower than the database-backed run, and * deliberately so. A stranger's write or delete that lands is a finding, because * no sharing model in existence makes an unrelated account's write to somebody * else's resource correct. * * A stranger's *read* depends on one further fact, and the default is silence. * Where nothing establishes who the resource belongs to, the read is an * observation and never a finding: a leak and a document the application shares * on purpose are byte-identical from out here, and asserting one would be * exactly the false positive that gets a test uninstalled. Where the run *did* * establish it — each account's own resource came back carrying that account's * own identifier, and the application did not flag the resource published — the * read is a finding like any other crossing, subject to every guard the * schema-backed path applies, the share-link rule included. `plant.ts` decides * that, this function carries it, and one condition inside `probeApiPlane` acts * on it — the same pass, the same ordering, the same guards, one line different. */ export interface HttpApiRunOptions { baseUrl: string; /** The endpoints, from discovery or from the config file. */ routes: Route[]; mode?: "read_only" | "full"; timeoutMs?: number; /** Overrides for the conventional signup and "who am I" paths. */ signupPaths?: string[]; mePaths?: string[]; /** How many corrections a creation body gets before we report what defeated it. */ rounds?: number; /** * Ownership the developer or the agent stated, keyed by collection path: * `{"/api/notes": "ownerId"}`. See `PlantOptions.declaredOwners`, which is * where it is weighed — and where a declaration this run can see is wrong is * refused rather than acted on. */ declaredOwners?: Record; /** * Existing accounts to log in as, instead of signing two up. * * For an application with closed registration, and for an agent that already * has test credentials. Held to exactly the bar a signup is: the credential * has to be proven by the application naming the account back, never assumed * from a 200. See `identity.ts`. */ accounts?: [LoginCredential, LoginCredential]; /** Where a login lives, in the order they are tried. */ loginPaths?: string[]; } export interface HttpApiRunResult { /** The probes, or null when the run never got far enough to make one. */ outcome: ApiProbeOutcome | null; identities: AppIdentities; planted: PlantOutcome | null; /** * What the run took each resource to be, returned so the report is built from * the same model the probes ran against rather than from a second guess at * it. * * Nothing here read a schema, so an entry is `unknown` unless this run * established ownership from the application's own answers — see * `establishOwnership`, which is where every reason it can be refused is * written down. */ model: OwnershipModel; /** * What this run created in the application and did not remove, in words. * * Empty is the good answer and a real one: where the run proved the owner's * own delete lands through a discovered endpoint, it uses that endpoint to * take its own resources back out and confirms each one is gone. Where it * could not prove that, nothing is deleted and everything is named here — * deleting on the strength of an endpoint whose correctness is the thing * under test is not on offer. */ residue: string[]; /** Why nothing was checked, when nothing was. Null on a run that checked something. */ unavailable: string | null; } export declare function runApiPlaneOverHttp(opts: HttpApiRunOptions): Promise; /** * Remove the resources this run created, and name whatever survives. * * The data plane does this with a transaction and the database-backed API plane * does it with a DELETE statement over its own connection. Here there is * neither: the only door onto the data is the application, and the application's * DELETE endpoint is one of the things under test. So the bar is a fact from * this run rather than an assumption about the endpoint: * * - the run must already have watched the owner's own delete land through * this exact endpoint. That is the positive check `probeApiPlane` makes * before any crossing is attempted, and it is what distinguishes "this * endpoint removes the caller's own resource" from "this endpoint might * remove anything at all"; * - the request is made as the account that created the resource, naming the * identifier the application itself assigned it; * - and it is *confirmed* afterwards by asking for the resource again as its * owner. A 200 from a DELETE proves nothing; the resource being gone does. * * Fail any of those and nothing is deleted and the resource is named instead. * Read-only runs delete nothing at all, by the developer's own instruction. */ export declare function removeWhatWeCreated(args: { opts: { baseUrl: string; timeoutMs?: number; headersFor(as: Persona): Record; }; created: CreatedResource[]; personas: Record; probes: ProbeResult[]; }): Promise; /** * The run, in the form a developer reads. * * Kept here rather than in the report renderer because it is the whole point of * the phase: what was established, what was observed and could not be * established, and what defeated the attempt to create data. A run that printed * only the findings would be hiding the two lists that say how much of the * application was actually reached. */ export declare function describeHttpRun(result: HttpApiRunResult): string;