Files
0pt/src/platform/errors.ts
T
Simon AxelssonandClaude Fable 5 b7038d7675 feat(platform): canonical SIAX context/errors + real-contract clients, no prod mock fallback
- src/platform/: SiaxRequestContext + propagation headers + 12 canonical
  error codes (documented mirror of cl0ud packages/contract), SiaxError,
  resource refs (st0re:artifact:...), and the §3/§10 dependency-mode guard:
  production runtime + unconfigured dependency/store => hard
  DEPENDENCY_UNAVAILABLE at construction, never silent mock/in-memory.
- src/clients/: all four platform clients rewritten against the REAL wire
  contracts verified on sibling main branches this session:
  * cl0ud: POST /api/policy/evaluate ({data} envelope, allow+requiresApproval)
    - the old /v1/cl0ud/policies/evaluate path never existed on CL0UD
  * st0re: camelCase ArtifactInput on /v1/st0re/artifacts/register incl.
    X-Tenant-Id/X-Actor-Id + X-Idempotency-Key - the old snake_case body
    would have been a 400 against the live registry
  * n0d: required_role + resource_requirements + body idempotency_key +
    CloudEvents callback_url per n0d createJobSchema; adds getJob/getJobResult
  * aud0: /v1/aud0/openlineage is AUD0's only lineage ingest -
    /v1/aud0/lineage/events never existed; legacy shape converts to a
    spec-shaped OpenLineage RunEvent (deterministic runId dedup)
- src/lineage/: OpenLineage subset types + LineageEmitter (never-throw,
  §16 evidence semantics)
- shared SiaxHttp transport: timeouts, typed SiaxError mapping, context
  propagation, no raw upstream bodies in errors

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 16:35:01 +02:00

123 lines
4.1 KiB
TypeScript

/**
* 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<string, unknown> {
return {
error: this.code,
source: this.source,
message: this.message,
retryable: this.retryable,
...(this.correlationId ? { correlation_id: this.correlationId } : {}),
};
}
}