@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/page.d.ts ADDED
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Page-token + WhatsApp business helpers.
3
+ *
4
+ * Meta's OAuth flow gives the connector a USER access token. For every
5
+ * surface except `metaWhatsAppConnector`, the actual API calls use a
6
+ * PAGE access token (or an IG-business-account token derived from one)
7
+ * because user tokens can't read Page-owned conversations, posts, or
8
+ * Instagram business inboxes.
9
+ *
10
+ * Why this matters for the connector:
11
+ * - The OAuth callback persists a user token in `af_connector_auths`.
12
+ * - Before the tools can run, we exchange that user token for a list
13
+ * of page tokens (`GET /me/accounts`) and the operator picks one in
14
+ * the connect modal.
15
+ * - We persist the picked page id + token alongside the user token so
16
+ * subsequent API calls can grab it from `ctx.metadata.pageId` /
17
+ * `ctx.getAccessToken()` without a roundtrip every turn.
18
+ *
19
+ * Why one helper instead of one per surface:
20
+ * - The `/me/accounts` payload is the same for Instagram, Messenger,
21
+ * and FB Pages — every Page object includes its `instagram_business_account`
22
+ * edge when the IG account is linked. So one fetch covers all three.
23
+ * - Three of the four connectors share the same Page-selector UX in
24
+ * the dashboard (analogous to Shopify's shop selector); only
25
+ * WhatsApp has its own selector (phone number id, not Page id).
26
+ *
27
+ * Long-lived tokens:
28
+ * - The user token returned by the OAuth callback is short-lived
29
+ * (~1h). Before calling `/me/accounts` we MUST upgrade it via
30
+ * `GET /oauth/access_token?grant_type=fb_exchange_token` — the
31
+ * long-lived form lasts ~60 days.
32
+ * - Page tokens derived from a long-lived user token are themselves
33
+ * **non-expiring** (as long as the user keeps a valid session with
34
+ * Facebook). That's the property we want — no refresh dance ever.
35
+ */
36
+ export declare class MetaPageSelectionError extends Error {
37
+ readonly raw: string;
38
+ constructor(raw: string, message: string);
39
+ }
40
+ /**
41
+ * Exchange a short-lived user access token for a long-lived one.
42
+ * Meta returns short-lived tokens (~1h) from the OAuth callback even
43
+ * when we asked for `offline` access — you MUST call this helper before
44
+ * deriving page tokens, or the page tokens inherit the short TTL.
45
+ *
46
+ * Endpoint:
47
+ * GET /oauth/access_token?grant_type=fb_exchange_token
48
+ * &client_id=...&client_secret=...&fb_exchange_token=<short-lived>
49
+ *
50
+ * Returns `{ access_token, token_type, expires_in }`. The
51
+ * `expires_in` is in seconds; for production we treat any value above
52
+ * 30 days as "long-lived enough" and don't pre-emptively refresh.
53
+ */
54
+ export interface LongLivedUserTokenResponse {
55
+ access_token: string;
56
+ token_type: string;
57
+ expires_in?: number;
58
+ }
59
+ export declare function exchangeForLongLivedUserToken(opts: {
60
+ shortLivedUserToken: string;
61
+ clientId: string;
62
+ clientSecret: string;
63
+ }): Promise<LongLivedUserTokenResponse>;
64
+ /**
65
+ * One row from `GET /me/accounts`. Includes the page access token and,
66
+ * when the IG account is linked, the IG business account id used by the
67
+ * Instagram tools.
68
+ */
69
+ export interface MetaManagedPage {
70
+ id: string;
71
+ name: string;
72
+ access_token: string;
73
+ /** Page roles the calling user has on this Page. We don't filter on
74
+ * this — tools that need a specific role surface a 403 with a clear
75
+ * message at runtime. */
76
+ tasks?: string[];
77
+ category?: string;
78
+ category_list?: Array<{
79
+ id: string;
80
+ name: string;
81
+ }>;
82
+ /** Present when the Page has an Instagram Business Account linked.
83
+ * This is the id the Instagram Graph API expects, NOT the @-handle. */
84
+ instagram_business_account?: {
85
+ id: string;
86
+ username?: string;
87
+ };
88
+ }
89
+ export interface ListManagedPagesResponse {
90
+ data: MetaManagedPage[];
91
+ paging?: {
92
+ cursors?: {
93
+ before?: string;
94
+ after?: string;
95
+ };
96
+ next?: string;
97
+ };
98
+ }
99
+ /**
100
+ * Fetch the Pages the operator manages, each with its own page access
101
+ * token. Used at connect-time to populate the page-selector modal.
102
+ *
103
+ * We request a wide-ish field set in one shot to avoid an N+1 (the
104
+ * naive flow without `fields=` only returns id + name, then each Page
105
+ * needs a follow-up for the access_token).
106
+ *
107
+ * The token argument should be a LONG-LIVED user token; passing a
108
+ * short-lived one technically works but the derived page tokens inherit
109
+ * the short TTL.
110
+ */
111
+ export declare function listManagedPages(longLivedUserToken: string): Promise<MetaManagedPage[]>;
112
+ /**
113
+ * Fetch a specific managed Page by id. Used by the page selector after
114
+ * the operator clicks one to confirm the choice, and at tool-run time
115
+ * if we ever need to re-resolve fresh metadata (e.g. the operator
116
+ * unlinked the IG account between OAuth and first tool call).
117
+ */
118
+ export declare function getManagedPage(longLivedUserToken: string, pageId: string): Promise<MetaManagedPage | null>;
119
+ /**
120
+ * One WhatsApp Business Account (WABA) the operator can pick. Each WABA
121
+ * owns one or more phone numbers; tools target a phone number id
122
+ * (`/{phone-number-id}/messages`), not the WABA id directly.
123
+ */
124
+ export interface MetaWhatsAppPhoneNumber {
125
+ id: string;
126
+ display_phone_number: string;
127
+ verified_name?: string;
128
+ quality_rating?: 'GREEN' | 'YELLOW' | 'RED';
129
+ }
130
+ export interface MetaWhatsAppBusinessAccount {
131
+ id: string;
132
+ name?: string;
133
+ phone_numbers: MetaWhatsAppPhoneNumber[];
134
+ }
135
+ /**
136
+ * Fetch the WABAs the operator's Business Manager owns, each with the
137
+ * phone numbers attached. Used by the WhatsApp connector's
138
+ * phone-number selector.
139
+ *
140
+ * Requires `whatsapp_business_management` scope. The endpoint is
141
+ * `/me/businesses/{business-id}/owned_whatsapp_business_accounts` —
142
+ * but to avoid a Business-Manager listing roundtrip we use the
143
+ * `/me?fields=businesses{owned_whatsapp_business_accounts{...}}` shape.
144
+ */
145
+ export interface ListWabasResponse {
146
+ data: Array<{
147
+ id: string;
148
+ businesses?: {
149
+ data: Array<{
150
+ id: string;
151
+ owned_whatsapp_business_accounts?: {
152
+ data: Array<{
153
+ id: string;
154
+ name?: string;
155
+ phone_numbers?: {
156
+ data: MetaWhatsAppPhoneNumber[];
157
+ };
158
+ }>;
159
+ };
160
+ }>;
161
+ };
162
+ }>;
163
+ }
164
+ export declare function listWhatsAppBusinessAccounts(longLivedUserToken: string): Promise<MetaWhatsAppBusinessAccount[]>;
package/dist/page.js ADDED
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ /**
3
+ * Page-token + WhatsApp business helpers.
4
+ *
5
+ * Meta's OAuth flow gives the connector a USER access token. For every
6
+ * surface except `metaWhatsAppConnector`, the actual API calls use a
7
+ * PAGE access token (or an IG-business-account token derived from one)
8
+ * because user tokens can't read Page-owned conversations, posts, or
9
+ * Instagram business inboxes.
10
+ *
11
+ * Why this matters for the connector:
12
+ * - The OAuth callback persists a user token in `af_connector_auths`.
13
+ * - Before the tools can run, we exchange that user token for a list
14
+ * of page tokens (`GET /me/accounts`) and the operator picks one in
15
+ * the connect modal.
16
+ * - We persist the picked page id + token alongside the user token so
17
+ * subsequent API calls can grab it from `ctx.metadata.pageId` /
18
+ * `ctx.getAccessToken()` without a roundtrip every turn.
19
+ *
20
+ * Why one helper instead of one per surface:
21
+ * - The `/me/accounts` payload is the same for Instagram, Messenger,
22
+ * and FB Pages — every Page object includes its `instagram_business_account`
23
+ * edge when the IG account is linked. So one fetch covers all three.
24
+ * - Three of the four connectors share the same Page-selector UX in
25
+ * the dashboard (analogous to Shopify's shop selector); only
26
+ * WhatsApp has its own selector (phone number id, not Page id).
27
+ *
28
+ * Long-lived tokens:
29
+ * - The user token returned by the OAuth callback is short-lived
30
+ * (~1h). Before calling `/me/accounts` we MUST upgrade it via
31
+ * `GET /oauth/access_token?grant_type=fb_exchange_token` — the
32
+ * long-lived form lasts ~60 days.
33
+ * - Page tokens derived from a long-lived user token are themselves
34
+ * **non-expiring** (as long as the user keeps a valid session with
35
+ * Facebook). That's the property we want — no refresh dance ever.
36
+ */
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.MetaPageSelectionError = void 0;
39
+ exports.exchangeForLongLivedUserToken = exchangeForLongLivedUserToken;
40
+ exports.listManagedPages = listManagedPages;
41
+ exports.getManagedPage = getManagedPage;
42
+ exports.listWhatsAppBusinessAccounts = listWhatsAppBusinessAccounts;
43
+ const http_1 = require("./http");
44
+ class MetaPageSelectionError extends Error {
45
+ constructor(raw, message) {
46
+ super(message);
47
+ this.raw = raw;
48
+ this.name = 'MetaPageSelectionError';
49
+ }
50
+ }
51
+ exports.MetaPageSelectionError = MetaPageSelectionError;
52
+ async function exchangeForLongLivedUserToken(opts) {
53
+ // This endpoint is OAuth-flavored and lives at the same /oauth/...
54
+ // base — but uses query-string params and no Authorization header,
55
+ // so we bypass the standard `metaGet` (which always sends Bearer).
56
+ const url = new URL(`https://graph.facebook.com/${http_1.META_GRAPH_VERSION}/oauth/access_token`);
57
+ url.searchParams.set('grant_type', 'fb_exchange_token');
58
+ url.searchParams.set('client_id', opts.clientId);
59
+ url.searchParams.set('client_secret', opts.clientSecret);
60
+ url.searchParams.set('fb_exchange_token', opts.shortLivedUserToken);
61
+ const res = await fetch(url.toString(), { method: 'GET' });
62
+ if (!res.ok) {
63
+ throw new http_1.MetaApiError(res.status, res.statusText, await res.text().catch(() => ''));
64
+ }
65
+ return (await res.json());
66
+ }
67
+ /**
68
+ * Fetch the Pages the operator manages, each with its own page access
69
+ * token. Used at connect-time to populate the page-selector modal.
70
+ *
71
+ * We request a wide-ish field set in one shot to avoid an N+1 (the
72
+ * naive flow without `fields=` only returns id + name, then each Page
73
+ * needs a follow-up for the access_token).
74
+ *
75
+ * The token argument should be a LONG-LIVED user token; passing a
76
+ * short-lived one technically works but the derived page tokens inherit
77
+ * the short TTL.
78
+ */
79
+ async function listManagedPages(longLivedUserToken) {
80
+ const res = await (0, http_1.metaGet)({
81
+ accessToken: longLivedUserToken,
82
+ path: '/me/accounts',
83
+ query: {
84
+ fields: 'id,name,access_token,tasks,category,category_list,instagram_business_account{id,username}',
85
+ limit: 100,
86
+ },
87
+ });
88
+ return res.data ?? [];
89
+ }
90
+ /**
91
+ * Fetch a specific managed Page by id. Used by the page selector after
92
+ * the operator clicks one to confirm the choice, and at tool-run time
93
+ * if we ever need to re-resolve fresh metadata (e.g. the operator
94
+ * unlinked the IG account between OAuth and first tool call).
95
+ */
96
+ async function getManagedPage(longLivedUserToken, pageId) {
97
+ try {
98
+ return await (0, http_1.metaGet)({
99
+ accessToken: longLivedUserToken,
100
+ path: `/${encodeURIComponent(pageId)}`,
101
+ query: {
102
+ fields: 'id,name,access_token,tasks,category,category_list,instagram_business_account{id,username}',
103
+ },
104
+ });
105
+ }
106
+ catch (err) {
107
+ // 404 / 100-invalid-parameter → the Page no longer exists or the
108
+ // user lost admin access. Treat as "not found" rather than rethrow
109
+ // so the selector UX can show a clean message.
110
+ if (err instanceof http_1.MetaApiError &&
111
+ (err.status === 404 || err.metaCode === 100)) {
112
+ return null;
113
+ }
114
+ throw err;
115
+ }
116
+ }
117
+ async function listWhatsAppBusinessAccounts(longLivedUserToken) {
118
+ // `me` returns a single user object, but we ask for the nested
119
+ // edges in one query so we don't pay an N+1 over Business Manager
120
+ // → WABA → phone numbers.
121
+ const res = await (0, http_1.metaGet)({
122
+ accessToken: longLivedUserToken,
123
+ path: '/me',
124
+ query: {
125
+ fields: 'businesses{id,owned_whatsapp_business_accounts{id,name,phone_numbers{id,display_phone_number,verified_name,quality_rating}}}',
126
+ },
127
+ });
128
+ const wabas = [];
129
+ for (const business of res.businesses?.data ?? []) {
130
+ for (const waba of business.owned_whatsapp_business_accounts?.data ?? []) {
131
+ wabas.push({
132
+ id: waba.id,
133
+ name: waba.name,
134
+ phone_numbers: waba.phone_numbers?.data ?? [],
135
+ });
136
+ }
137
+ }
138
+ return wabas;
139
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Shared helpers every Meta tool needs:
3
+ *
4
+ * - `resolvePageId(ctx)` — pulls the Page id the operator picked at
5
+ * connect-time out of `ctx.metadata.pageId`. Every Page-bound
6
+ * surface (IG / Messenger / FB Pages) needs it.
7
+ *
8
+ * - `resolveIgUserId(ctx)` — pulls the Instagram Business Account id
9
+ * (NOT the @-handle) out of `ctx.metadata.igUserId`. Only Instagram
10
+ * tools call this; Messenger + FB Pages don't.
11
+ *
12
+ * - `resolveWaPhoneId(ctx)` — pulls the WhatsApp phone-number id out
13
+ * of `ctx.metadata.waPhoneNumberId`. WhatsApp tools route by phone
14
+ * number, not by WABA — that's an easy thing to get wrong.
15
+ *
16
+ * Failure mode: every helper throws a descriptive error if the
17
+ * metadata is missing, pointing the operator at the fix ("reconnect
18
+ * the connector from the Directory"). The platform's tool runner
19
+ * surfaces that string back to the agent's turn, which surfaces it to
20
+ * the operator.
21
+ */
22
+ import type { ConnectorToolContext } from '@agentforge-io/core';
23
+ export declare function resolvePageId(ctx: ConnectorToolContext): string;
24
+ export declare function resolveIgUserId(ctx: ConnectorToolContext): string;
25
+ export declare function resolveWaPhoneId(ctx: ConnectorToolContext): string;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolvePageId = resolvePageId;
4
+ exports.resolveIgUserId = resolveIgUserId;
5
+ exports.resolveWaPhoneId = resolveWaPhoneId;
6
+ const RECONNECT_HINT = 'This usually means the user connected before the per-Page selection ' +
7
+ 'was wired. Ask them to disconnect and reconnect the Meta surface ' +
8
+ 'from the Directory.';
9
+ function resolvePageId(ctx) {
10
+ const pageId = ctx.metadata?.pageId;
11
+ if (typeof pageId !== 'string' || !pageId) {
12
+ throw new Error(`Meta connector is missing its pageId metadata. ${RECONNECT_HINT}`);
13
+ }
14
+ return pageId;
15
+ }
16
+ function resolveIgUserId(ctx) {
17
+ const igUserId = ctx.metadata?.igUserId;
18
+ if (typeof igUserId !== 'string' || !igUserId) {
19
+ throw new Error('Instagram connector is missing its igUserId metadata. The selected ' +
20
+ 'Page may not have an Instagram Business Account linked. Ask the ' +
21
+ 'operator to link an IG Business account to the Page and reconnect.');
22
+ }
23
+ return igUserId;
24
+ }
25
+ function resolveWaPhoneId(ctx) {
26
+ const phoneId = ctx.metadata?.waPhoneNumberId;
27
+ if (typeof phoneId !== 'string' || !phoneId) {
28
+ throw new Error(`WhatsApp connector is missing its phone-number id metadata. ${RECONNECT_HINT}`);
29
+ }
30
+ return phoneId;
31
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Facebook Pages tools.
3
+ *
4
+ * Five tools for managing a Page's published content + engagement:
5
+ *
6
+ * 1. fb_list_posts — Page's own posts, newest first
7
+ * 2. fb_get_post — single post with engagement breakdown
8
+ * 3. fb_create_post — publish or schedule a text/link post
9
+ * 4. fb_list_comments — comments on a post
10
+ * 5. fb_reply_comment — reply nested under a comment
11
+ *
12
+ * Token model:
13
+ * - `ctx.getAccessToken()` returns the PAGE access token.
14
+ * - `ctx.metadata.pageId` is the Page id — every endpoint here
15
+ * routes through it.
16
+ *
17
+ * Why `/posts` (Page-authored) and not `/feed` (everything):
18
+ * - `/{page-id}/feed` returns posts the Page made PLUS posts by
19
+ * other people on the Page wall. Agents usually want "what did
20
+ * WE publish?" so `/posts` is the right default; if we ever need
21
+ * the wall, a future `fb_list_feed` can take the same shape.
22
+ *
23
+ * Scheduled posts:
24
+ * - Setting `published: false` + `scheduled_publish_time` (unix
25
+ * seconds) defers publication. Window: 10 minutes to 6 months
26
+ * from now (Meta hard limit; sending outside returns code 100).
27
+ *
28
+ * Media:
29
+ * - This step covers text + link posts only. Photo/video uploads
30
+ * require a separate /photos or /videos endpoint with multipart
31
+ * bodies, which deserves its own tool (`fb_create_photo_post`)
32
+ * and isn't part of the MVP. Posting an external image via the
33
+ * `link` field is supported — Meta scrapes the URL for OG tags
34
+ * and renders the link preview.
35
+ */
36
+ import type { ConnectorToolFactory } from '@agentforge-io/core';
37
+ export declare const fbListPostsTool: ConnectorToolFactory;
38
+ export declare const fbGetPostTool: ConnectorToolFactory;
39
+ export declare const fbCreatePostTool: ConnectorToolFactory;
40
+ export declare const fbListCommentsTool: ConnectorToolFactory;
41
+ export declare const fbReplyCommentTool: ConnectorToolFactory;