@automate.ax/integration-contracts 0.105.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.
- package/dist/close/events.d.ts +13 -13
- package/dist/close/schemas.d.ts +9 -9
- package/dist/cloudflare/api.d.ts +93 -0
- package/dist/cloudflare/api.js +250 -0
- package/dist/cloudflare/events.d.ts +1340 -0
- package/dist/cloudflare/events.js +178 -0
- package/dist/cloudflare/index.d.ts +3 -0
- package/dist/cloudflare/index.js +3 -0
- package/dist/cloudflare/schemas.d.ts +608 -0
- package/dist/cloudflare/schemas.js +340 -0
- package/dist/linear/api.d.ts +1 -1
- package/dist/linear/api.js +9 -6
- package/dist/linear/index.d.ts +3479 -782
- package/dist/linear/index.js +249 -13
- package/dist/linear/schemas.d.ts +2721 -28
- package/dist/linear/schemas.js +515 -2
- package/dist/notion/schemas.d.ts +108 -108
- package/dist/outlook/schemas.d.ts +2 -2
- package/dist/resend/action-schemas.d.ts +9 -9
- package/dist/resend/index.d.ts +36 -36
- package/dist/resend/schemas.d.ts +40 -40
- package/dist/teams/schemas.d.ts +5 -5
- package/dist/triggers.d.ts +2 -1
- package/dist/whatsapp/index.d.ts +2 -2
- package/dist/whatsapp/schemas.d.ts +5 -5
- package/package.json +8 -2
- package/src/cloudflare/api.ts +340 -0
- package/src/cloudflare/events.ts +212 -0
- package/src/cloudflare/index.ts +3 -0
- package/src/cloudflare/schemas.ts +359 -0
- package/src/linear/api.ts +10 -6
- package/src/linear/index.ts +249 -26
- package/src/linear/schemas.ts +688 -2
- package/src/triggers.ts +2 -0
package/dist/whatsapp/index.d.ts
CHANGED
|
@@ -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" | "
|
|
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" | "
|
|
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.
|
|
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.
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|