import { z } from 'zod'; /** * The ONE error model for every surface on the platform (`docs/architecture/error-model.md`, * issue #113): RFC 9457 `application/problem+json`, a CLOSED code taxonomy, and one * mapper — replacing the seven hand-rolled `onError` handlers that today choose a * status by matching on error message TEXT. * * Three properties this is built for: * * - **Machine-readable.** `validation_failed on field 'email'` is recoverable by a * client — or by a build agent — without a person reading a log. `500 Something * went wrong` is not. * - **Documentable.** The same schema that validates a problem body is emitted into * `/openapi.json` (`openapi.ts`), so the API surface finally describes how it can * FAIL and not only how it succeeds. Decision 22 cashed in again. * - **Additive to adopt.** `toProblem` maps an unrecognised throw to `internal` exactly * as the hand-rolled handlers do, so each layer could adopt this without a flag day. * * ## Where the rollout stands * * Phases 1–3 are in: the taxonomy and `toProblem` (contracts), the kernel's own error * classes joined to it, and `wireFailure` — the value an error becomes when it has to * cross the ScopeDO boundary, because a throw cannot carry structure across it. What * remains is phase 4: the transports reading `code` instead of matching messages, and * the deprecated `error` duplicate coming back out of the body. */ /** * The base for `type` URIs. * * DERIVED from the code rather than written per entry, deliberately: whether we * actually serve a page at each of these URLs is still open (RFC §6 Q2), and nothing * throws a problem yet, so no `type` value has reached a client. Flipping the * decision stays a one-line change here for exactly as long as that holds. */ export declare const PROBLEM_TYPE_BASE = "https://substrat.net/errors"; /** What a problem body is served as. Never `application/json` — RFC 9457 §3. */ export declare const PROBLEM_CONTENT_TYPE = "application/problem+json"; /** * The `type` of a failure that has a status and nothing else. * * RFC 9457 §4.2.1: `about:blank` means "no semantics beyond the status code", and the * title is then the status phrase. That is the honest shape for the two cases a * transport cannot type — an untyped throw it refuses to call the platform's fault, * and a downstream status it is relaying — and it is what keeps the closed taxonomy * closed while every body still parses as a problem. */ export declare const PROBLEM_TYPE_BLANK = "about:blank"; /** * The taxonomy. CLOSED — an open one is a suggestion. * * A module never invents a code. It narrows an existing one with a `reason` slug it * owns (`conflict` + `reason: 'already_exported'`), which is the star topology * applied to failure: a vertical branches on an engine's reason without importing * the engine's types. */ export declare const errorCode: z.ZodEnum<{ conflict: "conflict"; forbidden: "forbidden"; internal: "internal"; not_found: "not_found"; permission_denied: "permission_denied"; precondition_failed: "precondition_failed"; rate_limited: "rate_limited"; unauthenticated: "unauthenticated"; unavailable: "unavailable"; validation_failed: "validation_failed"; }>; export type ErrorCode = z.infer; /** `permission_denied` → `https://substrat.net/errors/permission-denied`. */ export declare function problemTypeFor(code: ErrorCode): string; /** * Status and human title per code. `Record` on purpose: adding a code * without deciding what it means to HTTP is then a compile error, not a 500 found in * production. */ export declare const PROBLEM_CATALOG: { readonly unauthenticated: { readonly status: 401; readonly title: 'Unauthenticated'; }; readonly permission_denied: { readonly status: 403; readonly title: 'Permission denied'; }; readonly forbidden: { readonly status: 403; readonly title: 'Forbidden'; }; readonly not_found: { readonly status: 404; readonly title: 'Not found'; }; readonly conflict: { readonly status: 409; readonly title: 'Conflict'; }; readonly validation_failed: { readonly status: 400; readonly title: 'Validation failed'; }; readonly precondition_failed: { readonly status: 412; readonly title: 'Precondition failed'; }; readonly rate_limited: { readonly status: 429; readonly title: 'Rate limited'; }; readonly unavailable: { readonly status: 503; readonly title: 'Service unavailable'; }; readonly internal: { readonly status: 500; readonly title: 'Internal error'; }; }; /** One field-level complaint, mapped from a Zod issue. */ export declare const validationIssue: z.ZodObject<{ path: z.ZodString; message: z.ZodString; }, z.core.$strip>; export type ValidationIssue = z.infer; /** * The extension members each code may carry — declared per entry, never free-form. * * These are enforced where it matters: at the THROW site, by `substratError`, which * both types and parses them. The wire schema below is one flat object rather than a * ten-way discriminated union, because a `oneOf` of ten variants documents worse than * one object does and buys a narrowing no client asked for. Per-code narrowing of the * emitted document is RFC §6 Q1, deferred with the model layer that would own it. */ export declare const PROBLEM_EXTENSIONS: { readonly unauthenticated: z.ZodObject<{}, z.core.$strict>; readonly permission_denied: z.ZodObject<{ permission: z.ZodOptional; entity: z.ZodOptional>; }, z.core.$strip>; readonly forbidden: z.ZodObject<{ reason: z.ZodOptional; }, z.core.$strip>; readonly not_found: z.ZodObject<{}, z.core.$strict>; readonly conflict: z.ZodObject<{ reason: z.ZodOptional; }, z.core.$strip>; readonly validation_failed: z.ZodObject<{ errors: z.ZodOptional>>; }, z.core.$strip>; readonly precondition_failed: z.ZodObject<{ entity: z.ZodOptional>; }, z.core.$strip>; readonly rate_limited: z.ZodObject<{ retryAfter: z.ZodOptional; }, z.core.$strip>; readonly unavailable: z.ZodObject<{}, z.core.$strict>; readonly internal: z.ZodObject<{}, z.core.$strict>; }; /** The extensions legal on one code, as a type — what `substratError` accepts. */ export type ExtensionsFor = z.infer<(typeof PROBLEM_EXTENSIONS)[C]>; /** * The wire body. RFC 9457 members, plus `code`, plus every declared extension. * * `errors.test.ts` asserts this object carries every field any entry of * `PROBLEM_EXTENSIONS` declares — the join between the two is checked in CI rather * than by remembering to edit both. */ export declare const problem: z.ZodObject<{ type: z.ZodString; title: z.ZodString; status: z.ZodNumber; detail: z.ZodOptional; instance: z.ZodOptional; error: z.ZodOptional; code: z.ZodOptional>; permission: z.ZodOptional; entity: z.ZodOptional>; reason: z.ZodOptional; errors: z.ZodOptional>>; retryAfter: z.ZodOptional; }, z.core.$strip>; export type Problem = z.infer; /** * The sentence a FAILED response carried, whatever shape the body arrived in (#971). * * Four hand-rolled control-plane clients restated this fallback, and they did not agree: * the console and the vertical client read `detail ?? error`, the CLI read four members * against the published schema, and the DASHBOARD read `error` ALONE — so against a * transport that had adopted `toProblem` and dropped the deprecated duplicate, the * dashboard would throw away the sentence written for the occurrence and show * `409 Conflict` instead. That is the failure mode worth naming: not a crash, a message * that says nothing, on the screen where somebody is trying to find out why. * * The order is how specific each member is, and it is the union of what the four readers * separately reached for. `detail` is about THIS occurrence (RFC 9457 §3.1.4). `error` is * the deprecated duplicate of it, still what an older deployed control plane and several * hand-rolled `onError`s answer with. `message` is neither, but a relayed fault from * something in front of the control plane writes it and the CLI already read it. `title` * is last because it is stable per code — a class of failure, not an instance. * * It deliberately does NOT validate the body against `problem` first. Every member it * reads is an optional string under the same name either way, so a strict pass would * narrow nothing — and it would REFUSE the bodies this helper exists for: the relayed * fault with no `type`, the pre-#113 `{ error }`, the `onError` that never adopted * `toProblem`. A caller that needs the `code` or the field errors does parse strictly * (the CLI's `readProblem`), because for THOSE the schema is what makes the answer safe. * * Returns `undefined` — not a fabricated sentence — when the body said nothing readable. * The caller owns the fallback, because only it knows what it was doing: a status line, * a raw slice, the name of the command. Takes the PARSED body rather than the response, * so the callers that read a member BESIDE the sentence — the dashboard's `probe`, the * CLI's `issues` — do not parse it twice, and a caller with no `Response` at all (a * service-binding reply, a test) can still use it. */ export declare function problemDetail(body: unknown): string | undefined; /** * Where a `SubstratError` keeps its code when the class itself is unavailable. * * `name` is a SECOND reading of the code, not a transport for it. Phase 2 proposed it * as the way to cross the `ScopeDO` hop and that was wrong — measured against workerd, * a thrown error arrives carrying its message and nothing else, with `name` folded into * the message and reset. **Errors cross that boundary as a value now** (`wireFailure`, * below), not as a throw. * * What this prefix still earns: a duplicate copy of a package in one build, a structured * clone, or any other place the prototype is gone but the object survives — `errorCodeOf` * reads the name and still answers correctly. Cheap, and it costs nothing to keep. */ export declare const ERROR_NAME_PREFIX = "Substrat."; /** * A throw that already knows what it means. * * `message` stays the human sentence and nothing more, so logs, stack traces and the * contract suite's message assertions all read exactly as they do today. The code * rides in `name`, which is what lets it survive the hop. */ export declare class SubstratError extends Error { readonly code: ErrorCode; readonly status: number; readonly extensions: Readonly>; constructor(code: ErrorCode, message: string, extensions?: Record); } /** * The code a throw carries, however little of it survived. * * Three readings, in order of fidelity: the live `code` property (same isolate), the * `Substrat.` name (crossed a boundary), and the legacy class names above. A * throw this cannot classify is not ours, and `toProblem` answers `internal` for it. */ export declare function errorCodeOf(err: unknown): ErrorCode | undefined; /** * Build a typed error. The extensions are checked against the code at COMPILE time * and parsed at runtime, so a `retryAfter` on a `not_found` is caught at the throw * site rather than discovered in a response body. */ export declare function substratError(code: C, message: string, extensions?: ExtensionsFor): SubstratError; /** * Recognise one of ours — by shape, never by `instanceof` alone. * * Two copies of a package in one build already make `instanceof` a coin toss; a * serialising boundary makes it a certainty in the wrong direction. */ export declare function isSubstratError(err: unknown): err is SubstratError; /** Zod's issue list, flattened to the wire shape. */ export declare function validationIssuesFrom(error: z.ZodError): ValidationIssue[]; /** * Map any throw onto a problem body and its status — the one function replacing every * hand-rolled `onError` and the control plane's regex table. * * **`internal` never carries `detail`.** An unrecognised throw is by definition one * whose message nobody reviewed for what it discloses, and these surfaces have * cross-tenant reach. The existing posture is right; this preserves it rather than * quietly widening it in the name of better errors. */ export declare function toProblem(err: unknown, instance?: string): Problem; /** * A problem body for a status and nothing else — the `about:blank` form. * * Two callers, both transports, both relaying rather than raising: * * - **A throw the taxonomy does not recognise.** Every vertical answers one with the * caller's 400 and relays the message, deliberately (#559: an unrecognised throw must * not claim to be the platform's fault, because the control plane retries 5xx). That * status is a decision about blame, not a claim about what went wrong, and this is the * body that says so. * - **A status raised somewhere else.** A downstream vertical's own refusal, a Durable * Object fault the runtime named (502). Inventing a code for those would put our * vocabulary on someone else's failure. * * `detail` is carried as the caller passes it. That is safe here and not in `toProblem` * because a caller of THIS function has a status it chose or received, which means it * has already looked at what it is relaying; `toProblem`'s `internal` branch is the one * holding an unreviewed message, and it still refuses to disclose it. */ export declare function problemForStatus(status: number, detail?: string, instance?: string): Problem; /** * The statuses an operation can actually answer with today, for the emitted document. * * `precondition_failed` (412) and `rate_limited` (429) are declared in the taxonomy * so that `If-Match` (#129) and rate limiting (#130) add no vocabulary when they * land — but nothing raises them yet, and documenting a failure that cannot occur is * worse than documenting none. They join this list with the features that raise them. * * This narrows the RFC's §6 Q1 leaning ("emit the full set") on the same reasoning * that motivated the question. */ export declare const DOCUMENTED_ERROR_CODES: readonly ErrorCode[]; /** * An error flattened for a boundary that carries only data — the DO↔coordinator wire * (#113 phase 3, `docs/architecture/error-model.md` §3). * * This exists because a THROW cannot carry structure across the ScopeDO hop: workerd * delivers a thrown error's message and nothing else, folding `name` into it and * dropping every own property (measured — `adapter-cloudflare`'s contract suite pins * it). So the error stops being thrown across the boundary and starts being returned * across it, as a value, which is the one shape that survives intact. */ export declare const wireFailure: z.ZodObject<{ name: z.ZodString; message: z.ZodString; code: z.ZodOptional>; extensions: z.ZodOptional>; }, z.core.$strip>; export type WireFailure = z.infer; /** Flatten a throw for the wire, losing nothing this side of the boundary knows. */ export declare function toWireFailure(err: unknown): WireFailure; /** * Rebuild a throw from the wire. * * The rebuilt error is a `SubstratError` carrying the original `name`, NOT an instance * of the original class — contracts cannot import the kernel, and reviving arbitrary * classes over a wire is a capability nobody should want. That is enough for every * consumer in the repo, because they all read the code or the name, never the * constructor. `instanceof PermissionDenied` stays false here and always will; it is * the wrong question, and `errorCodeOf` is the right one. */ export declare function fromWireFailure(failure: WireFailure): Error; //# sourceMappingURL=errors.d.ts.map