import { type ActionDeclaration, type Route } from "./api/routes.js"; import type { ApiAuth } from "./api/session.js"; import type { Classification, OwnershipModel, TableOwnership } from "./types.js"; export interface CrosslineConfig { /** * Where the database is. Prefer `env:VAR_NAME` so a password never lands in a * file that gets committed. */ db?: string; schemas?: string[]; mode?: "read_only" | "full"; /** * The database roles this application's requests run as. Inferred from grants * and existing policies when omitted; set explicitly when inference cannot * tell two candidate roles apart, or when the names are unconventional. */ roles?: { anonymous?: string; authenticated?: string; }; /** * The table that holds application users, e.g. "public.app_users". Inferred * from structure when omitted (the table other tables point owner-shaped * columns at); set it when inference names the wrong table or none at all. */ userTable?: string; /** * API-plane checks against a running application. Requires an ephemeral * environment: unlike the data plane, these writes are real. */ api?: { /** * Where the running application is. Optional: with `server` set, or with * nothing set at all on a database that enforces nothing itself, Crossline * starts the app and uses the port it chose. */ target?: string; /** * How to start the application, in Playwright's `webServer` shape. * * `command` is read off `package.json`'s scripts when omitted, and only * where the answer is unambiguous. `url` is where it will listen; omitted, * a free port is chosen and passed as `PORT`. `false` refuses to start * anything. */ server?: false | { command?: string; url?: string; timeout?: number; reuseExistingServer?: boolean; env?: Record; }; auth?: /** * A bearer token signed with a symmetric secret. `supabase_jwt` is the * old name for the same thing and still works. */ { kind: "hs256_jwt" | "supabase_jwt"; secret: string; anonKey?: string; } | { /** * Supabase projects created from 1 October 2025, which sign sessions * with an asymmetric key rather than the shared JWT secret. * * `secretKey` must be written as `env:NAME`. Crossline will not read * a project's secret key out of a file that gets committed, and * refusing is cheaper than explaining afterwards. */ kind: "supabase_admin"; url: string; secretKey: string; publishableKey: string; } | { /** * The same Supabase project with no secret key at all — the one to * reach for. Both values are already public: `url` and the * publishable (`anon`) key are shipped in the browser bundle, so * neither has to be kept out of this file. */ kind: "supabase_anon"; url: string; publishableKey: string; } | { /** * Clerk. `secretKey` must be written as `env:NAME`, and must be a * *development* instance's `sk_test_…`: creating a session through * the Backend API is documented as available in testing only. */ kind: "clerk_admin"; secretKey: string; apiUrl?: string; expiresInSeconds?: number; } | { /** * Auth0. `audience` is required — without it the tokens Auth0 issues * are opaque and your application cannot validate them, so every * request would 401 and the refusals would be Crossline's rather than * the application's. Every secret must be written as `env:NAME`. */ kind: "auth0_admin"; domain: string; clientId: string; clientSecret: string; audience: string; connection?: string; managementToken?: string; managementClientId?: string; managementClientSecret?: string; } | { /** * Amazon Cognito. The AWS credential must be written as `env:NAME`, * and the app client must have `ALLOW_ADMIN_USER_PASSWORD_AUTH` * enabled. */ kind: "cognito_admin"; userPoolId: string; clientId: string; region?: string; accessKeyId: string; secretAccessKey: string; sessionToken?: string; token?: "id" | "access"; apiUrl?: string; } | { /** * Firebase Auth. `apiKey` is the project's Web API key, which ships * in the browser bundle and is public by construction — it is the one * strategy here that asks for no secret at all. */ kind: "firebase_auth"; apiKey: string; apiUrl?: string; tenantId?: string; } | { kind: "header"; template: Record; } | { kind: "authjs_session"; table?: string; columns?: { token?: string; user?: string; expires?: string; }; template?: Record; } | { kind: "none"; }; /** * Which field of a resource carries its owner, keyed by the collection the * resource is created in: `{"/api/notes": "ownerId"}`. * * Only read on a run with no database, where there is no schema to infer * ownership from and the run has to establish it from the application's own * answers. Where it cannot — an application whose account identifiers are * `u_1` and `u_2`, where a field holding one is as consistent with a counter * as with ownership — a cross-user read stays an observation and fails * nothing. This is how somebody who knows the application says which field * it is. * * It is data, not truth. The run still has to see the field on each * account's own resource holding that account's own identifier; a * declaration it can see is wrong is refused and says so, and a declaration * cannot buy a finding that the application's answers contradict. */ owners?: Record; /** * Two existing accounts to log in as, instead of signing two up. * * For an application whose registration is closed. They must be two * *unrelated* accounts — that is the premise the whole run rests on, and * supplying credentials is the one thing that moves it out of Crossline's * hands. * * `password` must be written as `env:NAME`. A password in a file that gets * committed is the failure this refuses, and refusing is cheaper than * explaining afterwards. */ accounts?: DeclaredAccount[]; }; /** * Endpoints to check, for a framework route discovery cannot read. * * `"GET /api/documents/:id"`, or `{ "method": "GET", "path": "/api/..." }`. * Merged with whatever discovery finds, so listing a handful of endpoints is * what makes SvelteKit, Nuxt, Remix, Nest, Rails or Django checkable without * waiting for a parser. */ routes?: (string | DeclaredRoute)[]; /** * How to call the Next.js server actions the build named. * * The one thing `next build` does not record. Without it an action is named * and never called; with it the same checks every endpoint gets are run * against it. Keyed by export name and source file, never by the id, which is * a hash of the build and changes underneath any file that pins it. */ serverActions?: ActionDeclaration[]; /** Corrections to the inferred ownership model, keyed by table id. */ overrides?: Record; /** * Private tables the developer accepts as uncheckable, by table id. * A table holding user data that cannot be checked makes the run * inconclusive; listing it here records that a human saw the gap and chose * to proceed. It still appears in the coverage list on every run. */ acceptUnchecked?: string[]; } export declare function configPath(cwd: string): string; export declare function loadConfig(cwd: string): CrosslineConfig; export declare function saveConfig(cwd: string, config: CrosslineConfig): void; /** * Where a run leaves what it wrote down — the last report, a generated fix. * * It ignores itself. These are artifacts of a run, not source, and the person * this tool is for did not ask for a directory and will not think to exclude * one: they will `git add .` and commit a report describing their own holes. * `.next` and `.vercel` do exactly this, for exactly this reason. * * Written only when the directory is created, so deleting the line is a * decision the developer gets to keep. */ export declare function stateDir(cwd: string): string; /** Where a connection string came from, for the line printed on every run. */ export type DbSource = { kind: "flag"; } | { kind: "config"; } | { kind: "config-env"; variable: string; } | { kind: "environment"; variable: string; } | { kind: "env-file"; file: string; variable: string; } | { kind: "supabase"; file: string; }; export interface ResolvedDatabase { connectionString: string; source: DbSource; /** * One line for the terminal, naming the database about to be worked on and * where the choice came from. Never contains the password. */ provenance: string; } export interface ResolveDatabaseOptions { /** `--db`. */ flag?: string; env?: NodeJS.ProcessEnv; /** * Where the command was run. Supplying it turns on discovery — reading the * repository for a database when nothing else names one. Omitting it means * the caller wants the four explicit sources and nothing else. */ cwd?: string; /** * True for the commands whose writes are real and cannot be rolled back: * `--api`, which drives a running application, and `fix --apply`, which * executes DDL. A database Crossline found by itself is not one it will * write to at a non-loopback address without being told to. */ realWrites?: boolean; /** `--allow-remote-db`: the developer saying they meant that host. */ allowRemote?: boolean; } /** * Work out which database to talk to: flag, then the config file, then the * ambient environment, then the repository. * * The file outranks the environment on purpose. This tool attempts writes, and * `fix --apply` executes DDL — a project whose config names its database must * not be silently redirected by whatever `DATABASE_URL` happens to be exported * in the shell, because "my .env pointed at production" is exactly the * afternoon this ordering prevents. `env:NAME` in the file still resolves * through the environment, so secrets stay out of committed files; a pointer * at an unset variable is an error that names it, never a silent fallback to * some other variable. * * Discovery sits at the bottom for the same reason, and the reason is worth * saying twice: reading `.env.local` is a convenience, and a convenience that * can outrank a decision already made is not a convenience. A developer who * exported `DATABASE_URL` chose that database; a file we found chose nothing. * Put discovery anywhere above the environment and the safety ordering above * silently reverses. */ export declare function resolveDatabase(config: CrosslineConfig, options?: ResolveDatabaseOptions): ResolvedDatabase; /** * The same resolution, answering "nothing names a database" with `null` rather * than with an error. * * This distinction is the whole trigger for the run with no database, so it is * worth being exact about what `null` means and what it does not. It means no * source *named* one: no `--db`, no `db` in the config file, none of the * environment variables, and nothing found in the repository. It is decided from * what is written down, before a single connection is attempted — so a Postgres * application whose database happens to be down still resolves a connection * string here, still takes the database path, and still fails loudly when the * connection is refused. There is no code path from a failed connection to this * returning `null`, and there must never be one: degrading to a weaker check and * calling the result a pass is the failure this whole tool exists to refuse. * * Every other error this function can raise — a config pointing at an unset * variable, a discovered remote host that `--api` would write to — is still * thrown. Those are not "no database"; they are a database nobody has confirmed. */ export declare function findDatabase(config: CrosslineConfig, options?: ResolveDatabaseOptions): ResolvedDatabase | null; /** * The same resolution, for callers that only want the string. * * No `cwd`, so no discovery: a caller that has not said where it is standing * has not asked for the repository to be read. */ export declare function resolveConnectionString(config: CrosslineConfig, flag?: string, env?: NodeJS.ProcessEnv): string; /** * How to write down, in a file meant to be committed, the database that was * just read — or nothing, if it has no name worth writing. * * Only an `env:NAME` pointer qualifies. A literal connection string in the * config file is a password in git history, and `--db` is by far the most * common way one arrives. So the rule is: if the resolved string is exactly the * value of an environment variable Crossline already looks at, name that * variable — which is true whether it arrived through the environment or was * typed as a flag. Otherwise write no `db` key, and let the next run resolve it * the same way this one did. */ export declare function dbSettingFor(resolved: string, env?: NodeJS.ProcessEnv): string | undefined; /** Convert config overrides into the shape the inference engine accepts. */ export declare function overridesFor(config: CrosslineConfig): Record>; /** * Seed the config with the tables we were unsure about, and only those. * * Writing every table into the file would freeze the model, so a new table * added next week would never get classified. Writing only the uncertain ones * keeps the confirmation step short and keeps inference live. * * `init` is re-run — after a migration, after adding a table, or just because * someone forgot they had. So the contract is that a second `init` never costs * a developer an edit they made by hand. This used to be enforced by listing * every key to carry forward, which meant every key added later started life * silently dropped: `acceptUnchecked` was one, and `roles` was still one until * this comment was written — the one setting people reach for precisely because * inference could not tell their two database roles apart, quietly deleted by * the command that is supposed to help. Spreading `existing` first inverts the * default: unknown keys survive, and only the three fields computed below are * allowed to overwrite anything. */ export declare function configFromModel(model: OwnershipModel, existing: CrosslineConfig, /** * How the database that was just read is named in the environment, from * `dbSettingFor`. Omitted, or undefined, means it has no name we can write * down — and then no `db` key is written at all. */ dbSetting?: string): CrosslineConfig; /** * The running application to probe, if anybody named one. * * Never guessed. A target nobody wrote down is a target Crossline does not * have, and defaulting to `localhost:3000` would mean sending real, unrollable * writes at whatever happened to be listening on that port. */ export declare function resolveApiTarget(config: CrosslineConfig, override?: string): string | undefined; /** * Resolve `env:NAME` indirections inside the API auth block, so a JWT signing * secret is referenced by a config file rather than stored in one. */ export declare function resolveApiAuth(config: CrosslineConfig, env?: NodeJS.ProcessEnv): ApiAuth; /** * One account that already exists, as whoever knows about it writes it down. * * The password is a reference and never a value. `resolveApiAccounts` refuses * anything else. */ export interface DeclaredAccount { /** What the application knows the account by: an email address, or a username. */ identifier: string; /** `env:NAME`. A literal is refused. */ password: string; /** Anything else the login body needs, merged over the conventional shape. */ extra?: Record; } /** * The ownership somebody declared, keyed by collection path. * * Validated here rather than where it is weighed, so a declaration that could * never be acted on is an error while the person who wrote it is still looking * at it. What it *means* is decided in `establishOwnership`, against what the * application actually returned — this only checks that it is the shape of a * thing that could be checked at all. */ export declare function declaredOwners(config: CrosslineConfig): Record | undefined; /** * The two accounts to log in as, with each password read out of the * environment. * * Two rules, both refusals rather than warnings. There have to be exactly two, * because the whole run is one account being asked for another's data and there * is no such thing as a third of a crossing. And the password has to be * `env:NAME`: this file is meant to be committed, `api.auth.secretKey` already * holds that line, and a login password is no less of a credential than a * signing key. */ export declare function resolveApiAccounts(config: CrosslineConfig, env?: NodeJS.ProcessEnv): [DeclaredAccount, DeclaredAccount] | undefined; /** The variable a `env:NAME` reference names, or null if it is not one. */ export declare function envNameOf(value: string): string | null; /** * The account list, checked for everything that does not need the environment. * * Split out so the MCP tool can refuse a bad declaration while the agent is * still holding the answer, using exactly the words the next run would use. * Whether the variable is *set* is deliberately not checked here: an agent may * well declare the reference before a human exports the value, and failing that * at declaration time would be refusing the right answer at the wrong moment. */ export declare function checkedAccounts(accounts: DeclaredAccount[] | undefined): [DeclaredAccount, DeclaredAccount] | undefined; /** * One endpoint, as whoever knows about it writes it down. * * `"GET /api/documents/:id"` covers the common case in one line. The object * form exists for the two things a line cannot carry, and both are knowledge * the file tree genuinely does not hold: which table the resource lives in when * the URL does not say, and a body the handler will accept. */ export interface DeclaredRoute { method: string; path: string; /** Table id, e.g. `public.documents`, when the path does not name it. */ resource?: string; /** A body this endpoint accepts, so a required field cannot fail the request. */ example?: Record; } /** * The endpoints named in the config file. * * The escape hatch that makes every framework Crossline cannot parse usable * anyway. A malformed entry throws rather than being dropped: a route the * developer wrote down and Crossline silently ignored is a check they believe * they are running and are not, which is the failure mode this whole file * exists to avoid. */ export declare function declaredRoutes(config: CrosslineConfig): Route[]; /** * The server action call shapes named in the config file. * * Held to the same rule as `routes`: a malformed entry throws. A declaration * that was quietly dropped is an action the developer believes is being called * and is not, and on this surface that belief is especially costly — a server * action is usually a *mutation*, and the whole reason this key exists is that * an application whose writes are all actions had none of them probed. */ export declare function declaredActions(config: CrosslineConfig): ActionDeclaration[];