export const HWLAB_ERROR_DIAGNOSTIC_VERSION = "hwlab-error-diagnostic-v1"; export function decorateErrorPayloadForHttp(payload, options = {}) { const statusCode = numericStatus(options.statusCode); if (!statusCode || statusCode < 400 || !plainObject(payload) || !("error" in payload)) return payload; const context = options.context ?? options.response?.hwlabHttpRequestContext ?? options.request?.hwlabHttpRequestContext ?? {}; const originalError = normalizeErrorObject(payload.error, payload); if (!originalError) return payload; const rawCode = originalError.code ?? payload.code ?? payload.error ?? statusCodeToCode(statusCode); const diagnosticCode = safeCode(rawCode); const errorCode = typeof originalError.code === "number" ? originalError.code : diagnosticCode; const message = firstText(originalError.message, payload.message, diagnosticCode); const userMessage = firstText(originalError.userMessage, payload.userMessage, message); const diagnostic = { contractVersion: HWLAB_ERROR_DIAGNOSTIC_VERSION, traceId: safeOtelTraceId(context.traceId) ?? safeOtelTraceId(originalError.diagnostic?.traceId) ?? safeOtelTraceId(originalError.otelTraceId) ?? safeOtelTraceId(payload.otelTraceId) ?? null, requestId: safeRequestId(context.requestId) ?? safeRequestId(originalError.diagnostic?.requestId) ?? safeRequestId(originalError.requestId) ?? safeRequestId(payload.requestId) ?? null, serviceId: safeToken(originalError.serviceId ?? payload.serviceId ?? options.serviceId, "hwlab-cloud-api"), route: safeRoute(originalError.route ?? payload.route ?? options.route ?? context.route), layer: safeToken(originalError.layer ?? options.layer, "api"), category: safeToken(originalError.category ?? options.category, categoryForStatus(statusCode)), code: diagnosticCode, httpStatus: statusCode, source: safeToken(options.source, "server"), observedAt: new Date().toISOString(), valuesPrinted: false }; const nextError = { ...originalError, code: errorCode, message, userMessage, diagnostic: { ...(plainObject(originalError.diagnostic) ? originalError.diagnostic : {}), ...diagnostic }, valuesPrinted: originalError.valuesPrinted === true ? true : false }; const httpContext = options.context ?? options.response?.hwlabHttpRequestContext ?? options.request?.hwlabHttpRequestContext; if (plainObject(httpContext)) { httpContext.lastHttpError = { code: diagnosticCode, category: diagnostic.category, layer: diagnostic.layer, httpStatus: statusCode, valuesPrinted: false }; } return { ...payload, error: nextError }; } export function logErrorEnvelope(payload, options = {}) { const statusCode = numericStatus(options.statusCode); if (!statusCode || statusCode < 400 || !plainObject(payload) || !plainObject(payload.error)) return; const diagnostic = plainObject(payload.error.diagnostic) ? payload.error.diagnostic : null; if (!diagnostic) return; const record = { event: "hwlab.error", statusCode, trace_id: diagnostic.traceId ?? null, request_id: diagnostic.requestId ?? null, route: diagnostic.route ?? null, serviceId: diagnostic.serviceId ?? "hwlab-cloud-api", error: { code: payload.error.code ?? diagnostic.code ?? "unknown_error", layer: payload.error.layer ?? diagnostic.layer ?? "api", category: payload.error.category ?? diagnostic.category ?? categoryForStatus(statusCode), retryable: payload.error.retryable ?? null }, valuesPrinted: false }; const logger = options.logger ?? console; const line = JSON.stringify(record); if (statusCode >= 500 && typeof logger.error === "function") { logger.error(line); } else if (typeof logger.warn === "function") { logger.warn(line); } } function normalizeErrorObject(value, payload) { if (plainObject(value)) return { ...value }; const text = firstText(value, payload?.message, "unknown_error"); return { code: safeCode(text), message: text, userMessage: text }; } function numericStatus(value) { const status = Number(value); return Number.isInteger(status) && status >= 100 && status <= 599 ? status : null; } function statusCodeToCode(statusCode) { if (statusCode === 400) return "bad_request"; if (statusCode === 401) return "auth_required"; if (statusCode === 403) return "forbidden"; if (statusCode === 404) return "not_found"; if (statusCode === 409) return "conflict"; if (statusCode === 429) return "rate_limited"; if (statusCode >= 500) return "internal_error"; return "error"; } function categoryForStatus(statusCode) { if (statusCode >= 500) return "server"; if (statusCode >= 400) return "client"; return "error"; } function firstText(...values) { for (const value of values) { const text = String(value ?? "").trim(); if (text) return text; } return "unknown_error"; } function safeCode(value) { const text = firstText(value).toLowerCase().replace(/[^a-z0-9_.:-]+/gu, "_").replace(/^_+|_+$/gu, ""); return text.slice(0, 96) || "unknown_error"; } function safeToken(value, fallback) { const text = String(value ?? "").trim(); return /^[A-Za-z0-9_.:-]{1,96}$/u.test(text) ? text : fallback; } function safeRoute(value) { const text = String(value ?? "").trim().split("?")[0] || "/"; if (!text.startsWith("/")) return "/"; return text.slice(0, 180); } function safeRequestId(value) { const text = String(value ?? "").trim(); return /^[A-Za-z0-9_.:-]{3,180}$/u.test(text) ? text : null; } function safeOtelTraceId(value) { const text = String(value ?? "").trim().toLowerCase(); return /^[0-9a-f]{32}$/u.test(text) && text !== "00000000000000000000000000000000" ? text : null; } function plainObject(value) { return Boolean(value && typeof value === "object" && !Array.isArray(value)); }