feat: Facebook Page integration

Four tools: facebook_get_page, facebook_get_posts, facebook_create_post
(text + optional link), facebook_create_photo_post (image URL + caption).

Uses Graph API v19.0 with Page access token. Credentials stored per-customer
in Redis under creds:{id}:facebook with pageId alongside the access token.
Env-var fallback: FACEBOOK_{ACCOUNT}_ACCESS_TOKEN + FACEBOOK_{ACCOUNT}_PAGE_ID.

Wired into Platform type, validPlatforms, /api/connections, manifest OpenAPI
spec, and manifest tool registry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Garfield
2026-05-08 12:47:20 -04:00
parent 6c7e56769e
commit ffb67560b9
5 changed files with 421 additions and 3 deletions

160
src/clients/facebook.ts Normal file
View File

@@ -0,0 +1,160 @@
import type { Customer } from '../billing/middleware.js';
import type { OAuthCredentials } from '../multitenancy/credential-store.js';
import { createToolAudit } from '../multitenancy/audit-log.js';
const FACEBOOK_API_BASE = 'https://graph.facebook.com/v19.0';
interface FacebookCredentials extends OAuthCredentials {
pageId: string;
}
function getEnvToken(account: string): string {
return process.env[`FACEBOOK_${account.toUpperCase()}_ACCESS_TOKEN`] ?? '';
}
function getEnvPageId(account: string): string {
return process.env[`FACEBOOK_${account.toUpperCase()}_PAGE_ID`] ?? '';
}
async function resolveCreds(
args: { account?: string },
customer?: Customer
): Promise<{ accessToken: string; pageId: string }> {
if (customer) {
const creds = await customer.getCredential<FacebookCredentials>('facebook');
if (!creds) throw new Error('Facebook not connected for this account');
return { accessToken: creds.accessToken, pageId: creds.pageId };
}
const account = args.account ?? 'default';
const accessToken = getEnvToken(account);
const pageId = getEnvPageId(account);
if (!accessToken || !pageId) {
throw new Error('Missing Facebook credentials. Set FACEBOOK_{ACCOUNT}_ACCESS_TOKEN and FACEBOOK_{ACCOUNT}_PAGE_ID');
}
return { accessToken, pageId };
}
async function fbRequest(
endpoint: string,
accessToken: string,
method: 'GET' | 'POST' = 'GET',
body?: Record<string, unknown>
) {
const url = new URL(`${FACEBOOK_API_BASE}${endpoint}`);
if (method === 'GET') url.searchParams.set('access_token', accessToken);
const res = await fetch(url.toString(), {
method,
headers: { 'Content-Type': 'application/json' },
body: method === 'POST' ? JSON.stringify({ ...body, access_token: accessToken }) : undefined,
signal: AbortSignal.timeout(15000),
});
if (!res.ok) {
const error = await res.text();
throw new Error(`Facebook API error (${res.status}): ${error}`);
}
return res.json();
}
export async function getPage(
args: { account?: string },
customer?: Customer
): Promise<{
id: string;
name: string;
category: string;
about?: string;
fan_count: number;
followers_count: number;
link?: string;
}> {
const { accessToken, pageId } = await resolveCreds(args, customer);
const data = await fbRequest(
`/${pageId}?fields=id,name,category,about,fan_count,followers_count,link`,
accessToken
);
return {
id: data.id ?? '',
name: data.name ?? '',
category: data.category ?? '',
about: data.about,
fan_count: data.fan_count ?? 0,
followers_count: data.followers_count ?? 0,
link: data.link,
};
}
export async function getPosts(
args: { limit?: number; account?: string },
customer?: Customer
): Promise<Array<{
id: string;
message?: string;
story?: string;
created_time: string;
permalink_url?: string;
}>> {
const { accessToken, pageId } = await resolveCreds(args, customer);
const limit = args.limit ?? 10;
const data = await fbRequest(
`/${pageId}/feed?fields=id,message,story,created_time,permalink_url&limit=${limit}`,
accessToken
);
return (data.data ?? []).map((p: Record<string, unknown>) => ({
id: String(p.id ?? ''),
message: p.message as string | undefined,
story: p.story as string | undefined,
created_time: String(p.created_time ?? ''),
permalink_url: p.permalink_url as string | undefined,
}));
}
export async function createPost(
args: { message: string; link?: string; account?: string },
customer?: Customer
): Promise<{ success: boolean; post_id: string }> {
const audit = customer ? createToolAudit(customer.id, 'facebook:createPost') : null;
const auditArgs = { message: args.message.slice(0, 80) };
const { accessToken, pageId } = await resolveCreds(args, customer);
try {
const body: Record<string, unknown> = { message: args.message };
if (args.link) body.link = args.link;
const data = await fbRequest(`/${pageId}/feed`, accessToken, 'POST', body);
const result = { success: true, post_id: String(data.id ?? '') };
if (audit) await audit.success(auditArgs);
return result;
} catch (err) {
if (audit) await audit.error(auditArgs, String(err));
throw err;
}
}
export async function createPhotoPost(
args: { image_url: string; caption?: string; account?: string },
customer?: Customer
): Promise<{ success: boolean; post_id: string; photo_id: string }> {
const audit = customer ? createToolAudit(customer.id, 'facebook:createPhotoPost') : null;
const auditArgs = { image_url: args.image_url };
const { accessToken, pageId } = await resolveCreds(args, customer);
try {
const data = await fbRequest(`/${pageId}/photos`, accessToken, 'POST', {
url: args.image_url,
caption: args.caption ?? '',
});
const result = {
success: true,
post_id: String(data.post_id ?? data.id ?? ''),
photo_id: String(data.id ?? ''),
};
if (audit) await audit.success(auditArgs);
return result;
} catch (err) {
if (audit) await audit.error(auditArgs, String(err));
throw err;
}
}