@agentforge-io/connectors-meta 0.1.0 → 0.1.2

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/connector.js CHANGED
@@ -21,15 +21,26 @@ const whatsapp_1 = require("./tools/whatsapp");
21
21
  // Each surface then adds its own product scopes on top. Operators who
22
22
  // want a stricter set can pass `scopes` explicitly.
23
23
  const IDENTITY_SCOPES = ['public_profile', 'pages_show_list'];
24
- /** Instagram DMs (`instagram_manage_messages`), media list
25
- * (`instagram_basic`), comments (`instagram_manage_comments`). Only
26
- * works for IG Business / Creator accounts linked to a FB Page —
27
- * personal IG accounts can't be read via the Graph API. */
24
+ /** Instagram (Business Login flow) uses IG-native scopes issued by
25
+ * instagram.com/oauth/authorize. The resulting token is an IG User
26
+ * Access Token that graph.instagram.com accepts. The legacy FB Login
27
+ * flow's scopes (`instagram_basic` / `pages_show_list`) produce a FB
28
+ * User Token that graph.instagram.com rejects with code=190
29
+ * "Cannot parse access token".
30
+ *
31
+ * IG expects scopes joined by COMMAS, not spaces — the core OAuth
32
+ * builder joins on space, so we collapse the IG scope list into a
33
+ * single comma-joined string here and let the builder treat it as one
34
+ * entry. (FB Login accepts both forms.)
35
+ *
36
+ * Only works for IG Business / Creator accounts. */
28
37
  const INSTAGRAM_SCOPES = [
29
- ...IDENTITY_SCOPES,
30
- 'instagram_basic',
31
- 'instagram_manage_messages',
32
- 'instagram_manage_comments',
38
+ [
39
+ 'instagram_business_basic',
40
+ 'instagram_business_manage_messages',
41
+ 'instagram_business_manage_comments',
42
+ 'instagram_business_content_publish',
43
+ ].join(','),
33
44
  ];
34
45
  /** Messenger Page DMs. `pages_messaging` is the send/receive scope;
35
46
  * `pages_read_engagement` is needed to read the existing thread list
@@ -97,18 +108,46 @@ function buildMetaSurfaceDef(args) {
97
108
  category: 'Messaging',
98
109
  iconUrl: args.opts.iconUrl,
99
110
  oauth: {
100
- authorizeUrl: 'https://www.facebook.com/v21.0/dialog/oauth',
101
- tokenUrl: 'https://graph.facebook.com/v21.0/oauth/access_token',
111
+ authorizeUrl: args.oauthOverride?.authorizeUrl ??
112
+ 'https://www.facebook.com/v21.0/dialog/oauth',
113
+ tokenUrl: args.oauthOverride?.tokenUrl ??
114
+ 'https://graph.facebook.com/v21.0/oauth/access_token',
102
115
  clientId: args.opts.clientId,
103
116
  clientSecret: args.opts.clientSecret,
104
117
  scopes: args.opts.scopes ?? args.defaultScopes,
105
- authorizeExtras: AUTHORIZE_EXTRAS,
106
- usePkce: true,
118
+ authorizeExtras: args.oauthOverride?.authorizeExtras ?? AUTHORIZE_EXTRAS,
119
+ usePkce: args.oauthOverride?.usePkce ?? true,
107
120
  },
108
121
  tools: args.tools,
109
122
  defaultToolPermissions: args.defaultToolPermissions,
123
+ // Every Meta surface shares the App ID + App Secret pair (one Meta
124
+ // App backs all four). Surface-specific secrets (e.g. webhook verify
125
+ // token for IG/Messenger inbox) are appended via `extraSecrets`.
126
+ requiredSecrets: [
127
+ {
128
+ key: 'META_APP_ID',
129
+ label: 'Meta App ID',
130
+ description: 'Numeric App ID shown at developers.facebook.com → My Apps → your app → Settings → Basic.',
131
+ sensitivity: 'low',
132
+ },
133
+ {
134
+ key: 'META_APP_SECRET',
135
+ label: 'Meta App Secret',
136
+ description: 'App Secret from the same Settings → Basic page (click "Show"). Used for OAuth code exchange and to verify webhook signatures (X-Hub-Signature-256).',
137
+ sensitivity: 'high',
138
+ },
139
+ ...(args.extraSecrets ?? []),
140
+ ],
110
141
  };
111
142
  }
143
+ /** Webhook verify token used by IG and Messenger inbox subscriptions. */
144
+ const META_WEBHOOK_VERIFY_SECRET = {
145
+ key: 'META_WEBHOOK_VERIFY_TOKEN',
146
+ label: 'Webhook verify token',
147
+ description: 'Any random string. Paste the same value into the Meta App\'s webhook subscription form — Meta echoes it back during the GET handshake so the platform can prove the receiver is ours.',
148
+ sensitivity: 'medium',
149
+ optional: true,
150
+ };
112
151
  // ── Public surface connectors ───────────────────────────────────────────
