@automate.ax/integration-contracts 0.105.0 → 0.109.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 (45) hide show
  1. package/dist/airtable/api.js +6 -4
  2. package/dist/airtable/index.d.ts +997 -19
  3. package/dist/airtable/index.js +30 -2
  4. package/dist/airtable/schemas.d.ts +2034 -44
  5. package/dist/airtable/schemas.js +231 -47
  6. package/dist/brevo/api.d.ts +41 -0
  7. package/dist/brevo/api.js +74 -4
  8. package/dist/close/events.d.ts +13 -13
  9. package/dist/close/schemas.d.ts +9 -9
  10. package/dist/cloudflare/api.d.ts +93 -0
  11. package/dist/cloudflare/api.js +250 -0
  12. package/dist/cloudflare/events.d.ts +1340 -0
  13. package/dist/cloudflare/events.js +178 -0
  14. package/dist/cloudflare/index.d.ts +3 -0
  15. package/dist/cloudflare/index.js +3 -0
  16. package/dist/cloudflare/schemas.d.ts +608 -0
  17. package/dist/cloudflare/schemas.js +340 -0
  18. package/dist/linear/api.d.ts +1 -1
  19. package/dist/linear/api.js +9 -6
  20. package/dist/linear/index.d.ts +3479 -782
  21. package/dist/linear/index.js +249 -13
  22. package/dist/linear/schemas.d.ts +2724 -31
  23. package/dist/linear/schemas.js +515 -2
  24. package/dist/notion/schemas.d.ts +112 -112
  25. package/dist/outlook/schemas.d.ts +2 -2
  26. package/dist/resend/action-schemas.d.ts +9 -9
  27. package/dist/resend/index.d.ts +36 -36
  28. package/dist/resend/schemas.d.ts +40 -40
  29. package/dist/teams/schemas.d.ts +5 -5
  30. package/dist/triggers.d.ts +2 -1
  31. package/dist/whatsapp/index.d.ts +2 -2
  32. package/dist/whatsapp/schemas.d.ts +5 -5
  33. package/package.json +8 -2
  34. package/src/airtable/api.ts +7 -4
  35. package/src/airtable/index.ts +36 -1
  36. package/src/airtable/schemas.ts +294 -63
  37. package/src/brevo/api.ts +103 -8
  38. package/src/cloudflare/api.ts +340 -0
  39. package/src/cloudflare/events.ts +212 -0
  40. package/src/cloudflare/index.ts +3 -0
  41. package/src/cloudflare/schemas.ts +359 -0
  42. package/src/linear/api.ts +10 -6
  43. package/src/linear/index.ts +249 -26
  44. package/src/linear/schemas.ts +688 -2
  45. package/src/triggers.ts +2 -0
package/src/brevo/api.ts CHANGED
@@ -10,6 +10,64 @@ const BREVO_ERROR_SCHEMA = z.looseObject({
10
10
  message: z.string().optional(),
11
11
  })
12
12
 
13
+ /** Provider rate-limit values returned with a Brevo response. */
14
+ export interface BrevoRateLimitMetadata {
15
+ /** Maximum requests allowed in the current window. */
16
+ limit?: number
17
+
18
+ /** Requests remaining in the current window. */
19
+ remaining?: number
20
+
21
+ /** Seconds until the current window resets. */
22
+ reset?: number
23
+ }
24
+
25
+ /** Structured failure returned by the Brevo API. */
26
+ export class BrevoApiError extends Error {
27
+ /** Provider error classification. */
28
+ readonly code?: string
29
+
30
+ /** API path that failed. */
31
+ readonly path: string
32
+
33
+ /** Provider rate-limit metadata. */
34
+ readonly rateLimit: BrevoRateLimitMetadata
35
+
36
+ /** Delay in seconds before the request should be retried. */
37
+ readonly retryAfter?: number
38
+
39
+ /** HTTP status returned by Brevo. */
40
+ readonly status: number
41
+
42
+ /**
43
+ * Creates a structured Brevo provider error.
44
+ *
45
+ * @param options - Provider and transport failure details.
46
+ * @param options.code - Provider error classification.
47
+ * @param options.message - Human-readable provider failure.
48
+ * @param options.path - API path that failed.
49
+ * @param options.rateLimit - Provider rate-limit metadata.
50
+ * @param options.retryAfter - Provider retry delay in seconds.
51
+ * @param options.status - HTTP status.
52
+ */
53
+ constructor(options: {
54
+ code?: string
55
+ message: string
56
+ path: string
57
+ rateLimit: BrevoRateLimitMetadata
58
+ retryAfter?: number
59
+ status: number
60
+ }) {
61
+ super(`Brevo ${options.path} failed: ${options.message}`)
62
+ this.name = "BrevoApiError"
63
+ this.code = options.code
64
+ this.path = options.path
65
+ this.rateLimit = options.rateLimit
66
+ this.retryAfter = options.retryAfter
67
+ this.status = options.status
68
+ }
69
+ }
70
+
13
71
  /** Options accepted by the authenticated Brevo request helper. */
