@classytic/social 0.1.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 (121) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +21 -0
  3. package/README.md +368 -0
  4. package/dist/base-Bw7e52V8.mjs +246 -0
  5. package/dist/base-Bw7e52V8.mjs.map +1 -0
  6. package/dist/base-DBtKFiSX.d.mts +226 -0
  7. package/dist/base-DBtKFiSX.d.mts.map +1 -0
  8. package/dist/chunk-DQk6qfdC.mjs +18 -0
  9. package/dist/client/index.d.mts +44 -0
  10. package/dist/client/index.d.mts.map +1 -0
  11. package/dist/client/index.mjs +154 -0
  12. package/dist/client/index.mjs.map +1 -0
  13. package/dist/common/index.d.mts +3 -0
  14. package/dist/common/index.mjs +7 -0
  15. package/dist/contracts-Cdwa4zlg.d.mts +121 -0
  16. package/dist/contracts-Cdwa4zlg.d.mts.map +1 -0
  17. package/dist/contracts-lCa069IK.mjs +221 -0
  18. package/dist/contracts-lCa069IK.mjs.map +1 -0
  19. package/dist/env-Bl0cwwjC.mjs +955 -0
  20. package/dist/env-Bl0cwwjC.mjs.map +1 -0
  21. package/dist/env-DxOZHf0p.d.mts +394 -0
  22. package/dist/env-DxOZHf0p.d.mts.map +1 -0
  23. package/dist/errors-Cm6LeKf7.mjs +32 -0
  24. package/dist/errors-Cm6LeKf7.mjs.map +1 -0
  25. package/dist/facebook-l_4CghaA.mjs +95 -0
  26. package/dist/facebook-l_4CghaA.mjs.map +1 -0
  27. package/dist/http-DpcLSR1M.mjs +197 -0
  28. package/dist/http-DpcLSR1M.mjs.map +1 -0
  29. package/dist/index.d.mts +42 -0
  30. package/dist/index.d.mts.map +1 -0
  31. package/dist/index.mjs +71 -0
  32. package/dist/index.mjs.map +1 -0
  33. package/dist/instagram-BGaeUFU2.mjs +90 -0
  34. package/dist/instagram-BGaeUFU2.mjs.map +1 -0
  35. package/dist/linkedin-70whtVKa.mjs +101 -0
  36. package/dist/linkedin-70whtVKa.mjs.map +1 -0
  37. package/dist/meta-D3vcJU1c.mjs +126 -0
  38. package/dist/meta-D3vcJU1c.mjs.map +1 -0
  39. package/dist/pkce-jq5II68b.mjs +72 -0
  40. package/dist/pkce-jq5II68b.mjs.map +1 -0
  41. package/dist/polling-DZ1apXtA.mjs +25 -0
  42. package/dist/polling-DZ1apXtA.mjs.map +1 -0
  43. package/dist/providers/facebook.d.mts +135 -0
  44. package/dist/providers/facebook.d.mts.map +1 -0
  45. package/dist/providers/facebook.mjs +450 -0
  46. package/dist/providers/facebook.mjs.map +1 -0
  47. package/dist/providers/instagram.d.mts +122 -0
  48. package/dist/providers/instagram.d.mts.map +1 -0
  49. package/dist/providers/instagram.mjs +496 -0
  50. package/dist/providers/instagram.mjs.map +1 -0
  51. package/dist/providers/linkedin.d.mts +145 -0
  52. package/dist/providers/linkedin.d.mts.map +1 -0
  53. package/dist/providers/linkedin.mjs +574 -0
  54. package/dist/providers/linkedin.mjs.map +1 -0
  55. package/dist/providers/reddit.d.mts +102 -0
  56. package/dist/providers/reddit.d.mts.map +1 -0
  57. package/dist/providers/reddit.mjs +657 -0
  58. package/dist/providers/reddit.mjs.map +1 -0
  59. package/dist/providers/telegram.d.mts +139 -0
  60. package/dist/providers/telegram.d.mts.map +1 -0
  61. package/dist/providers/telegram.mjs +517 -0
  62. package/dist/providers/telegram.mjs.map +1 -0
  63. package/dist/providers/tiktok.d.mts +116 -0
  64. package/dist/providers/tiktok.d.mts.map +1 -0
  65. package/dist/providers/tiktok.mjs +676 -0
  66. package/dist/providers/tiktok.mjs.map +1 -0
  67. package/dist/providers/twitter.d.mts +150 -0
  68. package/dist/providers/twitter.d.mts.map +1 -0
  69. package/dist/providers/twitter.mjs +628 -0
  70. package/dist/providers/twitter.mjs.map +1 -0
  71. package/dist/providers/whatsapp.d.mts +79 -0
  72. package/dist/providers/whatsapp.d.mts.map +1 -0
  73. package/dist/providers/whatsapp.mjs +376 -0
  74. package/dist/providers/whatsapp.mjs.map +1 -0
  75. package/dist/providers/youtube.d.mts +153 -0
  76. package/dist/providers/youtube.d.mts.map +1 -0
  77. package/dist/providers/youtube.mjs +902 -0
  78. package/dist/providers/youtube.mjs.map +1 -0
  79. package/dist/reddit-B10kS4Se.mjs +126 -0
  80. package/dist/reddit-B10kS4Se.mjs.map +1 -0
  81. package/dist/schemas/index.d.mts +819 -0
  82. package/dist/schemas/index.d.mts.map +1 -0
  83. package/dist/schemas/index.mjs +31 -0
  84. package/dist/schemas/index.mjs.map +1 -0
  85. package/dist/security-BXhfebWm.d.mts +338 -0
  86. package/dist/security-BXhfebWm.d.mts.map +1 -0
  87. package/dist/shared-Fvc6xQku.mjs +100 -0
  88. package/dist/shared-Fvc6xQku.mjs.map +1 -0
  89. package/dist/telegram-FaUHpZgB.mjs +107 -0
  90. package/dist/telegram-FaUHpZgB.mjs.map +1 -0
  91. package/dist/tiktok-B_bMk4G-.mjs +94 -0
  92. package/dist/tiktok-B_bMk4G-.mjs.map +1 -0
  93. package/dist/twitter-BC22zfuc.mjs +98 -0
  94. package/dist/twitter-BC22zfuc.mjs.map +1 -0
  95. package/dist/types-BFE4psYI.d.mts +102 -0
  96. package/dist/types-BFE4psYI.d.mts.map +1 -0
  97. package/dist/types-Bv27tcT0.d.mts +230 -0
  98. package/dist/types-Bv27tcT0.d.mts.map +1 -0
  99. package/dist/types-BwkKyqpi.d.mts +253 -0
  100. package/dist/types-BwkKyqpi.d.mts.map +1 -0
  101. package/dist/types-CJrHMDV9.mjs +27 -0
  102. package/dist/types-CJrHMDV9.mjs.map +1 -0
  103. package/dist/types-ClbVc2rc.d.mts +117 -0
  104. package/dist/types-ClbVc2rc.d.mts.map +1 -0
  105. package/dist/types-D91N16Ym.d.mts +242 -0
  106. package/dist/types-D91N16Ym.d.mts.map +1 -0
  107. package/dist/types-DfLp_ibQ.d.mts +178 -0
  108. package/dist/types-DfLp_ibQ.d.mts.map +1 -0
  109. package/dist/types-DfjDgEoJ.d.mts +88 -0
  110. package/dist/types-DfjDgEoJ.d.mts.map +1 -0
  111. package/dist/types-Dp5Z9VBr.mjs +23 -0
  112. package/dist/types-Dp5Z9VBr.mjs.map +1 -0
  113. package/dist/types-hriBJTsU.d.mts +129 -0
  114. package/dist/types-hriBJTsU.d.mts.map +1 -0
  115. package/dist/types-rn6UuLL8.d.mts +184 -0
  116. package/dist/types-rn6UuLL8.d.mts.map +1 -0
  117. package/dist/whatsapp-CFp7ryR4.mjs +101 -0
  118. package/dist/whatsapp-CFp7ryR4.mjs.map +1 -0
  119. package/dist/youtube-Bs0fdY7H.mjs +98 -0
  120. package/dist/youtube-Bs0fdY7H.mjs.map +1 -0
  121. package/package.json +148 -0
