feat: add TechLog Studio error mapping and CSRF token provider
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import type { HttpExecutionOutcome } from "../../../../adapters/http/http-execution-v3.ts";
|
||||
import { StudioGatewayError } from "../../application/ports/studio-gateway-error.ts";
|
||||
import type { ProblemDetails } from "../../contracts/studio/contract.ts";
|
||||
|
||||
/** canonical studio-v1.yaml `ProblemDetails.code` enum과 1:1이다. */
|
||||
export const STUDIO_ERROR_CODES = Object.freeze([
|
||||
"AUTHENTICATION_REQUIRED",
|
||||
"STUDIO_ACCESS_DENIED",
|
||||
"DOCUMENT_NOT_FOUND",
|
||||
"VERSION_CONFLICT",
|
||||
"REQUEST_VALIDATION_FAILED",
|
||||
"VALIDATION_FAILED",
|
||||
"VALIDATION_STALE",
|
||||
"PREVIEW_NOT_FOUND",
|
||||
"PREVIEW_STALE",
|
||||
"PREVIEW_EXPIRED",
|
||||
"PUBLICATION_NOT_FOUND",
|
||||
"PUBLICATION_CONFLICT",
|
||||
"PUBLICATION_EVENT_NOT_FOUND",
|
||||
"PUBLICATION_SNAPSHOT_NOT_FOUND",
|
||||
"WARNING_ACKNOWLEDGEMENT_REQUIRED",
|
||||
"IDEMPOTENCY_KEY_REUSED",
|
||||
"ASSET_NOT_FOUND",
|
||||
"ASSET_NOT_READY",
|
||||
"ASSET_IN_USE",
|
||||
"ASSET_QUARANTINED",
|
||||
"PAYLOAD_TOO_LARGE",
|
||||
"UNSUPPORTED_MEDIA_TYPE",
|
||||
"STUDIO_UNAVAILABLE",
|
||||
]) as readonly ProblemDetails["code"][];
|
||||
|
||||
const CODES = new Set<string>(STUDIO_ERROR_CODES);
|
||||
|
||||
function synthetic(
|
||||
code: ProblemDetails["code"],
|
||||
status: number,
|
||||
detail: string,
|
||||
retryable: boolean,
|
||||
): StudioGatewayError {
|
||||
return new StudioGatewayError({
|
||||
type: `https://techlog.local/problems/${code.toLowerCase().replaceAll("_", "-")}`,
|
||||
title: code,
|
||||
status,
|
||||
detail,
|
||||
code,
|
||||
retryable,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 서버가 계약 밖 코드를 보내면 도메인 코드를 지어내지 않는다. 전송 계층
|
||||
* 실패와 마찬가지로 `STUDIO_UNAVAILABLE`로 접는다.
|
||||
*/
|
||||
export function toStudioGatewayError(
|
||||
outcome: HttpExecutionOutcome<unknown, unknown>,
|
||||
operationId: string,
|
||||
): StudioGatewayError {
|
||||
switch (outcome.kind) {
|
||||
case "PROBLEM": {
|
||||
const problem = outcome.problem as ProblemDetails;
|
||||
if (problem && typeof problem.code === "string" && CODES.has(problem.code)) {
|
||||
return new StudioGatewayError(problem);
|
||||
}
|
||||
return synthetic(
|
||||
"STUDIO_UNAVAILABLE",
|
||||
outcome.metadata.status,
|
||||
`${operationId} returned an uncontracted problem code.`,
|
||||
false,
|
||||
);
|
||||
}
|
||||
case "UNAUTHENTICATED":
|
||||
return synthetic("AUTHENTICATION_REQUIRED", 401, `${operationId} requires authentication.`, false);
|
||||
case "FORBIDDEN":
|
||||
return synthetic("STUDIO_ACCESS_DENIED", 403, `${operationId} was denied.`, false);
|
||||
case "CANCELLED":
|
||||
return synthetic("STUDIO_UNAVAILABLE", 499, `${operationId} was cancelled.`, false);
|
||||
case "RATE_LIMITED":
|
||||
return synthetic("STUDIO_UNAVAILABLE", 429, `${operationId} was rate limited.`, true);
|
||||
case "TRANSPORT_FAILURE":
|
||||
return synthetic("STUDIO_UNAVAILABLE", 503, `${operationId} transport failed.`, true);
|
||||
case "AUTH_INTEGRATION_FAILURE":
|
||||
case "CONTRACT_VIOLATION":
|
||||
return synthetic("STUDIO_UNAVAILABLE", 502, `${operationId} broke its contract.`, false);
|
||||
case "SUCCESS":
|
||||
throw new Error(`${operationId}: success outcome is not an error`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user