@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.
- package/dist/connector.d.ts +80 -0
- package/dist/connector.js +263 -0
- package/dist/http.d.ts +89 -0
- package/dist/http.js +157 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +71 -0
- package/dist/page.d.ts +164 -0
- package/dist/page.js +139 -0
- package/dist/tools/_shared.d.ts +25 -0
- package/dist/tools/_shared.js +31 -0
- package/dist/tools/facebook-pages.d.ts +41 -0
- package/dist/tools/facebook-pages.js +362 -0
- package/dist/tools/instagram.d.ts +42 -0
- package/dist/tools/instagram.js +389 -0
- package/dist/tools/messenger.d.ts +43 -0
- package/dist/tools/messenger.js +320 -0
- package/dist/tools/whatsapp.d.ts +39 -0
- package/dist/tools/whatsapp.js +242 -0
- package/package.json +24 -0
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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; } });
|