UNPKG

@shirudo/base-error

Version:

A cross-environment base error class for TypeScript applications, designed for seamless use across Node.js, browsers, and edge runtimes.

1,210 lines (1,194 loc) 47.1 kB
var __typeError = (msg) => { throw TypeError(msg); }; var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg); var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj)); var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value); var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value), value); var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method); // src/errors/BaseError.ts var _redactor, _messageMask, _RECURSE, _MAX_REDACT_DEPTH, _MAX_REDACT_NODES, _ENVELOPE_KEYS, _ROOT_ENVELOPE_KEYS, _BaseError_static, applyMask_fn, isWalkable_fn, redactWalk_fn, childRegion_fn, _SAFE_TRIAGE_KEYS, _BaseError_instances, renderMessage_fn, setCause_fn, _MAX_CAUSE_DEPTH, serializeCause_fn, _MAX_JSON_NODES, serializeCircularObject_fn, installLazyStack_fn, filterInternalFrames_fn; var _BaseError = class _BaseError extends Error { /** * Creates a new BaseError instance with automatic name inference. * * @param message – Human-readable explanation (name will be inferred from constructor) * @param cause – Optional underlying error or extra context * @param options – Optional runtime name settings */ constructor(message, cause, options = {}) { super(message); __privateAdd(this, _BaseError_instances); /** Epoch-ms timestamp (numeric) */ this.timestamp = Date.now(); /** * ISO-8601 timestamp (string) for log aggregators that prefer text. Derived * from {@link timestamp} (one clock read), so the two can never disagree * across a millisecond boundary. */ this.timestampIso = new Date(this.timestamp).toISOString(); __privateAdd(this, _redactor); /** * Mask for the technical `message` in {@link toString}, set only by a * deny-list {@link redact} whose keys include `"message"`. `toString` (unlike * `toLogObject`) cannot run an arbitrary redactor, but a denied `message` is * an explicit statement that the text is sensitive, so the one string * rendering the library controls honors it. Follows the redactor's * last-wins semantics: `redactAllow`/`redactWith` clear it. */ __privateAdd(this, _messageMask); const resolvedName = options.name ?? this.constructor.name; this.name = resolvedName; this._tag = resolvedName; if (cause !== void 0) { __privateMethod(this, _BaseError_instances, setCause_fn).call(this, cause); } if (Object.getPrototypeOf(this) !== new.target.prototype) { Object.setPrototypeOf(this, new.target.prototype); } __privateMethod(this, _BaseError_instances, installLazyStack_fn).call(this); } /** * Redacts the given keys (deep, at any depth) from the **log** output * (`toLogObject`/`toJSON`). Sticky on the instance, so it also applies when a * logger auto-serializes the error via `JSON.stringify`. * * ⚠️ Scope: redaction rewrites the **log object**, not every string render. * When `keys` includes `"message"`, {@link toString} masks the technical * message too; everything else (`err.stack`, whose header carries the raw * message, and Node's `console.log(err)` inspection, which prints the stack) * stays unredacted. When redaction matters, log errors only through a * structured serializer that hits `toJSON`, never via string interpolation. * * @param keys - Property names to mask wherever they appear in the log object. * @param options - `mask` defaults to `"[REDACTED]"`. */ redact(keys, options) { const mask = options?.mask ?? "[REDACTED]"; const denied = new Set(keys); __privateSet(this, _messageMask, denied.has("message") ? mask : void 0); __privateSet(this, _redactor, (log) => { var _a; return __privateMethod(_a = _BaseError, _BaseError_static, redactWalk_fn).call(_a, log, (key, value) => { var _a2; return denied.has(key) ? __privateMethod(_a2 = _BaseError, _BaseError_static, applyMask_fn).call(_a2, mask, value, key) : __privateGet(_BaseError, _RECURSE); }, "root"); }); return this; } /** * Allow-list redaction (higher assurance than {@link redact}): within any * **data** region (a `details` subtree at any depth, the data-bearing * fields of a `cause`, and any subclass-added top-level field): masks every * leaf whose key is **not** listed, so a newly-added field leaks nothing by * default. Container objects are recursed so nested allowed leaves survive. * Only the library's own structural envelope is kept: the fixed top-level * fields ({@link BaseError.#ROOT_ENVELOPE_KEYS}: `name`/`message`/`stack`/ * `code`/`category`/`retryable`/`timestamp`/`timestampIso`/`cause`/`details`) * and a cause's top-level structural envelope keys (`name`/`message`/`stack`/ * `code`/`category`/`retryable`). Any other top-level field (e.g. one a * subclass adds via `buildLogObject`) is data: its leaves are masked unless * allow-listed. A cause's foreign fields (anything outside that fixed set, * and everything nested beneath them) are treated as data, so a plain object * that merely *looks* like a structured error cannot smuggle siblings (or * envelope-named keys buried in foreign subtrees) through. Sticky; last * redactor wins. * * ⚠️ Scope: rewrites the **log object** only. The technical `message` is part * of the kept structural envelope, so `toString`, `err.stack`, and Node's * `console.log(err)` inspection carry it unchanged; see {@link redact} for * masking the message itself. * * @param keys - Data leaf keys allowed to survive in the log. * @param options - `mask` defaults to `"[REDACTED]"`. */ redactAllow(keys, options) { const mask = options?.mask ?? "[REDACTED]"; const allow = new Set(keys); __privateSet(this, _messageMask, void 0); __privateSet(this, _redactor, (log) => { var _a; return __privateMethod(_a = _BaseError, _BaseError_static, redactWalk_fn).call(_a, log, (key, value, region) => { var _a2, _b; if (Array.isArray(value) || __privateMethod(_a2 = _BaseError, _BaseError_static, isWalkable_fn).call(_a2, value)) { return __privateGet(_BaseError, _RECURSE); } const kept = region === "root" && __privateGet(_BaseError, _ROOT_ENVELOPE_KEYS).has(key) || allow.has(key) || region === "cause" && __privateGet(_BaseError, _ENVELOPE_KEYS).has(key); return kept ? value : __privateMethod(_b = _BaseError, _BaseError_static, applyMask_fn).call(_b, mask, value, key); }, "root"); }); return this; } /** * Sets a custom redactor applied to the full log object. Use for allow-lists * or scrubbing the technical `message`. Sticky; the last redactor wins. * * ⚠️ Scope: applies to the **log object** only. A custom redactor cannot be * mapped onto the one-line {@link toString} render, so `toString`, * `err.stack`, and `console.log(err)` inspection keep the raw technical * message even when the redactor scrubs it from the log. */ redactWith(redactor) { __privateSet(this, _messageMask, void 0); __privateSet(this, _redactor, redactor); return this; } /** * Assembles the raw log object (no redaction). Subclasses override this to * add their own fields; the public {@link toLogObject} applies redaction to * the complete assembled object. */ buildLogObject() { const { name, message, timestamp, timestampIso, stack } = this; const cause = this.cause; const json = { name, message, // The original technical message timestamp, timestampIso, stack, cause: __privateMethod(this, _BaseError_instances, serializeCause_fn).call(this, cause, /* @__PURE__ */ new Set(), 0) }; return json; } /** * Serialises the error for logs. Includes technical message, stack and cause, * with the instance redactor applied (see {@link redact} / {@link redactWith}). * * ⚠️ This is a **log** serialization: it carries the technical message, stack, * cause chain and raw `details`. **Never return it to a client.** Anything that * auto-serializes the error (`JSON.stringify`, `res.json(err)`, `Response.json`, * `return err`) reaches {@link toJSON}, which is an alias of this method, and * leaks the same payload. For client-safe output use the `public-error` * subpath (`@shirudo/base-error/public-error`, `project`), which projects only * an allow-listed, message-free public view. */ toLogObject() { const raw = this.buildLogObject(); if (!__privateGet(this, _redactor)) { return raw; } try { return __privateGet(this, _redactor).call(this, raw); } catch { const safe = { message: "[log redaction failed]" }; for (const key of __privateGet(_BaseError, _SAFE_TRIAGE_KEYS)) { if (key in raw) { safe[key] = raw[key]; } } return safe; } } /** * JSON serialization for logging-oriented consumers. Alias of * {@link toLogObject}, so it returns the same **log** shape: technical message, * stack, cause chain and raw `details`. * * ⚠️ Because `JSON.stringify(err)`, `res.json(err)`, `Response.json(err)` and * `return err` all route through `toJSON`, sending an error down any of those * paths leaks the full technical payload to the client. **Never serialize an * error straight into a response.** Produce a client payload through the * `public-error` subpath (`project` / `toProblem`) instead. This shape is also * the input that {@link StructuredError.fromJSON} reconstructs, which is why it * intentionally retains the stack and cause chain. */ toJSON() { return this.toLogObject(); } /** * Readable one-liner plus full nested cause chain. Honors a deny-listed * `"message"` (see {@link redact}) per BaseError in the chain; other * redaction shapes rewrite only the log object. */ toString() { var _a; const parts = []; let current = this; const seen = /* @__PURE__ */ new Set(); while (current != null) { if (seen.has(current)) { parts.push("[Circular cause chain]"); break; } seen.add(current); if (current instanceof _BaseError) { parts.push(`[${current.name}] ${__privateMethod(_a = current, _BaseError_instances, renderMessage_fn).call(_a)}`); } else if (current instanceof Error) { parts.push(`${current.name}: ${current.message}`); } else { parts.push(String(current)); } current = typeof current === "object" && current !== null && "cause" in current ? current.cause : void 0; } return parts.join("\nCaused by: "); } }; _redactor = new WeakMap(); _messageMask = new WeakMap(); _RECURSE = new WeakMap(); _MAX_REDACT_DEPTH = new WeakMap(); _MAX_REDACT_NODES = new WeakMap(); _ENVELOPE_KEYS = new WeakMap(); _ROOT_ENVELOPE_KEYS = new WeakMap(); _BaseError_static = new WeakSet(); applyMask_fn = function(mask, value, key) { return typeof mask === "function" ? mask(value, key) : mask; }; isWalkable_fn = function(value) { if (value === null || typeof value !== "object" || Array.isArray(value)) { return false; } const proto = Object.getPrototypeOf(value); if (proto === Object.prototype || proto === null) return true; return Object.keys(value).length > 0; }; redactWalk_fn = function(value, decide, region, depth = 0, state = { nodes: 0 }) { var _a, _b, _c, _d, _e; if (Array.isArray(value) || __privateMethod(_a = _BaseError, _BaseError_static, isWalkable_fn).call(_a, value)) { if (depth >= __privateGet(_BaseError, _MAX_REDACT_DEPTH)) { return "[Max redaction depth exceeded]"; } if (++state.nodes > __privateGet(_BaseError, _MAX_REDACT_NODES)) { return "[Max redaction size exceeded]"; } } if (Array.isArray(value)) { return value.map( (item) => { var _a2; return __privateMethod(_a2 = _BaseError, _BaseError_static, redactWalk_fn).call(_a2, item, decide, region, depth + 1, state); } ); } if (__privateMethod(_b = _BaseError, _BaseError_static, isWalkable_fn).call(_b, value)) { const out = /* @__PURE__ */ Object.create(null); for (const [key, val] of Object.entries(value)) { const decision = decide(key, val, region); if (decision === __privateGet(_BaseError, _RECURSE)) { if (Array.isArray(val) || __privateMethod(_c = _BaseError, _BaseError_static, isWalkable_fn).call(_c, val)) { const childRegion = __privateMethod(_d = _BaseError, _BaseError_static, childRegion_fn).call(_d, region, key); const childDepth = childRegion === "cause" ? depth : depth + 1; out[key] = __privateMethod(_e = _BaseError, _BaseError_static, redactWalk_fn).call(_e, val, decide, childRegion, childDepth, state); } else { out[key] = val; } } else { out[key] = decision; } } return out; } return value; }; childRegion_fn = function(region, key) { if (region === "data") return "data"; if (key === "details") return "data"; if (key === "cause") return "cause"; if (region === "cause") { return __privateGet(_BaseError, _ENVELOPE_KEYS).has(key) ? "cause" : "data"; } return __privateGet(_BaseError, _ROOT_ENVELOPE_KEYS).has(key) ? "root" : "data"; }; _SAFE_TRIAGE_KEYS = new WeakMap(); _BaseError_instances = new WeakSet(); // ---------------------------------------------------------------- // Internal helpers // ---------------------------------------------------------------- /** * The message as {@link toString} renders it: masked when a deny-list * covers `"message"`, verbatim otherwise. Fail-closed: a throwing function * mask yields the default marker, never the raw message. */ renderMessage_fn = function() { var _a; if (__privateGet(this, _messageMask) === void 0) { return this.message; } try { return String( __privateMethod(_a = _BaseError, _BaseError_static, applyMask_fn).call(_a, __privateGet(this, _messageMask), this.message, "message") ); } catch { return "[REDACTED]"; } }; /** * Sets the cause property as non-enumerable (like native Error.cause). * * Uses Object.defineProperty instead of native `new Error(msg, { cause })` * for universal compatibility. This approach works across all runtimes * (Node.js 14+, Deno, Cloudflare Workers, browsers) without version detection, * since Object.defineProperty is ES5 and universally supported. */ setCause_fn = function(cause) { try { Object.defineProperty(this, "cause", { value: cause, configurable: true, writable: true, enumerable: false }); } catch { this.cause = cause; } }; _MAX_CAUSE_DEPTH = new WeakMap(); /** * Intelligently serializes the cause for JSON output. * Preserves stack traces, StructuredError fields, and nested data. * Uses a seen set to detect circular cause chains, and a depth bound so an * acyclic-but-very-deep chain is capped instead of recursing unbounded. */ serializeCause_fn = function(cause, seen, depth) { if (cause === void 0 || cause === null) { return cause; } if (depth >= __privateGet(_BaseError, _MAX_CAUSE_DEPTH)) { return "[Max cause depth exceeded]"; } if (cause instanceof Error) { if (seen.has(cause)) { return "[Circular cause chain]"; } seen.add(cause); const serialized = { name: cause.name, message: cause.message, stack: cause.stack }; const errorRecord = cause; if ("code" in cause) serialized.code = errorRecord.code; if ("category" in cause) serialized.category = errorRecord.category; if ("retryable" in cause) serialized.retryable = errorRecord.retryable; if ("details" in cause) serialized.details = errorRecord.details; if ("cause" in cause && errorRecord.cause !== void 0) { serialized.cause = __privateMethod(this, _BaseError_instances, serializeCause_fn).call(this, errorRecord.cause, seen, depth + 1); } return serialized; } if (typeof cause === "object" && cause !== null) { try { let nodes = 0; const json = JSON.stringify(cause, (_key, value) => { if (++nodes > __privateGet(_BaseError, _MAX_JSON_NODES)) { throw new Error("payload exceeds serialization bounds"); } return value; }); if (json === void 0) { return __privateMethod(this, _BaseError_instances, serializeCircularObject_fn).call(this, cause); } return JSON.parse(json); } catch { return __privateMethod(this, _BaseError_instances, serializeCircularObject_fn).call(this, cause); } } return cause; }; _MAX_JSON_NODES = new WeakMap(); /** * Creates a more useful representation of circular objects for debugging. * Instead of just "[object Object]", it extracts key information. */ serializeCircularObject_fn = function(obj) { const type = obj.constructor?.name || "Object"; const keys = Object.keys(obj).slice(0, 5); const keyInfo = keys.length > 0 ? ` with keys: [${keys.join(", ")}]` : ""; const moreKeys = Object.keys(obj).length > 5 ? "..." : ""; return `[Circular ${type}${keyInfo}${moreKeys}]`; }; /** * Captures the stack now but defers symbolization and filtering to the * first read. V8 formats stacks lazily (via `Error.prepareStackTrace`) only * when `stack` is accessed; reading it in the constructor would force that * work for every error, including ones that are caught and never logged. So * the raw capture lands on a side holder, and `this.stack` becomes a * memoizing accessor: the first get symbolizes, filters, and replaces * itself with a plain writable data property; a set before the first get * (a rehydrated or user-assigned stack) wins unfiltered. */ installLazyStack_fn = function() { const V8Error = Error; let readRawStack; if (typeof V8Error.captureStackTrace === "function") { const holder = {}; V8Error.captureStackTrace( holder, this.constructor ); readRawStack = () => holder.stack; } else { let tempStack; try { throw new Error(); } catch (e) { tempStack = e.stack; } readRawStack = () => tempStack; } const install = (value) => { Object.defineProperty(this, "stack", { value, writable: true, configurable: true, enumerable: false }); }; Object.defineProperty(this, "stack", { configurable: true, enumerable: false, get: () => { const filtered = __privateMethod(this, _BaseError_instances, filterInternalFrames_fn).call(this, readRawStack()); install(filtered); return filtered; }, set: install }); }; /** * Filters out internal BaseError frames and updates the error header. * This provides cleaner stack traces by removing implementation details. */ filterInternalFrames_fn = function(stack) { if (!stack) { return void 0; } const lines = stack.split("\n"); const filteredLines = []; filteredLines.push(`${this.name}: ${this.message}`); for (let i = 1; i < lines.length; i++) { const line = lines[i]; if (!line) { continue; } if (line.includes("#installLazyStack") || line.includes("#filterInternalFrames") || line.includes("BaseError.constructor") || line.includes("new BaseError") || line.includes("installLazyStack_fn") || // Compiled private method name line.includes("filterInternalFrames_fn") || // Compiled private method name // Skip the temporary error creation frame line.includes("Object.<anonymous>") && line.includes("installLazyStack")) { continue; } filteredLines.push(line); } return filteredLines.join("\n"); }; __privateAdd(_BaseError, _BaseError_static); /** Sentinel returned by a redaction decision to mean "descend / keep as-is". */ __privateAdd(_BaseError, _RECURSE, /* @__PURE__ */ Symbol("redact.recurse")); /** * Largest **data** nesting depth the redaction walker descends into. Bounded * so a pathologically deep `details` tree degrades to a marker at the deep end * (shallow fields survive) instead of overflowing the stack and tripping the * fail-closed path, which would drop the whole log. The cap is host-stack * independent, so behavior is identical on small isolate stacks (edge * runtimes). The cause chain is its own separately bounded spine ({@link * BaseError.#MAX_CAUSE_DEPTH}) and is **exempt** from this budget, so a deep * chain cannot marker-truncate a shallow `details` on a deep cause. */ __privateAdd(_BaseError, _MAX_REDACT_DEPTH, 100); /** * Total-node budget for one redaction walk. The depth cap bounds depth, not * width: shared (DAG) references are cloned once per reference, so a small * `details` value can legally expand exponentially. Past the budget any * further container degrades to a marker (like the depth cap), keeping the * logging path fail-safe instead of walking a blowup to completion. */ __privateAdd(_BaseError, _MAX_REDACT_NODES, 1e5); /** * Structural fields of an error envelope that survive an allow-list at the * **top level of a cause**. Everything else under a cause (foreign siblings * and anything nested beneath them, plus `details`) is treated as data, so a * plain object mimicking the structured shape cannot smuggle sensitive * siblings (or envelope-named keys buried in foreign subtrees) past * `redactAllow`. Private: it must not become a process-wide redaction toggle. */ __privateAdd(_BaseError, _ENVELOPE_KEYS, /* @__PURE__ */ new Set([ "name", "message", "stack", "code", "category", "retryable" ])); /** * The library's own **top-level** structural fields, the only root keys an * allow-list keeps (and, for containers, the only root keys that stay in the * root/cause regions). Everything else at the top level (a field a subclass * adds via `buildLogObject`) is data, so a subclass-added field leaks nothing * through `redactAllow` by default. Private for the same reason as * {@link BaseError.#ENVELOPE_KEYS}. */ __privateAdd(_BaseError, _ROOT_ENVELOPE_KEYS, /* @__PURE__ */ new Set([ ...__privateGet(_BaseError, _ENVELOPE_KEYS), "timestamp", "timestampIso", "cause", "details" ])); /** * Non-sensitive structural fields preserved in the fail-closed redaction * marker. Only those a given error's `buildLogObject()` actually emits are * copied (guarded by `key in raw`), so `code`/`category`/`retryable` appear * for a `StructuredError` but are simply absent for a plain `BaseError`. */ __privateAdd(_BaseError, _SAFE_TRIAGE_KEYS, [ "name", "code", "category", "retryable", "timestamp", "timestampIso" ]); /** * Largest cause-chain depth serialized into a log object. Matches the cap used * by `StructuredError.fromJSON` and the traversal helpers, so a pathologically * deep (but acyclic) chain can never overflow the stack while logging. */ __privateAdd(_BaseError, _MAX_CAUSE_DEPTH, 100); /** * Total-node budget for one plain-object cause payload, enforced by a * counting replacer so a shared-reference (DAG) blowup degrades to the * fallback marker instead of exhausting CPU. Matches the redaction and * wire-clone budgets. */ __privateAdd(_BaseError, _MAX_JSON_NODES, 1e5); var BaseError = _BaseError; // src/errors/defaults.ts var UNKNOWN_ERROR_DEFAULTS = { /** Internal code used when none is known. */ code: "UNKNOWN_ERROR", /** Internal category used when none is known. */ category: "INTERNAL", /** Unknown errors are not retryable by default. */ retryable: false, /** Technical message used when none can be derived. */ message: "Unknown error" }; // src/errors/StructuredError.ts var _MAX_DEPTH, _StructuredError_static, fromJSON_fn, rehydrate_fn, reconstructCause_fn; var _StructuredError = class _StructuredError extends BaseError { /** * Creates a new StructuredError with typed metadata. * * @param options - Configuration object containing all error metadata */ constructor(options) { super(options.message, options.cause, { name: options.code }); /** * Stable discriminant for the StructuredError family. Fixed as a literal so it * survives class-name minification. Narrow on {@link code} to distinguish * individual structured errors; subclasses that need their own tag override * this with their own literal. */ this._tag = "StructuredError"; this.code = options.code; this.category = options.category; this.retryable = options.retryable; this.details = options.details; } /** * Reconstruct a StructuredError from its serialized (`toJSON`/`toLogObject`) * shape. This is the inverse of {@link toJSON}. * * Intended for reconstruction **within a single trust/bounded-context * boundary**: Web Worker / `postMessage` (where `instanceof` is lost across * `structuredClone`), job queues / durable storage, and log replay. Across * services, reconstruct then translate through an Anti-Corruption Layer; do * not treat an upstream's `code` as your own. * * Lenient and safe: missing fields fall back to safe defaults * (`UNKNOWN_ERROR`/`INTERNAL`/non-retryable); malformed input yields that * envelope instead of throwing; only whitelisted fields are read (no * prototype pollution). `details` is copied shallowly (the top level is * decoupled from the payload; nested values stay shared). The original * `stack`/`timestamp` and the cause chain are restored. Reconstructed * fields are **not** an authority on trust: whoever produced the payload * can forge them. * * Always returns a base `StructuredError`: subclass identity and behavior are * **not** restored (a `ValidationError` round-trips to a `StructuredError`, * losing `publicIssues()`/`addIssue()`; its raw `details.issues` survive as * data). Narrow on `code`, not on `_tag`/instanceof. */ static fromJSON(json) { var _a; return __privateMethod(_a = _StructuredError, _StructuredError_static, fromJSON_fn).call(_a, json, 0); } /** * Extends BaseError's raw log object with code, category, retryable, and * details. Redaction (if configured) is applied by the inherited * {@link toLogObject} to the complete assembled object. */ buildLogObject() { const baseJson = super.buildLogObject(); return { ...baseJson, code: this.code, category: this.category, retryable: this.retryable, ...this.details !== void 0 && { details: this.details } }; } }; _MAX_DEPTH = new WeakMap(); _StructuredError_static = new WeakSet(); fromJSON_fn = function(json, depth) { var _a, _b, _c, _d; const obj = typeof json === "object" && json !== null ? json : {}; const code = typeof obj.code === "string" ? obj.code : UNKNOWN_ERROR_DEFAULTS.code; const category = typeof obj.category === "string" ? obj.category : UNKNOWN_ERROR_DEFAULTS.category; const retryable = typeof obj.retryable === "boolean" ? obj.retryable : UNKNOWN_ERROR_DEFAULTS.retryable; const message = typeof obj.message === "string" ? obj.message : UNKNOWN_ERROR_DEFAULTS.message; const details = typeof obj.details === "object" && obj.details !== null ? { ...obj.details } : void 0; const cause = __privateMethod(_a = _StructuredError, _StructuredError_static, reconstructCause_fn).call(_a, obj.cause, depth); const error = new _StructuredError({ code, category, retryable, message, ...details !== void 0 && { details }, ...cause !== void 0 && { cause } }); __privateMethod(_b = _StructuredError, _StructuredError_static, rehydrate_fn).call(_b, error, "stack", obj.stack, "string"); __privateMethod(_c = _StructuredError, _StructuredError_static, rehydrate_fn).call(_c, error, "timestamp", obj.timestamp, "number"); __privateMethod(_d = _StructuredError, _StructuredError_static, rehydrate_fn).call(_d, error, "timestampIso", obj.timestampIso, "string"); return error; }; rehydrate_fn = function(target, key, value, type) { if (typeof value === type) { Object.defineProperty(target, key, { value, configurable: true, writable: true, enumerable: key !== "stack" }); } }; reconstructCause_fn = function(value, depth) { var _a, _b; if (depth >= __privateGet(_StructuredError, _MAX_DEPTH)) { return void 0; } if (typeof value !== "object" || value === null) { return value; } const obj = value; if (typeof obj.code === "string" && typeof obj.category === "string" && typeof obj.retryable === "boolean") { return __privateMethod(_a = _StructuredError, _StructuredError_static, fromJSON_fn).call(_a, obj, depth + 1); } if (typeof obj.message === "string") { const err = new Error(obj.message); if (typeof obj.name === "string") { err.name = obj.name; } if (typeof obj.stack === "string") { err.stack = obj.stack; } const nested = __privateMethod(_b = _StructuredError, _StructuredError_static, reconstructCause_fn).call(_b, obj.cause, depth + 1); if (nested !== void 0) { err.cause = nested; } return err; } return value; }; __privateAdd(_StructuredError, _StructuredError_static); __privateAdd(_StructuredError, _MAX_DEPTH, 100); var StructuredError = _StructuredError; // src/errors/match.ts function matchError(error, cases) { const table = cases; const ownHandler = Object.prototype.hasOwnProperty.call(table, error.code) ? table[error.code] : void 0; const handler = ownHandler ?? table._; if (!handler) { throw new Error( `matchError: unhandled error code "${error.code}" (no matching case and no "_" catch-all)` ); } return handler(error); } // src/errors/match-thrown.ts function createMatcher(value, cases) { return { with(constructor, handler) { const nextCase = { test: (candidate) => candidate instanceof constructor, run: (candidate) => handler(candidate) }; return createMatcher(value, [...cases, nextCase]); }, withAny(constructors, handler) { const snapshot = [...constructors]; const nextCase = { test: (candidate) => snapshot.some((constructor) => candidate instanceof constructor), run: (candidate) => handler(candidate) }; return createMatcher(value, [...cases, nextCase]); }, when(predicate, handler) { const nextCase = { test: predicate, run: (candidate) => handler(candidate) }; return createMatcher(value, [...cases, nextCase]); }, otherwise(handler) { for (const matchCase of cases) { if (matchCase.test(value)) { return matchCase.run(value); } } return handler(value); } }; } function matchThrown(value) { return createMatcher(value, []); } // src/errors/error-class-set.ts function defineErrorClassSet(classes) { const ownKeys = Reflect.ownKeys(classes); if (ownKeys.some((key) => typeof key !== "string")) { throw new Error("defineErrorClassSet: keys must be strings"); } const keys = Object.keys(classes); if (keys.length === 0) { throw new Error("defineErrorClassSet: class definition must not be empty"); } if (keys.some((key) => key.trim() === "")) { throw new Error( "defineErrorClassSet: keys must not be empty or whitespace-only" ); } if (keys.some((key) => Number.isFinite(Number(key)))) { throw new Error("defineErrorClassSet: keys must not be numeric"); } const snapshot = Object.freeze({ ...classes }); const constructors = keys.map((key) => snapshot[key]); if (new Set(constructors).size !== constructors.length) { throw new Error("defineErrorClassSet: constructors must be unique"); } for (let earlier = 0; earlier < constructors.length; earlier++) { for (let later = earlier + 1; later < constructors.length; later++) { const earlierClass = constructors[earlier]; const laterClass = constructors[later]; if (laterClass.prototype instanceof earlierClass) { throw new Error( `defineErrorClassSet: "${keys[later]}" extends "${keys[earlier]}" and is listed after it; list subclasses before their base class, or the subclass handler is unreachable` ); } } } return Object.freeze({ match(value, handlers) { for (const key of keys) { const constructor = snapshot[key]; if (value instanceof constructor) { const handler = handlers[key]; return handler(value); } } throw new Error("value is outside the declared error class set"); } }); } // src/errors/catalog.ts function detailsType() { return Object.freeze({}); } var MAX_METADATA_NODES = 1e5; function cloneJsonValue(value, seen, state) { if (++state.nodes > MAX_METADATA_NODES) { throw new Error("defineErrors: metadata must be JSON-safe"); } if (value === null || typeof value === "string" || typeof value === "boolean") { return value; } if (typeof value === "number") { if (Number.isFinite(value)) return value; throw new Error("defineErrors: metadata must be JSON-safe"); } if (typeof value !== "object") { throw new Error("defineErrors: metadata must be JSON-safe"); } if (seen.has(value)) { throw new Error("defineErrors: metadata must be JSON-safe"); } seen.add(value); try { if (Array.isArray(value)) { for (let index = 0; index < value.length; index++) { if (!Object.prototype.hasOwnProperty.call(value, index)) { throw new Error("defineErrors: metadata must be JSON-safe"); } } return Object.freeze( value.map((item) => cloneJsonValue(item, seen, state)) ); } const prototype = Object.getPrototypeOf(value); if (prototype !== Object.prototype && prototype !== null) { throw new Error("defineErrors: metadata must be JSON-safe"); } if (Object.getOwnPropertySymbols(value).length > 0) { throw new Error("defineErrors: metadata must be JSON-safe"); } const clone = /* @__PURE__ */ Object.create(null); for (const [key, item] of Object.entries(value)) { clone[key] = cloneJsonValue(item, seen, state); } return Object.freeze(clone); } finally { seen.delete(value); } } function snapshotRedaction(value) { if (value === void 0) return void 0; if (typeof value !== "object" || value === null) { throw new Error("defineErrors: invalid redaction policy"); } const allowedKeys = /* @__PURE__ */ new Set(["mode", "keys", "mask"]); for (const key of Reflect.ownKeys(value)) { if (typeof key !== "string" || !allowedKeys.has(key)) { throw new Error(`defineErrors: unknown redaction field "${String(key)}"`); } } const policy = value; if (policy.mode !== "deny" && policy.mode !== "allow" || !Array.isArray(policy.keys) || !policy.keys.every((key) => typeof key === "string") || policy.mask !== void 0 && typeof policy.mask !== "string" && typeof policy.mask !== "function") { throw new Error("defineErrors: invalid redaction policy"); } return Object.freeze({ mode: policy.mode, keys: Object.freeze([...policy.keys]), ...policy.mask !== void 0 && { mask: policy.mask } }); } function defineErrors(catalog) { if (typeof catalog !== "object" || catalog === null || Array.isArray(catalog) || Object.getPrototypeOf(catalog) !== Object.prototype && Object.getPrototypeOf(catalog) !== null) { throw new Error("defineErrors: catalog must be a plain object"); } const ownKeys = Reflect.ownKeys(catalog); if (ownKeys.some((code) => typeof code !== "string")) { throw new Error("defineErrors: error codes must be strings"); } if (ownKeys.includes("")) { throw new Error("defineErrors: error codes must not be empty"); } const create = /* @__PURE__ */ Object.create(null); const provenance = /* @__PURE__ */ new WeakMap(); const codes = Object.keys(catalog); if (codes.length === 0) { throw new Error("defineErrors: catalog must not be empty"); } const snapshot = /* @__PURE__ */ Object.create(null); const metaSnapshot = /* @__PURE__ */ Object.create(null); const allowedSpecKeys = /* @__PURE__ */ new Set([ "category", "retryable", "metadata", "details", "redaction" ]); for (const code of codes) { const spec = catalog[code]; if (typeof spec !== "object" || spec === null || typeof spec.category !== "string" || spec.category.length === 0 || typeof spec.retryable !== "boolean") { throw new Error(`defineErrors: invalid definition for code "${code}"`); } for (const key of Reflect.ownKeys(spec)) { if (typeof key !== "string" || !allowedSpecKeys.has(key)) { throw new Error( `defineErrors: unknown definition field "${String(key)}" for code "${code}"` ); } } if (spec.metadata !== void 0 && (typeof spec.metadata !== "object" || spec.metadata === null || Array.isArray(spec.metadata))) { throw new Error("defineErrors: metadata must be an object"); } const metadata = spec.metadata === void 0 ? void 0 : cloneJsonValue(spec.metadata, /* @__PURE__ */ new Set(), { nodes: 0 }); const redaction = snapshotRedaction(spec.redaction); snapshot[code] = Object.freeze({ category: spec.category, retryable: spec.retryable, ...metadata !== void 0 && { metadata }, ...redaction !== void 0 && { redaction } }); metaSnapshot[code] = Object.freeze({ category: spec.category, retryable: spec.retryable, ...metadata !== void 0 && { metadata } }); } for (const code of codes) { const spec = snapshot[code]; create[code] = (message, options) => { const error = new StructuredError({ code, category: spec.category, retryable: spec.retryable, message, ...options?.details !== void 0 && { details: options.details }, ...options?.cause !== void 0 && { cause: options.cause } }); if (spec.redaction?.mode === "deny") { error.redact([...spec.redaction.keys], { mask: spec.redaction.mask }); } else if (spec.redaction?.mode === "allow") { error.redactAllow([...spec.redaction.keys], { mask: spec.redaction.mask }); } provenance.set(error, code); return error; }; } return Object.freeze({ create: Object.freeze(create), codes: Object.freeze(codes), meta(code) { if (!Object.prototype.hasOwnProperty.call(snapshot, code)) { throw new Error(`meta: unknown error code "${code}"`); } return metaSnapshot[code]; }, is(value, expectedCode) { if (!(value instanceof StructuredError)) return false; const actualCode = provenance.get(value); if (actualCode === void 0) return false; if (expectedCode !== void 0 && actualCode !== expectedCode) { return false; } const spec = snapshot[actualCode]; return spec !== void 0 && value.code === actualCode && value.category === spec.category && value.retryable === spec.retryable; } }); } // src/errors/coerce.ts function toStructuredError(value, options = {}) { if (value instanceof StructuredError) { return value; } const code = options.code ?? UNKNOWN_ERROR_DEFAULTS.code; const category = options.category ?? UNKNOWN_ERROR_DEFAULTS.category; const retryable = options.retryable ?? UNKNOWN_ERROR_DEFAULTS.retryable; let message; let cause; if (value instanceof Error) { message = options.message ?? value.message; cause = value; } else if (typeof value === "string") { message = options.message ?? value; cause = void 0; } else { message = options.message ?? UNKNOWN_ERROR_DEFAULTS.message; cause = value; } return new StructuredError({ code, category, retryable, message, ...cause !== void 0 && { cause } }); } // src/errors/validation.ts var _issues, _ValidationError_static, defaultProjection_fn, toPointer_fn; var _ValidationError = class _ValidationError extends StructuredError { constructor(message, options) { const issues = options?.issues ? [...options.issues] : []; super({ code: options?.code ?? "VALIDATION_FAILED", category: options?.category ?? "VALIDATION", retryable: false, message, details: { issues }, ...options?.cause !== void 0 && { cause: options.cause } }); this._tag = "ValidationError"; /** Live array shared by reference with `details.issues` (full, with extras). */ __privateAdd(this, _issues); __privateSet(this, _issues, issues); } addIssue(issue) { __privateGet(this, _issues).push(issue); return this; } addIssues(issues) { __privateGet(this, _issues).push(...issues); return this; } hasIssues() { return __privateGet(this, _issues).length > 0; } get issues() { return __privateGet(this, _issues); } /** * Client-safe projection of the issues. Returns only the fixed whitelist * (`message`, `path`, `code?`, `pointer?`); raw validator extras are never included. * Provide `mapIssue` to emit a fully custom wire shape (e.g. RFC-7807 * `{ name, reason }`). */ publicIssues(options) { const mapIssue = options?.mapIssue ?? __privateMethod(_ValidationError, _ValidationError_static, defaultProjection_fn); return __privateGet(this, _issues).map(mapIssue); } }; _issues = new WeakMap(); _ValidationError_static = new WeakSet(); defaultProjection_fn = function(issue) { var _a; const out = { message: issue.message }; if (issue.path !== void 0) { out.path = issue.path.map( (segment) => typeof segment === "object" && segment !== null ? { key: segment.key } : segment ); out.pointer = __privateMethod(_a = _ValidationError, _ValidationError_static, toPointer_fn).call(_a, issue.path); } const code = issue.code; if (typeof code === "string") { out.code = code; } return out; }; toPointer_fn = function(path) { return path.map((segment) => typeof segment === "object" ? segment.key : segment).map((key) => String(key)).join("."); }; __privateAdd(_ValidationError, _ValidationError_static); var ValidationError = _ValidationError; // src/utils/guard.ts function guard(condition, error) { if (!condition) { throw typeof error === "function" ? error() : error; } } // src/utils/redact.ts function partialMask(options) { const keepStart = options?.keepStart ?? 0; const keepEnd = options?.keepEnd ?? 4; const fill = options?.fill ?? "\u2026"; return (value) => { if (typeof value !== "string" || value.length <= keepStart + keepEnd) { return fill; } const start = value.slice(0, keepStart); const end = value.slice(value.length - keepEnd); return start + fill + end; }; } // src/errors/guards.ts function isError(value) { try { if (typeof value !== "object" || value === null || Array.isArray(value)) { return false; } const { name, message, stack } = value; return typeof name === "string" && typeof message === "string" && (stack === void 0 || typeof stack === "string"); } catch { return false; } } function hasErrorCode(code) { return (value) => { if (!isError(value)) return false; try { return value.code === code; } catch { return false; } }; } function isErrorOf(constructor, predicate) { return (value) => value instanceof constructor && (predicate === void 0 || predicate(value)); } function isAnyErrorOf(value, constructors) { return constructors.some((constructor) => value instanceof constructor); } function isAllOf(value, guards) { return guards.every((guard2) => guard2(value)); } function isBaseError(value) { return value instanceof BaseError; } function isStructuredError(value) { if (value instanceof StructuredError) return true; if (typeof value !== "object" || value === null) return false; const err = value; return typeof err.code === "string" && typeof err.category === "string" && typeof err.retryable === "boolean"; } function isRetryable(value) { return typeof value === "object" && value !== null && "retryable" in value && value.retryable === true; } // src/traversal/guards.ts function isErrorWithCause(value) { return typeof value === "object" && value !== null && "cause" in value && value.cause !== void 0; } function isRetryableStructuredError(value) { return isStructuredError(value) && value.retryable === true; } // src/traversal/cause-chain.ts function* traverseCauseChain(error, maxDepth) { let current = error; const seen = /* @__PURE__ */ new Set(); for (let depth = 0; depth <= maxDepth; depth++) { if (seen.has(current)) return; seen.add(current); yield current; if (!isErrorWithCause(current)) return; current = current.cause; } } function getRootCause(error, maxDepth = 100) { let last = error; for (const current of traverseCauseChain(error, maxDepth)) { last = current; } return last; } function findInCauseChain(error, predicate, maxDepth = 100) { for (const current of traverseCauseChain(error, maxDepth)) { if (predicate(current)) return current; } return void 0; } function filterCauseChain(error, predicate, maxDepth = 100) { const results = []; for (const current of traverseCauseChain(error, maxDepth)) { if (predicate(current)) results.push(current); } return results; } function someCauseChain(error, predicate, maxDepth = 100) { return findInCauseChain(error, predicate, maxDepth) !== void 0; } function everyCauseChain(error, predicate, maxDepth = 100) { for (const current of traverseCauseChain(error, maxDepth)) { if (!predicate(current)) return false; } return true; } // src/traversal/retryability.ts function isChainRetryable(error) { return someCauseChain(error, isRetryableStructuredError); } function someChainRetryable(error, maxDepth = 100) { return someCauseChain(error, isRetryable, maxDepth); } function getRootCauseRetryable(error) { const root = getRootCause(error); return isRetryableStructuredError(root); } function getFirstRetryableCause(error) { return findInCauseChain( error, (e) => isRetryableStructuredError(e) ); } export { BaseError, StructuredError, ValidationError, defineErrorClassSet, defineErrors, detailsType, everyCauseChain, filterCauseChain, findInCauseChain, getFirstRetryableCause, getRootCause, getRootCauseRetryable, guard, hasErrorCode, isAllOf, isAnyErrorOf, isBaseError, isChainRetryable, isError, isErrorOf, isErrorWithCause, isRetryable, isRetryableStructuredError, isStructuredError, matchError, matchThrown, partialMask, someCauseChain, someChainRetryable, toStructuredError }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map