- 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>
123 lines
4.1 KiB
TypeScript
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 } : {}),
|
|
};
|
|
}
|
|
}
|