@automate.ax/integration-contracts 0.104.0 → 0.107.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.
Files changed (41) hide show
  1. package/dist/close/events.d.ts +13 -13
  2. package/dist/close/schemas.d.ts +9 -9
  3. package/dist/cloudflare/api.d.ts +93 -0
  4. package/dist/cloudflare/api.js +250 -0
  5. package/dist/cloudflare/events.d.ts +1340 -0
  6. package/dist/cloudflare/events.js +178 -0
  7. package/dist/cloudflare/index.d.ts +3 -0
  8. package/dist/cloudflare/index.js +3 -0
  9. package/dist/cloudflare/schemas.d.ts +608 -0
  10. package/dist/cloudflare/schemas.js +340 -0
  11. package/dist/linear/api.d.ts +1 -1
  12. package/dist/linear/api.js +9 -6
  13. package/dist/linear/index.d.ts +3479 -782
  14. package/dist/linear/index.js +249 -13
  15. package/dist/linear/schemas.d.ts +2721 -28
  16. package/dist/linear/schemas.js +515 -2
  17. package/dist/notion/index.js +2 -2
  18. package/dist/notion/schemas.d.ts +2140 -58
  19. package/dist/notion/schemas.js +41 -2
  20. package/dist/outlook/schemas.d.ts +2 -2
  21. package/dist/resend/action-schemas.d.ts +9 -9
  22. package/dist/resend/index.d.ts +36 -36
  23. package/dist/resend/schemas.d.ts +40 -40
  24. package/dist/slack/event-schemas.d.ts +583 -583
  25. package/dist/slack/index.d.ts +303 -303
  26. package/dist/slack/schemas.d.ts +1 -1
  27. package/dist/teams/schemas.d.ts +5 -5
  28. package/dist/triggers.d.ts +2 -1
  29. package/dist/whatsapp/index.d.ts +2 -2
  30. package/dist/whatsapp/schemas.d.ts +5 -5
  31. package/package.json +8 -2
  32. package/src/cloudflare/api.ts +340 -0
  33. package/src/cloudflare/events.ts +212 -0
  34. package/src/cloudflare/index.ts +3 -0
  35. package/src/cloudflare/schemas.ts +359 -0
  36. package/src/linear/api.ts +10 -6
  37. package/src/linear/index.ts +249 -26
  38. package/src/linear/schemas.ts +688 -2
  39. package/src/notion/index.ts +2 -2
  40. package/src/notion/schemas.ts +58 -4
  41. package/src/triggers.ts +2 -0
@@ -110,8 +110,8 @@ export declare const SLACK_REACTION_ITEM_SCHEMA: z.ZodDiscriminatedUnion<[z.ZodO
110
110
  conversationId: z.ZodString;
111
111
  conversationType: z.ZodOptional<z.ZodEnum<{
112
112
  group: "group";
113
- channel: "channel";
114
113
  im: "im";
114
+ channel: "channel";
115
115
  mpim: "mpim";
116
116
  }>>;
117
117
  messageTimestamp: z.ZodString;
@@ -56,11 +56,11 @@ export declare const TEAMS_CHANNEL_SCHEMA: z.ZodObject<{
56
56
  channelId: z.ZodString;
57
57
  isArchived: z.ZodPrefault<z.ZodBoolean>;
58
58
  membershipType: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodEnum<{
59
+ standard: "standard";
59
60
  private: "private";
60
61
  shared: "shared";
61
62
  unknownFutureValue: "unknownFutureValue";
62
- standard: "standard";
63
- }>>>, z.ZodTransform<"private" | "shared" | "unknownFutureValue" | "standard" | undefined, "private" | "shared" | "unknownFutureValue" | "standard" | null | undefined>>;
63
+ }>>>, z.ZodTransform<"standard" | "private" | "shared" | "unknownFutureValue" | undefined, "standard" | "private" | "shared" | "unknownFutureValue" | null | undefined>>;
64
64
  webUrl: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
65
65
  }, z.core.$strip>;