113
152
  /**
114
153
  * Instagram connector — DMs, media list, comments.
@@ -153,7 +192,19 @@ function metaInstagramConnector(opts) {
153
192
  { name: 'ig_send_message', mode: 'approval' },
154
193
  { name: 'ig_reply_comment', mode: 'approval' },
155
194
  ],
195
+ extraSecrets: [META_WEBHOOK_VERIFY_SECRET],
156
196
  opts,
197
+ // IG Business Login OAuth endpoints. The token from this flow is the
198
+ // only kind graph.instagram.com accepts; FB Login tokens (the other
199
+ // three Meta surfaces) get rejected with code=190. IG Business Login
200
+ // does not support PKCE and rejects FB-specific extras like
201
+ // `display=popup` / `auth_type=rerequest` with "Invalid platform app".
202
+ oauthOverride: {
203
+ authorizeUrl: 'https://www.instagram.com/oauth/authorize/',
204
+ tokenUrl: 'https://api.instagram.com/oauth/access_token',
205
+ usePkce: false,
206
+ authorizeExtras: {},
207
+ },
157
208
  });
158
209
  }
159
210
  /**
@@ -187,6 +238,7 @@ function metaMessengerConnector(opts) {
187
238
  // send_message goes through approval — same reasoning as IG.
188
239
  { name: 'messenger_send_message', mode: 'approval' },
189
240
  ],
241
+ extraSecrets: [META_WEBHOOK_VERIFY_SECRET],
190
242
  opts,
191
243
  });
192
244
  }
package/dist/http.d.ts CHANGED
@@ -31,6 +31,15 @@
31
31
  */
32
32
  export declare const META_GRAPH_VERSION = "v21.0";
33
33
  export declare const META_GRAPH_BASE = "https://graph.facebook.com";
