import type { CapabilityUrl, Persona, PersonaName, ProbeResult, SeededRow } from "../types.js"; import type { Asking } from "./oracle.js"; import type { ResourceOwnership } from "./plant.js"; import { type Route } from "./routes.js"; /** * Cross-user reads of a resource this run proved belongs to somebody. * * A read that comes back with another account's resource is a fact either way. * What decides whether it is a *finding* is whether the run established who the * resource belongs to, and with no schema that is open by default: a document * an application shares on purpose and one it leaks produce byte-identical * responses. So the default stays what it was — the read is printed as an * observation and fails nothing. * * This pass is the exception, and it runs only over the resources where * ownership was *established* rather than assumed: each account's own resource, * read back by that account, carries a field holding that account's own * identifier, and those identifiers are wide enough that a match could not have * happened by accident. See `establishOwnership` in `plant.ts`, which is where * that is decided and where every reason it can be refused is written down. * * Once ownership is established the read is subject to every guard the * database-backed path applies, and for the same reasons: * * the oracle the 128-bit marker this run planted, and only when the * request did not carry it. Never the identifier, never a * similarity judgement about what a response looks like. * the owner's proof the same endpoint has to serve the resource to the * account it belongs to. An endpoint that answers nobody * has refused nobody, and reading its silence as * protection is scoring an outage as security. * the share-link rule the whole of it, unchanged, through the same * `classifyCapabilities` the schema path uses — with the * identifier's entropy measured from the identifiers this * application generated rather than read off a column. * That rule is what took the corpus from 17.8% to 6.7% * and it is not optional here. * * It lives beside `probeApiPlane` rather than inside it because the premise is * different: that function is told there is no ownership premise and behaves * accordingly, and this is the narrow case where the run built one for itself. * The reads it takes over are withheld from that function by the caller, so * every request is made exactly once and no read is both observed and reported. */ export interface AttestedReadOptions { baseUrl: string; timeoutMs?: number; /** The read endpoints over resources whose ownership was established. */ routes: Route[]; headersFor(as: Asking): Record; /** The planted rows, one per persona per resource. */ rows: SeededRow[]; personas: Record; /** What was established about each resource, keyed by resource id. */ ownership: Map; /** * Whether this run can be a specific signed-in account. When it cannot, the * signed-in half is not emitted at all — a request with no credential on it * labelled "a signed-in stranger" would be a claim about an actor that never * existed. */ signedIn: boolean; } export interface AttestedReadRun { probes: ProbeResult[]; settledRoutes: string[]; unreached: { route: string; reason: string; }[]; tablesReached: string[]; capabilityUrls: CapabilityUrl[]; } export declare function probeAttestedReads(opts: AttestedReadOptions): Promise;