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