@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
JavaScript
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