@agentforge-io/connectors-meta 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.
@@ -0,0 +1,80 @@
1
+ import type { ConnectorDefinition } from '@agentforge-io/core';
2
+ /**
3
+ * Shared options for every Meta surface connector.
4
+ *
5
+ * The operator configures ONE Meta App (a single appId/appSecret pair)
6
+ * and that pair is reused by Instagram, Messenger, Facebook Pages, and
7
+ * WhatsApp Cloud. This matches Meta's own model where one app declares
8
+ * the union of products + scopes and individual consent flows request
9
+ * a subset.
10
+ *
11
+ * Why one app, four connectors (vs. one combo "Meta" connector):
12
+ * - Each surface needs different scopes. Showing the operator a
13
+ * consent screen with `instagram_manage_messages` + `pages_messaging`
14
+ * + `pages_manage_posts` + `whatsapp_business_messaging` all at once
15
+ * makes Meta's review process much harder to pass — Meta wants the
16
+ * scope list to match the declared use case.
17
+ * - Per-surface `af_connector_auths` rows mean the operator can
18
+ * reconnect Instagram (e.g. after losing the IG password) without
19
+ * re-doing FB Pages + WhatsApp.
20
+ */
21
+ export interface MetaConnectorOptions {
22
+ /** Meta App ID (developer dashboard → Settings → Basic). */
23
+ clientId: string;
24
+ /** Meta App Secret. Treat as opaque — never log it. */
25
+ clientSecret: string;
26
+ /** Optional override of the default scope set for THIS surface. */
27
+ scopes?: string[];
28
+ /** Optional override of the logo asset URL shown in the Directory. */
29
+ iconUrl?: string;
30
+ }
31
+ /**
32
+ * Instagram connector — DMs, media list, comments.
33
+ *
34
+ * Connector id: `meta-instagram`.
35
+ *
36
+ * Token model: OAuth gives us a user token; the platform exchanges it
37
+ * for a long-lived user token, then derives a Page access token via
38
+ * `/me/accounts`. Instagram API calls use the IG Business Account id
39
+ * (`page.instagram_business_account.id`) with that page token.
40
+ *
41
+ * Tools land in Step 3 — this definition ships with an empty `tools[]`
42
+ * so the connector compiles and registers as a Directory card while we
43
+ * build the tools out.
44
+ */
45
+ export declare function metaInstagramConnector(opts: MetaConnectorOptions): ConnectorDefinition;
46
+ /**
47
+ * Messenger connector — Page-bound Messenger DMs.
48
+ *
49
+ * Connector id: `meta-messenger`.
50
+ *
51
+ * Same token model as Instagram (user → long-lived → page token).
52
+ * Tools land in Step 4.
53
+ */
54
+ export declare function metaMessengerConnector(opts: MetaConnectorOptions): ConnectorDefinition;
55
+ /**
56
+ * Facebook Pages connector — posts + comments on a Page.
57
+ *
58
+ * Connector id: `meta-facebook-pages`.
59
+ *
60
+ * Page token is required for every call (user tokens can't publish to
61
+ * a Page even when the user is admin). Tools land in Step 5.
62
+ */
63
+ export declare function metaFacebookPagesConnector(opts: MetaConnectorOptions): ConnectorDefinition;
64
+ /**
65
+ * WhatsApp Cloud connector — outbound messaging only.
66
+ *
67
+ * Connector id: `meta-whatsapp`.
68
+ *
69
+ * NOTE on scope vs. agentforge-platform's `WhatsAppModule`:
70
+ * The platform already ships a WhatsApp gateway receiver (inbound
71
+ * webhooks at `/webhooks/inbox/...`). This connector deliberately does
72
+ * NOT duplicate that — it only adds OUTBOUND send tools so an agent
73
+ * can initiate conversations through the Cloud API. The inbound
74
+ * webhook continues to flow through WhatsAppModule.
75
+ *
76
+ * Token model: OAuth gives a user token; we exchange for long-lived,
77
+ * then list WABAs + phone numbers via `/me/businesses{...}`. Tools
78
+ * target a phone-number id, not the WABA id. Tools land in Step 6.
79
+ */
80
+ export declare function metaWhatsAppConnector(opts: MetaConnectorOptions): ConnectorDefinition;
@@ -0,0 +1,263 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.metaInstagramConnector = metaInstagramConnector;
4
+ exports.metaMessengerConnector = metaMessengerConnector;
5
+ exports.metaFacebookPagesConnector = metaFacebookPagesConnector;
6
+ exports.metaWhatsAppConnector = metaWhatsAppConnector;
7
+ const instagram_1 = require("./tools/instagram");
8
+ const messenger_1 = require("./tools/messenger");
9
+ const facebook_pages_1 = require("./tools/facebook-pages");
10
+ const whatsapp_1 = require("./tools/whatsapp");
11
+ // ── Per-surface scope sets ──────────────────────────────────────────────
12
+ //
13
+ // Three scopes show up on every connector because Meta requires them
14
+ // to identify the user/business behind the token:
15
+ // - `public_profile` — always granted, no consent prompt
16
+ // - `pages_show_list` — required to call `/me/accounts` (page-token
17
+ // exchange happens at connect-time for every Page-bound surface)
18
+ // - `business_management` — required to call `/me/businesses` for
19
+ // WhatsApp's WABA + phone-number selector; harmless for the others.
20
+ //
21
+ // Each surface then adds its own product scopes on top. Operators who
22
+ // want a stricter set can pass `scopes` explicitly.
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. */
28
+ const INSTAGRAM_SCOPES = [
29
+ ...IDENTITY_SCOPES,
30
+ 'instagram_basic',
31
+ 'instagram_manage_messages',
32
+ 'instagram_manage_comments',
33
+ ];
34
+ /** Messenger Page DMs. `pages_messaging` is the send/receive scope;
35
+ * `pages_read_engagement` is needed to read the existing thread list
36
+ * (counter-intuitively, `pages_messaging` alone doesn't unlock thread
37
+ * listing). `pages_manage_metadata` is needed to mark threads as seen. */
38
+ const MESSENGER_SCOPES = [
39
+ ...IDENTITY_SCOPES,
40
+ 'pages_messaging',
41
+ 'pages_read_engagement',
42
+ 'pages_manage_metadata',
43
+ ];
44
+ /** Facebook Page posts + comments. `pages_manage_posts` is publish;
45
+ * `pages_read_engagement` reads posts/comments; `pages_manage_engagement`
46
+ * is required to reply to comments (read-engagement alone won't let
47
+ * you POST). */
48
+ const FACEBOOK_PAGE_SCOPES = [
49
+ ...IDENTITY_SCOPES,
50
+ 'pages_manage_posts',
51
+ 'pages_read_engagement',
52
+ 'pages_manage_engagement',
53
+ ];
54
+ /** WhatsApp Cloud outbound. `whatsapp_business_messaging` is the send
55
+ * scope; `whatsapp_business_management` is needed for the WABA +
56
+ * phone-number selector at connect-time. `business_management` lets
57
+ * us walk `/me/businesses` to enumerate WABAs. */
58
+ const WHATSAPP_SCOPES = [
59
+ ...IDENTITY_SCOPES,
60
+ 'whatsapp_business_messaging',
61
+ 'whatsapp_business_management',
62
+ 'business_management',
63
+ ];
64
+ // Meta uses standard OAuth 2.0 with PKCE supported but optional. We
65
+ // enable it because (a) Meta recommends it for native/SPA flows and
66
+ // (b) the rest of the AgentForge OAuth connectors (Google, Slack,
67
+ // Notion, GitHub) all run PKCE, so disabling it here would be the
68
+ // odd-one-out that surprises operators auditing logs.
69
+ //
70
+ // `auth_type=rerequest` forces a consent screen on every authorize so
71
+ // scope changes get a fresh grant — without it, Meta silently issues
72
+ // the OLD scope set when an operator reconnects after we widened the
73
+ // surface, which then surfaces as runtime 200-permission errors.
74
+ const AUTHORIZE_EXTRAS = {
75
+ auth_type: 'rerequest',
76
+ // `display=popup` keeps the consent inside the connect modal instead
77
+ // of a full-window redirect, matching how the operator already
78
+ // experiences Google + Slack from the same surface.
79
+ display: 'popup',
80
+ };
81
+ /**
82
+ * Internal helper. Every surface connector shares the same OAuth URL
83
+ * pair, PKCE flag, and authorize_extras — only `id`, `name`, scopes,
84
+ * and the tool list differ.
85
+ *
86
+ * The authorize/token URLs are Meta's standard v21.0 endpoints; bumping
87
+ * the version is a single edit to `META_GRAPH_VERSION` in `http.ts`
88
+ * (the OAuth dialog accepts any supported version on the path; we don't
89
+ * version-pin these URLs because Meta keeps the latest stable working
90
+ * indefinitely).
91
+ */
92
+ function buildMetaSurfaceDef(args) {
93
+ return {
94
+ id: args.id,
95
+ name: args.name,
96
+ description: args.description,
97
+ category: 'Messaging',
98
+ iconUrl: args.opts.iconUrl,
99
+ oauth: {
100
+ authorizeUrl: 'https://www.facebook.com/v21.0/dialog/oauth',
101
+ tokenUrl: 'https://graph.facebook.com/v21.0/oauth/access_token',
102
+ clientId: args.opts.clientId,
103
+ clientSecret: args.opts.clientSecret,
104
+ scopes: args.opts.scopes ?? args.defaultScopes,
105
+ authorizeExtras: AUTHORIZE_EXTRAS,
106
+ usePkce: true,
107
+ },
108
+ tools: args.tools,
109
+ defaultToolPermissions: args.defaultToolPermissions,
110
+ };
111
+ }
112
+ // ── Public surface connectors ───────────────────────────────────────────
113
+ /**
114
+ * Instagram connector — DMs, media list, comments.
115
+ *
116
+ * Connector id: `meta-instagram`.
117
+ *
118
+ * Token model: OAuth gives us a user token; the platform exchanges it
119
+ * for a long-lived user token, then derives a Page access token via
120
+ * `/me/accounts`. Instagram API calls use the IG Business Account id
121
+ * (`page.instagram_business_account.id`) with that page token.
122
+ *
123
+ * Tools land in Step 3 — this definition ships with an empty `tools[]`
124
+ * so the connector compiles and registers as a Directory card while we
125
+ * build the tools out.
126
+ */
127
+ function metaInstagramConnector(opts) {
128
+ return buildMetaSurfaceDef({
129
+ id: 'meta-instagram',
130
+ name: 'Instagram',
131
+ description: 'Read and respond to Instagram DMs, list media, and reply to ' +
132
+ 'post comments. Requires an Instagram Business or Creator ' +
133
+ 'account linked to a Facebook Page.',
134
+ defaultScopes: INSTAGRAM_SCOPES,
135
+ tools: [
136
+ instagram_1.igListThreadsTool,
137
+ instagram_1.igListMessagesTool,
138
+ instagram_1.igSendMessageTool,
139
+ instagram_1.igListMediaTool,
140
+ instagram_1.igListCommentsTool,
141
+ instagram_1.igReplyCommentTool,
142
+ ],
143
+ defaultToolPermissions: [
144
+ // Reads are safe — default to allow so the agent can browse the
145
+ // inbox + posts without an approval round-trip.
146
+ { name: 'ig_list_threads', mode: 'allow' },
147
+ { name: 'ig_list_messages', mode: 'allow' },
148
+ { name: 'ig_list_media', mode: 'allow' },
149
+ { name: 'ig_list_comments', mode: 'allow' },
150
+ // Writes go through approval. A wrong DM or a wrong public reply
151
+ // is high-reputation cost for the brand; the operator gates
152
+ // them per turn until they explicitly trust the agent.
153
+ { name: 'ig_send_message', mode: 'approval' },
154
+ { name: 'ig_reply_comment', mode: 'approval' },
155
+ ],
156
+ opts,
157
+ });
158
+ }
159
+ /**
160
+ * Messenger connector — Page-bound Messenger DMs.
161
+ *
162
+ * Connector id: `meta-messenger`.
163
+ *
164
+ * Same token model as Instagram (user → long-lived → page token).
165
+ * Tools land in Step 4.
166
+ */
167
+ function metaMessengerConnector(opts) {
168
+ return buildMetaSurfaceDef({
169
+ id: 'meta-messenger',
170
+ name: 'Messenger',
171
+ description: 'Read and respond to Messenger conversations on a Facebook Page. ' +
172
+ 'Personal Messenger inboxes are not accessible — only Page-owned ' +
173
+ 'threads.',
174
+ defaultScopes: MESSENGER_SCOPES,
175
+ tools: [
176
+ messenger_1.messengerListThreadsTool,
177
+ messenger_1.messengerListMessagesTool,
178
+ messenger_1.messengerSendMessageTool,
179
+ messenger_1.messengerMarkSeenTool,
180
+ ],
181
+ defaultToolPermissions: [
182
+ // Reads + mark_seen default to allow. mark_seen is a presence
183
+ // signal, not user-facing content — same risk profile as a read.
184
+ { name: 'messenger_list_threads', mode: 'allow' },
185
+ { name: 'messenger_list_messages', mode: 'allow' },
186
+ { name: 'messenger_mark_seen', mode: 'allow' },
187
+ // send_message goes through approval — same reasoning as IG.
188
+ { name: 'messenger_send_message', mode: 'approval' },
189
+ ],
190
+ opts,
191
+ });
192
+ }
193
+ /**
194
+ * Facebook Pages connector — posts + comments on a Page.
195
+ *
196
+ * Connector id: `meta-facebook-pages`.
197
+ *
198
+ * Page token is required for every call (user tokens can't publish to
199
+ * a Page even when the user is admin). Tools land in Step 5.
200
+ */
201
+ function metaFacebookPagesConnector(opts) {
202
+ return buildMetaSurfaceDef({
203
+ id: 'meta-facebook-pages',
204
+ name: 'Facebook Pages',
205
+ description: 'Publish, list, and engage with posts on a Facebook Page: ' +
206
+ 'create posts, read engagement, and reply to comments.',
207
+ defaultScopes: FACEBOOK_PAGE_SCOPES,
208
+ tools: [
209
+ facebook_pages_1.fbListPostsTool,
210
+ facebook_pages_1.fbGetPostTool,
211
+ facebook_pages_1.fbCreatePostTool,
212
+ facebook_pages_1.fbListCommentsTool,
213
+ facebook_pages_1.fbReplyCommentTool,
214
+ ],
215
+ defaultToolPermissions: [
216
+ // Reads default to allow.
217
+ { name: 'fb_list_posts', mode: 'allow' },
218
+ { name: 'fb_get_post', mode: 'allow' },
219
+ { name: 'fb_list_comments', mode: 'allow' },
220
+ // Writes to the public timeline + public comments are
221
+ // high-blast-radius — gate them per turn until trusted.
222
+ { name: 'fb_create_post', mode: 'approval' },
223
+ { name: 'fb_reply_comment', mode: 'approval' },
224
+ ],
225
+ opts,
226
+ });
227
+ }
228
+ /**
229
+ * WhatsApp Cloud connector — outbound messaging only.
230
+ *
231
+ * Connector id: `meta-whatsapp`.
232
+ *
233
+ * NOTE on scope vs. agentforge-platform's `WhatsAppModule`:
234
+ * The platform already ships a WhatsApp gateway receiver (inbound
235
+ * webhooks at `/webhooks/inbox/...`). This connector deliberately does
236
+ * NOT duplicate that — it only adds OUTBOUND send tools so an agent
237
+ * can initiate conversations through the Cloud API. The inbound
238
+ * webhook continues to flow through WhatsAppModule.
239
+ *
240
+ * Token model: OAuth gives a user token; we exchange for long-lived,
241
+ * then list WABAs + phone numbers via `/me/businesses{...}`. Tools
242
+ * target a phone-number id, not the WABA id. Tools land in Step 6.
243
+ */
244
+ function metaWhatsAppConnector(opts) {
245
+ return buildMetaSurfaceDef({
246
+ id: 'meta-whatsapp',
247
+ name: 'WhatsApp Cloud',
248
+ description: 'Send outbound WhatsApp messages (free-form within the 24h ' +
249
+ 'service window, or pre-approved templates outside it) from a ' +
250
+ 'WhatsApp Business phone number. Inbound is handled by the ' +
251
+ 'platform WhatsApp gateway.',
252
+ defaultScopes: WHATSAPP_SCOPES,
253
+ tools: [whatsapp_1.waSendTextTool, whatsapp_1.waSendTemplateTool],
254
+ defaultToolPermissions: [
255
+ // Both tools are writes that hit a customer's WhatsApp inbox —
256
+ // higher blast radius than IG/Messenger because users tend to
257
+ // treat WA as their personal phone. Both gate through approval.
258
+ { name: 'wa_send_text', mode: 'approval' },
259
+ { name: 'wa_send_template', mode: 'approval' },
260
+ ],
261
+ opts,
262
+ });
263
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Thin HTTP layer for the Meta Graph API. All four connectors in this
3
+ * package (Instagram, Messenger, Facebook Pages, WhatsApp) talk to
4
+ * `graph.facebook.com/v{version}` with the same auth + retry policy,
5
+ * so the transport lives here.
6
+ *
7
+ * Endpoints are **fixed** — unlike Shopify's per-shop hostnames, every
8
+ * Meta surface uses the same base URL and routes via the path
9
+ * (`/{ig-user-id}/conversations`, `/{page-id}/feed`, etc.). That's why
10
+ * we don't expose `resolveEndpoints` in the connector definitions.
11
+ *
12
+ * Auth: `Authorization: Bearer <accessToken>`. Meta also accepts
13
+ * `?access_token=` as a query param, but the header form keeps tokens
14
+ * out of access logs and matches what the official SDKs send.
15
+ *
16
+ * Rate limits (the painful ones to remember):
17
+ * - App-level: 200 calls/hr/user, tracked in `X-App-Usage` header.
18
+ * - Business use case: separate buckets for Messaging, Pages,
19
+ * Instagram, each in `X-Business-Use-Case-Usage` / `X-Ad-Account-Usage`.
20
+ * - When ≥95% of any bucket: 4xx with code 4 / 17 / 32 / 613.
21
+ * - Hard throttle: HTTP 429 (rare; usually they 4xx with the codes
22
+ * above first).
23
+ *
24
+ * Retry policy:
25
+ * - 429: up to 2 retries, honoring `Retry-After` (seconds).
26
+ * - 5xx: 1 retry with ~500ms jitter.
27
+ * - 4xx with rate-limit code (4, 17, 32, 613): same backoff as 429
28
+ * because Meta's "you're at 95%" responses come back as 4xx.
29
+ * - 401/403: NEVER retried — the token is invalid/expired/missing
30
+ * scope. Caller should surface a "Reconnect Meta" UX.
31
+ */
32
+ export declare const META_GRAPH_VERSION = "v21.0";
33
+ export declare const META_GRAPH_BASE = "https://graph.facebook.com";
34
+ export declare class MetaApiError extends Error {
35
+ readonly status: number;
36
+ readonly statusText: string;
37
+ readonly body: string;
38
+ /** Meta error code from the response body. The most useful
39
+ * routable values:
40
+ * - 4: app-level rate limit (back off)
41
+ * - 17: user-level rate limit
42
+ * - 32, 613: page/business use-case rate limit
43
+ * - 190: access token expired/invalid → re-auth
44
+ * - 200: missing permission → re-auth with broader scopes
45
+ * - 100: invalid parameter (bug in the tool input) */
46
+ readonly metaCode?: number | undefined;
47
+ /** Sub-error type for permission/scope failures. */
48
+ readonly metaSubcode?: number | undefined;
49
+ constructor(status: number, statusText: string, body: string,
50
+ /** Meta error code from the response body. The most useful
51
+ * routable values:
52
+ * - 4: app-level rate limit (back off)
53
+ * - 17: user-level rate limit
54
+ * - 32, 613: page/business use-case rate limit
55
+ * - 190: access token expired/invalid → re-auth
56
+ * - 200: missing permission → re-auth with broader scopes
57
+ * - 100: invalid parameter (bug in the tool input) */
58
+ metaCode?: number | undefined,
59
+ /** Sub-error type for permission/scope failures. */
60
+ metaSubcode?: number | undefined);
61
+ }
62
+ export interface MetaRequestOptions {
63
+ accessToken: string;
64
+ path: string;
65
+ query?: Record<string, string | number | boolean | undefined>;
66
+ apiVersion?: string;
67
+ }
68
+ export interface MetaMutateOptions extends MetaRequestOptions {
69
+ body: unknown;
70
+ }
71
+ export declare function metaGet<T>(opts: MetaRequestOptions): Promise<T>;
72
+ export declare function metaPost<T>(opts: MetaMutateOptions): Promise<T>;
73
+ export declare function metaDelete<T>(opts: MetaRequestOptions): Promise<T>;
74
+ /**
75
+ * Standard Meta paging envelope. Every list endpoint wraps results in
76
+ * `{ data: [...], paging: { cursors, next } }`. Tools should propagate
77
+ * the cursor back to the agent so it can fetch the next page.
78
+ */
79
+ export interface MetaPaging<T> {
80
+ data: T[];
81
+ paging?: {
82
+ cursors?: {
83
+ before?: string;
84
+ after?: string;
85
+ };
86
+ next?: string;
87
+ previous?: string;
88
+ };
89
+ }
package/dist/http.js ADDED
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ /**
3
+ * Thin HTTP layer for the Meta Graph API. All four connectors in this
4
+ * package (Instagram, Messenger, Facebook Pages, WhatsApp) talk to
5
+ * `graph.facebook.com/v{version}` with the same auth + retry policy,
6
+ * so the transport lives here.
7
+ *
8
+ * Endpoints are **fixed** — unlike Shopify's per-shop hostnames, every
9
+ * Meta surface uses the same base URL and routes via the path
10
+ * (`/{ig-user-id}/conversations`, `/{page-id}/feed`, etc.). That's why
11
+ * we don't expose `resolveEndpoints` in the connector definitions.
12
+ *
13
+ * Auth: `Authorization: Bearer <accessToken>`. Meta also accepts
14
+ * `?access_token=` as a query param, but the header form keeps tokens
15
+ * out of access logs and matches what the official SDKs send.
16
+ *
17
+ * Rate limits (the painful ones to remember):
18
+ * - App-level: 200 calls/hr/user, tracked in `X-App-Usage` header.
19
+ * - Business use case: separate buckets for Messaging, Pages,
20
+ * Instagram, each in `X-Business-Use-Case-Usage` / `X-Ad-Account-Usage`.
21
+ * - When ≥95% of any bucket: 4xx with code 4 / 17 / 32 / 613.
22
+ * - Hard throttle: HTTP 429 (rare; usually they 4xx with the codes
23
+ * above first).
24
+ *
25
+ * Retry policy:
26
+ * - 429: up to 2 retries, honoring `Retry-After` (seconds).
27
+ * - 5xx: 1 retry with ~500ms jitter.
28
+ * - 4xx with rate-limit code (4, 17, 32, 613): same backoff as 429
29
+ * because Meta's "you're at 95%" responses come back as 4xx.
30
+ * - 401/403: NEVER retried — the token is invalid/expired/missing
31
+ * scope. Caller should surface a "Reconnect Meta" UX.
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.MetaApiError = exports.META_GRAPH_BASE = exports.META_GRAPH_VERSION = void 0;
35
+ exports.metaGet = metaGet;
36
+ exports.metaPost = metaPost;
37
+ exports.metaDelete = metaDelete;
38
+ exports.META_GRAPH_VERSION = 'v21.0';
39
+ exports.META_GRAPH_BASE = 'https://graph.facebook.com';
40
+ class MetaApiError extends Error {
41
+ constructor(status, statusText, body,
42
+ /** Meta error code from the response body. The most useful
43
+ * routable values:
44
+ * - 4: app-level rate limit (back off)
45
+ * - 17: user-level rate limit
46
+ * - 32, 613: page/business use-case rate limit
47
+ * - 190: access token expired/invalid → re-auth
48
+ * - 200: missing permission → re-auth with broader scopes
49
+ * - 100: invalid parameter (bug in the tool input) */
50
+ metaCode,
51
+ /** Sub-error type for permission/scope failures. */
52
+ metaSubcode) {
53
+ super(`Meta API ${status} ${statusText}${metaCode !== undefined ? ` (code=${metaCode}${metaSubcode !== undefined ? `/sub=${metaSubcode}` : ''})` : ''}: ${body.slice(0, 500)}`);
54
+ this.status = status;
55
+ this.statusText = statusText;
56
+ this.body = body;
57
+ this.metaCode = metaCode;
58
+ this.metaSubcode = metaSubcode;
59
+ }
60
+ }
61
+ exports.MetaApiError = MetaApiError;
62
+ const MAX_429_RETRIES = 2;
63
+ const MAX_5XX_RETRIES = 1;
64
+ /** Meta error codes that mean "back off and retry" — surfaced as 4xx
65
+ * with a normal JSON body rather than 429. */
66
+ const RATE_LIMIT_CODES = new Set([4, 17, 32, 613]);
67
+ async function send(opts) {
68
+ 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}`}`);
70
+ for (const [k, v] of Object.entries(opts.query ?? {})) {
71
+ if (v === undefined || v === null)
72
+ continue;
73
+ url.searchParams.set(k, String(v));
74
+ }
75
+ const headers = {
76
+ authorization: `Bearer ${opts.accessToken}`,
77
+ accept: 'application/json',
78
+ };
79
+ let body;
80
+ if (opts.body !== undefined && opts.method !== 'GET') {
81
+ headers['content-type'] = 'application/json';
82
+ body = JSON.stringify(opts.body);
83
+ }
84
+ let attempt429 = 0;
85
+ let attempt5xx = 0;
86
+ while (true) {
87
+ const res = await fetch(url.toString(), {
88
+ method: opts.method,
89
+ headers,
90
+ body,
91
+ });
92
+ // HTTP 429 — honor Retry-After.
93
+ if (res.status === 429 && attempt429 < MAX_429_RETRIES) {
94
+ const retryAfter = Number(res.headers.get('retry-after') ?? '2');
95
+ const delayMs = (Number.isFinite(retryAfter) ? retryAfter : 2) * 1000;
96
+ await sleep(delayMs);
97
+ attempt429++;
98
+ continue;
99
+ }
100
+ // 4xx with a rate-limit code in the body. We need to peek at the
101
+ // body to know — peek non-destructively via clone() so the caller's
102
+ // error path still has access to the original Response.
103
+ if (res.status >= 400 && res.status < 500 && attempt429 < MAX_429_RETRIES) {
104
+ const peeked = await res.clone().text().catch(() => '');
105
+ const code = parseMetaCode(peeked);
106
+ if (code !== undefined && RATE_LIMIT_CODES.has(code)) {
107
+ await sleep(2000);
108
+ attempt429++;
109
+ continue;
110
+ }
111
+ }
112
+ if (res.status >= 500 && res.status < 600 && attempt5xx < MAX_5XX_RETRIES) {
113
+ await sleep(400 + Math.floor(Math.random() * 200));
114
+ attempt5xx++;
115
+ continue;
116
+ }
117
+ return res;
118
+ }
119
+ }
120
+ function parseMetaCode(body) {
121
+ try {
122
+ const parsed = JSON.parse(body);
123
+ return parsed.error?.code;
124
+ }
125
+ catch {
126
+ return undefined;
127
+ }
128
+ }
129
+ function sleep(ms) {
130
+ return new Promise((r) => setTimeout(r, ms));
131
+ }
132
+ async function unwrap(res) {
133
+ if (!res.ok) {
134
+ const body = await res.text().catch(() => '');
135
+ let metaCode;
136
+ let metaSubcode;
137
+ try {
138
+ const parsed = JSON.parse(body);
139
+ metaCode = parsed.error?.code;
140
+ metaSubcode = parsed.error?.error_subcode;
141
+ }
142
+ catch {
143
+ /* not JSON — body stays as-is for the error message */
144
+ }
145
+ throw new MetaApiError(res.status, res.statusText, body, metaCode, metaSubcode);
146
+ }
147
+ return (await res.json());
148
+ }
149
+ async function metaGet(opts) {
150
+ return unwrap(await send({ ...opts, method: 'GET' }));
151
+ }
152
+ async function metaPost(opts) {
153
+ return unwrap(await send({ ...opts, method: 'POST' }));
154
+ }
155
+ async function metaDelete(opts) {
156
+ return unwrap(await send({ ...opts, method: 'DELETE' }));
157
+ }
@@ -0,0 +1,9 @@
1
+ export { metaInstagramConnector, metaMessengerConnector, metaFacebookPagesConnector, metaWhatsAppConnector, type MetaConnectorOptions, } from './connector';
2
+ export { exchangeForLongLivedUserToken, listManagedPages, getManagedPage, listWhatsAppBusinessAccounts, MetaPageSelectionError, } from './page';
3
+ export { igListThreadsTool, igListMessagesTool, igSendMessageTool, igListMediaTool, igListCommentsTool, igReplyCommentTool, } from './tools/instagram';
4
+ export { messengerListThreadsTool, messengerListMessagesTool, messengerSendMessageTool, messengerMarkSeenTool, } from './tools/messenger';
5
+ export { fbListPostsTool, fbGetPostTool, fbCreatePostTool, fbListCommentsTool, fbReplyCommentTool, } from './tools/facebook-pages';
6
+ export { waSendTextTool, waSendTemplateTool, } from './tools/whatsapp';
7
+ export type { LongLivedUserTokenResponse, MetaManagedPage, ListManagedPagesResponse, MetaWhatsAppPhoneNumber, MetaWhatsAppBusinessAccount, } from './page';
8
+ export { metaGet, metaPost, metaDelete, MetaApiError, META_GRAPH_VERSION, META_GRAPH_BASE, } from './http';
9
+ export type { MetaRequestOptions, MetaMutateOptions, MetaPaging, } from './http';
package/dist/index.js ADDED
@@ -0,0 +1,71 @@
1
+ "use strict";
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;
4
+ var connector_1 = require("./connector");
5
+ Object.defineProperty(exports, "metaInstagramConnector", { enumerable: true, get: function () { return connector_1.metaInstagramConnector; } });
6
+ Object.defineProperty(exports, "metaMessengerConnector", { enumerable: true, get: function () { return connector_1.metaMessengerConnector; } });
7
+ Object.defineProperty(exports, "metaFacebookPagesConnector", { enumerable: true, get: function () { return connector_1.metaFacebookPagesConnector; } });
8
+ Object.defineProperty(exports, "metaWhatsAppConnector", { enumerable: true, get: function () { return connector_1.metaWhatsAppConnector; } });
9
+ var page_1 = require("./page");
10
+ Object.defineProperty(exports, "exchangeForLongLivedUserToken", { enumerable: true, get: function () { return page_1.exchangeForLongLivedUserToken; } });
11
+ Object.defineProperty(exports, "listManagedPages", { enumerable: true, get: function () { return page_1.listManagedPages; } });
12
+ Object.defineProperty(exports, "getManagedPage", { enumerable: true, get: function () { return page_1.getManagedPage; } });
13
+ Object.defineProperty(exports, "listWhatsAppBusinessAccounts", { enumerable: true, get: function () { return page_1.listWhatsAppBusinessAccounts; } });
14
+ Object.defineProperty(exports, "MetaPageSelectionError", { enumerable: true, get: function () { return page_1.MetaPageSelectionError; } });
15
+ var instagram_1 = require("./tools/instagram");
16
+ Object.defineProperty(exports, "igListThreadsTool", { enumerable: true, get: function () { return instagram_1.igListThreadsTool; } });
17
+ Object.defineProperty(exports, "igListMessagesTool", { enumerable: true, get: function () { return instagram_1.igListMessagesTool; } });
18
+ Object.defineProperty(exports, "igSendMessageTool", { enumerable: true, get: function () { return instagram_1.igSendMessageTool; } });
19
+ Object.defineProperty(exports, "igListMediaTool", { enumerable: true, get: function () { return instagram_1.igListMediaTool; } });
20
+ Object.defineProperty(exports, "igListCommentsTool", { enumerable: true, get: function () { return instagram_1.igListCommentsTool; } });
21
+ Object.defineProperty(exports, "igReplyCommentTool", { enumerable: true, get: function () { return instagram_1.igReplyCommentTool; } });
22
+ var messenger_1 = require("./tools/messenger");
23
+ Object.defineProperty(exports, "messengerListThreadsTool", { enumerable: true, get: function () { return messenger_1.messengerListThreadsTool; } });
24
+ Object.defineProperty(exports, "messengerListMessagesTool", { enumerable: true, get: function () { return messenger_1.messengerListMessagesTool; } });
25
+ Object.defineProperty(exports, "messengerSendMessageTool", { enumerable: true, get: function () { return messenger_1.messengerSendMessageTool; } });
26
+ Object.defineProperty(exports, "messengerMarkSeenTool", { enumerable: true, get: function () { return messenger_1.messengerMarkSeenTool; } });
27
+ var facebook_pages_1 = require("./tools/facebook-pages");
28
+ Object.defineProperty(exports, "fbListPostsTool", { enumerable: true, get: function () { return facebook_pages_1.fbListPostsTool; } });
29
+ Object.defineProperty(exports, "fbGetPostTool", { enumerable: true, get: function () { return facebook_pages_1.fbGetPostTool; } });
30
+ Object.defineProperty(exports, "fbCreatePostTool", { enumerable: true, get: function () { return facebook_pages_1.fbCreatePostTool; } });
31
+ Object.defineProperty(exports, "fbListCommentsTool", { enumerable: true, get: function () { return facebook_pages_1.fbListCommentsTool; } });
32
+ Object.defineProperty(exports, "fbReplyCommentTool", { enumerable: true, get: function () { return facebook_pages_1.fbReplyCommentTool; } });
33
+ var whatsapp_1 = require("./tools/whatsapp");
34
+ Object.defineProperty(exports, "waSendTextTool", { enumerable: true, get: function () { return whatsapp_1.waSendTextTool; } });
35
+ Object.defineProperty(exports, "waSendTemplateTool", { enumerable: true, get: function () { return whatsapp_1.waSendTemplateTool; } });
36
+ // Public API of @agentforge-io/connectors-meta.
37
+ //
38
+ // Meta ships as four independent connectors — one per product surface.
39
+ // They share a single Meta App OAuth client (same clientId/clientSecret
40
+ // across all four) but each one only requests the scopes its tools
41
+ // need, lives in its own `af_connector_auths` row, and shows up as a
42
+ // separate card in the Directory.
43
+ //
44
+ // import {
45
+ // metaInstagramConnector,
46
+ // metaMessengerConnector,
47
+ // metaFacebookPagesConnector,
48
+ // metaWhatsAppConnector,
49
+ // } from '@agentforge-io/connectors-meta';
50
+ //
51
+ // const creds = { clientId: env.META_APP_ID, clientSecret: env.META_APP_SECRET };
52
+ // registry.register(metaInstagramConnector(creds));
53
+ // registry.register(metaMessengerConnector(creds));
54
+ // registry.register(metaFacebookPagesConnector(creds));
55
+ // registry.register(metaWhatsAppConnector(creds));
56
+ //
57
+ // The four connectors are coming online incrementally:
58
+ // Step 1 (DONE) — package skeleton + shared HTTP layer
59
+ // Step 2 (pending) — page-token exchange + connector definitions
60
+ // Step 3 (pending) — Instagram tools (DMs + media + comments)
61
+ // Step 4 (pending) — Messenger tools
62
+ // Step 5 (pending) — Facebook Pages tools (posts + comments)
63
+ // Step 6 (pending) — WhatsApp Cloud outbound tools
64
+ // Step 7 (pending) — Platform wiring (secrets binding + UI)
65
+ var http_1 = require("./http");
66
+ Object.defineProperty(exports, "metaGet", { enumerable: true, get: function () { return http_1.metaGet; } });
67
+ Object.defineProperty(exports, "metaPost", { enumerable: true, get: function () { return http_1.metaPost; } });
68
+ Object.defineProperty(exports, "metaDelete", { enumerable: true, get: function () { return http_1.metaDelete; } });
69
+ Object.defineProperty(exports, "MetaApiError", { enumerable: true, get: function () { return http_1.MetaApiError; } });
70
+ Object.defineProperty(exports, "META_GRAPH_VERSION", { enumerable: true, get: function () { return http_1.META_GRAPH_VERSION; } });
71
+ Object.defineProperty(exports, "META_GRAPH_BASE", { enumerable: true, get: function () { return http_1.META_GRAPH_BASE; } });