14
72
  export interface BrevoRequestOptions extends RequestInit {
15
73
  /** Query parameters appended to the request URL. */
@@ -63,14 +121,25 @@ export function getBrevoApi(secret: Record<string, unknown>) {
63
121
  const response = await fetch(url, { ...requestInit, headers })
64
122
  if (response.ok) return response
65
123
 
66
- const error = BREVO_ERROR_SCHEMA.safeParse(
67
- await response.json().catch(() => ({})),
68
- )
69
- throw new Error(
70
- error.success && error.data.message
71
- ? `Brevo API error (${response.status}): ${error.data.message}`
72
- : `Brevo API request failed with status ${response.status}.`,
73
- )
124
+ const responseText = await response.text()
125
+ const error = BREVO_ERROR_SCHEMA.safeParse(parseJson(responseText))
126
+ throw new BrevoApiError({
127
+ code: error.success ? error.data.code : undefined,
128
+ message:
129
+ (error.success ? error.data.message : undefined) ??
130
+ (responseText.trim() || `HTTP ${response.status}`),
131
+ path: `${url.pathname}${url.search}`,
132
+ rateLimit: {
133
+ limit: parseNumberHeader(response.headers, "x-sib-ratelimit-limit"),
134
+ remaining: parseNumberHeader(
135
+ response.headers,
136
+ "x-sib-ratelimit-remaining",
137
+ ),
138
+ reset: parseNumberHeader(response.headers, "x-sib-ratelimit-reset"),
139
+ },
140
+ retryAfter: parseNumberHeader(response.headers, "retry-after"),
141
+ status: response.status,
142
+ })
74
143
  },
75
144
  }
76
145
  }
@@ -87,3 +156,29 @@ export async function parseBrevoResponse<T extends z.ZodType>(
87
156
  ): Promise<z.output<T>> {
88
157
  return schema.parse(await response.json())
89
158
  }
