| @@ -4,7 +4,7 @@ import { · 3 unmodified lines |
| 4 | 4 | NotFoundException, |
| 5 | 5 | WorkOS as WorkOSClient, |
| 6 | 6 | } from "@workos-inc/node/worker"; |
| 7 | | −import { Data, Effect, Result } from "effect"; |
| 7 | +import { Data, Effect, Option, Result, Schema } from "effect"; |
| 8 | 8 | |
| 9 | 9 | export interface WorkOSVaultObjectMetadata { |
| 10 | 10 | readonly context: Record<string, unknown>; |
| @@ -20,44 +20,49 @@ export interface WorkOSVaultObject { · 9 unmodified lines |
| 20 | 20 | readonly value?: string; |
| 21 | 21 | } |
| 22 | 22 | |
| 23 | | −// Minimal shape carrying an HTTP-style status code. Production WorkOS errors |
| 24 | | −// (`GenericServerException`/`NotFoundException`) and test fakes both populate |
| 25 | | −// a numeric `status`, so the boundary normalises against this named type |
| 26 | | −// rather than probing arbitrary unknown shapes. |
| 27 | | −interface ErrorWithStatus extends Error { |
| 28 | | − readonly status: number; |
| 29 | | −} |
| 23 | +const WORKOS_KEK_NOT_READY_MESSAGE = |
| 24 | + "KEK was created but is not yet ready. This request can be retried."; |
| 30 | 25 | |
| 31 | | −const isErrorWithStatus = (cause: unknown): cause is ErrorWithStatus => |
| 32 | | − cause instanceof Error && typeof (cause as ErrorWithStatus).status === "number"; |
| 26 | +const CauseWithStatusSchema = Schema.Struct({ |
| 27 | + status: Schema.Number, |
| 28 | +}); |
| 33 | 29 | |
| 34 | 30 | const statusFromWorkOSCause = (cause: unknown): number | undefined => { |
| 35 | 31 | if (cause instanceof GenericServerException || cause instanceof NotFoundException) { |
| 36 | 32 | return cause.status; |
| 37 | 33 | } |
| 38 | | − if (isErrorWithStatus(cause)) return cause.status; |
| 39 | | − return undefined; |
| 34 | + return Option.match(Schema.decodeUnknownOption(CauseWithStatusSchema)(cause), { |
| 35 | + onNone: () => undefined, |
| 36 | + onSome: (decoded) => decoded.status, |
| 37 | + }); |
| 40 | 38 | }; |
| 41 | 39 | |
| 42 | | −const messageFromWorkOSCause = (cause: unknown): string => |
| 43 | | − cause instanceof Error ? cause.message : typeof cause === "string" ? cause : ""; |
| 40 | +const isKekNotReadyWorkOSCause = (cause: unknown): boolean => |
| 41 | + cause instanceof GenericServerException && |
| 42 | + // oxlint-disable-next-line executor/no-unknown-error-message -- boundary: WorkOS only exposes this retryable Vault condition through its SDK exception message |
| 43 | + cause.message.endsWith(WORKOS_KEK_NOT_READY_MESSAGE); |
| 44 | 44 | |
| 45 | 45 | export class WorkOSVaultClientError extends Data.TaggedError("WorkOSVaultClientError")<{ |
| 46 | 46 | readonly cause: unknown; |
| 47 | 47 | readonly message: string; |
| 48 | 48 | readonly operation: string; |
| 49 | + readonly retryKind?: "kek_not_ready"; |
| 49 | 50 | readonly status?: number; |
| 50 | 51 | }> { |
| 51 | 52 | constructor(options: { |
| 52 | 53 | readonly cause: unknown; |
| 53 | 54 | readonly message?: string; |
| 54 | 55 | readonly operation: string; |
| 56 | + readonly retryKind?: "kek_not_ready"; |
| 55 | 57 | readonly status?: number; |
| 56 | 58 | }) { |
| 57 | 59 | super({ |
| 58 | 60 | cause: options.cause, |
| 59 | | − message: options.message ?? messageFromWorkOSCause(options.cause), |
| 61 | + message: options.message ?? `WorkOS Vault ${options.operation} failed`, |
| 60 | 62 | operation: options.operation, |
| 63 | + retryKind: |
| 64 | + options.retryKind ?? |
| 65 | + (isKekNotReadyWorkOSCause(options.cause) ? "kek_not_ready" : undefined), |
| 61 | 66 | status: options.status ?? statusFromWorkOSCause(options.cause), |
| 62 | 67 | }); |
| 63 | 68 | } |