@distilled.cloud/core 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.ts ADDED
@@ -0,0 +1,511 @@
1
+ /**
2
+ * REST API Client
3
+ *
4
+ * Provides the core API.make() factory for building typed Effect-based API operations.
5
+ * This is the shared client for REST/OpenAPI-style SDKs (PlanetScale, Neon, GCP).
6
+ *
7
+ * AWS and Cloudflare have their own more specialized client implementations,
8
+ * but they share the same OperationMethod pattern.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { API } from "@distilled.cloud/sdk-core/client";
13
+ *
14
+ * const listDatabases = API.make(() => ({
15
+ * inputSchema: ListDatabasesInput,
16
+ * outputSchema: ListDatabasesOutput,
17
+ * errors: [NotFound, Forbidden] as const,
18
+ * }));
19
+ *
20
+ * // Direct call
21
+ * const result = yield* listDatabases({ organization: "my-org" });
22
+ *
23
+ * // Yield first for requirement-free function
24
+ * const fn = yield* listDatabases;
25
+ * const result = yield* fn({ organization: "my-org" });
26
+ * ```
27
+ */
28
+ import * as Effect from "effect/Effect";
29
+ import { pipeArguments } from "effect/Pipeable";
30
+ import * as Schema from "effect/Schema";
31
+ import * as AST from "effect/SchemaAST";
32
+ import * as Stream from "effect/Stream";
33
+ import * as HttpBody from "effect/unstable/http/HttpBody";
34
+ import * as HttpClient from "effect/unstable/http/HttpClient";
35
+ import * as HttpClientError from "effect/unstable/http/HttpClientError";
36
+ import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
37
+ import { SingleShotGen } from "effect/Utils";
38
+ import * as Traits from "./traits.ts";
39
+ import { getPath } from "./traits.ts";
40
+ import type { PaginatedTrait } from "./pagination.ts";
41
+
42
+ // ============================================================================
43
+ // Client Types
44
+ // ============================================================================
45
+
46
+ /**
47
+ * An operation that can be used in two ways:
48
+ * 1. Direct call: `yield* operation(input)` - returns Effect with requirements
49
+ * 2. Yield first: `const fn = yield* operation` - captures services, returns requirement-free function
50
+ */
51
+ export type OperationMethod<I, A, E, R> = Effect.Effect<
52
+ (input: I) => Effect.Effect<A, E, never>,
53
+ never,
54
+ R
55
+ > &
56
+ ((input: I) => Effect.Effect<A, E, R>);
57
+
58
+ /**
59
+ * A paginated operation that additionally has `.pages()` and `.items()` methods.
60
+ */
61
+ export type PaginatedOperationMethod<I, A, E, R> = OperationMethod<
62
+ I,
63
+ A,
64
+ E,
65
+ R
66
+ > & {
67
+ pages: (input: I) => Stream.Stream<A, E, R>;
68
+ items: (input: I) => Stream.Stream<unknown, E, R>;
69
+ };
70
+
71
+ /**
72
+ * Configuration for the API client factory.
73
+ * SDKs provide this to customize how errors are matched and credentials are applied.
74
+ */
75
+ export interface ClientConfig<Creds> {
76
+ /** The credentials service tag */
77
+ credentials: {
78
+ new (): Creds;
79
+ };
80
+
81
+ /** Get the base URL from credentials */
82
+ getBaseUrl: (creds: Creds) => string;
83
+
84
+ /** Get authorization header(s) from credentials */
85
+ getAuthHeaders: (creds: Creds) => Record<string, string>;
86
+
87
+ /** Match an error response body to a typed error.
88
+ * Should return Effect.fail(error) for known errors,
89
+ * or Effect.fail(fallbackError) for unknown errors.
90
+ * The optional `errors` parameter provides per-operation typed error classes.
91
+ */
92
+ matchError: (
93
+ status: number,
94
+ body: unknown,
95
+ errors?: readonly ApiErrorClass[],
96
+ ) => Effect.Effect<never, unknown>;
97
+
98
+ /** Parse error class for schema decode failures */
99
+ ParseError: new (props: { body: unknown; cause: unknown }) => unknown;
100
+
101
+ /**
102
+ * Optional transform applied to the response body before schema decoding.
103
+ * For example, Cloudflare wraps responses in `{ result: <data>, ... }`.
104
+ */
105
+ transformResponse?: (body: unknown) => unknown;
106
+ }
107
+
108
+ /**
109
+ * Base API error type - any error class with at least a _tag and message.
110
+ * Uses `new (...args: any[])` to accommodate error classes with extra fields (e.g. `code`).
111
+ */
112
+ export type ApiErrorClass = {
113
+ new (...args: any[]): {
114
+ readonly _tag: string;
115
+ readonly message: string;
116
+ };
117
+ };
118
+
119
+ /**
120
+ * Operation configuration with optional operation-specific errors.
121
+ * Supports both `inputSchema`/`outputSchema` and `input`/`output` aliases.
122
+ */
123
+ export interface OperationConfig<
124
+ I extends Schema.Top,
125
+ O extends Schema.Top,
126
+ E extends readonly ApiErrorClass[] = readonly ApiErrorClass[],
127
+ > {
128
+ inputSchema?: I;
129
+ outputSchema?: O;
130
+ /** Alias for inputSchema (used by Cloudflare/GCP generators) */
131
+ input?: I;
132
+ /** Alias for outputSchema (used by Cloudflare/GCP generators) */
133
+ output?: O;
134
+ errors?: E;
135
+ }
136
+
137
+ /**
138
+ * Paginated operation configuration.
139
+ */
140
+ export interface PaginatedOperationConfig<
141
+ I extends Schema.Top,
142
+ O extends Schema.Top,
143
+ E extends readonly ApiErrorClass[] = readonly ApiErrorClass[],
144
+ > extends OperationConfig<I, O, E> {
145
+ pagination?: PaginatedTrait;
146
+ }
147
+
148
+ // ============================================================================
149
+ // AST Helpers
150
+ // ============================================================================
151
+
152
+ /**
153
+ * Check if a schema AST represents an array type.
154
+ * Follows encoding chains and Suspend wrappers.
155
+ */
156
+ function isArrayAST(ast: AST.AST): boolean {
157
+ if (ast._tag === "Arrays") return true;
158
+ if (ast._tag === "Suspend") return isArrayAST(ast.thunk());
159
+ if (ast.encoding && ast.encoding.length > 0)
160
+ return isArrayAST(ast.encoding[0].to);
161
+ return false;
162
+ }
163
+
164
+ // ============================================================================
165
+ // Multipart FormData Builder
166
+ // ============================================================================
167
+
168
+ /**
169
+ * Check if a value is a File or Blob.
170
+ */
171
+ function isFileOrBlob(value: unknown): value is File | Blob {
172
+ return (
173
+ (typeof File !== "undefined" && value instanceof File) ||
174
+ (typeof Blob !== "undefined" && value instanceof Blob)
175
+ );
176
+ }
177
+
178
+ /**
179
+ * Build a FormData from a record of body properties.
180
+ * Handles files/blobs, arrays of files, objects (as JSON blobs), and primitives.
181
+ *
182
+ * This is used for multipart operations (e.g., Cloudflare Workers script uploads)
183
+ * where the body contains a mix of metadata objects and file uploads.
184
+ */
185
+ function buildFormData(body: Record<string, unknown>): FormData {
186
+ const formData = new FormData();
187
+
188
+ for (const [key, value] of Object.entries(body)) {
189
+ if (value === undefined || value === null) continue;
190
+
191
+ if (isFileOrBlob(value)) {
192
+ // Single file/blob
193
+ formData.append(key, value, value instanceof File ? value.name : key);
194
+ } else if (
195
+ Array.isArray(value) &&
196
+ value.length > 0 &&
197
+ isFileOrBlob(value[0])
198
+ ) {
199
+ // Array of files/blobs — append each individually
200
+ for (const file of value) {
201
+ if (isFileOrBlob(file)) {
202
+ formData.append(
203
+ file instanceof File ? file.name : key,
204
+ file,
205
+ file instanceof File ? file.name : undefined,
206
+ );
207
+ }
208
+ }
209
+ } else if (typeof value === "object" && value !== null) {
210
+ // Object → append as JSON blob
211
+ formData.append(
212
+ key,
213
+ new Blob([JSON.stringify(value)], { type: "application/json" }),
214
+ key,
215
+ );
216
+ } else {
217
+ // Primitive → append as string
218
+ formData.append(key, String(value));
219
+ }
220
+ }
221
+
222
+ return formData;
223
+ }
224
+
225
+ // ============================================================================
226
+ // API Client Factory
227
+ // ============================================================================
228
+
229
+ /**
230
+ * Creates an API namespace bound to a specific SDK's client configuration.
231
+ *
232
+ * @example
233
+ * ```ts
234
+ * // In planetscale-sdk/src/client.ts
235
+ * export const API = makeAPI({
236
+ * credentials: Credentials,
237
+ * getBaseUrl: (c) => c.apiBaseUrl,
238
+ * getAuthHeaders: (c) => ({ Authorization: c.token }),
239
+ * matchError: matchPlanetScaleError,
240
+ * ParseError: PlanetScaleParseError,
241
+ * });
242
+ * ```
243
+ */
244
+ export const makeAPI = <Creds>(config: ClientConfig<Creds>) => {
245
+ type _ClientErrors = HttpClientError.HttpClientError | HttpBody.HttpBodyError;
246
+
247
+ return {
248
+ make: <
249
+ I extends Schema.Top,
250
+ O extends Schema.Top,
251
+ const E extends readonly ApiErrorClass[] = readonly [],
252
+ >(
253
+ configFn: () => OperationConfig<I, O, E>,
254
+ ): any => {
255
+ const opConfig = configFn();
256
+ // Support both input/output and inputSchema/outputSchema aliases
257
+ const inputSchema = (opConfig.inputSchema ?? opConfig.input)!;
258
+ const outputSchema = (opConfig.outputSchema ?? opConfig.output)!;
259
+ type Input = Schema.Schema.Type<I>;
260
+
261
+ // Read HTTP trait from input schema annotations
262
+ const httpTrait = Traits.getHttpTrait(inputSchema.ast);
263
+
264
+ if (!httpTrait) {
265
+ throw new Error("Input schema must have Http trait");
266
+ }
267
+
268
+ const method = httpTrait.method;
269
+
270
+ const fn = (input: Input): Effect.Effect<any, any, any> =>
271
+ Effect.gen(function* () {
272
+ const creds = yield* config.credentials as any;
273
+ const client = yield* HttpClient.HttpClient;
274
+
275
+ const baseUrl = config.getBaseUrl(creds as Creds);
276
+ const authHeaders = config.getAuthHeaders(creds as Creds);
277
+
278
+ // Use schema-aware request builder for proper camelCase → wire_name mapping
279
+ const parts = Traits.buildRequestParts(
280
+ inputSchema.ast,
281
+ httpTrait,
282
+ input as Record<string, unknown>,
283
+ inputSchema,
284
+ );
285
+
286
+ let request = HttpClientRequest.make(method)(
287
+ baseUrl + parts.path,
288
+ ).pipe(
289
+ HttpClientRequest.setHeaders(authHeaders),
290
+ HttpClientRequest.setHeaders(parts.headers),
291
+ HttpClientRequest.setHeader("Accept", "application/json"),
292
+ );
293
+
294
+ // Set Content-Type based on body type (skip for FormData — browser sets boundary)
295
+ if (!parts.isMultipart) {
296
+ request = HttpClientRequest.setHeader(
297
+ "Content-Type",
298
+ "application/json",
299
+ )(request);
300
+ }
301
+
302
+ if (Object.keys(parts.query).length > 0) {
303
+ request = HttpClientRequest.setUrlParams(request, parts.query);
304
+ }
305
+ if (method !== "GET" && parts.body !== undefined) {
306
+ if (parts.isMultipart) {
307
+ // Build FormData from body properties for multipart operations
308
+ const formData = buildFormData(
309
+ parts.body as Record<string, unknown>,
310
+ );
311
+ request = HttpClientRequest.setBody(HttpBody.formData(formData))(
312
+ request,
313
+ );
314
+ } else {
315
+ request = yield* HttpClientRequest.bodyJson(parts.body)(request);
316
+ }
317
+ } else if (method === "GET" && parts.body !== undefined) {
318
+ // For GET requests, remaining non-annotated fields go as query params
319
+ const extraQuery: Record<string, string> = {};
320
+ for (const [key, value] of Object.entries(
321
+ parts.body as Record<string, unknown>,
322
+ )) {
323
+ if (value !== undefined) {
324
+ extraQuery[key] = String(value);
325
+ }
326
+ }
327
+ if (Object.keys(extraQuery).length > 0) {
328
+ request = HttpClientRequest.setUrlParams(request, extraQuery);
329
+ }
330
+ }
331
+
332
+ const response = yield* client.execute(request).pipe(Effect.scoped);
333
+
334
+ if (response.status >= 400) {
335
+ // Try to parse error body as JSON; fall back to text if not JSON
336
+ const errorBody = yield* response.json.pipe(
337
+ Effect.catchIf(
338
+ () => true,
339
+ () =>
340
+ response.text.pipe(
341
+ Effect.map(
342
+ (text) =>
343
+ ({ _nonJsonError: true, body: text }) as unknown,
344
+ ),
345
+ Effect.catchIf(
346
+ () => true,
347
+ () =>
348
+ Effect.succeed({
349
+ _nonJsonError: true,
350
+ body: `HTTP ${response.status}`,
351
+ } as unknown),
352
+ ),
353
+ ),
354
+ ),
355
+ );
356
+ return yield* config.matchError(
357
+ response.status,
358
+ errorBody,
359
+ opConfig.errors,
360
+ );
361
+ }
362
+
363
+ // For void-returning operations (e.g. DELETE 204 No Content)
364
+ if (AST.isVoid(outputSchema.ast)) {
365
+ return undefined;
366
+ }
367
+
368
+ // For 204 No Content: if schema is not Unknown, return undefined.
369
+ // If schema IS Unknown, return empty string (so callers get a defined value).
370
+ if (response.status === 204) {
371
+ if (outputSchema.ast._tag === "Unknown") {
372
+ return "";
373
+ }
374
+ return undefined;
375
+ }
376
+
377
+ // Try to parse response as JSON; fall back to text for non-JSON responses
378
+ // (e.g., multipart/form-data worker scripts, raw KV values)
379
+ const rawBody = yield* response.json.pipe(
380
+ Effect.catchIf(
381
+ () => true,
382
+ () => response.text.pipe(Effect.map((text) => text as unknown)),
383
+ ),
384
+ );
385
+ let responseBody = config.transformResponse
386
+ ? config.transformResponse(rawBody)
387
+ : rawBody;
388
+
389
+ // Handle Cloudflare-style paginated responses where result is
390
+ // { items: [...] } but the schema expects an array
391
+ if (
392
+ isArrayAST(outputSchema.ast) &&
393
+ !Array.isArray(responseBody) &&
394
+ typeof responseBody === "object" &&
395
+ responseBody !== null &&
396
+ "items" in responseBody &&
397
+ Array.isArray((responseBody as Record<string, unknown>).items)
398
+ ) {
399
+ responseBody = (responseBody as Record<string, unknown>).items;
400
+ }
401
+
402
+ return yield* Schema.decodeUnknownEffect(outputSchema)(
403
+ responseBody,
404
+ ).pipe(
405
+ Effect.catchTag("SchemaError", (cause) =>
406
+ Effect.fail(new config.ParseError({ body: rawBody, cause })),
407
+ ),
408
+ );
409
+ });
410
+
411
+ const Proto = {
412
+ [Symbol.iterator]() {
413
+ return new SingleShotGen(this);
414
+ },
415
+ pipe() {
416
+ return pipeArguments(this.asEffect(), arguments);
417
+ },
418
+ asEffect() {
419
+ return Effect.map(
420
+ Effect.services(),
421
+ (sm) => (input: Input) => fn(input).pipe(Effect.provide(sm)),
422
+ );
423
+ },
424
+ };
425
+
426
+ return Object.assign(fn, Proto);
427
+ },
428
+
429
+ makePaginated: <
430
+ I extends Schema.Top,
431
+ O extends Schema.Top,
432
+ const E extends readonly ApiErrorClass[] = readonly [],
433
+ >(
434
+ configFn: () => PaginatedOperationConfig<I, O, E>,
435
+ paginateFn?: (
436
+ baseFn: (input: any) => Effect.Effect<any, any, any>,
437
+ input: any,
438
+ pagination: PaginatedTrait,
439
+ ) => Stream.Stream<any, any, any>,
440
+ ): any => {
441
+ const opConfig = configFn();
442
+ const pagination = opConfig.pagination!;
443
+
444
+ // Create the base operation
445
+ const baseFn = makeAPI(config).make(() => ({
446
+ inputSchema: opConfig.inputSchema ?? opConfig.input,
447
+ outputSchema: opConfig.outputSchema ?? opConfig.output,
448
+ errors: opConfig.errors,
449
+ }));
450
+
451
+ type Input = Schema.Schema.Type<I>;
452
+
453
+ // Default pagination: token-based (works for Cloudflare/GCP style)
454
+ const defaultPaginateFn = (
455
+ op: (input: any) => Effect.Effect<any, any, any>,
456
+ input: any,
457
+ pag: PaginatedTrait,
458
+ ): Stream.Stream<any, any, any> => {
459
+ type State = { token: unknown; done: boolean };
460
+ return Stream.unfold(
461
+ { token: undefined, done: false } as State,
462
+ (state: State) =>
463
+ Effect.gen(function* () {
464
+ if (state.done) return undefined;
465
+ const requestPayload =
466
+ state.token !== undefined
467
+ ? { ...input, [pag.inputToken]: state.token }
468
+ : input;
469
+ const response = yield* op(requestPayload);
470
+ const nextToken = getPath(response, pag.outputToken);
471
+ return [
472
+ response,
473
+ {
474
+ token: nextToken,
475
+ done: nextToken === undefined || nextToken === null,
476
+ },
477
+ ] as const;
478
+ }),
479
+ );
480
+ };
481
+
482
+ const paginate = paginateFn ?? defaultPaginateFn;
483
+
484
+ // Stream all pages
485
+ const pagesFn = (input: Omit<Input, string>) =>
486
+ paginate(baseFn, input, pagination);
487
+
488
+ // Stream individual items
489
+ const itemsFn = (input: Omit<Input, string>) =>
490
+ pagesFn(input).pipe(
491
+ Stream.flatMap((page) => {
492
+ if (!pagination.items) return Stream.make(page);
493
+ const items = getPath(page, pagination.items) as
494
+ | readonly unknown[]
495
+ | undefined;
496
+ return Stream.fromIterable(items ?? []);
497
+ }),
498
+ );
499
+
500
+ const result = baseFn as typeof baseFn & {
501
+ pages: typeof pagesFn;
502
+ items: typeof itemsFn;
503
+ };
504
+
505
+ result.pages = pagesFn;
506
+ result.items = itemsFn;
507
+
508
+ return result;
509
+ },
510
+ };
511
+ };
package/src/errors.ts ADDED
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Common error types shared across SDKs.
3
+ *
4
+ * Each SDK defines its own provider-specific errors (e.g., Unauthorized, NotFound)
5
+ * using the category system. This module provides base error types and utilities
6
+ * that are used across all SDKs.
7
+ */
8
+ import * as Schema from "effect/Schema";
9
+ import * as Category from "./category.ts";
10
+
11
+ // ============================================================================
12
+ // Common HTTP Status Error Classes
13
+ // ============================================================================
14
+
15
+ /**
16
+ * Unauthorized - Authentication failure (401).
17
+ */
18
+ export class Unauthorized extends Schema.TaggedErrorClass<Unauthorized>()(
19
+ "Unauthorized",
20
+ { message: Schema.String },
21
+ ).pipe(Category.withAuthError) {}
22
+
23
+ /**
24
+ * Forbidden - Access denied (403).
25
+ */
26
+ export class Forbidden extends Schema.TaggedErrorClass<Forbidden>()(
27
+ "Forbidden",
28
+ { message: Schema.String },
29
+ ).pipe(Category.withAuthError) {}
30
+
31
+ /**
32
+ * NotFound - Resource not found (404).
33
+ */
34
+ export class NotFound extends Schema.TaggedErrorClass<NotFound>()("NotFound", {
35
+ message: Schema.String,
36
+ }).pipe(Category.withNotFoundError) {}
37
+
38
+ /**
39
+ * BadRequest - Invalid request (400).
40
+ */
41
+ export class BadRequest extends Schema.TaggedErrorClass<BadRequest>()(
42
+ "BadRequest",
43
+ { message: Schema.String },
44
+ ).pipe(Category.withBadRequestError) {}
45
+
46
+ /**
47
+ * Conflict - Resource conflict (409).
48
+ */
49
+ export class Conflict extends Schema.TaggedErrorClass<Conflict>()("Conflict", {
50
+ message: Schema.String,
51
+ }).pipe(Category.withConflictError) {}
52
+
53
+ /**
54
+ * UnprocessableEntity - Validation error (422).
55
+ */
56
+ export class UnprocessableEntity extends Schema.TaggedErrorClass<UnprocessableEntity>()(
57
+ "UnprocessableEntity",
58
+ { message: Schema.String },
59
+ ).pipe(Category.withBadRequestError) {}
60
+
61
+ /**
62
+ * TooManyRequests - Rate limited (429).
63
+ */
64
+ export class TooManyRequests extends Schema.TaggedErrorClass<TooManyRequests>()(
65
+ "TooManyRequests",
66
+ { message: Schema.String },
67
+ ).pipe(
68
+ Category.withThrottlingError,
69
+ Category.withRetryable({ throttling: true }),
70
+ ) {}
71
+
72
+ /**
73
+ * Locked - Resource locked (423).
74
+ */
75
+ export class Locked extends Schema.TaggedErrorClass<Locked>()("Locked", {
76
+ message: Schema.String,
77
+ }).pipe(Category.withLockedError, Category.withRetryable()) {}
78
+
79
+ /**
80
+ * InternalServerError - Server error (500).
81
+ */
82
+ export class InternalServerError extends Schema.TaggedErrorClass<InternalServerError>()(
83
+ "InternalServerError",
84
+ { message: Schema.String },
85
+ ).pipe(Category.withServerError, Category.withRetryable()) {}
86
+
87
+ /**
88
+ * ServiceUnavailable - Service unavailable (503).
89
+ */
90
+ export class ServiceUnavailable extends Schema.TaggedErrorClass<ServiceUnavailable>()(
91
+ "ServiceUnavailable",
92
+ { message: Schema.String },
93
+ ).pipe(Category.withServerError, Category.withRetryable()) {}
94
+
95
+ /**
96
+ * Configuration error - missing or invalid configuration.
97
+ */
98
+ export class ConfigError extends Schema.TaggedErrorClass<ConfigError>()(
99
+ "ConfigError",
100
+ { message: Schema.String },
101
+ ).pipe(Category.withConfigurationError) {}
102
+
103
+ // ============================================================================
104
+ // Error Maps
105
+ // ============================================================================
106
+
107
+ /**
108
+ * Mapping from HTTP status codes to common error classes.
109
+ */
110
+ export const HTTP_STATUS_MAP = {
111
+ 400: BadRequest,
112
+ 401: Unauthorized,
113
+ 403: Forbidden,
114
+ 404: NotFound,
115
+ 409: Conflict,
116
+ 422: UnprocessableEntity,
117
+ 423: Locked,
118
+ 429: TooManyRequests,
119
+ 500: InternalServerError,
120
+ 503: ServiceUnavailable,
121
+ } as const;
122
+
123
+ /**
124
+ * HTTP status codes that are considered "default" errors (always present).
125
+ * These are excluded from per-operation error types since they're handled globally.
126
+ */
127
+ export const DEFAULT_ERROR_STATUSES = new Set([401, 429, 500, 503]);
128
+
129
+ /**
130
+ * All common API error classes.
131
+ */
132
+ export const API_ERRORS = [
133
+ Unauthorized,
134
+ Forbidden,
135
+ NotFound,
136
+ BadRequest,
137
+ Conflict,
138
+ UnprocessableEntity,
139
+ TooManyRequests,
140
+ Locked,
141
+ InternalServerError,
142
+ ServiceUnavailable,
143
+ ] as const;
144
+
145
+ /**
146
+ * Default errors that apply to ALL operations.
147
+ * These are infrastructure-level errors that can occur regardless of the operation.
148
+ */
149
+ export const DEFAULT_ERRORS = [
150
+ Unauthorized,
151
+ TooManyRequests,
152
+ InternalServerError,
153
+ ServiceUnavailable,
154
+ ] as const;
155
+
156
+ export type DefaultErrors = InstanceType<(typeof DEFAULT_ERRORS)[number]>;