@automate.ax/integration-contracts 0.160.0 → 0.163.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.
@@ -86,6 +86,110 @@ export const META_ADS_LEAD_CREATED_EVENT_SCHEMA = z.object({
86
86
  leadgenId: META_ADS_ID_SCHEMA,
87
87
  pageId: META_ADS_ID_SCHEMA,
88
88
  });
89
+ const META_ADS_MESSAGE_ATTACHMENT_SCHEMA = z.object({
90
+ /** Provider attachment payload, such as `url`, `title`, or `stickerId`. */
91
+ payload: z.record(z.string(), encodableSchema).optional(),
92
+ /**
93
+ * Provider attachment type, such as `image`, `video`, `ig_reel`, or
94
+ * `story_mention`.
95
+ */
96
+ type: z.string(),
97
+ });
98
+ const META_ADS_MESSAGE_REFERRAL_SCHEMA = z.object({
99
+ adId: z.string().optional(),
100
+ adsContextData: z
101
+ .object({
102
+ adTitle: z.string().optional(),
103
+ flowId: z.string().optional(),
104
+ photoUrl: z.string().optional(),
105
+ postId: z.string().optional(),
106
+ productId: z.string().optional(),
107
+ videoUrl: z.string().optional(),
108
+ })
109
+ .optional(),
110
+ ref: z.string().optional(),
111
+ source: z.string().optional(),
112
+ type: z.string().optional(),
113
+ });
114
+ const META_ADS_RECEIVED_MESSAGE_BASE_SCHEMA = z.object({
115
+ attachments: META_ADS_MESSAGE_ATTACHMENT_SCHEMA.array(),
116
+ messageId: META_ADS_ID_SCHEMA,
117
+ quickReplyPayload: z.string().optional(),
118
+ recipientId: META_ADS_ID_SCHEMA,
119
+ referral: META_ADS_MESSAGE_REFERRAL_SCHEMA.optional(),
120
+ senderId: META_ADS_ID_SCHEMA,
121
+ text: z.string().optional(),
122
+ timestamp: z.number().int().nonnegative(),
123
+ });
124
+ export const META_ADS_PAGE_COMMENT_CREATED_EVENT_SCHEMA = z.object({
125
+ commentId: META_ADS_ID_SCHEMA,
126
+ createdTime: z.number().int().nonnegative(),
127
+ from: z.object({ id: z.string(), name: z.string().optional() }).optional(),
128
+ message: z.string().optional(),
129
+ pageId: META_ADS_ID_SCHEMA,
130
+ parentId: z.string().optional(),
131
+ photo: z.string().optional(),
132
+ post: z
133
+ .object({
134
+ id: z.string().optional(),
135
+ isPublished: z.boolean().optional(),
136
+ permalinkUrl: z.string().optional(),
137
+ promotionStatus: z.string().optional(),
138
+ statusType: z.string().optional(),
139
+ updatedTime: z.string().optional(),
140
+ })
141
+ .optional(),
142
+ postId: META_ADS_ID_SCHEMA,
143
+ video: z.string().optional(),
144
+ });
145
+ export const META_ADS_MESSENGER_MESSAGE_RECEIVED_EVENT_SCHEMA = META_ADS_RECEIVED_MESSAGE_BASE_SCHEMA.extend({
146
+ pageId: META_ADS_ID_SCHEMA,
147
+ replyToMessageId: z.string().optional(),
148
+ });
149
+ export const META_ADS_INSTAGRAM_COMMENT_CREATED_EVENT_SCHEMA = z.object({
150
+ commentId: META_ADS_ID_SCHEMA,
151
+ from: z.object({ id: z.string(), username: z.string().optional() }),
152
+ instagramAccountId: META_ADS_ID_SCHEMA,
153
+ media: z.object({
154
+ adId: z.string().optional(),
155
+ adTitle: z.string().optional(),
156
+ id: META_ADS_ID_SCHEMA,
157
+ mediaProductType: z.string().optional(),
158
+ originalMediaId: z.string().optional(),
159
+ }),
160
+ occurredAt: z.number().int().nonnegative(),
161
+ parentId: z.string().optional(),
162
+ text: z.string().optional(),
163
+ });
164
+ export const META_ADS_INSTAGRAM_MENTION_CREATED_EVENT_SCHEMA = z.object({
165
+ commentId: z.string().optional(),
166
+ instagramAccountId: META_ADS_ID_SCHEMA,
167
+ kind: z.enum(["caption", "comment"]),
168
+ mediaId: META_ADS_ID_SCHEMA,
169
+ occurredAt: z.number().int().nonnegative(),
170
+ });
171
+ export const META_ADS_INSTAGRAM_MESSAGE_RECEIVED_EVENT_SCHEMA = META_ADS_RECEIVED_MESSAGE_BASE_SCHEMA.extend({
172
+ instagramAccountId: META_ADS_ID_SCHEMA,
173
+ isUnsupported: z.boolean(),
174
+ replyTo: z
175
+ .object({
176
+ messageId: z.string().optional(),
177
+ story: z
178
+ .object({
179
+ id: z.string(),
180
+ linkStickerUrl: z.string().optional(),
181
+ url: z.string().optional(),
182
+ })
183
+ .optional(),
184
+ })
185
+ .optional(),
186
+ });
187
+ export const META_ADS_INSTAGRAM_MESSAGE_DELETED_EVENT_SCHEMA = z.object({
188
+ instagramAccountId: META_ADS_ID_SCHEMA,
189
+ messageId: META_ADS_ID_SCHEMA,
190
+ senderId: META_ADS_ID_SCHEMA,
191
+ timestamp: z.number().int().nonnegative(),
192
+ });
89
193
  export const META_ADS_TRIGGER_CONFIG_SCHEMA = z.object({
90
194
  account: z.string().optional(),
91
195
  adAccountId: META_ADS_ID_SCHEMA,
@@ -94,6 +198,14 @@ export const META_ADS_LEAD_TRIGGER_CONFIG_SCHEMA = z.object({
94
198
  account: z.string().optional(),
95
199
  pageId: META_ADS_ID_SCHEMA,
96
200
  });
201
+ export const META_ADS_PAGE_TRIGGER_CONFIG_SCHEMA = z.object({
202
+ account: z.string().optional(),
203
+ pageId: META_ADS_ID_SCHEMA,
204
+ });
205
+ export const META_ADS_INSTAGRAM_TRIGGER_CONFIG_SCHEMA = z.object({
206
+ account: z.string().optional(),
207
+ instagramAccountId: META_ADS_ID_SCHEMA,
208
+ });
97
209
  export const metaAdsTriggerContracts = {
98
210
  "meta-ads.ad.effectiveStatusChanged": {
99
211
  configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
@@ -131,6 +243,22 @@ export const metaAdsTriggerContracts = {
131
243
  configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
132
244
  eventSchema: META_ADS_SUBSCRIPTION_EVENT_SCHEMA,
133
245
  },
246
+ "meta-ads.instagramComment.created": {
247
+ configSchema: META_ADS_INSTAGRAM_TRIGGER_CONFIG_SCHEMA,
248
+ eventSchema: META_ADS_INSTAGRAM_COMMENT_CREATED_EVENT_SCHEMA,
249
+ },
250
+ "meta-ads.instagramMention.created": {
251
+ configSchema: META_ADS_INSTAGRAM_TRIGGER_CONFIG_SCHEMA,
252
+ eventSchema: META_ADS_INSTAGRAM_MENTION_CREATED_EVENT_SCHEMA,
253
+ },
254
+ "meta-ads.instagramMessage.deleted": {
255
+ configSchema: META_ADS_INSTAGRAM_TRIGGER_CONFIG_SCHEMA,
256
+ eventSchema: META_ADS_INSTAGRAM_MESSAGE_DELETED_EVENT_SCHEMA,
257
+ },
258
+ "meta-ads.instagramMessage.received": {
259
+ configSchema: META_ADS_INSTAGRAM_TRIGGER_CONFIG_SCHEMA,
260
+ eventSchema: META_ADS_INSTAGRAM_MESSAGE_RECEIVED_EVENT_SCHEMA,
261
+ },
134
262
  "meta-ads.insights.updated": {
135
263
  configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
136
264
  eventSchema: META_ADS_SUBSCRIPTION_EVENT_SCHEMA,
@@ -139,6 +267,10 @@ export const metaAdsTriggerContracts = {
139
267
  configSchema: META_ADS_LEAD_TRIGGER_CONFIG_SCHEMA,
140
268
  eventSchema: META_ADS_LEAD_CREATED_EVENT_SCHEMA,
141
269
  },
270
+ "meta-ads.messengerMessage.received": {
271
+ configSchema: META_ADS_PAGE_TRIGGER_CONFIG_SCHEMA,
272
+ eventSchema: META_ADS_MESSENGER_MESSAGE_RECEIVED_EVENT_SCHEMA,
273
+ },
142
274
  "meta-ads.object.created": {
143
275
  configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
144
276
  eventSchema: META_ADS_SUBSCRIPTION_EVENT_SCHEMA,
@@ -147,47 +279,282 @@ export const metaAdsTriggerContracts = {
147
279
  configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
148
280
  eventSchema: META_ADS_SUBSCRIPTION_EVENT_SCHEMA,
149
281
  },
282
+ "meta-ads.pageComment.created": {
283
+ configSchema: META_ADS_PAGE_TRIGGER_CONFIG_SCHEMA,
284
+ eventSchema: META_ADS_PAGE_COMMENT_CREATED_EVENT_SCHEMA,
285
+ },
150
286
  };
287
+ const LEADGEN_VALUE_SCHEMA = z.object({
288
+ ad_id: z.string().optional(),
289
+ adgroup_id: z.string().optional(),
290
+ created_time: z.number().int().nonnegative(),
291
+ form_id: z.string(),
292
+ leadgen_id: z.string(),
293
+ page_id: z.string(),
294
+ });
295
+ const FEED_COMMENT_ADD_SCHEMA = z.object({
296
+ item: z.literal("comment"),
297
+ verb: z.literal("add"),
298
+ });
299
+ const FEED_COMMENT_VALUE_SCHEMA = META_ADS_PAGE_COMMENT_CREATED_EVENT_SCHEMA.omit({ pageId: true }).extend({
300
+ createdTime: z.union([
301
+ META_ADS_PAGE_COMMENT_CREATED_EVENT_SCHEMA.shape.createdTime,
302
+ z
303
+ .string()
304
+ .transform((value) => Math.floor(Date.parse(value) / 1_000))
305
+ .pipe(META_ADS_PAGE_COMMENT_CREATED_EVENT_SCHEMA.shape.createdTime),
306
+ ]),
307
+ });
308
+ const INSTAGRAM_COMMENT_VALUE_SCHEMA = z.object({
309
+ commentId: z.string().optional(),
310
+ from: META_ADS_INSTAGRAM_COMMENT_CREATED_EVENT_SCHEMA.shape.from.extend({
311
+ selfIgScopedId: z.string().optional(),
312
+ }),
313
+ id: z.string().optional(),
314
+ media: META_ADS_INSTAGRAM_COMMENT_CREATED_EVENT_SCHEMA.shape.media,
315
+ parentId: z.string().optional(),
316
+ text: z.string().optional(),
317
+ });
318
+ const INSTAGRAM_MENTION_VALUE_SCHEMA = z.object({
319
+ commentId: z.string().optional(),
320
+ mediaId: META_ADS_ID_SCHEMA,
321
+ });
322
+ const MESSAGING_EVENT_SCHEMA = z.object({
323
+ message: z.object({
324
+ attachments: META_ADS_MESSAGE_ATTACHMENT_SCHEMA.extend({
325
+ payload: META_ADS_MESSAGE_ATTACHMENT_SCHEMA.shape.payload.nullable(),
326
+ })
327
+ .array()
328
+ .optional(),
329
+ isDeleted: z.boolean().optional(),
330
+ isEcho: z.boolean().optional(),
331
+ isUnsupported: z.boolean().optional(),
332
+ mid: META_ADS_ID_SCHEMA,
333
+ quickReply: z.object({ payload: z.string() }).optional(),
334
+ referral: META_ADS_MESSAGE_REFERRAL_SCHEMA.optional(),
335
+ replyTo: z
336
+ .object({
337
+ mid: z.string().optional(),
338
+ story: z
339
+ .object({
340
+ id: z.string(),
341
+ linkStickerUrl: z.string().optional(),
342
+ url: z.string().optional(),
343
+ })
344
+ .optional(),
345
+ })
346
+ .optional(),
347
+ text: z.string().optional(),
348
+ }),
349
+ recipient: z.object({ id: META_ADS_ID_SCHEMA }),
350
+ sender: z.object({ id: META_ADS_ID_SCHEMA }),
351
+ timestamp: z.number().int().nonnegative(),
352
+ });
151
353
  /**
152
- * Normalizes Meta's Page and ad-account envelopes into stable public events.
354
+ * Normalizes Meta's Page, Instagram, and ad-account envelopes into stable
355
+ * public events.
356
+ *
357
+ * Classification happens here: only new comments, inbound messages, and
358
+ * mentions become semantic events. The Page's own comments, Instagram's own
359
+ * comments and message echoes are dropped, and unsent Instagram messages become
360
+ * deletion events. Malformed comment, mention, and message items are dropped so
361
+ * they can't reject a delivery that also carries leads.
153
362
  *
154
363
  * @param input - Untrusted provider webhook envelope.
155
364
  */
156
365
  export function normalizeMetaAdsWebhook(input) {
157
366
  const envelope = META_ADS_WEBHOOK_ENVELOPE_SCHEMA.parse(input);
158
- const adAccountEvents = [];
159
- const leadEvents = [];
160
- for (const entry of envelope.entry) {
161
- for (const change of entry.changes) {
162
- if (envelope.object === "page" && change.field === "leadgen") {
163
- const value = z
164
- .object({
165
- ad_id: z.string().optional(),
166
- adgroup_id: z.string().optional(),
167
- created_time: z.number().int().nonnegative(),
168
- form_id: z.string(),
169
- leadgen_id: z.string(),
170
- page_id: z.string(),
171
- })
172
- .parse(change.value);
173
- leadEvents.push({
174
- adId: value.ad_id,
175
- adgroupId: value.adgroup_id,
176
- createdTime: value.created_time,
177
- formId: value.form_id,
178
- leadgenId: value.leadgen_id,
179
- pageId: value.page_id,
180
- });
181
- }
182
- else if (envelope.object === "ad_account") {
183
- adAccountEvents.push({
184
- accountId: entry.id,
185
- field: change.field,
186
- occurredAt: entry.time,
187
- value: encodableSchema.parse(fromMeta(change.value)),
188
- });
189
- }
190
- }
367
+ const entries = envelope.entry.map((entry) => ({
368
+ ...entry,
369
+ changes: entry.changes ?? [],
370
+ messaging: entry.messaging ?? [],
371
+ }));
372
+ const pageEntries = envelope.object === "page" ? entries : [];
373
+ const instagramEntries = envelope.object === "instagram" ? entries : [];
374
+ return {
375
+ adAccountEvents: envelope.object === "ad_account"
376
+ ? entries.flatMap((entry) => entry.changes.map((change) => ({
377
+ accountId: entry.id,
378
+ field: change.field,
379
+ occurredAt: entry.time,
380
+ value: encodableSchema.parse(fromMeta(change.value)),
381
+ })))
382
+ : [],
383
+ instagramCommentEvents: instagramEntries.flatMap((entry) => entry.changes
384
+ .filter(({ field }) => field === "comments")
385
+ .flatMap(({ value }) => parseInstagramComment(entry, value) ?? [])),
386
+ instagramMentionEvents: instagramEntries.flatMap((entry) => entry.changes
387
+ .filter(({ field }) => field === "mentions")
388
+ .flatMap(({ value }) => parseInstagramMention(entry, value) ?? [])),
389
+ instagramMessageDeletedEvents: instagramEntries.flatMap((entry) => entry.messaging.flatMap((event) => {
390
+ const message = parseReceivedMessage(event);
391
+ if (!message?.isDeleted || message.isEcho)
392
+ return [];
393
+ return [
394
+ {
395
+ instagramAccountId: message.common.recipientId,
396
+ messageId: message.common.messageId,
397
+ senderId: message.common.senderId,
398
+ timestamp: message.common.timestamp,
399
+ },
400
+ ];
401
+ })),
402
+ instagramMessageEvents: instagramEntries.flatMap((entry) => entry.messaging.flatMap((event) => {
403
+ const message = parseReceivedMessage(event);
404
+ if (!message || message.isEcho || message.isDeleted)
405
+ return [];
406
+ return [
407
+ {
408
+ ...message.common,
409
+ instagramAccountId: message.common.recipientId,
410
+ isUnsupported: message.isUnsupported,
411
+ ...(message.replyTo && {
412
+ replyTo: {
413
+ messageId: message.replyTo.mid,
414
+ story: message.replyTo.story,
415
+ },
416
+ }),
417
+ },
418
+ ];
419
+ })),
420
+ leadEvents: pageEntries.flatMap((entry) => entry.changes
421
+ .filter(({ field }) => field === "leadgen")
422
+ .map(({ value }) => {
423
+ const lead = LEADGEN_VALUE_SCHEMA.parse(value);
424
+ return {
425
+ adId: lead.ad_id,
426
+ adgroupId: lead.adgroup_id,
427
+ createdTime: lead.created_time,
428
+ formId: lead.form_id,
429
+ leadgenId: lead.leadgen_id,
430
+ pageId: lead.page_id,
431
+ };
432
+ })),
433
+ messengerMessageEvents: pageEntries.flatMap((entry) => entry.messaging.flatMap((event) => {
434
+ const message = parseReceivedMessage(event);
435
+ if (!message || message.isEcho)
436
+ return [];
437
+ return [
438
+ {
439
+ ...message.common,
440
+ pageId: entry.id,
441
+ replyToMessageId: message.replyTo?.mid,
442
+ },
443
+ ];
444
+ })),
445
+ pageCommentEvents: pageEntries.flatMap((entry) => entry.changes.flatMap(({ field, value }) => {
446
+ if (field !== "feed" ||
447
+ !FEED_COMMENT_ADD_SCHEMA.safeParse(value).success)
448
+ return [];
449
+ const comment = parsePageComment(entry.id, value);
450
+ return !comment || comment.from?.id === entry.id ? [] : [comment];
451
+ })),
452
+ };
453
+ }
454
+ /**
455
+ * Normalizes one Page `feed` comment-add change, or returns `undefined` when
456
+ * the item is malformed so one bad item can't reject a whole delivery.
457
+ *
458
+ * Meta documents `created_time` only as a datetime, so both Unix seconds and
459
+ * ISO strings are accepted. Top-level comments may carry the post ID as
460
+ * `parent_id`; only a distinct parent identifies a reply.
461
+ *
462
+ * @param pageId - Page that owns the feed.
463
+ * @param input - Raw feed change value.
464
+ */
465
+ function parsePageComment(pageId, input) {
466
+ const parsed = FEED_COMMENT_VALUE_SCHEMA.safeParse(fromMeta(input));
467
+ if (!parsed.success)
468
+ return undefined;
469
+ const { parentId, ...value } = parsed.data;
470
+ return {
471
+ ...value,
472
+ pageId,
473
+ ...(parentId && parentId !== value.postId && { parentId }),
474
+ };
475
+ }
476
+ /**
477
+ * Normalizes one Instagram `comments` change, or returns `undefined` when the
478
+ * item is malformed or the account wrote it.
479
+ *
480
+ * Meta's reference schema names the comment `id`, while its Facebook Login
481
+ * samples name it `comment_id`; both are accepted. The account's own comments
482
+ * match either the entry ID or the commenter's `self_ig_scoped_id`; dropping
483
+ * them keeps reply automations from triggering themselves.
484
+ *
485
+ * @param entry - Instagram webhook entry.
486
+ * @param entry.id - Instagram professional account ID.
487
+ * @param entry.time - Delivery time in Unix seconds.
488
+ * @param input - Raw comments change value.
489
+ */
490
+ function parseInstagramComment(entry, input) {
491
+ const parsed = INSTAGRAM_COMMENT_VALUE_SCHEMA.safeParse(fromMeta(input));
492
+ if (!parsed.success)
493
+ return undefined;
494
+ const { commentId, from: { selfIgScopedId, ...from }, id, ...value } = parsed.data;
495
+ const resolvedId = id ?? commentId;
496
+ if (!resolvedId || from.id === entry.id || from.id === selfIgScopedId) {
497
+ return undefined;
191
498
  }
192
- return { adAccountEvents, leadEvents };
499
+ return {
500
+ ...value,
501
+ commentId: resolvedId,
502
+ from,
503
+ instagramAccountId: entry.id,
504
+ occurredAt: entry.time,
505
+ };
506
+ }
507
+ /**
508
+ * Normalizes one Instagram `mentions` change into a caption or comment mention,
509
+ * or returns `undefined` when the item is malformed.
510
+ *
511
+ * @param entry - Instagram webhook entry.
512
+ * @param entry.id - Instagram professional account ID.
513
+ * @param entry.time - Delivery time in Unix seconds.
514
+ * @param input - Raw mentions change value.
515
+ */
516
+ function parseInstagramMention(entry, input) {
517
+ const parsed = INSTAGRAM_MENTION_VALUE_SCHEMA.safeParse(fromMeta(input));
518
+ if (!parsed.success)
519
+ return undefined;
520
+ return {
521
+ ...parsed.data,
522
+ instagramAccountId: entry.id,
523
+ kind: parsed.data.commentId ? "comment" : "caption",
524
+ occurredAt: entry.time,
525
+ };
526
+ }
527
+ /**
528
+ * Parses one Messenger or Instagram `messaging` event that carries a message.
529
+ *
530
+ * Returns `undefined` for other event kinds, such as reads, reactions,
531
+ * postbacks, referrals, and handoff `admin_text` notices without a message ID,
532
+ * and for malformed messages.
533
+ *
534
+ * @param input - Raw messaging event.
535
+ */
536
+ function parseReceivedMessage(input) {
537
+ const parsed = MESSAGING_EVENT_SCHEMA.safeParse(fromMeta(input));
538
+ if (!parsed.success)
539
+ return undefined;
540
+ const { message, recipient, sender, timestamp } = parsed.data;
541
+ return {
542
+ common: {
543
+ attachments: (message.attachments ?? []).map(({ payload, type }) => ({
544
+ type,
545
+ ...(payload && { payload }),
546
+ })),
547
+ messageId: message.mid,
548
+ quickReplyPayload: message.quickReply?.payload,
549
+ recipientId: recipient.id,
550
+ referral: message.referral,
551
+ senderId: sender.id,
552
+ text: message.text,
553
+ timestamp,
554
+ },
555
+ isDeleted: message.isDeleted === true,
556
+ isEcho: message.isEcho === true,
557
+ isUnsupported: message.isUnsupported === true,
558
+ replyTo: message.replyTo,
559
+ };
193
560
  }
@@ -1,3 +1,4 @@
1
1
  export * from "./api.js";
2
2
  export * from "./events.js";
3
+ export * from "./pages.js";
3
4
  export * from "./schemas.js";
@@ -1,3 +1,4 @@
1
1
  export * from "./api.js";
2
2
  export * from "./events.js";
3
+ export * from "./pages.js";
3
4
  export * from "./schemas.js";
@@ -0,0 +1,34 @@
1
+ import * as z from "zod";
2
+ import { type MetaAdsResolvedAccount } from "./api.js";
3
+ /**
4
+ * Creates a Graph client authenticated as one Page.
5
+ *
6
+ * Page comment, messaging, conversation, and `subscribed_apps` endpoints need a
7
+ * Page access token. The token is derived from the connected user token on each
8
+ * call and never persisted.
9
+ *
10
+ * @param account - Resolved Meta Ads OAuth account.
11
+ * @param pageId - Facebook Page the connected identity manages.
12
+ */
13
+ export declare function getMetaAdsPageApi(account: MetaAdsResolvedAccount, pageId: string): Promise<{
14
+ call: <TSchema extends z.ZodType>(path: string, options: import("./api").MetaAdsRequestOptions<TSchema>) => Promise<z.core.output<TSchema>>;
15
+ page: <TSchema extends z.ZodType>(path: string, options: import("./api").MetaAdsRequestOptions<TSchema>) => Promise<import("./api").MetaAdsPage<z.core.output<TSchema>>>;
16
+ }>;
17
+ /**
18
+ * Finds the managed Facebook Page linked to one Instagram professional account
19
+ * and creates a Graph client authenticated as that Page.
20
+ *
21
+ * Instagram messaging with Facebook Login sends through the linked Page, and
22
+ * Instagram webhooks are enabled by installing the app on that Page.
23
+ *
24
+ * @param account - Resolved Meta Ads OAuth account.
25
+ * @param instagramAccountId - Instagram professional account ID.
26
+ * @throws When no managed Page links to the Instagram account.
27
+ */
28
+ export declare function getMetaAdsInstagramPageApi(account: MetaAdsResolvedAccount, instagramAccountId: string): Promise<{
29
+ api: {
30
+ call: <TSchema extends z.ZodType>(path: string, options: import("./api").MetaAdsRequestOptions<TSchema>) => Promise<z.core.output<TSchema>>;
31
+ page: <TSchema extends z.ZodType>(path: string, options: import("./api").MetaAdsRequestOptions<TSchema>) => Promise<import("./api").MetaAdsPage<z.core.output<TSchema>>>;
32
+ };
33
+ pageId: string;
34
+ }>;
@@ -0,0 +1,79 @@
1
+ import * as z from "zod";
2
+ import { getMetaAdsApi } from "./api.js";
3
+ const PAGE_TOKEN_SCHEMA = z.object({ accessToken: z.string().min(1) });
4
+ const LINKED_PAGE_SCHEMA = z
5
+ .object({
6
+ accessToken: z.string().min(1),
7
+ id: z.string(),
8
+ instagramBusinessAccount: z.object({ id: z.string() }).optional(),
9
+ })
10
+ .array();
11
+ /**
12
+ * Creates a Graph client authenticated as one Page.
13
+ *
14
+ * Page comment, messaging, conversation, and `subscribed_apps` endpoints need a
15
+ * Page access token. The token is derived from the connected user token on each
16
+ * call and never persisted.
17
+ *
18
+ * @param account - Resolved Meta Ads OAuth account.
19
+ * @param pageId - Facebook Page the connected identity manages.
20
+ */
21
+ export async function getMetaAdsPageApi(account, pageId) {
22
+ const { accessToken } = await getMetaAdsApi(account).call(pageId, {
23
+ query: { fields: ["access_token"] },
24
+ responseSchema: PAGE_TOKEN_SCHEMA,
25
+ });
26
+ return pageClient(accessToken);
27
+ }
28
+ /**
29
+ * Finds the managed Facebook Page linked to one Instagram professional account
30
+ * and creates a Graph client authenticated as that Page.
31
+ *
32
+ * Instagram messaging with Facebook Login sends through the linked Page, and
33
+ * Instagram webhooks are enabled by installing the app on that Page.
34
+ *
35
+ * @param account - Resolved Meta Ads OAuth account.
36
+ * @param instagramAccountId - Instagram professional account ID.
37
+ * @throws When no managed Page links to the Instagram account.
38
+ */
39
+ export async function getMetaAdsInstagramPageApi(account, instagramAccountId) {
40
+ const page = await findLinkedPage(account, instagramAccountId);
41
+ if (!page) {
42
+ throw new Error(`Instagram account ${instagramAccountId} is not linked to a Facebook Page this Meta Ads account manages.`);
43
+ }
44
+ return { api: pageClient(page.accessToken), pageId: page.id };
45
+ }
46
+ /**
47
+ * Walks the connected identity's Pages until one links to the Instagram
48
+ * account.
49
+ *
50
+ * @param account - Resolved Meta Ads OAuth account.
51
+ * @param instagramAccountId - Instagram professional account ID.
52
+ * @param after - Cursor for the next `me/accounts` page.
53
+ */
54
+ async function findLinkedPage(account, instagramAccountId, after) {
55
+ const { items, pageInfo } = await getMetaAdsApi(account).page("me/accounts", {
56
+ query: {
57
+ after,
58
+ fields: ["id", "access_token", "instagram_business_account"],
59
+ limit: 100,
60
+ },
61
+ responseSchema: LINKED_PAGE_SCHEMA,
62
+ });
63
+ const page = items.find(({ instagramBusinessAccount }) => instagramBusinessAccount?.id === instagramAccountId);
64
+ if (page || !pageInfo?.next || !pageInfo.after)
65
+ return page;
66
+ return findLinkedPage(account, instagramAccountId, pageInfo.after);
67
+ }
68
+ /**
69
+ * Wraps one Page access token in the shared Graph client.
70
+ *
71
+ * @param accessToken - Page access token.
72
+ */
73
+ function pageClient(accessToken) {
74
+ return getMetaAdsApi({
75
+ connectionMethodId: "oauth",
76
+ secret: { accessToken, expiresAt: null, tokenType: "bearer" },
77
+ serviceId: "meta-ads",
78
+ });
79
+ }