66
66
  export declare const GRAPH_CHANNEL_SCHEMA: z.ZodPipe<z.ZodObject<{
@@ -71,10 +71,10 @@ export declare const GRAPH_CHANNEL_SCHEMA: z.ZodPipe<z.ZodObject<{
71
71
  id: z.ZodString;
72
72
  isArchived: z.ZodOptional<z.ZodBoolean>;
73
73
  membershipType: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
74
+ standard: "standard";
74
75
  private: "private";
75
76
  shared: "shared";
76
77
  unknownFutureValue: "unknownFutureValue";
77
- standard: "standard";
78
78
  }>>>;
79
79
  webUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
80
80
  }, z.core.$loose>, z.ZodTransform<{
@@ -84,7 +84,7 @@ export declare const GRAPH_CHANNEL_SCHEMA: z.ZodPipe<z.ZodObject<{
84
84
  email: string | undefined;
85
85
  channelId: string;
86
86
  isArchived: boolean;
87
- membershipType: "private" | "shared" | "unknownFutureValue" | "standard" | undefined;
87
+ membershipType: "standard" | "private" | "shared" | "unknownFutureValue" | undefined;
88
88
  webUrl: string | undefined;
89
89
  }, {
90
90
  [x: string]: unknown;
@@ -94,7 +94,7 @@ export declare const GRAPH_CHANNEL_SCHEMA: z.ZodPipe<z.ZodObject<{
94
94
  description?: string | null | undefined;
95
95
  email?: string | null | undefined;
96
96
  isArchived?: boolean | undefined;
97
- membershipType?: "private" | "shared" | "unknownFutureValue" | "standard" | null | undefined;
97
+ membershipType?: "standard" | "private" | "shared" | "unknownFutureValue" | null | undefined;
98
98
  webUrl?: string | null | undefined;
99
99
  }>>;
100
100
  export declare const TEAMS_MEMBER_SCHEMA: z.ZodObject<{
@@ -4,6 +4,7 @@ import type { asanaTriggerContracts } from "./asana/index.js";
4
4
  import type { brevoTriggerContracts } from "./brevo/index.js";
5
5
  import type { convexTriggerContracts } from "./convex/index.js";
6
6
  import type { closeTriggerContracts } from "./close/index.js";
7
+ import type { cloudflareTriggerContracts } from "./cloudflare/index.js";
7
8
  import type { githubTriggerContracts } from "./github/index.js";
8
9
  import type { gmailTriggerContracts } from "./gmail/index.js";
9
10
  import type { googleCalendarTriggerContracts } from "./google-calendar/index.js";
@@ -21,7 +22,7 @@ import type { vercelTriggerContracts } from "./vercel/index.js";
21
22
  import type { webflowTriggerContracts } from "./webflow/index.js";
22
23
  import type { whatsappTriggerContracts } from "./whatsapp/index.js";
23
24
  import type { z } from "zod";
24
- export type TriggerContractMap = typeof airtableTriggerContracts & typeof automateTriggerContracts & typeof asanaTriggerContracts & typeof brevoTriggerContracts & typeof convexTriggerContracts & typeof closeTriggerContracts & typeof githubTriggerContracts & typeof gmailTriggerContracts & typeof googleCalendarTriggerContracts & typeof googleFormsTriggerContracts & typeof googleMeetTriggerContracts & typeof linearTriggerContracts & typeof notionTriggerContracts & typeof outlookTriggerContracts & typeof resendTriggerContracts & typeof slackTriggerContracts & typeof stripeTriggerContracts & typeof teamsTriggerContracts & typeof trelloTriggerContracts & typeof vercelTriggerContracts & typeof webflowTriggerContracts & typeof whatsappTriggerContracts;
25
+ export type TriggerContractMap = typeof airtableTriggerContracts & typeof automateTriggerContracts & typeof asanaTriggerContracts & typeof brevoTriggerContracts & typeof convexTriggerContracts & typeof closeTriggerContracts & typeof cloudflareTriggerContracts & typeof githubTriggerContracts & typeof gmailTriggerContracts & typeof googleCalendarTriggerContracts & typeof googleFormsTriggerContracts & typeof googleMeetTriggerContracts & typeof linearTriggerContracts & typeof notionTriggerContracts & typeof outlookTriggerContracts & typeof resendTriggerContracts & typeof slackTriggerContracts & typeof stripeTriggerContracts & typeof teamsTriggerContracts & typeof trelloTriggerContracts & typeof vercelTriggerContracts & typeof webflowTriggerContracts & typeof whatsappTriggerContracts;
25
26
  export type IntegrationTriggerType = keyof TriggerContractMap;
26
27
  /** Canonical authoring configuration for one integration trigger type. */
27
28
  export type TriggerConfig<TType extends IntegrationTriggerType> = z.input<TriggerContractMap[TType]["configSchema"]> extends Record<string, never> ? object : z.input<TriggerContractMap[TType]["configSchema"]>;
@@ -324,8 +324,8 @@ export declare const whatsappTriggerContracts: {
324
324
  type: z.ZodEnum<{
325
325
  unknown: "unknown";
326
326
  text: "text";
327
- document: "document";
328
327
  audio: "audio";
328
+ document: "document";
329
329
  image: "image";
330
330
  video: "video";
331
331
  location: "location";
@@ -419,8 +419,8 @@ export declare const whatsappTriggerContracts: {
419
419
  type: z.ZodEnum<{
420
420
  unknown: "unknown";
421
421
  text: "text";
422
- document: "document";
423
422
  audio: "audio";
423
+ document: "document";
424
424
  image: "image";
425
425
  video: "video";
426
426
  location: "location";
@@ -90,8 +90,8 @@ export declare const WHATSAPP_WEBHOOK_SCHEMA: z.ZodObject<{
90
90
  type: z.ZodEnum<{
91
91
  unknown: "unknown";
92
92
  text: "text";
93
- document: "document";
94
93
  audio: "audio";
94
+ document: "document";
95
95
  image: "image";
96
96
  video: "video";
97
97
  location: "location";
@@ -183,8 +183,8 @@ export declare const WHATSAPP_MESSAGE_RECEIVED_EVENT_SCHEMA: z.ZodObject<{
183
183
  type: z.ZodEnum<{
184
184
  unknown: "unknown";
185
185
  text: "text";
186
- document: "document";
187
186
  audio: "audio";
187
+ document: "document";
188
188
  image: "image";
189
189
  video: "video";
190
190
  location: "location";
@@ -278,8 +278,8 @@ export declare const WHATSAPP_MESSAGE_RECEIVED_EVENT_SCHEMA: z.ZodObject<{
278
278
  type: z.ZodEnum<{
279
279
  unknown: "unknown";
280
280
  text: "text";
281
- document: "document";
282
281
  audio: "audio";
282
+ document: "document";
283
283
  image: "image";
284
284
  video: "video";
285
285
  location: "location";
@@ -768,7 +768,7 @@ export declare function normalizeWhatsAppWebhook(input: unknown): {
768
768
  message: {
769
769
  id: string;
770
770
  timestamp: string;
771
- type: "unknown" | "text" | "document" | "audio" | "image" | "video" | "location" | "system" | "order" | "reaction" | "button" | "contacts" | "interactive" | "sticker" | "unsupported";
771
+ type: "unknown" | "text" | "audio" | "document" | "image" | "video" | "location" | "system" | "order" | "reaction" | "button" | "contacts" | "interactive" | "sticker" | "unsupported";
772
772
  content?: z.core.util.JSONType | undefined;
773
773
  context?: {
774
774
  messageId: string;
@@ -781,7 +781,7 @@ export declare function normalizeWhatsAppWebhook(input: unknown): {
781
781
  from: string;
782
782
  id: string;
783
783
  timestamp: string;
784
- type: "unknown" | "text" | "document" | "audio" | "image" | "video" | "location" | "system" | "order" | "reaction" | "button" | "contacts" | "interactive" | "sticker" | "unsupported";
784
+ type: "unknown" | "text" | "audio" | "document" | "image" | "video" | "location" | "system" | "order" | "reaction" | "button" | "contacts" | "interactive" | "sticker" | "unsupported";
785
785
  audio?: {
786
786
  id: string;
787
787
  mime_type?: string | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automate.ax/integration-contracts",
3
- "version": "0.104.0",
3
+ "version": "0.107.0",
4
4
  "description": "Shared integration payload contracts and provider primitives for Automate.ax.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,6 +22,7 @@
22
22
  "./automate": "./src/automate/index.ts",
23
23
  "./asana": "./src/asana/index.ts",
24
24
  "./brevo": "./src/brevo/index.ts",
25
+ "./cloudflare": "./src/cloudflare/index.ts",
25
26
  "./convex": "./src/convex/index.ts",
26
27
  "./close": "./src/close/index.ts",
27
28
  "./gmail": "./src/gmail/index.ts",
@@ -49,7 +50,7 @@
49
50
  }
50
51
  },
51
52
  "dependencies": {
52
- "@automate.ax/codec": "0.104.0",
53
+ "@automate.ax/codec": "0.107.0",
53
54
  "@cfworker/json-schema": "^4.1.1",
54
55
  "@googleapis/calendar": "^15.0.0",
55
56
  "@googleapis/forms": "^6.0.1",
@@ -120,6 +121,11 @@
120
121
  "types": "./dist/brevo/index.d.ts",
121
122
  "default": "./dist/brevo/index.js"
122
123
  },
124
+ "./cloudflare": {
125
+ "bun": "./src/cloudflare/index.ts",
126
+ "types": "./dist/cloudflare/index.d.ts",
127
+ "default": "./dist/cloudflare/index.js"
128
+ },
123
129
  "./convex": {
124
130
  "bun": "./src/convex/index.ts",
125
131
  "types": "./dist/convex/index.d.ts",
@@ -0,0 +1,340 @@
1
+ import type { Encodable } from "@automate.ax/codec"
2
+ import * as z from "zod"
3
+
4
+ const CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4/"
5
+ const CLOUDFLARE_API_ORIGIN = new URL(CLOUDFLARE_API_BASE_URL).origin
6
+ const CLOUDFLARE_API_TOKEN_SECRET_SCHEMA = z.object({
7
+ apiKey: z.string().min(1),
8
+ })
9
+ const CLOUDFLARE_OAUTH_SECRET_SCHEMA = z.object({
10
+ accessToken: z.string().min(1),
11
+ })
12
+ const CLOUDFLARE_RESPONSE_INFO_SCHEMA = z.looseObject({
13
+ code: z.number().optional(),
14
+ documentation_url: z.string().optional(),
15
+ message: z.string(),
16
+ source: z.object({ pointer: z.string().optional() }).optional(),
17
+ })
18
+ const CLOUDFLARE_ENVELOPE_SCHEMA = z.looseObject({
19
+ errors: CLOUDFLARE_RESPONSE_INFO_SCHEMA.array().default([]),
20
+ messages: CLOUDFLARE_RESPONSE_INFO_SCHEMA.array().default([]),
21
+ result: z.unknown().optional(),
22
+ result_info: z
23
+ .looseObject({
24
+ count: z.number().int().nonnegative().optional(),
25
+ page: z.number().int().positive().optional(),
26
+ per_page: z.number().int().positive().optional(),
27
+ total_count: z.number().int().nonnegative().optional(),
28
+ total_pages: z.number().int().nonnegative().optional(),
29
+ })
30
+ .optional(),
31
+ success: z.boolean(),
32
+ })
33
+ const CLOUDFLARE_RESULT_INFO_SCHEMA = z.looseObject({
34
+ count: z.number().int().nonnegative().optional(),
35
+ page: z.number().int().positive().optional(),
36
+ perPage: z.number().int().positive().optional(),
37
+ totalCount: z.number().int().nonnegative().optional(),
38
+ totalPages: z.number().int().nonnegative().optional(),
39
+ })
40
+
41
+ /** Scalar query value supported by the Cloudflare client. */
42
+ type CloudflareQueryValue = boolean | number | string | undefined
43
+
44
+ /** Resolved Cloudflare account accepted by the shared API client. */
45
+ export interface CloudflareResolvedAccount {
46
+ connectionMethodId: string
47
+ secret: Record<string, unknown>
48
+ serviceId: "cloudflare"
49
+ }
50
+
51
+ /** Options for one authenticated Cloudflare REST request. */
52
+ export interface CloudflareRequestOptions<TSchema extends z.ZodType> {
53
+ /** Public camelCase JSON body. */
54
+ body?: Encodable
55
+
56
+ /** HTTP verb. Defaults to `GET`. */
57
+ method?: "DELETE" | "GET" | "PATCH" | "POST" | "PUT"
58
+
59
+ /** Public camelCase query parameters. */
60
+ query?: Record<string, CloudflareQueryValue>
61
+
62
+ /** Schema for the normalized provider result. */
63
+ responseSchema: TSchema
64
+ }
65
+
66
+ /** One normalized Cloudflare result page. */
67
+ export interface CloudflarePage<T> {
68
+ result: T
69
+ resultInfo?: {
70
+ count?: number
71
+ page?: number
72
+ perPage?: number
73
+ totalCount?: number
74
+ totalPages?: number
75
+ }
76
+ }
77
+
78
+ /** Structured Cloudflare REST error. */
79
+ export class CloudflareApiError extends Error {
80
+ /** Provider error records. */
81
+ readonly errors: z.output<typeof CLOUDFLARE_RESPONSE_INFO_SCHEMA>[]
82
+
83
+ /** Parsed Cloudflare rate-limit header, when present. */
84
+ readonly rateLimit?: string
85
+
86
+ /** Parsed Cloudflare rate-limit policy header, when present. */
87
+ readonly rateLimitPolicy?: string
88
+
89
+ /** Retry delay in seconds, when Cloudflare returned one. */
90
+ readonly retryAfter?: number
91
+
92
+ /** HTTP status returned by Cloudflare. */
93
+ readonly status: number
94
+
95
+ /**
96
+ * Creates a structured error from one Cloudflare response.
97
+ *
98
+ * @param options - Response status, provider errors, and limit metadata.
99
+ * @param options.errors - Provider error records.
100
+ * @param options.rateLimit - Current rate-limit header.
101
+ * @param options.rateLimitPolicy - Current rate-limit policy header.
102
+ * @param options.retryAfter - Retry delay in seconds.
103
+ * @param options.status - HTTP response status.
104
+ */
105
+ constructor(options: {
106
+ errors?: z.output<typeof CLOUDFLARE_RESPONSE_INFO_SCHEMA>[]
107
+ rateLimit?: string
108
+ rateLimitPolicy?: string
109
+ retryAfter?: number
110
+ status: number
111
+ }) {
112
+ const errors = options.errors ?? []
113
+ super(
114
+ errors[0]?.message
115
+ ? `Cloudflare API error (${options.status}): ${errors[0].message}`
116
+ : `Cloudflare API request failed with status ${options.status}.`,
117
+ )
118
+ this.name = "CloudflareApiError"
119
+ this.errors = errors
120
+ this.rateLimit = options.rateLimit
121
+ this.rateLimitPolicy = options.rateLimitPolicy
122
+ this.retryAfter = options.retryAfter
123
+ this.status = options.status
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Creates an authenticated Cloudflare REST client.
129
+ *
130
+ * @param account - Resolved OAuth or API-token account.
131
+ * @throws When the connection method or secret shape is unsupported.
132
+ */
133
+ export function getCloudflareApi(account: CloudflareResolvedAccount) {
134
+ const accessToken =
135
+ account.connectionMethodId === "oauth"
136
+ ? CLOUDFLARE_OAUTH_SECRET_SCHEMA.parse(account.secret).accessToken
137
+ : account.connectionMethodId === "api-token"
138
+ ? CLOUDFLARE_API_TOKEN_SECRET_SCHEMA.parse(account.secret).apiKey
139
+ : undefined
140
+ if (!accessToken) {
141
+ throw new Error(
142
+ `Unsupported Cloudflare connection method: ${account.connectionMethodId}`,
143
+ )
144
+ }
145
+
146
+ /**
147
+ * Runs one request and returns its normalized result.
148
+ *
149
+ * @param path - Cloudflare API path relative to the v4 root.
150
+ * @param options - Method, parameters, and response schema.
151
+ */
152
+ async function request<TSchema extends z.ZodType>(
153
+ path: string,
154
+ options: CloudflareRequestOptions<TSchema>,
155
+ ): Promise<z.output<TSchema>> {
156
+ return (await requestPage(path, options)).result
157
+ }
158
+
159
+ /**
160
+ * Runs one request and preserves Cloudflare page metadata.
161
+ *
162
+ * @param path - Cloudflare API path relative to the v4 root.
163
+ * @param options - Method, parameters, and response schema.
164
+ */
165
+ async function requestPage<TSchema extends z.ZodType>(
166
+ path: string,
167
+ options: CloudflareRequestOptions<TSchema>,
168
+ ): Promise<CloudflarePage<z.output<TSchema>>> {
169
+ const url = new URL(path.replace(/^\//, ""), CLOUDFLARE_API_BASE_URL)
170
+ if (url.origin !== CLOUDFLARE_API_ORIGIN) {
171
+ throw new Error(
172
+ "Cloudflare API paths must use the Cloudflare API origin.",
173
+ )
174
+ }
175
+ for (const [name, value] of Object.entries(options.query ?? {})) {
176
+ if (value !== undefined)
177
+ url.searchParams.set(toSnakeCase(name), String(value))
178
+ }
179
+ const response = await fetch(url, {
180
+ body:
181
+ options.body === undefined
182
+ ? undefined
183
+ : JSON.stringify(toCloudflare(options.body)),
184
+ headers: {
185
+ Accept: "application/json",
186
+ Authorization: `Bearer ${accessToken}`,
187
+ ...(options.body === undefined
188
+ ? {}
189
+ : { "Content-Type": "application/json" }),
190
+ },
191
+ method: options.method ?? "GET",
192
+ })
193
+ const parsed = CLOUDFLARE_ENVELOPE_SCHEMA.safeParse(
194
+ parseJson(await response.text()),
195
+ )
196
+ if (!response.ok || !parsed.success || !parsed.data.success) {
197
+ throw new CloudflareApiError({
198
+ errors: parsed.success ? parsed.data.errors : undefined,
199
+ rateLimit: response.headers.get("Ratelimit") ?? undefined,
200
+ rateLimitPolicy: response.headers.get("Ratelimit-Policy") ?? undefined,
201
+ retryAfter: parseFiniteNumber(response.headers.get("Retry-After")),
202
+ status: response.status,
203
+ })
204
+ }
205
+ return {
206
+ result: options.responseSchema.parse(fromCloudflare(parsed.data.result)),
207
+ ...(parsed.data.result_info && {
208
+ resultInfo: CLOUDFLARE_RESULT_INFO_SCHEMA.parse(
209
+ fromCloudflare(parsed.data.result_info),
210
+ ),
211
+ }),
212
+ }
213
+ }
214
+
215
+ return { request, requestPage }
216
+ }
217
+
218
+ /**
219
+ * Converts public camelCase JSON to Cloudflare wire keys.
220
+ *
221
+ * @param value - Codec-safe public value.
222
+ */
223
+ export function toCloudflare(value: Encodable): Encodable {
224
+ return convertToCloudflare(value, false)
225
+ }
226
+
227
+ /**
228
+ * Converts one value while optionally preserving object keys.
229
+ *
230
+ * @param value - Public value to convert.
231
+ * @param preserveKeys - Whether this object is a provider-defined map.
232
+ */
233
+ function convertToCloudflare(
234
+ value: Encodable,
235
+ preserveKeys: boolean,
236
+ ): Encodable {
237
+ if (value instanceof Date) return value.toISOString()
238
+ if (Array.isArray(value))
239
+ return value.map((item) => convertToCloudflare(item, preserveKeys))
240
+ if (!isPlainObject(value)) return value
241
+ return Object.fromEntries(
242
+ Object.entries(value).map(([key, item]) => [
243
+ preserveKeys ? key : toSnakeCase(key),
244
+ convertToCloudflare(item, key === "header" || key === "headers"),
245
+ ]),
246
+ )
247
+ }
248
+
249
+ /**
250
+ * Converts Cloudflare wire JSON to public camelCase keys.
251
+ *
252
+ * @param value - Untrusted provider JSON value.
253
+ */
254
+ export function fromCloudflare(value: unknown): Encodable {
255
+ return convertFromCloudflare(value, false)
256
+ }
257
+
258
+ /**
259
+ * Converts one provider value while optionally preserving object keys.
260
+ *
261
+ * @param value - Provider value to convert.
262
+ * @param preserveKeys - Whether this object is a provider-defined map.
263
+ */
264
+ function convertFromCloudflare(
265
+ value: unknown,
266
+ preserveKeys: boolean,
267
+ ): Encodable {
268
+ if (Array.isArray(value))
269
+ return value.map((item) => convertFromCloudflare(item, preserveKeys))
270
+ if (!isPlainObject(value)) return CLOUDFLARE_VALUE_SCHEMA.parse(value)
271
+ return Object.fromEntries(
272
+ Object.entries(value).map(([key, item]) => [
273
+ preserveKeys ? key : toCamelCase(key),
274
+ convertFromCloudflare(item, key === "header" || key === "headers"),
275
+ ]),
276
+ )
277
+ }
278
+
279
+ const CLOUDFLARE_VALUE_SCHEMA = z.union([
280
+ z.boolean(),
281
+ z.number(),
282
+ z.string(),
283
+ z.null(),
284
+ z.undefined(),
285
+ ])
286
+
287
+ /**
288
+ * Returns whether a value is a plain JSON object.
289
+ *
290
+ * @param value - Candidate value.
291
+ */
292
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
293
+ return typeof value === "object" && value !== null && !Array.isArray(value)
294
+ }
295
+
296
+ /**
297
+ * Parses JSON without obscuring the eventual structured API error.
298
+ *
299
+ * @param value - Raw response text.
300
+ */
301
+ function parseJson(value: string): unknown {
302
+ try {
303
+ return JSON.parse(value)
304
+ } catch {
305
+ return undefined
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Parses a finite numeric response header.
311
+ *
312
+ * @param value - Header value.
313
+ */
314
+ function parseFiniteNumber(value: string | null) {
315
+ if (value === null) return undefined
316
+ const number = Number(value)
317
+ return Number.isFinite(number) ? number : undefined
318
+ }
319
+
320
+ /**
321
+ * Converts one Cloudflare snake_case key to camelCase.
322
+ *
323
+ * @param value - Provider key.
324
+ */
325
+ function toCamelCase(value: string) {
326
+ return value.replace(/_([a-z0-9])/g, (_match, character: string) =>
327
+ character.toUpperCase(),
328
+ )
329
+ }
330
+
331
+ /**
332
+ * Converts one public camelCase key to snake_case.
333
+ *
334
+ * @param value - Public key.
335
+ */
336
+ function toSnakeCase(value: string) {
337
+ return value
338
+ .replace(/([a-zA-Z])([0-9])/g, "$1_$2")
339
+ .replace(/[A-Z]/g, (character) => `_${character.toLowerCase()}`)
340
+ }