/** * src/platform/errors.ts * ======================= * The canonical SIAX cross-product error vocabulary (Master-DoD §6), mirrored * from cl0ud's `packages/contract/src/request-context.ts` (`SIAX_ERROR_CODES` * + `httpStatusForSiaxErrorCode`) under the same documented mirror posture as * src/platform/siax-context.ts. tests/contracts/siax-context.contract.test.ts * pins the full code list and status mapping. * * `SiaxError` is the typed error every 0pt platform client and capability * service throws at a product boundary. Retryable vs permanent is explicit * (§6: "Retryable vs permanent är explicit"), correlation id travels in the * error (§6: "Correlation ID finns i felet"), and no sensitive internals are * embedded — `detail` is a short operator-facing string, never a raw body. */ export const SIAX_ERROR_CODES = [ "UNAUTHENTICATED", "AUTHORIZATION_DENIED", "ENTITLEMENT_REQUIRED", "CAPABILITY_UNAVAILABLE", "QUOTA_EXCEEDED", "RATE_LIMITED", "INVALID_REQUEST", "NOT_FOUND", "CONFLICT", "DEPENDENCY_UNAVAILABLE", "RETRYABLE_FAILURE", "PERMANENT_FAILURE", ] as const; /** One canonical SIAX error code. */ export type SiaxErrorCode = (typeof SIAX_ERROR_CODES)[number]; /** Canonical HTTP status for a {@link SiaxErrorCode} (transport guidance, pinned to cl0ud). */ export function httpStatusForSiaxErrorCode(code: SiaxErrorCode): number { switch (code) { case "UNAUTHENTICATED": return 401; case "AUTHORIZATION_DENIED": return 403; case "ENTITLEMENT_REQUIRED": case "QUOTA_EXCEEDED": return 402; case "CAPABILITY_UNAVAILABLE": return 404; case "RATE_LIMITED": return 429; case "INVALID_REQUEST": return 400; case "NOT_FOUND": return 404; case "CONFLICT": return 409; case "DEPENDENCY_UNAVAILABLE": case "RETRYABLE_FAILURE": return 503; case "PERMANENT_FAILURE": return 500; default: { const _never: never = code; return _never; } } } /** Whether a caller may retry the failed operation (with its idempotency key). */ export function isRetryableSiaxErrorCode(code: SiaxErrorCode): boolean { return code === "DEPENDENCY_UNAVAILABLE" || code === "RETRYABLE_FAILURE" || code === "RATE_LIMITED"; } /** Map an upstream HTTP status (from a SIAX dependency) onto the canonical vocabulary. */ export function siaxErrorCodeForUpstreamStatus(status: number): SiaxErrorCode { if (status === 400) return "INVALID_REQUEST"; if (status === 401) return "UNAUTHENTICATED"; if (status === 402) return "ENTITLEMENT_REQUIRED"; if (status === 403) return "AUTHORIZATION_DENIED"; if (status === 404) return "NOT_FOUND"; if (status === 409) return "CONFLICT"; if (status === 429) return "RATE_LIMITED"; if (status === 501) return "CAPABILITY_UNAVAILABLE"; if (status === 502 || status === 503 || status === 504) return "DEPENDENCY_UNAVAILABLE"; if (status >= 500) return "RETRYABLE_FAILURE"; return "PERMANENT_FAILURE"; } /** The typed error thrown at 0pt product boundaries. */ export class SiaxError extends Error { readonly code: SiaxErrorCode; /** Which dependency/capability produced the failure, e.g. "st0re", "0pt.search.query". */ readonly source: string; /** Correlation id of the failing operation, when known (§6). */ readonly correlationId?: string; readonly retryable: boolean; readonly httpStatus: number; constructor( code: SiaxErrorCode, source: string, detail: string, opts?: { correlationId?: string; cause?: unknown } ) { super(`[${code}] ${source}: ${detail}`, { cause: opts?.cause }); this.name = "SiaxError"; this.code = code; this.source = source; this.correlationId = opts?.correlationId; this.retryable = isRetryableSiaxErrorCode(code); this.httpStatus = httpStatusForSiaxErrorCode(code); } /** Structured log/JSON form. Never includes stack or raw upstream bodies. */ toJSON(): Record { return { error: this.code, source: this.source, message: this.message, retryable: this.retryable, ...(this.correlationId ? { correlation_id: this.correlationId } : {}), }; } }