import type { FileInfo, Files } from "../../index.js"; import type { JsonValue } from "../json.js"; import { RouterError } from "../router-core/envelope.js"; import type { WireFilesError, WireUploadedFile } from "./protocol.js"; /** How the bytes reached storage: direct to a presigned target, through the gateway's proxy PUT, or a keyed `upload(key, body)`. */ export type UploadVia = "presign" | "proxy" | "keyed"; /** What the hook sees of the landed object: its {@link FileInfo}, the key caller-facing (authorize's `keyPrefix` stripped). */ export type UploadedFileInfo = FileInfo; export interface UploadCompleteContext { /** The landed object, as `head()` reports it (keyed uploads: as `upload()` returned it). */ file: UploadedFileInfo; /** The full key in `files` — `file.key` with authorize's `keyPrefix` applied. Use it to read the landed bytes. */ storageKey: string; /** The `Files` instance this request resolved to (the per-request factory's result, if one is configured). */ files: Files; /** The completing request — cookies, headers and session live here. */ req: Request; /** The `context` the `authorize` hook returned for this request, if any. */ context: TContext | undefined; /** * Stable per upload: a hash of the upload token for keyless uploads, so a * replayed `complete` carries the same id (dedupe on it); a fresh id per * keyed request. */ uploadId: string; via: UploadVia; } /** * Runs once per verified upload. The resolved value is JSON-serialized and * handed back to the client as the upload result's `data`; throw to reject * the upload. */ export type OnUploadComplete = (ctx: UploadCompleteContext) => TData | Promise; /** What a {@link CompletionStore} remembers for a completed upload — replayed verbatim to a repeated `complete`. */ export interface CompletionRecord { file: WireUploadedFile; } /** * Makes keyless completions single-use. Before `onUploadComplete` runs, the * gateway looks the upload's id up; a hit is answered with the recorded result * and the hook does not fire again. After the hook resolves, the result is * written with a TTL (ms) that lasts until the upload token expires — after * that the token itself is refused. A `Map`-backed store works for a single * process; back it with Redis/KV/a table across instances. * * A get-then-set store narrows the replay window to two `complete`s racing * each other; make the hook idempotent on `uploadId` (a unique column) where * that matters. */ export interface CompletionStore { get: (uploadId: string) => CompletionRecord | undefined | Promise; set: (uploadId: string, record: CompletionRecord, ttl: number) => void | Promise; } /** * Throw from `onUploadComplete` to refuse an upload with a client-facing * reason: the client gets a 422 (`Validation`, reason `rejected`) carrying * `message`. A `FilesError` reaches the client the same way, under its own * code; any other throw is passed to the router's `onError` and the client * gets a generic 500, so its message (a SQL error, a connection string) never * leaves the server. */ export declare class UploadRejectedError extends RouterError { constructor(message: string); } /** * What the hook returns, as the gateway handles it: a value bound for * `JSON.stringify` on the wire. */ export type UploadData = JsonValue | undefined; export interface UploadLifecycle { onUploadComplete?: OnUploadComplete; /** What to do with the object when the hook (or the complete-time `maxSize` check) rejects it. */ onRejected: "delete" | "keep"; completions?: CompletionStore; req: Request; /** The `context` authorize returned for this request. */ context: unknown; } /** A stable, opaque id for an upload token: base64url(SHA-256(token)), 128 bits. */ export declare const uploadIdFor: (token: string) => Promise; /** * Delete a rejected object (unless configured to keep it). The rejection is * what the client needs to hear, so a removal that fails doesn't replace it; * it's reported alongside, as the message suffix this returns ("" when the * object is gone or kept). The suffix carries only what the client may read * of the failure; `report` (the router's `onError`) gets the original when * its detail is withheld. */ export declare const discardRejected: (files: Files, lifecycle: UploadLifecycle | undefined, storageKey: string, signal: AbortSignal, report: (error: unknown) => void) => Promise; /** * A hook failure as a per-completion bulk error on the keyless `complete` * response. A `RouterError` (`UploadRejectedError`) or `FilesError` is the * hook's message to the client; anything else — a plain `Error` from the app's * database, say — is reported to `report` and reaches the client only as a * generic failure. */ export declare const rejectionToWire: (cause: unknown, report: (error: unknown) => void) => WireFilesError; //# sourceMappingURL=upload-complete.d.ts.map