34
+ /**
35
+ * Instagram Graph API base. Used by apps created with "Instagram with
36
+ * Facebook Login" (Instagram-business apps); those apps expose IG
37
+ * Messaging on `graph.instagram.com` instead of `graph.facebook.com`.
38
+ * Calls to `graph.facebook.com` from such apps fail with error code 3
39
+ * "Application does not have the capability to make this API call",
40
+ * even when the token has the right scopes.
41
+ */
42
+ export declare const META_INSTAGRAM_GRAPH_BASE = "https://graph.instagram.com";
34
43
  export declare class MetaApiError extends Error {
35
44
  readonly status: number;
36
45
  readonly statusText: string;
@@ -64,6 +73,8 @@ export interface MetaRequestOptions {
64
73
  path: string;
65
74
  query?: Record<string, string | number | boolean | undefined>;
66
75
  apiVersion?: string;
76
+ /** Override the base host. See `SendOptions.apiBase`. */
77
+ apiBase?: string;
67
78
  }
68
79
  export interface MetaMutateOptions extends MetaRequestOptions {
69
80
  body: unknown;
package/dist/http.js CHANGED
@@ -31,12 +31,21 @@
31
31
  * scope. Caller should surface a "Reconnect Meta" UX.
32
32
  */
33
33
  Object.defineProperty(exports, "__esModule", { value: true });
34
- exports.MetaApiError = exports.META_GRAPH_BASE = exports.META_GRAPH_VERSION = void 0;
34
+ exports.MetaApiError = exports.META_INSTAGRAM_GRAPH_BASE = exports.META_GRAPH_BASE = exports.META_GRAPH_VERSION = void 0;
35
35
  exports.metaGet = metaGet;
36
36
  exports.metaPost = metaPost;
37
37
  exports.metaDelete = metaDelete;
38
38
  exports.META_GRAPH_VERSION = 'v21.0';
39
39
  exports.META_GRAPH_BASE = 'https://graph.facebook.com';
40
+ /**
41
+ * Instagram Graph API base. Used by apps created with "Instagram with
42
+ * Facebook Login" (Instagram-business apps); those apps expose IG
43
+ * Messaging on `graph.instagram.com` instead of `graph.facebook.com`.
44
+ * Calls to `graph.facebook.com` from such apps fail with error code 3
45
+ * "Application does not have the capability to make this API call",
46
+ * even when the token has the right scopes.
47
+ */
48
+ exports.META_INSTAGRAM_GRAPH_BASE = 'https://graph.instagram.com';
40
49
  class MetaApiError extends Error {
41
50
  constructor(status, statusText, body,
42
51
  /** Meta error code from the response body. The most useful
@@ -66,7 +75,8 @@ const MAX_5XX_RETRIES = 1;
66
75
  const RATE_LIMIT_CODES = new Set([4, 17, 32, 613]);
67
76
  async function send(opts) {
68
77
  const version = opts.apiVersion ?? exports.META_GRAPH_VERSION;
69
- const url = new URL(`${exports.META_GRAPH_BASE}/${version}${opts.path.startsWith('/') ? opts.path : `/${opts.path}`}`);
78
+ const base = opts.apiBase ?? exports.META_GRAPH_BASE;
79
+ const url = new URL(`${base}/${version}${opts.path.startsWith('/') ? opts.path : `/${opts.path}`}`);
70
80
  for (const [k, v] of Object.entries(opts.query ?? {})) {
71
81
  if (v === undefined || v === null)
72
82
  continue;
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  export { metaInstagramConnector, metaMessengerConnector, metaFacebookPagesConnector, metaWhatsAppConnector, type MetaConnectorOptions, } from './connector';
2
- export { exchangeForLongLivedUserToken, listManagedPages, getManagedPage, listWhatsAppBusinessAccounts, MetaPageSelectionError, } from './page';
2
+ export { exchangeForLongLivedUserToken, exchangeForLongLivedIgUserToken, listManagedPages, getManagedPage, listWhatsAppBusinessAccounts, MetaPageSelectionError, } from './page';
3
3
  export { igListThreadsTool, igListMessagesTool, igSendMessageTool, igListMediaTool, igListCommentsTool, igReplyCommentTool, } from './tools/instagram';
4
4
  export { messengerListThreadsTool, messengerListMessagesTool, messengerSendMessageTool, messengerMarkSeenTool, } from './tools/messenger';
5
5
  export { fbListPostsTool, fbGetPostTool, fbCreatePostTool, fbListCommentsTool, fbReplyCommentTool, } from './tools/facebook-pages';
6
6
  export { waSendTextTool, waSendTemplateTool, } from './tools/whatsapp';
7
7
  export type { LongLivedUserTokenResponse, MetaManagedPage, ListManagedPagesResponse, MetaWhatsAppPhoneNumber, MetaWhatsAppBusinessAccount, } from './page';
8
- export { metaGet, metaPost, metaDelete, MetaApiError, META_GRAPH_VERSION, META_GRAPH_BASE, } from './http';
8
+ export { metaGet, metaPost, metaDelete, MetaApiError, META_GRAPH_VERSION, META_GRAPH_BASE, META_INSTAGRAM_GRAPH_BASE, } from './http';
9
9
  export type { MetaRequestOptions, MetaMutateOptions, MetaPaging, } from './http';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.META_GRAPH_BASE = exports.META_GRAPH_VERSION = exports.MetaApiError = exports.metaDelete = exports.metaPost = exports.metaGet = exports.waSendTemplateTool = exports.waSendTextTool = exports.fbReplyCommentTool = exports.fbListCommentsTool = exports.fbCreatePostTool = exports.fbGetPostTool = exports.fbListPostsTool = exports.messengerMarkSeenTool = exports.messengerSendMessageTool = exports.messengerListMessagesTool = exports.messengerListThreadsTool = exports.igReplyCommentTool = exports.igListCommentsTool = exports.igListMediaTool = exports.igSendMessageTool = exports.igListMessagesTool = exports.igListThreadsTool = exports.MetaPageSelectionError = exports.listWhatsAppBusinessAccounts = exports.getManagedPage = exports.listManagedPages = exports.exchangeForLongLivedUserToken = exports.metaWhatsAppConnector = exports.metaFacebookPagesConnector = exports.metaMessengerConnector = exports.metaInstagramConnector = void 0;
3
+ exports.META_INSTAGRAM_GRAPH_BASE = exports.META_GRAPH_BASE = exports.META_GRAPH_VERSION = exports.MetaApiError = exports.metaDelete = exports.metaPost = exports.metaGet = exports.waSendTemplateTool = exports.waSendTextTool = exports.fbReplyCommentTool = exports.fbListCommentsTool = exports.fbCreatePostTool = exports.fbGetPostTool = exports.fbListPostsTool = exports.messengerMarkSeenTool = exports.messengerSendMessageTool = exports.messengerListMessagesTool = exports.messengerListThreadsTool = exports.igReplyCommentTool = exports.igListCommentsTool = exports.igListMediaTool = exports.igSendMessageTool = exports.igListMessagesTool = exports.igListThreadsTool = exports.MetaPageSelectionError = exports.listWhatsAppBusinessAccounts = exports.getManagedPage = exports.listManagedPages = exports.exchangeForLongLivedIgUserToken = exports.exchangeForLongLivedUserToken = exports.metaWhatsAppConnector = exports.metaFacebookPagesConnector = exports.metaMessengerConnector = exports.metaInstagramConnector = void 0;
4
4
  var connector_1 = require("./connector");
5
5
  Object.defineProperty(exports, "metaInstagramConnector", { enumerable: true, get: function () { return connector_1.metaInstagramConnector; } });
6
6
  Object.defineProperty(exports, "metaMessengerConnector", { enumerable: true, get: function () { return connector_1.metaMessengerConnector; } });
@@ -8,6 +8,7 @@ Object.defineProperty(exports, "metaFacebookPagesConnector", { enumerable: true,
8
8
  Object.defineProperty(exports, "metaWhatsAppConnector", { enumerable: true, get: function () { return connector_1.metaWhatsAppConnector; } });
9
9
  var page_1 = require("./page");
10
10
  Object.defineProperty(exports, "exchangeForLongLivedUserToken", { enumerable: true, get: function () { return page_1.exchangeForLongLivedUserToken; } });
11
+ Object.defineProperty(exports, "exchangeForLongLivedIgUserToken", { enumerable: true, get: function () { return page_1.exchangeForLongLivedIgUserToken; } });
11
12
  Object.defineProperty(exports, "listManagedPages", { enumerable: true, get: function () { return page_1.listManagedPages; } });
12
13
  Object.defineProperty(exports, "getManagedPage", { enumerable: true, get: function () { return page_1.getManagedPage; } });
13
14
  Object.defineProperty(exports, "listWhatsAppBusinessAccounts", { enumerable: true, get: function () { return page_1.listWhatsAppBusinessAccounts; } });
@@ -69,3 +70,4 @@ Object.defineProperty(exports, "metaDelete", { enumerable: true, get: function (
69
70
  Object.defineProperty(exports, "MetaApiError", { enumerable: true, get: function () { return http_1.MetaApiError; } });
70
71
  Object.defineProperty(exports, "META_GRAPH_VERSION", { enumerable: true, get: function () { return http_1.META_GRAPH_VERSION; } });
71
72
  Object.defineProperty(exports, "META_GRAPH_BASE", { enumerable: true, get: function () { return http_1.META_GRAPH_BASE; } });
73
+ Object.defineProperty(exports, "META_INSTAGRAM_GRAPH_BASE", { enumerable: true, get: function () { return http_1.META_INSTAGRAM_GRAPH_BASE; } });
package/dist/page.d.ts CHANGED
@@ -61,6 +61,22 @@ export declare function exchangeForLongLivedUserToken(opts: {
61
61
  clientId: string;
62
62
  clientSecret: string;
63
63
  }): Promise<LongLivedUserTokenResponse>;
64
+ /**
65
+ * IG Business Login long-lived exchange.
66
+ *
67
+ * Unlike Facebook Login (which uses `fb_exchange_token` and a Bearer-less
68
+ * call to graph.facebook.com), Instagram Business Login uses
69
+ * `ig_exchange_token` against graph.instagram.com with the short-lived
70
+ * IG User Token in the query string. The result is an IG User Token
71
+ * (~60 days) that graph.instagram.com accepts on every subsequent call.
72
+ *
73
+ * The FB Login exchange does NOT work here — passing an IG short-lived
74
+ * token to `fb_exchange_token` fails with code=190.
75
+ */
76
+ export declare function exchangeForLongLivedIgUserToken(opts: {
77
+ shortLivedIgUserToken: string;
78
+ clientSecret: string;
79
+ }): Promise<LongLivedUserTokenResponse>;
64
80
  /**
65
81
  * One row from `GET /me/accounts`. Includes the page access token and,
66
82
  * when the IG account is linked, the IG business account id used by the
package/dist/page.js CHANGED
@@ -37,6 +37,7 @@
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
38
  exports.MetaPageSelectionError = void 0;
39
39
  exports.exchangeForLongLivedUserToken = exchangeForLongLivedUserToken;
40
+ exports.exchangeForLongLivedIgUserToken = exchangeForLongLivedIgUserToken;
40
41
  exports.listManagedPages = listManagedPages;
41
42
  exports.getManagedPage = getManagedPage;
42
43
  exports.listWhatsAppBusinessAccounts = listWhatsAppBusinessAccounts;
@@ -64,6 +65,29 @@ async function exchangeForLongLivedUserToken(opts) {
64
65
  }
65
66
  return (await res.json());
66
67
  }
68
+ /**
69
+ * IG Business Login long-lived exchange.
70
+ *
71
+ * Unlike Facebook Login (which uses `fb_exchange_token` and a Bearer-less
72
+ * call to graph.facebook.com), Instagram Business Login uses
73
+ * `ig_exchange_token` against graph.instagram.com with the short-lived
74
+ * IG User Token in the query string. The result is an IG User Token
75
+ * (~60 days) that graph.instagram.com accepts on every subsequent call.
76
+ *
77
+ * The FB Login exchange does NOT work here — passing an IG short-lived
78
+ * token to `fb_exchange_token` fails with code=190.
79
+ */
80
+ async function exchangeForLongLivedIgUserToken(opts) {
81
+ const url = new URL(`https://graph.instagram.com/access_token`);
82
+ url.searchParams.set('grant_type', 'ig_exchange_token');
83
+ url.searchParams.set('client_secret', opts.clientSecret);
84
+ url.searchParams.set('access_token', opts.shortLivedIgUserToken);
85
+ const res = await fetch(url.toString(), { method: 'GET' });
86
+ if (!res.ok) {
87
+ throw new http_1.MetaApiError(res.status, res.statusText, await res.text().catch(() => ''));
88
+ }
89
+ return (await res.json());
90
+ }
67
91
  /**
68
92
  * Fetch the Pages the operator manages, each with its own page access
69
93
  * token. Used at connect-time to populate the page-selector modal.
@@ -67,6 +67,7 @@ exports.igListThreadsTool = {
67
67
  const limit = Math.min(Math.max(Number(input.limit ?? 20), 1), 50);
68
68
  const result = await (0, http_1.metaGet)({
69
69
  accessToken,
70
+ apiBase: http_1.META_INSTAGRAM_GRAPH_BASE,
70
71
  path: `/${encodeURIComponent(igUserId)}/conversations`,
71
72
  query: {
72
73
  platform: 'instagram',
@@ -134,6 +135,7 @@ exports.igListMessagesTool = {
134
135
  // GETs — it costs the same quota but saves N round-trips.
135
136
  const result = await (0, http_1.metaGet)({
136
137
  accessToken,
138
+ apiBase: http_1.META_INSTAGRAM_GRAPH_BASE,
137
139
  path: `/${encodeURIComponent(input.threadId)}`,
138
140
  query: {
139
141
  fields: `messages.limit(${limit}){id,message,from{id,username},to{id,username},created_time,attachments{id,mime_type,name,file_url,image_data,video_data}}`,
@@ -210,6 +212,7 @@ exports.igSendMessageTool = {
210
212
  : { id: recipientId };
211
213
  const result = await (0, http_1.metaPost)({
212
214
  accessToken,
215
+ apiBase: http_1.META_INSTAGRAM_GRAPH_BASE,
213
216
  path: `/${encodeURIComponent(igUserId)}/messages`,
214
217
  body: {
215
218
  recipient,
@@ -251,6 +254,7 @@ exports.igListMediaTool = {
251
254
  const limit = Math.min(Math.max(Number(input.limit ?? 20), 1), 50);
252
255
  const result = await (0, http_1.metaGet)({
253
256
  accessToken,
257
+ apiBase: http_1.META_INSTAGRAM_GRAPH_BASE,
254
258
  path: `/${encodeURIComponent(igUserId)}/media`,
255
259
  query: {
256
260
  fields: 'id,caption,media_type,media_url,permalink,thumbnail_url,timestamp,comments_count,like_count',
@@ -322,6 +326,7 @@ exports.igListCommentsTool = {
322
326
  : `/${encodeURIComponent(mediaId)}/comments`;
323
327
  const result = await (0, http_1.metaGet)({
324
328
  accessToken,
329
+ apiBase: http_1.META_INSTAGRAM_GRAPH_BASE,
325
330
  path,
326
331
  query: {
327
332
  fields: 'id,text,timestamp,username,like_count,hidden,replies.limit(0)',
@@ -381,6 +386,7 @@ exports.igReplyCommentTool = {
381
386
  throw new Error('message is required.');
382
387
  const result = await (0, http_1.metaPost)({
383
388
  accessToken,
389
+ apiBase: http_1.META_INSTAGRAM_GRAPH_BASE,
384
390
  path: `/${encodeURIComponent(commentId)}/replies`,
385
391
  body: { message },
386
392
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge-io/connectors-meta",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Meta Graph API connectors for AgentForge — Instagram DMs/comments, Messenger Page DMs, Facebook Page posts/comments, WhatsApp Cloud outbound. One OAuth client → four independent connectors so the operator only grants the scopes the surface needs.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",