@automate.ax/integration-contracts 0.139.1 → 0.141.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/closebot/api.d.ts +39 -0
- package/dist/closebot/api.js +117 -0
- package/dist/closebot/index.d.ts +2 -0
- package/dist/closebot/index.js +2 -0
- package/dist/closebot/schemas.d.ts +775 -0
- package/dist/closebot/schemas.js +467 -0
- package/dist/meta-ads/api.d.ts +90 -0
- package/dist/meta-ads/api.js +260 -0
- package/dist/meta-ads/events.d.ts +391 -0
- package/dist/meta-ads/events.js +193 -0
- package/dist/meta-ads/index.d.ts +3 -0
- package/dist/meta-ads/index.js +3 -0
- package/dist/meta-ads/schemas.d.ts +184 -0
- package/dist/meta-ads/schemas.js +181 -0
- package/dist/triggers.d.ts +2 -1
- package/package.json +14 -2
- package/src/closebot/api.ts +157 -0
- package/src/closebot/index.ts +2 -0
- package/src/closebot/schemas.ts +517 -0
- package/src/meta-ads/api.ts +327 -0
- package/src/meta-ads/events.ts +218 -0
- package/src/meta-ads/index.ts +3 -0
- package/src/meta-ads/schemas.ts +206 -0
- package/src/triggers.ts +2 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import type { Encodable } from "@automate.ax/codec"
|
|
2
|
+
import * as z from "zod"
|
|
3
|
+
import { META_ADS_PAGE_INFO_SCHEMA } from "./schemas"
|
|
4
|
+
|
|
5
|
+
export const META_ADS_GRAPH_API_VERSION = "v26.0"
|
|
6
|
+
const META_ADS_API_BASE_URL = `https://graph.facebook.com/${META_ADS_GRAPH_API_VERSION}/`
|
|
7
|
+
const META_ADS_API_ORIGIN = new URL(META_ADS_API_BASE_URL).origin
|
|
8
|
+
export const META_ADS_OAUTH_SECRET_SCHEMA = z.object({
|
|
9
|
+
accessToken: z.string().min(1),
|
|
10
|
+
expiresAt: z.number().nullable(),
|
|
11
|
+
tokenType: z.string(),
|
|
12
|
+
})
|
|
13
|
+
const META_ERROR_SCHEMA = z.looseObject({
|
|
14
|
+
code: z.number().optional(),
|
|
15
|
+
errorSubcode: z.number().optional(),
|
|
16
|
+
errorUserMessage: z.string().optional(),
|
|
17
|
+
fbtraceId: z.string().optional(),
|
|
18
|
+
isTransient: z.boolean().optional(),
|
|
19
|
+
message: z.string(),
|
|
20
|
+
type: z.string().optional(),
|
|
21
|
+
})
|
|
22
|
+
const ERROR_ENVELOPE_SCHEMA = z.looseObject({ error: META_ERROR_SCHEMA })
|
|
23
|
+
const PAGE_SCHEMA = z.looseObject({
|
|
24
|
+
data: z.unknown(),
|
|
25
|
+
paging: z
|
|
26
|
+
.looseObject({
|
|
27
|
+
cursors: z
|
|
28
|
+
.looseObject({
|
|
29
|
+
after: z.string().optional(),
|
|
30
|
+
before: z.string().optional(),
|
|
31
|
+
})
|
|
32
|
+
.optional(),
|
|
33
|
+
next: z.string().optional(),
|
|
34
|
+
previous: z.string().optional(),
|
|
35
|
+
})
|
|
36
|
+
.optional(),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/** Query values serialized by the shared Graph client. */
|
|
40
|
+
type QueryValue = boolean | number | string | string[] | undefined
|
|
41
|
+
|
|
42
|
+
export interface MetaAdsResolvedAccount {
|
|
43
|
+
connectionMethodId: string
|
|
44
|
+
secret: Record<string, unknown>
|
|
45
|
+
serviceId: "meta-ads"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface MetaAdsRequestOptions<TSchema extends z.ZodType> {
|
|
49
|
+
body?: Encodable
|
|
50
|
+
method?: "DELETE" | "GET" | "PATCH" | "POST"
|
|
51
|
+
query?: Record<string, QueryValue>
|
|
52
|
+
responseSchema: TSchema
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface MetaAdsPage<T> {
|
|
56
|
+
items: T
|
|
57
|
+
pageInfo?: z.output<typeof META_ADS_PAGE_INFO_SCHEMA>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Structured Graph API failure with Meta quota and trace diagnostics. */
|
|
61
|
+
export class MetaAdsApiError extends Error {
|
|
62
|
+
readonly appUsage?: string
|
|
63
|
+
readonly businessUseCaseUsage?: string
|
|
64
|
+
readonly code?: number
|
|
65
|
+
readonly fbtraceId?: string
|
|
66
|
+
readonly isTransient: boolean
|
|
67
|
+
readonly adAccountUsage?: string
|
|
68
|
+
readonly errorSubcode?: number
|
|
69
|
+
readonly retryAfter?: number
|
|
70
|
+
readonly status: number
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Creates a structured Meta Ads failure.
|
|
74
|
+
*
|
|
75
|
+
* @param options - Transport, provider, and quota details.
|
|
76
|
+
* @param options.adAccountUsage - Raw ad-account usage header.
|
|
77
|
+
* @param options.appUsage - Raw app usage header.
|
|
78
|
+
* @param options.businessUseCaseUsage - Raw business-use-case usage header.
|
|
79
|
+
* @param options.error - Parsed provider error.
|
|
80
|
+
* @param options.retryAfter - Retry delay in seconds.
|
|
81
|
+
* @param options.status - HTTP response status.
|
|
82
|
+
*/
|
|
83
|
+
constructor(options: {
|
|
84
|
+
adAccountUsage?: string
|
|
85
|
+
appUsage?: string
|
|
86
|
+
businessUseCaseUsage?: string
|
|
87
|
+
error?: z.output<typeof META_ERROR_SCHEMA>
|
|
88
|
+
retryAfter?: number
|
|
89
|
+
status: number
|
|
90
|
+
}) {
|
|
91
|
+
const providerError = options.error
|
|
92
|
+
super(
|
|
93
|
+
providerError?.message
|
|
94
|
+
? `Meta Ads API error (${providerError.code ?? options.status}): ${providerError.message}`
|
|
95
|
+
: `Meta Ads API request failed with status ${options.status}.`,
|
|
96
|
+
)
|
|
97
|
+
this.name = "MetaAdsApiError"
|
|
98
|
+
this.adAccountUsage = options.adAccountUsage
|
|
99
|
+
this.appUsage = options.appUsage
|
|
100
|
+
this.businessUseCaseUsage = options.businessUseCaseUsage
|
|
101
|
+
this.code = providerError?.code
|
|
102
|
+
this.errorSubcode = providerError?.errorSubcode
|
|
103
|
+
this.fbtraceId = providerError?.fbtraceId
|
|
104
|
+
this.isTransient = providerError?.isTransient ?? false
|
|
105
|
+
this.retryAfter = options.retryAfter
|
|
106
|
+
this.status = options.status
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Creates the authenticated Graph API helper used by packaged and custom
|
|
112
|
+
* actions.
|
|
113
|
+
*
|
|
114
|
+
* @param account - Resolved Meta Ads OAuth account.
|
|
115
|
+
* @throws When the connection method or stored credential is invalid.
|
|
116
|
+
*/
|
|
117
|
+
export function getMetaAdsApi(account: MetaAdsResolvedAccount) {
|
|
118
|
+
if (account.connectionMethodId !== "oauth") {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Unsupported Meta Ads connection method: ${account.connectionMethodId}`,
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
const { accessToken } = META_ADS_OAUTH_SECRET_SCHEMA.parse(account.secret)
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Calls one Graph endpoint and validates its normalized result.
|
|
127
|
+
*
|
|
128
|
+
* @param path - Path relative to the pinned Graph root.
|
|
129
|
+
* @param options - Request options and response schema.
|
|
130
|
+
*/
|
|
131
|
+
async function call<TSchema extends z.ZodType>(
|
|
132
|
+
path: string,
|
|
133
|
+
options: MetaAdsRequestOptions<TSchema>,
|
|
134
|
+
): Promise<z.output<TSchema>> {
|
|
135
|
+
return options.responseSchema.parse(
|
|
136
|
+
fromMeta(await rawRequest(path, options)),
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Calls one Graph edge and preserves cursor metadata.
|
|
142
|
+
*
|
|
143
|
+
* @param path - Path relative to the pinned Graph root.
|
|
144
|
+
* @param options - Request options and item schema.
|
|
145
|
+
*/
|
|
146
|
+
async function page<TSchema extends z.ZodType>(
|
|
147
|
+
path: string,
|
|
148
|
+
options: MetaAdsRequestOptions<TSchema>,
|
|
149
|
+
): Promise<MetaAdsPage<z.output<TSchema>>> {
|
|
150
|
+
const parsed = PAGE_SCHEMA.parse(await rawRequest(path, options))
|
|
151
|
+
const paging = parsed.paging
|
|
152
|
+
return {
|
|
153
|
+
items: options.responseSchema.parse(fromMeta(parsed.data)),
|
|
154
|
+
...(paging && {
|
|
155
|
+
pageInfo: META_ADS_PAGE_INFO_SCHEMA.parse({
|
|
156
|
+
after: paging.cursors?.after,
|
|
157
|
+
before: paging.cursors?.before,
|
|
158
|
+
next: paging.next,
|
|
159
|
+
previous: paging.previous,
|
|
160
|
+
}),
|
|
161
|
+
}),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Executes one authenticated request and returns its raw JSON value.
|
|
167
|
+
*
|
|
168
|
+
* @param path - Path relative to the pinned Graph root.
|
|
169
|
+
* @param options - Request method, query, and body.
|
|
170
|
+
*/
|
|
171
|
+
async function rawRequest<TSchema extends z.ZodType>(
|
|
172
|
+
path: string,
|
|
173
|
+
options: MetaAdsRequestOptions<TSchema>,
|
|
174
|
+
) {
|
|
175
|
+
const url = new URL(path.replace(/^\/+/, ""), META_ADS_API_BASE_URL)
|
|
176
|
+
if (
|
|
177
|
+
url.origin !== META_ADS_API_ORIGIN ||
|
|
178
|
+
!url.pathname.startsWith(`/${META_ADS_GRAPH_API_VERSION}/`)
|
|
179
|
+
) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Meta Ads API paths must remain under /${META_ADS_GRAPH_API_VERSION}.`,
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
for (const [name, value] of Object.entries(options.query ?? {})) {
|
|
185
|
+
if (value !== undefined) {
|
|
186
|
+
url.searchParams.set(
|
|
187
|
+
toSnakeCase(name),
|
|
188
|
+
Array.isArray(value) ? value.join(",") : String(value),
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const body =
|
|
193
|
+
options.body === undefined ? undefined : toFormData(options.body)
|
|
194
|
+
const response = await fetch(url, {
|
|
195
|
+
body,
|
|
196
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
197
|
+
method: options.method ?? (body ? "POST" : "GET"),
|
|
198
|
+
})
|
|
199
|
+
const payload = parseJson(await response.text())
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
const error = ERROR_ENVELOPE_SCHEMA.safeParse(fromMeta(payload))
|
|
202
|
+
throw new MetaAdsApiError({
|
|
203
|
+
adAccountUsage: response.headers.get("X-Ad-Account-Usage") ?? undefined,
|
|
204
|
+
appUsage: response.headers.get("X-App-Usage") ?? undefined,
|
|
205
|
+
businessUseCaseUsage:
|
|
206
|
+
response.headers.get("X-Business-Use-Case-Usage") ?? undefined,
|
|
207
|
+
error: error.success ? error.data.error : undefined,
|
|
208
|
+
retryAfter: numberOrUndefined(response.headers.get("Retry-After")),
|
|
209
|
+
status: response.status,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
return payload
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { call, page }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Encodes one public request body for Graph's form boundary.
|
|
220
|
+
*
|
|
221
|
+
* @param value - Codec-safe public request body.
|
|
222
|
+
* @throws When the body is not an object.
|
|
223
|
+
*/
|
|
224
|
+
function toFormData(value: Encodable) {
|
|
225
|
+
if (!isRecord(value))
|
|
226
|
+
throw new Error("Meta Ads request bodies must be objects.")
|
|
227
|
+
const form = new URLSearchParams()
|
|
228
|
+
for (const [name, item] of Object.entries(value)) {
|
|
229
|
+
if (item === undefined) continue
|
|
230
|
+
form.set(
|
|
231
|
+
toSnakeCase(name),
|
|
232
|
+
typeof item === "string" ||
|
|
233
|
+
typeof item === "number" ||
|
|
234
|
+
typeof item === "boolean"
|
|
235
|
+
? String(item)
|
|
236
|
+
: item instanceof Date
|
|
237
|
+
? item.toISOString()
|
|
238
|
+
: JSON.stringify(toMeta(item)),
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
return form
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Converts public camelCase object keys to Meta wire keys recursively.
|
|
246
|
+
*
|
|
247
|
+
* @param value - Codec-safe public value.
|
|
248
|
+
*/
|
|
249
|
+
export function toMeta(value: Encodable): Encodable {
|
|
250
|
+
if (value instanceof Date) return value.toISOString()
|
|
251
|
+
if (Array.isArray(value)) return value.map(toMeta)
|
|
252
|
+
if (!isRecord(value)) return value
|
|
253
|
+
return Object.fromEntries(
|
|
254
|
+
Object.entries(value).map(([key, item]) => [
|
|
255
|
+
toSnakeCase(key),
|
|
256
|
+
toMeta(item),
|
|
257
|
+
]),
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Converts Meta wire keys to public camelCase keys recursively.
|
|
263
|
+
*
|
|
264
|
+
* @param value - Provider value.
|
|
265
|
+
*/
|
|
266
|
+
export function fromMeta(value: unknown): unknown {
|
|
267
|
+
if (Array.isArray(value)) return value.map(fromMeta)
|
|
268
|
+
if (!isRecord(value)) return value
|
|
269
|
+
return Object.fromEntries(
|
|
270
|
+
Object.entries(value).map(([key, item]) => [
|
|
271
|
+
toCamelCase(key),
|
|
272
|
+
fromMeta(item),
|
|
273
|
+
]),
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Converts one camelCase key to snake_case.
|
|
279
|
+
*
|
|
280
|
+
* @param value - Public key.
|
|
281
|
+
*/
|
|
282
|
+
function toSnakeCase(value: string) {
|
|
283
|
+
return value.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Converts one snake_case key to camelCase.
|
|
288
|
+
*
|
|
289
|
+
* @param value - Provider key.
|
|
290
|
+
*/
|
|
291
|
+
function toCamelCase(value: string) {
|
|
292
|
+
return value.replace(/_([a-z])/g, (_, letter: string) => letter.toUpperCase())
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Parses a response body while retaining non-JSON provider failures.
|
|
297
|
+
*
|
|
298
|
+
* @param text - Raw response body.
|
|
299
|
+
*/
|
|
300
|
+
function parseJson(text: string): unknown {
|
|
301
|
+
if (text === "") return undefined
|
|
302
|
+
try {
|
|
303
|
+
return JSON.parse(text)
|
|
304
|
+
} catch {
|
|
305
|
+
return text
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Parses an optional finite numeric header.
|
|
311
|
+
*
|
|
312
|
+
* @param value - Raw header value.
|
|
313
|
+
*/
|
|
314
|
+
function numberOrUndefined(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
|
+
* Narrows one codec-safe object value.
|
|
322
|
+
*
|
|
323
|
+
* @param value - Unknown value.
|
|
324
|
+
*/
|
|
325
|
+
function isRecord(value: unknown): value is Record<string, Encodable> {
|
|
326
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
327
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { encodableSchema } from "@automate.ax/codec"
|
|
2
|
+
import * as z from "zod"
|
|
3
|
+
import { fromMeta } from "./api"
|
|
4
|
+
import { META_ADS_ID_SCHEMA, META_ADS_WEBHOOK_ENVELOPE_SCHEMA } from "./schemas"
|
|
5
|
+
|
|
6
|
+
export const META_ADS_AD_ACCOUNT_FIELDS = [
|
|
7
|
+
"ad_recommendations",
|
|
8
|
+
"creative_fatigue",
|
|
9
|
+
"effective_status",
|
|
10
|
+
"in_process_ad_objects",
|
|
11
|
+
"subscriptions",
|
|
12
|
+
"with_issues_ad_objects",
|
|
13
|
+
] as const
|
|
14
|
+
|
|
15
|
+
const META_ADS_EVENT_BASE_SCHEMA = z.object({
|
|
16
|
+
accountId: META_ADS_ID_SCHEMA,
|
|
17
|
+
field: z.string(),
|
|
18
|
+
occurredAt: z.number().int().nonnegative(),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export const META_ADS_EVENT_SCHEMA = META_ADS_EVENT_BASE_SCHEMA.extend({
|
|
22
|
+
value: encodableSchema,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export const META_ADS_EFFECTIVE_STATUS_EVENT_SCHEMA =
|
|
26
|
+
META_ADS_EVENT_BASE_SCHEMA.extend({
|
|
27
|
+
field: z.literal("field_changed"),
|
|
28
|
+
value: z.object({
|
|
29
|
+
changedFields: z.literal("effective_status").array(),
|
|
30
|
+
objectId: META_ADS_ID_SCHEMA,
|
|
31
|
+
objectType: z.enum(["ad", "adset", "campaign"]),
|
|
32
|
+
}),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export const META_ADS_SUBSCRIPTION_EVENT_SCHEMA =
|
|
36
|
+
META_ADS_EVENT_BASE_SCHEMA.extend({
|
|
37
|
+
field: z.literal("subscriptions"),
|
|
38
|
+
value: z.object({
|
|
39
|
+
accountId: z.union([META_ADS_ID_SCHEMA, z.number()]).transform(String),
|
|
40
|
+
currentValue: z.string().optional(),
|
|
41
|
+
field: z.string().optional(),
|
|
42
|
+
objectId: z.union([META_ADS_ID_SCHEMA, z.number()]).transform(String),
|
|
43
|
+
objectType: z.enum(["ad", "adset", "campaign"]),
|
|
44
|
+
subscriptionId: z
|
|
45
|
+
.union([META_ADS_ID_SCHEMA, z.number()])
|
|
46
|
+
.transform(String),
|
|
47
|
+
}),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
export const META_ADS_CREATIVE_FATIGUE_EVENT_SCHEMA =
|
|
51
|
+
META_ADS_EVENT_BASE_SCHEMA.extend({
|
|
52
|
+
field: z.literal("creative_fatigue"),
|
|
53
|
+
value: z.object({
|
|
54
|
+
adAccountId: META_ADS_ID_SCHEMA,
|
|
55
|
+
adgroupId: META_ADS_ID_SCHEMA,
|
|
56
|
+
creativeFatigueLevel: z.enum(["HIGH", "LOW", "MEDIUM"]),
|
|
57
|
+
creativeFatigueMessage: z.string(),
|
|
58
|
+
}),
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
export const META_ADS_RECOMMENDATION_EVENT_SCHEMA =
|
|
62
|
+
META_ADS_EVENT_BASE_SCHEMA.extend({
|
|
63
|
+
field: z.literal("ad_recommendations"),
|
|
64
|
+
value: z.object({
|
|
65
|
+
adAccountId: META_ADS_ID_SCHEMA,
|
|
66
|
+
adObjectIds: META_ADS_ID_SCHEMA.array(),
|
|
67
|
+
recommendationHash: z.string(),
|
|
68
|
+
recommendationMessage: z.string(),
|
|
69
|
+
recommendationSignature: z.string(),
|
|
70
|
+
recommendationStage: z.string(),
|
|
71
|
+
recommendationType: z.string(),
|
|
72
|
+
}),
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
export const META_ADS_PROCESSING_EVENT_SCHEMA =
|
|
76
|
+
META_ADS_EVENT_BASE_SCHEMA.extend({
|
|
77
|
+
field: z.literal("in_process_ad_objects"),
|
|
78
|
+
value: z.object({
|
|
79
|
+
id: META_ADS_ID_SCHEMA,
|
|
80
|
+
level: z.enum(["AD", "AD_SET", "CAMPAIGN", "CREATIVE"]),
|
|
81
|
+
statusName: z.string(),
|
|
82
|
+
}),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
export const META_ADS_ISSUES_EVENT_SCHEMA = META_ADS_EVENT_BASE_SCHEMA.extend({
|
|
86
|
+
field: z.literal("with_issues_ad_objects"),
|
|
87
|
+
value: z.object({
|
|
88
|
+
errorCode: z.string(),
|
|
89
|
+
errorMessage: z.string(),
|
|
90
|
+
errorSummary: z.string(),
|
|
91
|
+
id: META_ADS_ID_SCHEMA,
|
|
92
|
+
level: z.enum(["AD", "AD_SET", "CAMPAIGN", "CREATIVE"]),
|
|
93
|
+
}),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
export const META_ADS_LEAD_CREATED_EVENT_SCHEMA = z.object({
|
|
97
|
+
adId: z.string().optional(),
|
|
98
|
+
adgroupId: z.string().optional(),
|
|
99
|
+
createdTime: z.number().int().nonnegative(),
|
|
100
|
+
formId: META_ADS_ID_SCHEMA,
|
|
101
|
+
leadgenId: META_ADS_ID_SCHEMA,
|
|
102
|
+
pageId: META_ADS_ID_SCHEMA,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
export const META_ADS_TRIGGER_CONFIG_SCHEMA = z.object({
|
|
106
|
+
account: z.string().optional(),
|
|
107
|
+
adAccountId: META_ADS_ID_SCHEMA,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
export const META_ADS_LEAD_TRIGGER_CONFIG_SCHEMA = z.object({
|
|
111
|
+
account: z.string().optional(),
|
|
112
|
+
pageId: META_ADS_ID_SCHEMA,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
export interface NormalizedMetaAdsWebhook {
|
|
116
|
+
adAccountEvents: z.output<typeof META_ADS_EVENT_SCHEMA>[]
|
|
117
|
+
leadEvents: z.output<typeof META_ADS_LEAD_CREATED_EVENT_SCHEMA>[]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const metaAdsTriggerContracts = {
|
|
121
|
+
"meta-ads.ad.effectiveStatusChanged": {
|
|
122
|
+
configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
|
|
123
|
+
eventSchema: META_ADS_EFFECTIVE_STATUS_EVENT_SCHEMA,
|
|
124
|
+
},
|
|
125
|
+
"meta-ads.ad.recommendation": {
|
|
126
|
+
configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
|
|
127
|
+
eventSchema: META_ADS_RECOMMENDATION_EVENT_SCHEMA,
|
|
128
|
+
},
|
|
129
|
+
"meta-ads.adObject.issuesChanged": {
|
|
130
|
+
configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
|
|
131
|
+
eventSchema: META_ADS_ISSUES_EVENT_SCHEMA,
|
|
132
|
+
},
|
|
133
|
+
"meta-ads.adObject.processingCompleted": {
|
|
134
|
+
configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
|
|
135
|
+
eventSchema: META_ADS_PROCESSING_EVENT_SCHEMA,
|
|
136
|
+
},
|
|
137
|
+
"meta-ads.adSet.effectiveStatusChanged": {
|
|
138
|
+
configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
|
|
139
|
+
eventSchema: META_ADS_EFFECTIVE_STATUS_EVENT_SCHEMA,
|
|
140
|
+
},
|
|
141
|
+
"meta-ads.campaign.effectiveStatusChanged": {
|
|
142
|
+
configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
|
|
143
|
+
eventSchema: META_ADS_EFFECTIVE_STATUS_EVENT_SCHEMA,
|
|
144
|
+
},
|
|
145
|
+
"meta-ads.creativeFatigue.changed": {
|
|
146
|
+
configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
|
|
147
|
+
eventSchema: META_ADS_CREATIVE_FATIGUE_EVENT_SCHEMA,
|
|
148
|
+
},
|
|
149
|
+
"meta-ads.event": {
|
|
150
|
+
configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
|
|
151
|
+
eventSchema: META_ADS_EVENT_SCHEMA,
|
|
152
|
+
},
|
|
153
|
+
"meta-ads.insights.milestoneReached": {
|
|
154
|
+
configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
|
|
155
|
+
eventSchema: META_ADS_SUBSCRIPTION_EVENT_SCHEMA,
|
|
156
|
+
},
|
|
157
|
+
"meta-ads.insights.updated": {
|
|
158
|
+
configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
|
|
159
|
+
eventSchema: META_ADS_SUBSCRIPTION_EVENT_SCHEMA,
|
|
160
|
+
},
|
|
161
|
+
"meta-ads.lead.created": {
|
|
162
|
+
configSchema: META_ADS_LEAD_TRIGGER_CONFIG_SCHEMA,
|
|
163
|
+
eventSchema: META_ADS_LEAD_CREATED_EVENT_SCHEMA,
|
|
164
|
+
},
|
|
165
|
+
"meta-ads.object.created": {
|
|
166
|
+
configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
|
|
167
|
+
eventSchema: META_ADS_SUBSCRIPTION_EVENT_SCHEMA,
|
|
168
|
+
},
|
|
169
|
+
"meta-ads.object.updated": {
|
|
170
|
+
configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
|
|
171
|
+
eventSchema: META_ADS_SUBSCRIPTION_EVENT_SCHEMA,
|
|
172
|
+
},
|
|
173
|
+
} as const
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Normalizes Meta's Page and ad-account envelopes into stable public events.
|
|
177
|
+
*
|
|
178
|
+
* @param input - Untrusted provider webhook envelope.
|
|
179
|
+
*/
|
|
180
|
+
export function normalizeMetaAdsWebhook(
|
|
181
|
+
input: unknown,
|
|
182
|
+
): NormalizedMetaAdsWebhook {
|
|
183
|
+
const envelope = META_ADS_WEBHOOK_ENVELOPE_SCHEMA.parse(input)
|
|
184
|
+
const adAccountEvents: z.output<typeof META_ADS_EVENT_SCHEMA>[] = []
|
|
185
|
+
const leadEvents: z.output<typeof META_ADS_LEAD_CREATED_EVENT_SCHEMA>[] = []
|
|
186
|
+
for (const entry of envelope.entry) {
|
|
187
|
+
for (const change of entry.changes) {
|
|
188
|
+
if (envelope.object === "page" && change.field === "leadgen") {
|
|
189
|
+
const value = z
|
|
190
|
+
.object({
|
|
191
|
+
ad_id: z.string().optional(),
|
|
192
|
+
adgroup_id: z.string().optional(),
|
|
193
|
+
created_time: z.number().int().nonnegative(),
|
|
194
|
+
form_id: z.string(),
|
|
195
|
+
leadgen_id: z.string(),
|
|
196
|
+
page_id: z.string(),
|
|
197
|
+
})
|
|
198
|
+
.parse(change.value)
|
|
199
|
+
leadEvents.push({
|
|
200
|
+
adId: value.ad_id,
|
|
201
|
+
adgroupId: value.adgroup_id,
|
|
202
|
+
createdTime: value.created_time,
|
|
203
|
+
formId: value.form_id,
|
|
204
|
+
leadgenId: value.leadgen_id,
|
|
205
|
+
pageId: value.page_id,
|
|
206
|
+
})
|
|
207
|
+
} else if (envelope.object === "ad_account") {
|
|
208
|
+
adAccountEvents.push({
|
|
209
|
+
accountId: entry.id,
|
|
210
|
+
field: change.field,
|
|
211
|
+
occurredAt: entry.time,
|
|
212
|
+
value: encodableSchema.parse(fromMeta(change.value)),
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return { adAccountEvents, leadEvents }
|
|
218
|
+
}
|