159
+
160
+ /**
161
+ * Parses provider JSON without hiding non-JSON error bodies.
162
+ *
163
+ * @param value - Provider response text.
164
+ */
165
+ function parseJson(value: string): unknown {
166
+ try {
167
+ return JSON.parse(value)
168
+ } catch {
169
+ return undefined
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Parses an optional numeric HTTP header.
175
+ *
176
+ * @param headers - Provider response headers.
177
+ * @param name - Case-insensitive header name.
178
+ */
179
+ function parseNumberHeader(headers: Headers, name: string) {
180
+ const value = headers.get(name)
181
+ if (value === null) return undefined
182
+ const parsed = Number(value)
183
+ return Number.isFinite(parsed) ? parsed : undefined
184
+ }
@@ -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
+ }
@@ -0,0 +1,212 @@
1
+ import * as z from "zod"
2
+ import { encodableSchema } from "@automate.ax/codec"
3
+ import { fromCloudflare } from "./api"
4
+ import {
5
+ CLOUDFLARE_ALERT_TYPE_SCHEMA,
6
+ CLOUDFLARE_NOTIFICATION_FILTERS_SCHEMA,
7
+ } from "./schemas"
8
+
9
+ const PROVIDER_EVENT_SCHEMA = z.looseObject({
10
+ account_id: z.string().optional(),
11
+ alert_correlation_id: z.string().optional(),
12
+ alert_event: z.string().optional(),
13
+ alert_type: CLOUDFLARE_ALERT_TYPE_SCHEMA.optional(),
14
+ data: z.record(z.string(), z.unknown()).optional(),
15
+ name: z.string().optional(),
16
+ policy_id: z.string().optional(),
17
+ policy_name: z.string().optional(),
18
+ text: z.string().optional(),
19
+ ts: z.union([z.number(), z.string()]).optional(),
20
+ })
21
+
22
+ export const CLOUDFLARE_EVENT_SCHEMA = z.object({
23
+ accountId: z.string().optional(),
24
+ alertCorrelationId: z.string().optional(),
25
+ alertEvent: z.string().optional(),
26
+ alertType: CLOUDFLARE_ALERT_TYPE_SCHEMA,
27
+ data: z.record(z.string(), encodableSchema).prefault({}),
28
+ name: z.string().optional(),
29
+ policyId: z.string().optional(),
30
+ policyName: z.string().optional(),
31
+ text: z.string().optional(),
32
+ timestamp: z.union([z.number(), z.string()]).optional(),
33
+ })
34
+
35
+ const LAYER_4_DDOS_DATA_SCHEMA = z.object({
36
+ accountName: z.string().optional(),
37
+ accountTag: z.string().optional(),
38
+ action: z.string().optional(),
39
+ attackId: z.string().optional(),
40
+ attackVector: z.string().optional(),
41
+ dashboardLink: z.string().optional(),
42
+ maxRate: z.string().optional(),
43
+ megabitsPerSecond: z.number().optional(),
44
+ mitigation: z.string().optional(),
45
+ packetsPerSecond: z.number().optional(),
46
+ protocol: z.string().optional(),
47
+ ruleDescription: z.string().optional(),
48
+ ruleId: z.string().optional(),
49
+ ruleName: z.string().optional(),
50
+ rulesetId: z.string().optional(),
51
+ rulesetOverrideId: z.string().optional(),
52
+ startTime: z.string().optional(),
53
+ targetId: z.string().optional(),
54
+ targetIp: z.string().optional(),
55
+ targetPort: z.number().optional(),
56
+ })
57
+ const LAYER_7_DDOS_DATA_SCHEMA = z.object({
58
+ accountName: z.string().optional(),
59
+ accountTag: z.string().optional(),
60
+ action: z.string().optional(),
61
+ attackId: z.string().optional(),
62
+ attackType: z.string().optional(),
63
+ dashboardLink: z.string().optional(),
64
+ maxRate: z.string().optional(),
65
+ mitigation: z.string().optional(),
66
+ requestsPerSecond: z.number().optional(),
67
+ ruleDescription: z.string().optional(),
68
+ ruleId: z.string().optional(),
69
+ ruleLink: z.string().optional(),
70
+ rulesetId: z.string().optional(),
71
+ rulesetOverrideId: z.string().optional(),
72
+ startTime: z.string().optional(),
73
+ targetHostname: z.string().optional(),
74
+ zoneName: z.string().optional(),
75
+ zoneTag: z.string().optional(),
76
+ })
77
+ const SSL_DATA_SCHEMA = z.object({
78
+ accountName: z.string().optional(),
79
+ accountTag: z.string().optional(),
80
+ certificateId: z.string().optional(),
81
+ certificatePackId: z.string().optional(),
82
+ certificateStatus: z.string().optional(),
83
+ eventType: z.string().optional(),
84
+ hostnames: z.string().optional(),
85
+ packCa: z.string().optional(),
86
+ packId: z.string().optional(),
87
+ packStatus: z.string().optional(),
88
+ packValidation: z.string().optional(),
89
+ zoneName: z.string().optional(),
90
+ zoneTag: z.string().optional(),
91
+ })
92
+ const HEALTH_CHECK_DATA_SCHEMA = z.object({
93
+ accountName: z.string().optional(),
94
+ accountTag: z.string().optional(),
95
+ failingRegions: z.string().optional(),
96
+ healthCheckId: z.string().optional(),
97
+ healthCheckName: z.string().optional(),
98
+ newHealthStatus: z.string().optional(),
99
+ newStatus: z.string().optional(),
100
+ oldStatus: z.string().optional(),
101
+ originIp: z.string().optional(),
102
+ reason: z.string().optional(),
103
+ statusChangeTime: z.string().optional(),
104
+ timeSinceLastFailure: z.string().optional(),
105
+ zoneName: z.string().optional(),
106
+ zoneTag: z.string().optional(),
107
+ })
108
+ const ACCESS_CERTIFICATE_DATA_SCHEMA = z.object({
109
+ accountName: z.string().optional(),
110
+ accountTag: z.string().optional(),
111
+ certificateId: z.string().optional(),
112
+ daysTilExpiration: z.number().optional(),
113
+ hostnames: z.string().optional(),
114
+ zoneName: z.string().optional(),
115
+ zoneTag: z.string().optional(),
116
+ })
117
+
118
+ /**
119
+ * Builds one alert-type-specific Cloudflare event schema.
120
+ *
121
+ * @param alertType - Exact Cloudflare policy alert type.
122
+ * @param data - Stable documented alert data schema.
123
+ */
124
+ function semanticEvent<
125
+ TType extends z.ZodLiteral<string>,
126
+ TData extends z.ZodType,
127
+ >(alertType: TType, data: TData) {
128
+ return CLOUDFLARE_EVENT_SCHEMA.extend({ alertType, data })
129
+ }
130
+ export const CLOUDFLARE_LAYER_4_DDOS_EVENT_SCHEMA = semanticEvent(
131
+ z.literal("advanced_ddos_attack_l4_alert"),
132
+ LAYER_4_DDOS_DATA_SCHEMA,
133
+ )
134
+ export const CLOUDFLARE_LAYER_7_DDOS_EVENT_SCHEMA = semanticEvent(
135
+ z.literal("advanced_ddos_attack_l7_alert"),
136
+ LAYER_7_DDOS_DATA_SCHEMA,
137
+ )
138
+ export const CLOUDFLARE_SSL_CERTIFICATE_EVENT_SCHEMA = semanticEvent(
139
+ z.literal("dedicated_ssl_certificate_event_type"),
140
+ SSL_DATA_SCHEMA,
141
+ )
142
+ export const CLOUDFLARE_ORIGIN_HEALTH_CHECK_EVENT_SCHEMA = semanticEvent(
143
+ z.literal("health_check_status_notification"),
144
+ HEALTH_CHECK_DATA_SCHEMA,
145
+ )
146
+ export const CLOUDFLARE_ACCESS_CERTIFICATE_EVENT_SCHEMA = semanticEvent(
147
+ z.literal("access_custom_certificate_expiration_type"),
148
+ ACCESS_CERTIFICATE_DATA_SCHEMA,
149
+ )
150
+
151
+ export const CLOUDFLARE_TRIGGER_CONFIG_SCHEMA = z.object({
152
+ accountId: z.string().trim().min(1),
153
+ alertTypes: CLOUDFLARE_ALERT_TYPE_SCHEMA.array()
154
+ .min(1)
155
+ .refine(
156
+ (alertTypes) => new Set(alertTypes).size === alertTypes.length,
157
+ "Choose each alert type once.",
158
+ ),
159
+ filters: CLOUDFLARE_NOTIFICATION_FILTERS_SCHEMA.optional(),
160
+ })
161
+
162
+ export const cloudflareTriggerContracts = {
163
+ "cloudflare.accessCertificate.expiring": {
164
+ configSchema: CLOUDFLARE_TRIGGER_CONFIG_SCHEMA,
165
+ eventSchema: CLOUDFLARE_ACCESS_CERTIFICATE_EVENT_SCHEMA,
166
+ },
167
+ "cloudflare.layer4DdosAttack": {
168
+ configSchema: CLOUDFLARE_TRIGGER_CONFIG_SCHEMA,
169
+ eventSchema: CLOUDFLARE_LAYER_4_DDOS_EVENT_SCHEMA,
170
+ },
171
+ "cloudflare.layer7DdosAttack": {
172
+ configSchema: CLOUDFLARE_TRIGGER_CONFIG_SCHEMA,
173
+ eventSchema: CLOUDFLARE_LAYER_7_DDOS_EVENT_SCHEMA,
174
+ },
175
+ "cloudflare.notification": {
176
+ configSchema: CLOUDFLARE_TRIGGER_CONFIG_SCHEMA,
177
+ eventSchema: CLOUDFLARE_EVENT_SCHEMA,
178
+ },
179
+ "cloudflare.originHealthCheckStatusChanged": {
180
+ configSchema: CLOUDFLARE_TRIGGER_CONFIG_SCHEMA,
181
+ eventSchema: CLOUDFLARE_ORIGIN_HEALTH_CHECK_EVENT_SCHEMA,
182
+ },
183
+ "cloudflare.sslCertificate.event": {
184
+ configSchema: CLOUDFLARE_TRIGGER_CONFIG_SCHEMA,
185
+ eventSchema: CLOUDFLARE_SSL_CERTIFICATE_EVENT_SCHEMA,
186
+ },
187
+ } as const
188
+
189
+ /**
190
+ * Normalizes an authenticated Cloudflare notification webhook.
191
+ *
192
+ * @param value - Untrusted provider webhook payload.
193
+ * @param expectedAlertType - Alert type owned by the authenticated source.
194
+ */
195
+ export function normalizeCloudflareEvent(
196
+ value: unknown,
197
+ expectedAlertType?: z.output<typeof CLOUDFLARE_ALERT_TYPE_SCHEMA>,
198
+ ) {
199
+ const event = PROVIDER_EVENT_SCHEMA.parse(value)
200
+ return CLOUDFLARE_EVENT_SCHEMA.parse({
201
+ accountId: event.account_id,
202
+ alertCorrelationId: event.alert_correlation_id,
203
+ alertEvent: event.alert_event,
204
+ alertType: event.alert_type ?? expectedAlertType,
205
+ data: fromCloudflare(event.data ?? {}),
206
+ name: event.name,
207
+ policyId: event.policy_id,
208
+ policyName: event.policy_name,
209
+ text: event.text,
210
+ timestamp: event.ts,
211
+ })
212
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./api"
2
+ export * from "./events"
3
+ export * from "./schemas"