/** * Find the endpoints without being told about them. * * File-system routing (Next.js, Remix, SvelteKit) is convention-based and can be * read exactly. Express-style registration is discovered by reading the source, * which is best-effort by nature — so anything we cannot resolve is reported as * a gap rather than quietly dropped, and `routes` in the config file is always * available as the escape hatch. */ export interface Route { method: string; /** e.g. "/api/documents/:id" */ path: string; /** Dynamic segment names, in order. */ params: string[]; /** Where we found it, for the report. */ source: string; framework: "next-app" | "next-pages" | "express" | "sveltekit" | "nitro" | "openapi" | "config"; /** * The table the resource behind this endpoint lives in, said rather than * guessed. * * Only ever set on a declared endpoint. Without it a route is matched to a * table by its path — `/api/documents/:id` finds `documents` — which is right * often enough to be the default and wrong whenever the URL and the table * disagree. `/api/v2/files/:id` over `public.documents` is unprobeable * otherwise: nothing tells the run whose row to ask for, and it is reported * as unreached. * * A name no table in the schema answers to is a mistake in the declaration, * not a licence to fall back to the path — see how it is read in `probe.ts`. */ resource?: string; /** * A request body the application accepts, supplied by whoever declared this * endpoint. * * Merged *under* the fields a probe needs — the identifier it is crossing * with and the marker it plants — so an example can satisfy a handler's * required fields without being able to overwrite what the check is about. * Nothing is invented from it: the fields are sent exactly as given. */ example?: Record; } /** * A registration we could see but could not read a path out of. * * Discovery is best-effort by design, and the honest failure is not "found * nothing" — it is "found a call that registers an endpoint and could not say * which URL". Silently dropping it makes an unchecked resource look like a * resource with nothing wrong with it, which is the one thing this tool must * never do. So it is named, with the file and the call, and `routes` in the * config file closes it in one line. */ export interface RouteGap { /** Path of the file, relative to the project root. */ source: string; /** The call as far as we could read it, e.g. "router.get(`/${r}/:id`, …)". */ detail: string; reason: string; } export interface RouteDiscovery { routes: Route[]; gaps: RouteGap[]; /** * Next.js server actions found in the build output. * * Kept apart from `routes` rather than folded in, because they are not routes * and nothing downstream should probe them as though the arguments were * known. They exist here so the run can name them. */ serverActions: ServerActionDiscovery; /** * Something about *where* we looked that a reader needs in order to read the * numbers correctly. Null when there is nothing to say, which is the usual * case. */ note: string | null; } export declare function discoverRoutes(cwd: string): Route[]; export declare function discoverRoutesAndGaps(cwd: string): RouteDiscovery; /** * Directories the workspace declaration says are packages. * * Exported because route discovery is not the only thing that has to look past * the root: whether the repository serves HTTP at all, and which command starts * it, are the same question asked one directory up. Two walks would be two * chances to disagree about what a package is. * * Three declarations are read, in the order a tool would resolve them: * `pnpm-workspace.yaml`, the `workspaces` key in `package.json` (npm, yarn, * bun), and `turbo.json`. The first two name their packages. `turbo.json` does * not — Turborepo delegates that to the package manager — so its presence is * taken as "this is a monorepo" and the conventional `apps/*` + `packages/*` * are walked, with a `package.json` required in each candidate so that a * directory which merely happens to be called `apps` contributes nothing. */ export declare function workspacePackages(cwd: string): string[]; /** * One callable server action, as the build recorded it. * * Every field here is copied out of Next's own manifest rather than inferred. * What is *not* here is the one thing needed to call it — the argument list. * See {@link discoverServerActions}. */ export interface ServerAction { /** The opaque id the framework dispatches on, sent as `Next-Action`. */ id: string; /** The URL a request carrying that id must be POSTed to, e.g. `/documents`. */ path: string; /** Source file it was declared in, relative to the project root. */ source: string; /** * The export name for a `"use server"` module — `renameDocument` — or Next's * synthetic `$$RSC_SERVER_ACTION_0` for one declared inline in a component. * * {@link UNATTRIBUTED} on a build older than Next 15.5, which records no name * at all. See `attributed`. */ exportedName: string; /** * False when the build predates Next 15.5 and recorded neither the file nor * the export name — so the action is real, callable and named only by its id * and its URL. Kept as a field rather than inferred from the string, because * "we could not tell you which function this is" is a different statement to * make than "this one is inline", and the report makes both. */ attributed: boolean; /** True when the name above is synthetic, i.e. the action has no export name. */ inline: boolean; runtime: "node" | "edge"; } /** Stand-in name for an action a pre-15.5 build declined to attribute. */ export declare const UNATTRIBUTED = "(name not recorded by this Next.js build)"; export interface ServerActionDiscovery { actions: ServerAction[]; /** * Set when the source tree declares server actions and the build output that * would name them is absent. * * This is the case that has to be loud. Without a build there is no manifest, * with no manifest there are no ids, and with no ids the run would find no * actions — which is indistinguishable, in the output, from an application * that has none. So the *source* is checked for `"use server"` separately, * and disagreeing with the build is reported rather than resolved. */ unbuilt: string | null; } /** * Read the server actions out of a production build. * * `.next/server/server-reference-manifest.json` maps each action id to the file * it was declared in, its export name, and the *pages* that can dispatch it — * and `.next/app-path-routes-manifest.json` turns one of those page keys into * the URL the request goes to. Both are Next's own output, so the page URL is * exact rather than a second implementation of the segment rules. * * Two things this deliberately does not do. * * It never reads `encryptionKey`, which sits in the same file and is a build * secret. Nothing here needs it and nothing here should be able to print it. * * And it does not try to describe how to *call* an action. The manifest records * no argument types, and a call built on a guessed signature is not a weak * check — it is rejected by the framework before the action's own body decides * anything, and reading that rejection as a refusal would be scoring an * exception as authorization. So these are reported as found and not checked. */ export declare function discoverServerActions(cwd: string): ServerActionDiscovery; /** The slot the target resource's identifier goes into. */ export declare const ACTION_ID = "{id}"; /** The slot this run's own planted value goes into, for an action that writes. */ export declare const ACTION_MARKER = "{marker}"; /** * How to call one server action, as its author describes it. * * Keyed by export name and source file rather than by id, because the id is a * hash of the built module: it changes on every build, so a declaration keyed * on one would be stale the moment it was written down. The join against the * build is what turns this into a callable endpoint. */ export interface ActionDeclaration { /** The export name, exactly as the build recorded it, e.g. `renameDocument`. */ exportedName: string; /** * Source file relative to the project root, e.g. `app/documents/actions.js`. * * Optional, and required in practice as soon as two files export the same * name — see {@link joinDeclaredActions}, which refuses to choose. */ source?: string; /** * The positional argument list. * * {@link ACTION_ID} anywhere inside it is replaced with the identifier of the * row being crossed to, and {@link ACTION_MARKER} with a value this run mints * and then looks for in the database. Everything else is sent verbatim. * * An argument list with no {@link ACTION_ID} is not an error: an action that * takes no identifier is a collection read, and "did the other user's row * come back" is exactly as answerable there. */ args: unknown[]; /** * The column {@link ACTION_MARKER} ends up in, when it is not the one the * seeder planted its canary in. Only consulted for an action that writes. */ writes?: string; /** The table the resource lives in, when the page URL does not name it. */ resource?: string; } /** A declaration and the built action it turned out to describe. */ export interface DeclaredAction { action: ServerAction; declaration: ActionDeclaration; } export interface ActionJoin { /** Declarations that named a real action in the build. Callable. */ matched: DeclaredAction[]; /** Declarations that named nothing the build has, with the reason. */ unmatched: { declaration: ActionDeclaration; reason: string; }[]; /** Actions the build named that no declaration describes. Still unchecked. */ undeclared: ServerAction[]; } /** * Join what the agent said to what the build actually contains. * * The join is the whole safety property of this feature. A declaration is a * claim from outside the run and can be wrong in the ordinary ways — a renamed * export, a moved file, a build that has not been re-run — and the failure to * avoid is the quiet one: a declaration that matched nothing, silently dropped, * leaving the developer believing an action was checked. So every declaration * lands in exactly one of `matched` or `unmatched`, and every built action in * `matched` or `undeclared`. * * A name that matches actions in two different files with no `source` to tell * them apart is refused rather than resolved. Picking one would mean calling a * function nobody named with arguments meant for a different one — and the * result would be a 500 that looks exactly like a refusal. */ export declare function joinDeclaredActions(actions: ServerAction[], declarations: ActionDeclaration[]): ActionJoin; /** * Put the run's own values into the argument list its author wrote. * * Whole-string replacement of the two placeholders, anywhere they appear — * inside an array, inside an object, or as part of a longer string, because an * action taking `{ where: { id: "{id}" } }` or a path-shaped argument is as * ordinary as one taking the bare value. Nothing else is touched. */ export declare function fillActionArgs(args: unknown[], values: { id: string | null; marker: string | null; }): unknown[]; /** Does this argument list mention the placeholder at all? */ export declare function argsMention(args: unknown[], placeholder: string): boolean; export interface RouteJoin { /** Declared endpoints a parser found too. Probed once, not twice. */ agreed: Route[]; /** Declared endpoints no parser found. The reason to write one down. */ added: Route[]; /** * Declared endpoints whose path a parser did read, and at which it saw a * different set of methods. * * Not an error and not dropped — the declaration wins, because the person * writing it knows things the file does not say. But a parser that read the * file and did not see this method is a disagreement worth putting in front * of someone, since one of the two is wrong and neither can tell which. */ contradicted: { declared: Route; found: string[]; }[]; } /** Compare a declaration list against what discovery read, without deciding. */ export declare function joinDeclaredRoutes(declared: Route[], discovered: Route[]): RouteJoin; export declare function paramsIn(path: string): string[]; /** * Work out which table an endpoint serves, so we know whose row to ask for. * `/api/documents/:id` -> documents. Falls back to the singular form. */ export declare function tableForRoute(route: Route, tableIds: string[]): string | null;