@@ -0,0 +1,496 @@
1
+ import { t as PlatformProvider } from "../base-Bw7e52V8.mjs";
2
+ import { t as SocialError } from "../errors-Cm6LeKf7.mjs";
3
+ import { c as META_GRAPH_VERSION, i as metaRefreshLongLivedInstagram, n as metaExchangeLongLived, o as META_AUTH_URL, s as META_GRAPH_BASE } from "../meta-D3vcJU1c.mjs";
4
+ import { t as pollUntilComplete } from "../polling-DZ1apXtA.mjs";
5
+ import { t as InstagramCredentialsSchema } from "../instagram-BGaeUFU2.mjs";
6
+
7
+ //#region src/providers/instagram/index.ts
8
+ /**
9
+ * Instagram Platform Provider
10
+ * ===========================
11
+ * Instagram integration via Facebook/Meta Graph API.
12
+ *
13
+ * Features:
14
+ * - OAuth2 via Facebook Login dialog
15
+ * - Two-step token exchange (short-lived → long-lived, 60 days)
16
+ * - Instagram Business Account discovery from Facebook Page
17
+ * - Reels upload (via public video URL)
18
+ * - Single photo post (via public image URL)
19
+ * - Carousel post (mixed images/videos)
20
+ * - Container status polling
21
+ * - Credential validation
22
+ *
23
+ * Instagram API Quirks:
24
+ * - Uses Facebook OAuth (not Instagram's own dialog)
25
+ * - Token exchange is two-step: code → short-lived → long-lived
26
+ * - Token refresh uses access_token itself (not a separate refresh_token)
27
+ * - Refresh endpoint is on graph.instagram.com (not graph.facebook.com)
28
+ * - Video/image upload requires publicly accessible URLs (no binary upload)
29
+ * - Publishing is two-step: create container → poll status → publish
30
+ * - Unpublished containers expire after 24 hours (not true drafts)
31
+ * - Only Business/Creator accounts work (personal accounts unsupported since Dec 2024)
32
+ * - IG Business Account must be linked to a Facebook Page
33
+ */
34
+ const GRAPH_API_VERSION = META_GRAPH_VERSION;
35
+ const FB_AUTH_URL = META_AUTH_URL;
36
+ const GRAPH_API_BASE = META_GRAPH_BASE;
37
+ var InstagramProvider = class extends PlatformProvider {
38
+ defaultRedirectUri;
39
+ scopes;
40
+ constructor(cfg = {}) {
41
+ super(cfg);
42
+ this.name = "instagram";
43
+ this.displayName = "Instagram";
44
+ this.authType = "oauth2";
45
+ this.defaultRedirectUri = cfg.redirectUri || `http://localhost:${cfg.port || 8060}/api/oauth/instagram/callback`;
46
+ this.scopes = [
47
+ "business_management",
48
+ "instagram_business_basic",
49
+ "instagram_business_content_publish",
50
+ "instagram_business_manage_comments",
51
+ "pages_show_list",
52
+ "pages_read_engagement"
53
+ ];
54
+ }
55
+ /**
56
+ * Get Facebook OAuth authorization URL
57
+ * Instagram uses Facebook's OAuth dialog for authorization
58
+ */
59
+ getAuthUrl(state, credentials = {}, _options) {
60
+ return `${FB_AUTH_URL}?${new URLSearchParams({
61
+ client_id: credentials.appId,
62
+ redirect_uri: credentials.redirectUri || this.defaultRedirectUri,
63
+ scope: this.scopes.join(","),
64
+ response_type: "code",
65
+ state
66
+ }).toString()}`;
67
+ }
68
+ /**
69
+ * Exchange authorization code for tokens (two-step).
70
+ *
71
+ * Step 1: code → short-lived token (1 hour)
72
+ * Step 2: short-lived → long-lived token (60 days)
73
+ */
74
+ async exchangeCode(code, credentials = {}) {
75
+ return metaExchangeLongLived("instagram", {
76
+ graphVersion: GRAPH_API_VERSION,
77
+ clientId: credentials.appId,
78
+ clientSecret: credentials.appSecret,
79
+ redirectUri: credentials.redirectUri || this.defaultRedirectUri,
80
+ code
81
+ });
82
+ }
83
+ /**
84
+ * Refresh long-lived token. Instagram uses the access token itself as the
85
+ * refresh credential (must be ≥24h old, not yet expired) and hits the
86
+ * `graph.instagram.com` refresh endpoint.
87
+ */
88
+ async refreshToken(refreshToken) {
89
+ return metaRefreshLongLivedInstagram({ refreshToken });
90
+ }
91
+ /**
92
+ * Get Instagram Business Account info
93
+ *
94
+ * Flow:
95
+ * 1. Get user's Facebook Pages via /me/accounts
96
+ * 2. For first page, get linked instagram_business_account
97
+ * 3. Get IG profile details
98
+ */
99
+ async getAccountInfo(accessToken) {
100
+ const pagesData = await this._graphGet("/me/accounts", accessToken);
101
+ if (!pagesData.data || pagesData.data.length === 0) throw new SocialError("instagram", "No Facebook Pages found. Instagram Business accounts must be linked to a Facebook Page.", {
102
+ statusCode: 400,
103
+ hint: "Create a Facebook Page and link your Instagram Business account to it."
104
+ });
105
+ let igUserId = null;
106
+ for (const page of pagesData.data) {
107
+ const pageData = await this._graphGet(`/${page.id}?fields=instagram_business_account`, accessToken);
108
+ if (pageData.instagram_business_account?.id) {
109
+ igUserId = pageData.instagram_business_account.id;
110
+ break;
111
+ }
112
+ }
113
+ if (!igUserId) throw new SocialError("instagram", "No Instagram Business Account found linked to your Facebook Pages. Make sure your Instagram account is a Business or Creator account connected to a Facebook Page.", {
114
+ statusCode: 400,
115
+ hint: "Switch your Instagram to a Business or Creator account and link it to a Facebook Page."
116
+ });
117
+ const profile = await this._graphGet(`/${igUserId}?fields=id,username,name,profile_picture_url,followers_count,media_count`, accessToken);
118
+ return {
119
+ id: igUserId,
120
+ ig_user_id: igUserId,
121
+ name: profile.name || profile.username,
122
+ username: profile.username,
123
+ profileImage: profile.profile_picture_url,
124
+ followersCount: profile.followers_count,
125
+ mediaCount: profile.media_count
126
+ };
127
+ }
128
+ /**
129
+ * Test credential validity
130
+ */
131
+ async testCredential(credentialData) {
132
+ try {
133
+ if (credentialData.oauthTokenData) {
134
+ const tokenData = typeof credentialData.oauthTokenData === "string" ? JSON.parse(credentialData.oauthTokenData) : credentialData.oauthTokenData;
135
+ const accountInfo = await this.getAccountInfo(tokenData.access_token);
136
+ return {
137
+ status: "OK",
138
+ message: "Instagram credential is valid",
139
+ data: {
140
+ channelId: accountInfo.id,
141
+ channelTitle: accountInfo.name,
142
+ profileImage: accountInfo.profileImage ?? void 0
143
+ }
144
+ };
145
+ }
146
+ if (!credentialData.appId || !credentialData.appSecret) return {
147
+ status: "Error",
148
+ message: "Instagram credentials not configured — Meta App ID and App Secret are required"
149
+ };
150
+ return {
151
+ status: "Pending",
152
+ message: "Credential needs OAuth authorization. Click \"Connect Account\" to link your Instagram."
153
+ };
154
+ } catch (error) {
155
+ return {
156
+ status: "Error",
157
+ message: error.message || "Failed to validate Instagram credential"
158
+ };
159
+ }
160
+ }
161
+ /**
162
+ * Upload a video as a Reel to Instagram
163
+ *
164
+ * Instagram requires publicly accessible video URLs (no binary upload).
165
+ * Flow: create container → poll status → publish
166
+ *
167
+ * @param params
168
+ * @param params.videoUrl - Publicly accessible video URL (required)
169
+ * @param params.title - Used as caption
170
+ * @param params.description - Appended to caption if title is empty
171
+ * @param params.tokens - { access_token, ig_user_id }
172
+ * @param params.onProgress - Progress callback
173
+ */
174
+ async uploadVideo(params) {
175
+ const { videoUrl, title = "", description = "", tokens, scheduledAt, onProgress } = params;
176
+ if (scheduledAt) throw new SocialError("instagram", "Instagram does not support scheduled publishing via the API.", {
177
+ statusCode: 400,
178
+ retryable: false,
179
+ hint: "Use the job queue scheduledFor field instead — the scheduler will execute the upload at the scheduled time."
180
+ });
181
+ const accessToken = tokens.access_token;
182
+ const igUserId = tokens.ig_user_id;
183
+ if (!igUserId) throw new SocialError("instagram", "Instagram User ID not found in tokens. Re-connect your Instagram account.", { statusCode: 400 });
184
+ if (!videoUrl) throw new SocialError("instagram", "Instagram requires a publicly accessible video URL. Provide videoUrl in params.", { statusCode: 400 });
185
+ const caption = (title || description || "").substring(0, 2200);
186
+ const containerData = await this._graphPost(`/${igUserId}/media`, {
187
+ media_type: "REELS",
188
+ video_url: videoUrl,
189
+ caption
190
+ }, accessToken);
191
+ if (containerData.error) throw new SocialError("instagram", containerData.error.message || "Instagram Reels container creation failed");
192
+ const containerId = containerData.id;
193
+ if (onProgress) onProgress(10);
194
+ await this._pollContainerStatus(accessToken, containerId);
195
+ if (onProgress) onProgress(80);
196
+ const publishData = await this._graphPost(`/${igUserId}/media_publish`, { creation_id: containerId }, accessToken);
197
+ if (publishData.error) throw new SocialError("instagram", publishData.error.message || "Instagram Reels publish failed");
198
+ if (onProgress) onProgress(100);
199
+ return {
200
+ platformVideoId: publishData.id,
201
+ platformUrl: `https://www.instagram.com/reel/${publishData.id}/`,
202
+ status: "published",
203
+ uploadedAt: /* @__PURE__ */ new Date(),
204
+ metadata: {
205
+ title: caption,
206
+ containerId,
207
+ mediaId: publishData.id
208
+ }
209
+ };
210
+ }
211
+ /**
212
+ * Create a single photo post on Instagram
213
+ *
214
+ * @param params
215
+ * @param params.imageUrl - Publicly accessible image URL
216
+ * @param params.caption - Post caption (max 2200 chars)
217
+ * @param params.tokens - { access_token, ig_user_id }
218
+ */
219
+ async uploadPhoto(params) {
220
+ const { imageUrl, caption = "", tokens } = params;
221
+ const accessToken = tokens.access_token;
222
+ const igUserId = tokens.ig_user_id;
223
+ if (!igUserId) throw new SocialError("instagram", "Instagram User ID not found in tokens. Re-connect your Instagram account.", { statusCode: 400 });
224
+ const containerData = await this._graphPost(`/${igUserId}/media`, {
225
+ image_url: imageUrl,
226
+ caption: caption.substring(0, 2200)
227
+ }, accessToken);
228
+ if (containerData.error) throw new SocialError("instagram", containerData.error.message || "Instagram photo container creation failed");
229
+ await this._pollContainerStatus(accessToken, containerData.id);
230
+ const publishData = await this._graphPost(`/${igUserId}/media_publish`, { creation_id: containerData.id }, accessToken);
231
+ if (publishData.error) throw new SocialError("instagram", publishData.error.message || "Instagram photo publish failed");
232
+ return {
233
+ mediaId: publishData.id,
234
+ status: "published"
235
+ };
236
+ }
237
+ /**
238
+ * Create a carousel post on Instagram
239
+ *
240
+ * @param params
241
+ * @param params.items - 2-10 media items with type ('IMAGE'|'VIDEO') and url
242
+ * @param params.caption - Post caption (max 2200 chars)
243
+ * @param params.tokens - { access_token, ig_user_id }
244
+ */
245
+ async uploadCarousel(params) {
246
+ const { items, caption = "", tokens } = params;
247
+ const accessToken = tokens.access_token;
248
+ const igUserId = tokens.ig_user_id;
249
+ if (!igUserId) throw new SocialError("instagram", "Instagram User ID not found in tokens. Re-connect your Instagram account.", { statusCode: 400 });
250
+ if (!items || items.length < 2 || items.length > 10) throw new SocialError("instagram", "Carousel requires 2-10 media items", { statusCode: 400 });
251
+ const childIds = [];
252
+ for (const item of items) {
253
+ const body = item.type === "VIDEO" ? {
254
+ media_type: "VIDEO",
255
+ video_url: item.url,
256
+ is_carousel_item: true
257
+ } : {
258
+ image_url: item.url,
259
+ is_carousel_item: true
260
+ };
261
+ const child = await this._graphPost(`/${igUserId}/media`, body, accessToken);
262
+ if (child.error) throw new SocialError("instagram", child.error.message || "Instagram carousel child container failed");
263
+ childIds.push(child.id);
264
+ }
265
+ for (const childId of childIds) await this._pollContainerStatus(accessToken, childId);
266
+ const carouselData = await this._graphPost(`/${igUserId}/media`, {
267
+ media_type: "CAROUSEL",
268
+ children: childIds.join(","),
269
+ caption: caption.substring(0, 2200)
270
+ }, accessToken);
271
+ if (carouselData.error) throw new SocialError("instagram", carouselData.error.message || "Instagram carousel container creation failed");
272
+ const publishData = await this._graphPost(`/${igUserId}/media_publish`, { creation_id: carouselData.id }, accessToken);
273
+ if (publishData.error) throw new SocialError("instagram", publishData.error.message || "Instagram carousel publish failed");
274
+ return {
275
+ mediaId: publishData.id,
276
+ status: "published"
277
+ };
278
+ }
279
+ /**
280
+ * Get a single media object by ID
281
+ *
282
+ * @param accessToken - Valid access token
283
+ * @param mediaId - Instagram media ID
284
+ */
285
+ async getMedia(accessToken, mediaId) {
286
+ try {
287
+ const data = await this._graphGet(`/${mediaId}?fields=id,caption,media_type,media_url,thumbnail_url,permalink,timestamp,like_count,comments_count`, accessToken);
288
+ return {
289
+ id: data.id,
290
+ caption: data.caption ?? null,
291
+ mediaType: data.media_type,
292
+ mediaUrl: data.media_url ?? null,
293
+ thumbnailUrl: data.thumbnail_url ?? null,
294
+ permalink: data.permalink ?? null,
295
+ timestamp: data.timestamp,
296
+ likeCount: data.like_count ?? null,
297
+ commentsCount: data.comments_count ?? null
298
+ };
299
+ } catch (error) {
300
+ if (error instanceof SocialError) throw error;
301
+ throw new SocialError("instagram", error.message || "Failed to get media", { originalError: error });
302
+ }
303
+ }
304
+ /**
305
+ * List media for an Instagram user
306
+ *
307
+ * @param accessToken - Valid access token
308
+ * @param userId - IG user ID (if omitted, fetched via getAccountInfo)
309
+ * @param options - Pagination options (limit, after, before)
310
+ */
311
+ async listMedia(accessToken, userId, options = {}) {
312
+ try {
313
+ let path = `/${userId || (await this.getAccountInfo(accessToken)).id}/media?fields=id,caption,media_type,media_url,thumbnail_url,permalink,timestamp,like_count,comments_count&limit=${Math.min(Math.max(options.limit || 25, 1), 100)}`;
314
+ if (options.after) path += `&after=${options.after}`;
315
+ if (options.before) path += `&before=${options.before}`;
316
+ const data = await this._graphGet(path, accessToken);
317
+ return {
318
+ media: (data.data || []).map((item) => ({
319
+ id: item.id,
320
+ caption: item.caption ?? null,
321
+ mediaType: item.media_type,
322
+ mediaUrl: item.media_url ?? null,
323
+ thumbnailUrl: item.thumbnail_url ?? null,
324
+ permalink: item.permalink ?? null,
325
+ timestamp: item.timestamp,
326
+ likeCount: item.like_count ?? null,
327
+ commentsCount: item.comments_count ?? null
328
+ })),
329
+ paging: {
330
+ after: data.paging?.cursors?.after ?? null,
331
+ before: data.paging?.cursors?.before ?? null
332
+ }
333
+ };
334
+ } catch (error) {
335
+ if (error instanceof SocialError) throw error;
336
+ throw new SocialError("instagram", error.message || "Failed to list media", { originalError: error });
337
+ }
338
+ }
339
+ /**
340
+ * Get insights for a specific media object
341
+ *
342
+ * @param accessToken - Valid access token
343
+ * @param mediaId - Instagram media ID
344
+ */
345
+ async getMediaInsights(accessToken, mediaId) {
346
+ try {
347
+ const data = await this._graphGet(`/${mediaId}/insights?metric=reach,impressions,engagement,saved,likes,comments,shares`, accessToken);
348
+ const insights = {
349
+ reach: null,
350
+ impressions: null,
351
+ engagement: null,
352
+ saved: null,
353
+ likes: null,
354
+ comments: null,
355
+ shares: null
356
+ };
357
+ if (data.data) for (const entry of data.data) {
358
+ const key = entry.name;
359
+ if (key in insights) insights[key] = entry.values?.[0]?.value ?? null;
360
+ }
361
+ return insights;
362
+ } catch (error) {
363
+ if (error instanceof SocialError) throw error;
364
+ throw new SocialError("instagram", error.message || "Failed to get media insights", { originalError: error });
365
+ }
366
+ }
367
+ getCredentialZodSchema() {
368
+ return InstagramCredentialsSchema;
369
+ }
370
+ getCredentialSchema() {
371
+ return [{
372
+ name: "appId",
373
+ displayName: "Meta App ID",
374
+ type: "text",
375
+ required: true,
376
+ description: "App ID from Meta Developer Portal (developers.facebook.com)"
377
+ }, {
378
+ name: "appSecret",
379
+ displayName: "Meta App Secret",
380
+ type: "password",
381
+ required: true,
382
+ description: "App Secret from Meta Developer Portal"
383
+ }];
384
+ }
385
+ getMetadata() {
386
+ return {
387
+ name: this.name,
388
+ displayName: this.displayName,
389
+ authType: this.authType,
390
+ icon: "instagram",
391
+ brandColor: "#E4405F",
392
+ description: "Share reels and posts on Instagram",
393
+ scopes: this.scopes,
394
+ scopeDescriptions: {
395
+ instagram_business_basic: "View your Instagram profile info and media",
396
+ instagram_business_content_publish: "Publish posts and reels to your Instagram account",
397
+ instagram_business_manage_comments: "Read and manage comments on your Instagram posts",
398
+ pages_show_list: "View your Facebook Pages (required to find linked Instagram account)",
399
+ pages_read_engagement: "Read engagement data from your Facebook Pages"
400
+ },
401
+ setupGuide: [
402
+ {
403
+ step: 1,
404
+ title: "Create Meta Developer Account",
405
+ description: "Go to developers.facebook.com and create an account or log in"
406
+ },
407
+ {
408
+ step: 2,
409
+ title: "Create a New App",
410
+ description: "Click \"My Apps\" → \"Create App\". Enter your app name and email. On the use case step, filter by \"Content management\" and select \"Manage messaging & content on Instagram\""
411
+ },
412
+ {
413
+ step: 3,
414
+ title: "Connect Business Portfolio",
415
+ description: "Select or create a Business Portfolio when prompted. Click \"Create App\" to proceed to the dashboard"
416
+ },
417
+ {
418
+ step: 4,
419
+ title: "Add Permissions",
420
+ description: "Click \"Add all required permissions\" in step 1. Then go to \"Permissions and features\" and click \"+ Add\" for: instagram_business_content_publish, pages_show_list, pages_read_engagement. The use case auto-adds instagram_business_basic and instagram_business_manage_comments"
421
+ },
422
+ {
423
+ step: 5,
424
+ title: "Set Redirect URI",
425
+ description: `Go to "Facebook Login for Business" → "Settings". Add this as a Valid OAuth Redirect URI: ${this.defaultRedirectUri}`
426
+ },
427
+ {
428
+ step: 6,
429
+ title: "Copy Credentials",
430
+ description: "Go to App Settings → Basic. Copy the Facebook App ID and App Secret (not the Instagram-specific ones). These are used for the OAuth flow"
431
+ },
432
+ {
433
+ step: 7,
434
+ title: "Go Live",
435
+ description: "Switch app to \"Live\" mode. For development, Standard Access works with app role users. For production, request Advanced Access via App Review"
436
+ },
437
+ {
438
+ step: 8,
439
+ title: "Account Requirements",
440
+ description: "Your Instagram account must be a Business or Creator account linked to a Facebook Page. Personal accounts are not supported"
441
+ }
442
+ ],
443
+ supportsScheduling: false,
444
+ supportsEnvironment: false,
445
+ redirectUriPattern: this.defaultRedirectUri,
446
+ credentialSchema: this.getCredentialSchema()
447
+ };
448
+ }
449
+ /**
450
+ * Make a GET request to the Graph API
451
+ * @private
452
+ */
453
+ async _graphGet(path, accessToken) {
454
+ const url = `${GRAPH_API_BASE}${path}${path.includes("?") ? "&" : "?"}access_token=${accessToken}`;
455
+ const data = await (await fetch(url)).json();
456
+ if (data.error) throw new SocialError("instagram", data.error.message || `Graph API GET failed: ${path}`);
457
+ return data;
458
+ }
459
+ /**
460
+ * Make a POST request to the Graph API
461
+ * @private
462
+ */
463
+ async _graphPost(path, body, accessToken) {
464
+ const url = `${GRAPH_API_BASE}${path}`;
465
+ return (await fetch(url, {
466
+ method: "POST",
467
+ headers: { "Content-Type": "application/json" },
468
+ body: JSON.stringify({
469
+ ...body,
470
+ access_token: accessToken
471
+ })
472
+ })).json();
473
+ }
474
+ /**
475
+ * Poll container status until FINISHED, ERROR, or timeout
476
+ * @private
477
+ */
478
+ async _pollContainerStatus(accessToken, containerId, maxAttempts = 30, intervalMs = 5e3) {
479
+ return pollUntilComplete({
480
+ fn: () => this._graphGet(`/${containerId}?fields=status_code,status`, accessToken),
481
+ isComplete: (data) => data.status_code === "FINISHED",
482
+ getError: (data) => {
483
+ if (data.status_code === "ERROR") return new SocialError("instagram", `Container processing failed: ${data.status || "Unknown error"}`, { retryable: true });
484
+ if (data.status_code === "EXPIRED") return new SocialError("instagram", "Container expired before publishing (24-hour limit)", { retryable: false });
485
+ return null;
486
+ },
487
+ maxAttempts,
488
+ intervalMs,
489
+ label: "InstagramProvider"
490
+ });
491
+ }
492
+ };
493
+
494
+ //#endregion
495
+ export { InstagramProvider };
496
+ //# sourceMappingURL=instagram.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instagram.mjs","names":[],"sources":["../../src/providers/instagram/index.ts"],"sourcesContent":["/**\n * Instagram Platform Provider\n * ===========================\n * Instagram integration via Facebook/Meta Graph API.\n *\n * Features:\n * - OAuth2 via Facebook Login dialog\n * - Two-step token exchange (short-lived → long-lived, 60 days)\n * - Instagram Business Account discovery from Facebook Page\n * - Reels upload (via public video URL)\n * - Single photo post (via public image URL)\n * - Carousel post (mixed images/videos)\n * - Container status polling\n * - Credential validation\n *\n * Instagram API Quirks:\n * - Uses Facebook OAuth (not Instagram's own dialog)\n * - Token exchange is two-step: code → short-lived → long-lived\n * - Token refresh uses access_token itself (not a separate refresh_token)\n * - Refresh endpoint is on graph.instagram.com (not graph.facebook.com)\n * - Video/image upload requires publicly accessible URLs (no binary upload)\n * - Publishing is two-step: create container → poll status → publish\n * - Unpublished containers expire after 24 hours (not true drafts)\n * - Only Business/Creator accounts work (personal accounts unsupported since Dec 2024)\n * - IG Business Account must be linked to a Facebook Page\n */\n\nimport {\n PlatformProvider,\n type AuthUrlOptions,\n type CredentialField,\n type ProviderMetadata,\n type UploadResult,\n type OAuthTokens,\n type ProviderConfig,\n} from '../../base.js';\nimport { SocialError } from '../../errors.js';\nimport { pollUntilComplete } from '../../utils/polling.js';\nimport { InstagramCredentialsSchema } from '../../schemas/instagram.js';\nimport { META_GRAPH_VERSION, META_GRAPH_BASE, META_AUTH_URL, META_TOKEN_URL } from '../../common/meta.js';\nimport {\n metaExchangeLongLived,\n metaRefreshLongLivedInstagram,\n} from '../../common/oauth/index.js';\nimport type { z } from 'zod';\nimport type {\n InstagramTokens,\n InstagramUploadVideoParams,\n InstagramUploadPhotoParams,\n InstagramUploadCarouselParams,\n CarouselItem,\n InstagramAccountInfo,\n InstagramCredentialData,\n InstagramTestResult,\n InstagramMedia,\n ListMediaOptions,\n ListMediaResult,\n MediaInsights,\n} from './types.js';\n\nexport type {\n InstagramTokens,\n InstagramUploadVideoParams,\n InstagramUploadPhotoParams,\n InstagramUploadCarouselParams,\n CarouselItem,\n InstagramAccountInfo,\n InstagramCredentialData,\n InstagramTestResult,\n InstagramMedia,\n InstagramMediaType,\n InstagramUserTag,\n ListMediaOptions,\n ListMediaResult,\n MediaInsights,\n ContainerStatusCode,\n ContainerStatus,\n InstagramSetupStep,\n} from './types.js';\n\n// ─── Graph API Endpoints (version in src/common/meta.ts) ───────────────────\n\nconst GRAPH_API_VERSION = META_GRAPH_VERSION;\nconst FB_AUTH_URL = META_AUTH_URL;\nconst FB_TOKEN_URL = META_TOKEN_URL;\nconst GRAPH_API_BASE = META_GRAPH_BASE;\nconst IG_REFRESH_URL = 'https://graph.instagram.com/refresh_access_token';\n\n// ─── Provider ───────────────────────────────────────────────────────────────\n\nexport class InstagramProvider extends PlatformProvider {\n private defaultRedirectUri: string;\n private scopes: string[];\n\n constructor(cfg: ProviderConfig = {}) {\n super(cfg);\n this.name = 'instagram';\n this.displayName = 'Instagram';\n this.authType = 'oauth2';\n\n this.defaultRedirectUri =\n cfg.redirectUri ||\n `http://localhost:${cfg.port || 8060}/api/oauth/instagram/callback`;\n\n this.scopes = [\n 'business_management',\n 'instagram_business_basic',\n 'instagram_business_content_publish',\n 'instagram_business_manage_comments',\n 'pages_show_list',\n 'pages_read_engagement',\n ];\n }\n\n // ─── OAuth ──────────────────────────────────────────────────────────────\n\n /**\n * Get Facebook OAuth authorization URL\n * Instagram uses Facebook's OAuth dialog for authorization\n */\n getAuthUrl(state: string, credentials: Record<string, any> = {}, _options?: AuthUrlOptions): string {\n const params = new URLSearchParams({\n client_id: credentials.appId,\n redirect_uri: credentials.redirectUri || this.defaultRedirectUri,\n scope: this.scopes.join(','),\n response_type: 'code',\n state,\n });\n\n return `${FB_AUTH_URL}?${params.toString()}`;\n }\n\n /**\n * Exchange authorization code for tokens (two-step).\n *\n * Step 1: code → short-lived token (1 hour)\n * Step 2: short-lived → long-lived token (60 days)\n */\n async exchangeCode(code: string, credentials: Record<string, any> = {}): Promise<OAuthTokens> {\n return metaExchangeLongLived('instagram', {\n graphVersion: GRAPH_API_VERSION,\n clientId: credentials.appId,\n clientSecret: credentials.appSecret,\n redirectUri: credentials.redirectUri || this.defaultRedirectUri,\n code,\n });\n }\n\n /**\n * Refresh long-lived token. Instagram uses the access token itself as the\n * refresh credential (must be ≥24h old, not yet expired) and hits the\n * `graph.instagram.com` refresh endpoint.\n */\n async refreshToken(refreshToken: string): Promise<OAuthTokens> {\n return metaRefreshLongLivedInstagram({ refreshToken });\n }\n\n // ─── Account Info ───────────────────────────────────────────────────────\n\n /**\n * Get Instagram Business Account info\n *\n * Flow:\n * 1. Get user's Facebook Pages via /me/accounts\n * 2. For first page, get linked instagram_business_account\n * 3. Get IG profile details\n */\n async getAccountInfo(accessToken: string): Promise<InstagramAccountInfo> {\n // Step 1: Get user's Facebook Pages\n const pagesData: any = await this._graphGet('/me/accounts', accessToken);\n\n if (!pagesData.data || pagesData.data.length === 0) {\n throw new SocialError('instagram',\n 'No Facebook Pages found. Instagram Business accounts must be linked to a Facebook Page.',\n { statusCode: 400, hint: 'Create a Facebook Page and link your Instagram Business account to it.' },\n );\n }\n\n // Step 2: Find a page with a linked Instagram Business Account\n let igUserId: string | null = null;\n for (const page of pagesData.data) {\n const pageData: any = await this._graphGet(\n `/${page.id}?fields=instagram_business_account`,\n accessToken,\n );\n\n if (pageData.instagram_business_account?.id) {\n igUserId = pageData.instagram_business_account.id;\n break;\n }\n }\n\n if (!igUserId) {\n throw new SocialError('instagram',\n 'No Instagram Business Account found linked to your Facebook Pages. ' +\n 'Make sure your Instagram account is a Business or Creator account connected to a Facebook Page.',\n { statusCode: 400, hint: 'Switch your Instagram to a Business or Creator account and link it to a Facebook Page.' },\n );\n }\n\n // Step 3: Get Instagram profile details\n const profile: any = await this._graphGet(\n `/${igUserId}?fields=id,username,name,profile_picture_url,followers_count,media_count`,\n accessToken,\n );\n\n return {\n id: igUserId,\n ig_user_id: igUserId,\n name: profile.name || profile.username,\n username: profile.username,\n profileImage: profile.profile_picture_url,\n followersCount: profile.followers_count,\n mediaCount: profile.media_count,\n };\n }\n\n /**\n * Test credential validity\n */\n async testCredential(credentialData: InstagramCredentialData): Promise<InstagramTestResult> {\n try {\n if (credentialData.oauthTokenData) {\n const tokenData: OAuthTokens = typeof credentialData.oauthTokenData === 'string'\n ? JSON.parse(credentialData.oauthTokenData)\n : credentialData.oauthTokenData;\n const accountInfo = await this.getAccountInfo(tokenData.access_token);\n\n return {\n status: 'OK',\n message: 'Instagram credential is valid',\n data: {\n channelId: accountInfo.id,\n channelTitle: accountInfo.name,\n profileImage: accountInfo.profileImage ?? undefined,\n },\n };\n }\n\n if (!credentialData.appId || !credentialData.appSecret) {\n return {\n status: 'Error',\n message: 'Instagram credentials not configured — Meta App ID and App Secret are required',\n };\n }\n\n return {\n status: 'Pending',\n message: 'Credential needs OAuth authorization. Click \"Connect Account\" to link your Instagram.',\n };\n } catch (error: any) {\n return {\n status: 'Error',\n message: error.message || 'Failed to validate Instagram credential',\n };\n }\n }\n\n // ─── Video Upload (Reels) ──────────────────────────────────────────────\n\n /**\n * Upload a video as a Reel to Instagram\n *\n * Instagram requires publicly accessible video URLs (no binary upload).\n * Flow: create container → poll status → publish\n *\n * @param params\n * @param params.videoUrl - Publicly accessible video URL (required)\n * @param params.title - Used as caption\n * @param params.description - Appended to caption if title is empty\n * @param params.tokens - { access_token, ig_user_id }\n * @param params.onProgress - Progress callback\n */\n async uploadVideo(params: InstagramUploadVideoParams): Promise<UploadResult> {\n const {\n videoUrl,\n title = '',\n description = '',\n tokens,\n scheduledAt,\n onProgress,\n } = params;\n\n // Instagram API does not support scheduled publishing\n if (scheduledAt) {\n throw new SocialError('instagram',\n 'Instagram does not support scheduled publishing via the API.',\n { statusCode: 400, retryable: false, hint: 'Use the job queue scheduledFor field instead — the scheduler will execute the upload at the scheduled time.' },\n );\n }\n\n const accessToken: string = tokens.access_token;\n const igUserId: string = tokens.ig_user_id;\n\n if (!igUserId) {\n throw new SocialError('instagram', 'Instagram User ID not found in tokens. Re-connect your Instagram account.', { statusCode: 400 });\n }\n\n if (!videoUrl) {\n throw new SocialError('instagram', 'Instagram requires a publicly accessible video URL. Provide videoUrl in params.', { statusCode: 400 });\n }\n\n const caption: string = (title || description || '').substring(0, 2200);\n\n // Step 1: Create Reels container\n const containerData: any = await this._graphPost(`/${igUserId}/media`, {\n media_type: 'REELS',\n video_url: videoUrl,\n caption,\n }, accessToken);\n\n if (containerData.error) {\n throw new SocialError('instagram', containerData.error.message || 'Instagram Reels container creation failed');\n }\n\n const containerId: string = containerData.id;\n if (onProgress) onProgress(10);\n\n // Step 2: Poll container status until FINISHED\n await this._pollContainerStatus(accessToken, containerId);\n if (onProgress) onProgress(80);\n\n // Step 3: Publish\n const publishData: any = await this._graphPost(`/${igUserId}/media_publish`, {\n creation_id: containerId,\n }, accessToken);\n\n if (publishData.error) {\n throw new SocialError('instagram', publishData.error.message || 'Instagram Reels publish failed');\n }\n\n if (onProgress) onProgress(100);\n\n return {\n platformVideoId: publishData.id,\n platformUrl: `https://www.instagram.com/reel/${publishData.id}/`,\n status: 'published',\n uploadedAt: new Date(),\n metadata: {\n title: caption,\n containerId,\n mediaId: publishData.id,\n },\n };\n }\n\n // ─── Photo Post ─────────────────────────────────────────────────────────\n\n /**\n * Create a single photo post on Instagram\n *\n * @param params\n * @param params.imageUrl - Publicly accessible image URL\n * @param params.caption - Post caption (max 2200 chars)\n * @param params.tokens - { access_token, ig_user_id }\n */\n async uploadPhoto(params: InstagramUploadPhotoParams): Promise<{ mediaId: string; status: string }> {\n const { imageUrl, caption = '', tokens } = params;\n\n const accessToken: string = tokens.access_token;\n const igUserId: string = tokens.ig_user_id;\n\n if (!igUserId) {\n throw new SocialError('instagram', 'Instagram User ID not found in tokens. Re-connect your Instagram account.', { statusCode: 400 });\n }\n\n // Step 1: Create image container\n const containerData: any = await this._graphPost(`/${igUserId}/media`, {\n image_url: imageUrl,\n caption: caption.substring(0, 2200),\n }, accessToken);\n\n if (containerData.error) {\n throw new SocialError('instagram', containerData.error.message || 'Instagram photo container creation failed');\n }\n\n // Step 2: Poll (images are usually instant, but check anyway)\n await this._pollContainerStatus(accessToken, containerData.id);\n\n // Step 3: Publish\n const publishData: any = await this._graphPost(`/${igUserId}/media_publish`, {\n creation_id: containerData.id,\n }, accessToken);\n\n if (publishData.error) {\n throw new SocialError('instagram', publishData.error.message || 'Instagram photo publish failed');\n }\n\n return {\n mediaId: publishData.id,\n status: 'published',\n };\n }\n\n // ─── Carousel Post ──────────────────────────────────────────────────────\n\n /**\n * Create a carousel post on Instagram\n *\n * @param params\n * @param params.items - 2-10 media items with type ('IMAGE'|'VIDEO') and url\n * @param params.caption - Post caption (max 2200 chars)\n * @param params.tokens - { access_token, ig_user_id }\n */\n async uploadCarousel(params: InstagramUploadCarouselParams): Promise<{ mediaId: string; status: string }> {\n const { items, caption = '', tokens } = params;\n\n const accessToken: string = tokens.access_token;\n const igUserId: string = tokens.ig_user_id;\n\n if (!igUserId) {\n throw new SocialError('instagram', 'Instagram User ID not found in tokens. Re-connect your Instagram account.', { statusCode: 400 });\n }\n\n if (!items || items.length < 2 || items.length > 10) {\n throw new SocialError('instagram', 'Carousel requires 2-10 media items', { statusCode: 400 });\n }\n\n // Step 1: Create child containers\n const childIds: string[] = [];\n for (const item of items) {\n const body: Record<string, any> = item.type === 'VIDEO'\n ? { media_type: 'VIDEO', video_url: item.url, is_carousel_item: true }\n : { image_url: item.url, is_carousel_item: true };\n\n const child: any = await this._graphPost(`/${igUserId}/media`, body, accessToken);\n if (child.error) {\n throw new SocialError('instagram', child.error.message || 'Instagram carousel child container failed');\n }\n childIds.push(child.id);\n }\n\n // Step 2: Wait for all children to finish processing\n for (const childId of childIds) {\n await this._pollContainerStatus(accessToken, childId);\n }\n\n // Step 3: Create carousel container\n const carouselData: any = await this._graphPost(`/${igUserId}/media`, {\n media_type: 'CAROUSEL',\n children: childIds.join(','),\n caption: caption.substring(0, 2200),\n }, accessToken);\n\n if (carouselData.error) {\n throw new SocialError('instagram', carouselData.error.message || 'Instagram carousel container creation failed');\n }\n\n // Step 4: Publish\n const publishData: any = await this._graphPost(`/${igUserId}/media_publish`, {\n creation_id: carouselData.id,\n }, accessToken);\n\n if (publishData.error) {\n throw new SocialError('instagram', publishData.error.message || 'Instagram carousel publish failed');\n }\n\n return {\n mediaId: publishData.id,\n status: 'published',\n };\n }\n\n // ─── Media Read ──────────────────────────────────────────────────────\n\n /**\n * Get a single media object by ID\n *\n * @param accessToken - Valid access token\n * @param mediaId - Instagram media ID\n */\n async getMedia(accessToken: string, mediaId: string): Promise<InstagramMedia> {\n try {\n const data: any = await this._graphGet(\n `/${mediaId}?fields=id,caption,media_type,media_url,thumbnail_url,permalink,timestamp,like_count,comments_count`,\n accessToken,\n );\n\n return {\n id: data.id,\n caption: data.caption ?? null,\n mediaType: data.media_type,\n mediaUrl: data.media_url ?? null,\n thumbnailUrl: data.thumbnail_url ?? null,\n permalink: data.permalink ?? null,\n timestamp: data.timestamp,\n likeCount: data.like_count ?? null,\n commentsCount: data.comments_count ?? null,\n };\n } catch (error: any) {\n if (error instanceof SocialError) throw error;\n throw new SocialError('instagram', error.message || 'Failed to get media', { originalError: error });\n }\n }\n\n /**\n * List media for an Instagram user\n *\n * @param accessToken - Valid access token\n * @param userId - IG user ID (if omitted, fetched via getAccountInfo)\n * @param options - Pagination options (limit, after, before)\n */\n async listMedia(accessToken: string, userId?: string, options: ListMediaOptions = {}): Promise<ListMediaResult> {\n try {\n const igUserId = userId || (await this.getAccountInfo(accessToken)).id;\n const limit = Math.min(Math.max(options.limit || 25, 1), 100);\n\n let path = `/${igUserId}/media?fields=id,caption,media_type,media_url,thumbnail_url,permalink,timestamp,like_count,comments_count&limit=${limit}`;\n if (options.after) path += `&after=${options.after}`;\n if (options.before) path += `&before=${options.before}`;\n\n const data: any = await this._graphGet(path, accessToken);\n\n const media: InstagramMedia[] = (data.data || []).map((item: any) => ({\n id: item.id,\n caption: item.caption ?? null,\n mediaType: item.media_type,\n mediaUrl: item.media_url ?? null,\n thumbnailUrl: item.thumbnail_url ?? null,\n permalink: item.permalink ?? null,\n timestamp: item.timestamp,\n likeCount: item.like_count ?? null,\n commentsCount: item.comments_count ?? null,\n }));\n\n return {\n media,\n paging: {\n after: data.paging?.cursors?.after ?? null,\n before: data.paging?.cursors?.before ?? null,\n },\n };\n } catch (error: any) {\n if (error instanceof SocialError) throw error;\n throw new SocialError('instagram', error.message || 'Failed to list media', { originalError: error });\n }\n }\n\n /**\n * Get insights for a specific media object\n *\n * @param accessToken - Valid access token\n * @param mediaId - Instagram media ID\n */\n async getMediaInsights(accessToken: string, mediaId: string): Promise<MediaInsights> {\n try {\n const data: any = await this._graphGet(\n `/${mediaId}/insights?metric=reach,impressions,engagement,saved,likes,comments,shares`,\n accessToken,\n );\n\n const insights: MediaInsights = {\n reach: null,\n impressions: null,\n engagement: null,\n saved: null,\n likes: null,\n comments: null,\n shares: null,\n };\n\n if (data.data) {\n for (const entry of data.data) {\n const key = entry.name as keyof MediaInsights;\n if (key in insights) {\n insights[key] = entry.values?.[0]?.value ?? null;\n }\n }\n }\n\n return insights;\n } catch (error: any) {\n if (error instanceof SocialError) throw error;\n throw new SocialError('instagram', error.message || 'Failed to get media insights', { originalError: error });\n }\n }\n\n // ─── Schema & Metadata ────────────────────────────────────────────────\n\n getCredentialZodSchema(): z.ZodType {\n return InstagramCredentialsSchema;\n }\n\n getCredentialSchema(): CredentialField[] {\n return [\n {\n name: 'appId',\n displayName: 'Meta App ID',\n type: 'text',\n required: true,\n description: 'App ID from Meta Developer Portal (developers.facebook.com)',\n },\n {\n name: 'appSecret',\n displayName: 'Meta App Secret',\n type: 'password',\n required: true,\n description: 'App Secret from Meta Developer Portal',\n },\n ];\n }\n\n getMetadata(): ProviderMetadata {\n return {\n name: this.name,\n displayName: this.displayName,\n authType: this.authType,\n icon: 'instagram',\n brandColor: '#E4405F',\n description: 'Share reels and posts on Instagram',\n scopes: this.scopes,\n scopeDescriptions: {\n instagram_business_basic: 'View your Instagram profile info and media',\n instagram_business_content_publish: 'Publish posts and reels to your Instagram account',\n instagram_business_manage_comments: 'Read and manage comments on your Instagram posts',\n pages_show_list: 'View your Facebook Pages (required to find linked Instagram account)',\n pages_read_engagement: 'Read engagement data from your Facebook Pages',\n },\n setupGuide: [\n {\n step: 1,\n title: 'Create Meta Developer Account',\n description: 'Go to developers.facebook.com and create an account or log in',\n },\n {\n step: 2,\n title: 'Create a New App',\n description: 'Click \"My Apps\" → \"Create App\". Enter your app name and email. On the use case step, filter by \"Content management\" and select \"Manage messaging & content on Instagram\"',\n },\n {\n step: 3,\n title: 'Connect Business Portfolio',\n description: 'Select or create a Business Portfolio when prompted. Click \"Create App\" to proceed to the dashboard',\n },\n {\n step: 4,\n title: 'Add Permissions',\n description: 'Click \"Add all required permissions\" in step 1. Then go to \"Permissions and features\" and click \"+ Add\" for: instagram_business_content_publish, pages_show_list, pages_read_engagement. The use case auto-adds instagram_business_basic and instagram_business_manage_comments',\n },\n {\n step: 5,\n title: 'Set Redirect URI',\n description: `Go to \"Facebook Login for Business\" → \"Settings\". Add this as a Valid OAuth Redirect URI: ${this.defaultRedirectUri}`,\n },\n {\n step: 6,\n title: 'Copy Credentials',\n description: 'Go to App Settings → Basic. Copy the Facebook App ID and App Secret (not the Instagram-specific ones). These are used for the OAuth flow',\n },\n {\n step: 7,\n title: 'Go Live',\n description: 'Switch app to \"Live\" mode. For development, Standard Access works with app role users. For production, request Advanced Access via App Review',\n },\n {\n step: 8,\n title: 'Account Requirements',\n description: 'Your Instagram account must be a Business or Creator account linked to a Facebook Page. Personal accounts are not supported',\n },\n ],\n supportsScheduling: false,\n supportsEnvironment: false,\n redirectUriPattern: this.defaultRedirectUri,\n credentialSchema: this.getCredentialSchema(),\n };\n }\n\n // ─── Private Helpers ──────────────────────────────────────────────────\n\n /**\n * Make a GET request to the Graph API\n * @private\n */\n private async _graphGet(path: string, accessToken: string): Promise<any> {\n const separator = path.includes('?') ? '&' : '?';\n const url = `${GRAPH_API_BASE}${path}${separator}access_token=${accessToken}`;\n\n const response = await fetch(url);\n const data: any = await response.json();\n\n if (data.error) {\n throw new SocialError('instagram', data.error.message || `Graph API GET failed: ${path}`);\n }\n\n return data;\n }\n\n /**\n * Make a POST request to the Graph API\n * @private\n */\n private async _graphPost(path: string, body: Record<string, any>, accessToken: string): Promise<any> {\n const url = `${GRAPH_API_BASE}${path}`;\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ ...body, access_token: accessToken }),\n });\n\n return response.json();\n }\n\n /**\n * Poll container status until FINISHED, ERROR, or timeout\n * @private\n */\n private async _pollContainerStatus(\n accessToken: string,\n containerId: string,\n maxAttempts: number = 30,\n intervalMs: number = 5000,\n ): Promise<any> {\n return pollUntilComplete({\n fn: () => this._graphGet(`/${containerId}?fields=status_code,status`, accessToken),\n isComplete: (data: any) => data.status_code === 'FINISHED',\n getError: (data: any) => {\n if (data.status_code === 'ERROR')\n return new SocialError('instagram', `Container processing failed: ${data.status || 'Unknown error'}`, { retryable: true });\n if (data.status_code === 'EXPIRED')\n return new SocialError('instagram', 'Container expired before publishing (24-hour limit)', { retryable: false });\n return null;\n },\n maxAttempts,\n intervalMs,\n label: 'InstagramProvider',\n });\n }\n}\n"],"mappings":"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"}