UNPKG

@kitiumai/error

Version:

Enterprise-grade error primitives for Kitium products: rich metadata, HTTP/Problem Details mapping, observability, and registry-driven error governance.

560 lines (559 loc) 17.9 kB
import "./chunk-55J6XMHW.js"; var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; // src/index.ts import { getLogger } from "@kitiumai/logger"; var DEFAULT_DOCS_URL = "https://docs.kitium.ai/errors"; var log = getLogger(); function isObject(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function cloneValue(value) { return isObject(value) ? JSON.parse(JSON.stringify(value)) : value; } function redactValueAtPath(target, path, redaction) { const [head, ...tail] = path; if (!head) { return; } if (!(head in target)) { return; } if (tail.length === 0) { target[head] = redaction; return; } const next = target[head]; if (isObject(next)) { redactValueAtPath(next, tail, redaction); } } function applyRedactions(context, redactPaths, redaction = "[REDACTED]") { if (!context || !redactPaths?.length) { return context; } const clone = cloneValue(context); redactPaths.forEach((path) => redactValueAtPath(clone, path.split("."), redaction)); return clone; } var ERROR_CODE_PATTERN = /^[a-z0-9_]+(\/[a-z0-9_]+)*$/; var errorMetrics = { totalErrors: 0, errorsByKind: { business: 0, validation: 0, auth: 0, rate_limit: 0, not_found: 0, conflict: 0, dependency: 0, internal: 0 }, errorsBySeverity: { fatal: 0, error: 0, warning: 0, info: 0, debug: 0 }, retryableErrors: 0, nonRetryableErrors: 0 }; function isValidErrorCode(code) { return ERROR_CODE_PATTERN.test(code); } function validateErrorCode(code) { if (!isValidErrorCode(code)) { throw new Error( `Invalid error code format: "${code}". Error codes must match pattern: ${ERROR_CODE_PATTERN.source}. Examples: "auth/forbidden", "validation/required_field"` ); } } function validateLifecycle(lifecycle) { if (!lifecycle) { return; } if (!["draft", "active", "deprecated"].includes(lifecycle)) { throw new Error(`Invalid lifecycle state: ${lifecycle}. Allowed: draft | active | deprecated.`); } } var KitiumError = class extends Error { constructor(shape, validateCode = true) { super(shape.message); __publicField(this, "code"); __publicField(this, "statusCode"); __publicField(this, "severity"); __publicField(this, "kind"); __publicField(this, "lifecycle"); __publicField(this, "schemaVersion"); __publicField(this, "retryable"); __publicField(this, "retryDelay"); __publicField(this, "maxRetries"); __publicField(this, "backoff"); __publicField(this, "help"); __publicField(this, "docs"); __publicField(this, "source"); __publicField(this, "userMessage"); __publicField(this, "i18nKey"); __publicField(this, "redact"); __publicField(this, "context"); __publicField(this, "cause"); this.name = "KitiumError"; if (validateCode) { validateErrorCode(shape.code); } validateLifecycle(shape.lifecycle); this.code = shape.code; this.statusCode = shape.statusCode; this.severity = shape.severity; this.kind = shape.kind; this.lifecycle = shape.lifecycle; this.schemaVersion = shape.schemaVersion; this.retryable = shape.retryable; this.retryDelay = shape.retryDelay; this.maxRetries = shape.maxRetries; this.backoff = shape.backoff; this.help = shape.help; this.docs = shape.docs; this.source = shape.source; this.userMessage = shape.userMessage; this.i18nKey = shape.i18nKey; this.redact = shape.redact; this.context = shape.context; this.cause = shape.cause; errorMetrics.totalErrors++; errorMetrics.errorsByKind[this.kind]++; errorMetrics.errorsBySeverity[this.severity]++; if (this.retryable) { errorMetrics.retryableErrors++; } else { errorMetrics.nonRetryableErrors++; } } toJSON() { return { code: this.code, message: this.message, ...this.statusCode !== void 0 ? { statusCode: this.statusCode } : {}, severity: this.severity, kind: this.kind, ...this.lifecycle !== void 0 ? { lifecycle: this.lifecycle } : {}, ...this.schemaVersion !== void 0 ? { schemaVersion: this.schemaVersion } : {}, retryable: this.retryable, ...this.retryDelay !== void 0 ? { retryDelay: this.retryDelay } : {}, ...this.maxRetries !== void 0 ? { maxRetries: this.maxRetries } : {}, ...this.backoff !== void 0 ? { backoff: this.backoff } : {}, ...this.help !== void 0 ? { help: this.help } : {}, ...this.docs !== void 0 ? { docs: this.docs } : {}, ...this.source !== void 0 ? { source: this.source } : {}, ...this.userMessage !== void 0 ? { userMessage: this.userMessage } : {}, ...this.i18nKey !== void 0 ? { i18nKey: this.i18nKey } : {}, ...this.redact !== void 0 ? { redact: this.redact } : {}, ...this.context !== void 0 ? { context: applyRedactions(this.context, this.redact) } : {}, ...this.cause !== void 0 ? { cause: this.cause } : {} }; } }; function createErrorRegistry(defaults) { const entries = /* @__PURE__ */ new Map(); const toProblemDetails = (error) => { const entry = entries.get(error.code) ?? defaults; const redactions = error.redact ?? entry?.redact; const context = applyRedactions(error.context, redactions); const typeUrl = entry?.docs ?? error.docs ?? `${DEFAULT_DOCS_URL}/${error.code}`; const status = error.statusCode ?? entry?.statusCode; const userMessage = error.userMessage ?? entry?.userMessage; const lifecycle = error.lifecycle ?? entry?.lifecycle; if (entry?.toProblem) { return entry.toProblem(error); } return { type: typeUrl, title: userMessage ?? error.message, ...status !== void 0 ? { status } : {}, ...error.help !== void 0 ? { detail: error.help } : {}, ...error.context?.correlationId ?? error.context?.requestId ? { instance: error.context?.correlationId ?? error.context?.requestId } : {}, extensions: { code: error.code, severity: error.severity, retryable: error.retryable, ...lifecycle !== void 0 ? { lifecycle } : {}, ...error.schemaVersion !== void 0 ? { schemaVersion: error.schemaVersion } : {}, ...error.retryDelay !== void 0 ? { retryDelay: error.retryDelay } : {}, ...error.maxRetries !== void 0 ? { maxRetries: error.maxRetries } : {}, ...error.backoff !== void 0 ? { backoff: error.backoff } : {}, kind: error.kind, ...context !== void 0 ? { context } : {}, ...userMessage !== void 0 ? { userMessage } : {}, ...error.i18nKey ?? entry?.i18nKey ? { i18nKey: error.i18nKey ?? entry?.i18nKey } : {}, ...error.source ?? entry?.source ? { source: error.source ?? entry?.source } : {} } }; }; return { register(entry) { validateErrorCode(entry.code); validateLifecycle(entry.lifecycle ?? defaults?.lifecycle); entries.set(entry.code, { ...defaults, ...entry }); }, resolve(code) { return entries.get(code); }, toProblemDetails }; } function toKitiumError(error, fallback) { if (error instanceof KitiumError) { return error; } if (isObject(error) && "code" in error && "message" in error) { const shape = error; return new KitiumError( { code: String(shape["code"]), message: String(shape["message"]), statusCode: typeof shape["statusCode"] === "number" ? shape["statusCode"] : void 0, severity: shape["severity"] ?? "error", kind: shape["kind"] ?? "internal", retryable: Boolean(shape["retryable"]), retryDelay: typeof shape["retryDelay"] === "number" ? shape["retryDelay"] : void 0, maxRetries: typeof shape["maxRetries"] === "number" ? shape["maxRetries"] : void 0, backoff: ["linear", "exponential", "fixed"].includes(String(shape["backoff"])) ? shape["backoff"] : void 0, help: typeof shape["help"] === "string" ? shape["help"] : void 0, docs: typeof shape["docs"] === "string" ? shape["docs"] : void 0, source: typeof shape["source"] === "string" ? shape["source"] : void 0, lifecycle: ["draft", "active", "deprecated"].includes(String(shape["lifecycle"])) ? shape["lifecycle"] : void 0, schemaVersion: typeof shape["schemaVersion"] === "string" ? shape["schemaVersion"] : void 0, userMessage: typeof shape["userMessage"] === "string" ? shape["userMessage"] : void 0, i18nKey: typeof shape["i18nKey"] === "string" ? shape["i18nKey"] : void 0, redact: Array.isArray(shape["redact"]) ? shape["redact"] : void 0, context: isObject(shape["context"]) ? shape["context"] : void 0, cause: shape["cause"] }, false // Don't validate code for normalized errors ); } if (fallback) { return new KitiumError(fallback); } return new KitiumError({ code: "unknown_error", message: error instanceof Error ? error.message : "Unknown error", statusCode: 500, severity: "error", kind: "internal", retryable: false, cause: error }); } function logError(error) { const entry = httpErrorRegistry.resolve(error.code); const context = applyRedactions(error.context, error.redact ?? entry?.redact); const payload = { code: error.code, message: error.message, severity: error.severity, retryable: error.retryable, ...error.statusCode !== void 0 ? { statusCode: error.statusCode } : {}, ...error.retryDelay !== void 0 ? { retryDelay: error.retryDelay } : {}, ...error.maxRetries !== void 0 ? { maxRetries: error.maxRetries } : {}, ...error.backoff !== void 0 ? { backoff: error.backoff } : {}, ...context !== void 0 ? { context } : {}, ...error.source !== void 0 ? { source: error.source } : {}, ...error.schemaVersion !== void 0 ? { schemaVersion: error.schemaVersion } : {}, ...error.lifecycle !== void 0 ? { lifecycle: error.lifecycle } : {}, fingerprint: getErrorFingerprint(error) }; switch (error.severity) { case "fatal": case "error": log?.error(error.message, payload); break; case "warning": log?.warn(error.message, payload); break; case "info": log?.info(error.message, payload); break; default: log?.debug(error.message, payload); } } var SPAN_STATUS_ERROR = 2; function recordException(error, span) { if (!span) { return; } const fingerprint = getErrorFingerprint(error); span.setAttribute("kitium.error.code", error.code); span.setAttribute("kitium.error.kind", error.kind); span.setAttribute("kitium.error.severity", error.severity); span.setAttribute("kitium.error.retryable", error.retryable); span.setAttribute("kitium.error.fingerprint", fingerprint); if (error.lifecycle) { span.setAttribute("kitium.error.lifecycle", error.lifecycle); } if (error.schemaVersion) { span.setAttribute("kitium.error.schema_version", error.schemaVersion); } if (error.statusCode) { span.setAttribute("http.status_code", error.statusCode); } span.recordException({ ...error.toJSON(), name: error.name }); span.setStatus({ code: SPAN_STATUS_ERROR, message: error.message }); } var httpErrorRegistry = createErrorRegistry({ statusCode: 500, severity: "error", kind: "internal", retryable: false }); function problemDetailsFrom(error) { return httpErrorRegistry.toProblemDetails(error); } function enrichError(error, context) { const mergedContext = { ...error.context ?? {}, ...context }; return new KitiumError({ ...error.toJSON(), context: mergedContext }, false); } function computeDelay(baseDelay, backoff, attempt) { if (backoff === "fixed") { return baseDelay; } if (backoff === "linear") { return baseDelay * attempt; } return baseDelay * 2 ** Math.max(0, attempt - 1); } async function runWithRetry(operation, options) { const maxAttempts = Math.max(1, options?.maxAttempts ?? 3); const baseDelay = options?.baseDelayMs ?? 200; const backoff = options?.backoff ?? "exponential"; let attempt = 0; let lastDelay; while (attempt < maxAttempts) { attempt++; try { const result = await operation(); return { attempts: attempt, result, lastDelayMs: lastDelay }; } catch (err) { const kitiumError = toKitiumError(err); options?.onAttempt?.(attempt, kitiumError); if (!kitiumError.retryable || attempt >= maxAttempts) { return { attempts: attempt, error: kitiumError, lastDelayMs: lastDelay }; } const delay = kitiumError.retryDelay ?? baseDelay; const backoffStrategy = kitiumError.backoff ?? backoff; lastDelay = computeDelay(delay, backoffStrategy, attempt); await new Promise((resolve) => setTimeout(resolve, lastDelay)); } } return { attempts: attempt, lastDelayMs: lastDelay }; } function getErrorFingerprint(error) { if (error instanceof KitiumError) { const shape = error.toJSON(); const entry = httpErrorRegistry.resolve(shape.code); if (entry?.fingerprint) { return entry.fingerprint; } return `${shape.code}:${shape.kind}`; } return `${error.code}:${error.kind}`; } function getErrorMetrics() { return { totalErrors: errorMetrics.totalErrors, errorsByKind: { ...errorMetrics.errorsByKind }, errorsBySeverity: { ...errorMetrics.errorsBySeverity }, retryableErrors: errorMetrics.retryableErrors, nonRetryableErrors: errorMetrics.nonRetryableErrors }; } function resetErrorMetrics() { errorMetrics.totalErrors = 0; errorMetrics.errorsByKind = { business: 0, validation: 0, auth: 0, rate_limit: 0, not_found: 0, conflict: 0, dependency: 0, internal: 0 }; errorMetrics.errorsBySeverity = { fatal: 0, error: 0, warning: 0, info: 0, debug: 0 }; errorMetrics.retryableErrors = 0; errorMetrics.nonRetryableErrors = 0; } var ValidationError = class extends KitiumError { constructor(shape) { super( { ...shape, kind: shape.kind ?? "validation", severity: shape.severity ?? "warning", statusCode: shape.statusCode ?? 400 }, true ); this.name = "ValidationError"; } }; var AuthenticationError = class extends KitiumError { constructor(shape) { super( { ...shape, kind: shape.kind ?? "auth", severity: shape.severity ?? "error", statusCode: shape.statusCode ?? 401 }, true ); this.name = "AuthenticationError"; } }; var AuthorizationError = class extends KitiumError { constructor(shape) { super( { ...shape, kind: shape.kind ?? "auth", severity: shape.severity ?? "warning", statusCode: shape.statusCode ?? 403 }, true ); this.name = "AuthorizationError"; } }; var NotFoundError = class extends KitiumError { constructor(shape) { super( { ...shape, kind: shape.kind ?? "not_found", severity: shape.severity ?? "warning", statusCode: shape.statusCode ?? 404 }, true ); this.name = "NotFoundError"; } }; var ConflictError = class extends KitiumError { constructor(shape) { super( { ...shape, kind: shape.kind ?? "conflict", severity: shape.severity ?? "warning", statusCode: shape.statusCode ?? 409 }, true ); this.name = "ConflictError"; } }; var RateLimitError = class extends KitiumError { constructor(shape) { super( { ...shape, kind: shape.kind ?? "rate_limit", severity: shape.severity ?? "warning", statusCode: shape.statusCode ?? 429, retryable: shape.retryable ?? true, retryDelay: shape.retryDelay ?? 1e3, backoff: shape.backoff ?? "exponential" }, true ); this.name = "RateLimitError"; } }; var DependencyError = class extends KitiumError { constructor(shape) { super( { ...shape, kind: shape.kind ?? "dependency", severity: shape.severity ?? "error", statusCode: shape.statusCode ?? 502, retryable: shape.retryable ?? true, retryDelay: shape.retryDelay ?? 500, backoff: shape.backoff ?? "exponential", maxRetries: shape.maxRetries ?? 3 }, true ); this.name = "DependencyError"; } }; var BusinessError = class extends KitiumError { constructor(shape) { super( { ...shape, kind: shape.kind ?? "business", severity: shape.severity ?? "error", statusCode: shape.statusCode ?? 400, retryable: shape.retryable ?? false }, true ); this.name = "BusinessError"; } }; var InternalError = class extends KitiumError { constructor(shape) { super( { ...shape, kind: shape.kind ?? "internal", severity: shape.severity ?? "error", statusCode: shape.statusCode ?? 500, retryable: shape.retryable ?? false }, true ); this.name = "InternalError"; } }; export { AuthenticationError, AuthorizationError, BusinessError, ConflictError, DependencyError, InternalError, KitiumError, NotFoundError, RateLimitError, ValidationError, createErrorRegistry, enrichError, getErrorFingerprint, getErrorMetrics, httpErrorRegistry, isValidErrorCode, logError, problemDetailsFrom, recordException, resetErrorMetrics, runWithRetry, toKitiumError, validateErrorCode }; //# sourceMappingURL=index.js.map