import type { Db } from "../db/connect.js"; import type { Persona, SeededRow } from "../types.js"; /** * The four questions the API plane asks about a row it planted. * * Every claim this engine makes about an endpoint rests on one of them, and * none of them is a reading of what a response *looked* like: * * - what does the row hold right now? * - did it take the exact value this request supplied? * - is it still there? * - if a probe destroyed it, can it be put back? * * They were four functions holding SQL, which quietly made "check the * application's authorization" mean "and also have superuser-ish access to its * Postgres". Everything above them — the request variants, the write decision * tree, the capability rule, the owner gates — is already storage-agnostic: it * reasons about a row we planted and a value we chose, not about tables. * * So this is the seam. `postgresOracle` is what has always run and answers by * querying the database directly. A second implementation answering the same * four questions through the application's own endpoints is what makes the * check work against MySQL, Mongo, Firebase or a Rails app, where there is no * connection string to hand and the only door onto the data is the API itself. * * The oracle is deliberately *not* asked anything of the form "does this look * like a leak". Every answer here is an exact fact about a row we planted, * matched by primary key, which is what keeps a finding a fact rather than a * judgement. * * Three things about this shape are worth knowing before a second * implementation is written against it, because they are where an * application-plane oracle will not simply drop in: * * `ensurePresent` is the odd one out — it is the only method that changes * anything, and it re-plants a row *under the primary key we chose*. Most * APIs assign their own identifiers on create, so an HTTP implementation * will often have no way to honour that and will have to answer `false`. * That is survivable — it costs delete checks, which are reported as * unmade — but it is the method most likely to need rethinking rather * than reimplementing. * `snapshot` is specified as the row by *column* name, and `changedColumns` * compares those names. What an endpoint hands back is a representation: * renamed, camel-cased, partial. An HTTP implementation can still answer * the question the caller actually asks — "did anything about this row * move?" — but only over the fields the app chooses to show. * Nothing here said *who is asking*, and now it does — see {@link Asking}. */ export interface ResourceOracle { /** * The whole row as it stands, for telling "unchanged" from "changed * elsewhere". `null` when it cannot be read at all — which is not the same * as an empty row and must never be read as one. */ snapshot(row: SeededRow, as: Asking): Promise | null>; /** * Did this row take the value the request supplied? * * The response body is a hopeless oracle for a write. A handler that modifies * another user's row and answers `{"ok":true}` echoes nothing we planted, so * it read as "no evidence of a leak" — which is silence dressed as safety. * * Because we choose the value, finding it in the row is an exact match on * something only this request could have put there. */ fieldEquals(row: SeededRow, column: string, expected: string, as: Asking): Promise; /** * Does the row still exist? The only trustworthy oracle for a delete. * * Answers `true` when it cannot tell. An implementation that guessed * "missing" would credit a delete that never happened. */ exists(row: SeededRow, as: Asking): Promise; /** * Put the row back, if an earlier probe took it, and say whether it is there * afterwards. * * Re-planting is exactly the same act as seeding it in the first place: the * values recorded when we planted it, so the primary key that is the oracle * stays the primary key it was. Nothing is inferred and nothing is matched * approximately. * * Best-effort by nature: a cascade may have taken children with it, and a * generated column may refuse the value it generated. A failure costs a * check, which is reported, rather than costing the truth of one. * * An implementation is allowed to put the row back under a *different* * identifier and rewrite `row` in place to say so — an application that * assigns its own ids has no other option. Callers must therefore rebuild any * request that names the row after calling this, rather than reusing a URL * they computed earlier. */ ensurePresent(row: SeededRow, as: Asking): Promise; } /** * Who is asking. * * Against Postgres this is always the privileged run connection and the answer * does not vary, so the implementation ignores it. Over HTTP there is no such * thing as an unattributed read: every question has to be asked as somebody, * and asking as the wrong somebody turns "the row is still there" into "the row * is gone" — which is the difference between a delete check and a false * positive. So the persona travels with the question rather than being fixed * when the oracle is built. * * `null` means no particular identity, which an HTTP implementation must treat * as "cannot tell" rather than as "anonymous, and the answer stands". */ export type Asking = Persona | null; /** * The implementation that has always run: ask Postgres directly. * * Exact by construction — every query is keyed on the primary key we planted, * and every failure is swallowed into the answer that costs a check rather than * the answer that invents one. * * {@link Asking} is accepted and ignored, deliberately and not as an oversight: * this connection sees every row regardless of who the application thinks is * logged in, so answering "as" a persona here would be pretending to a * restriction that does not exist. The parameter is part of the interface * because the HTTP implementation cannot work without it. */ export declare function postgresOracle(db: Db): ResourceOracle;