@hasna/connectors 1.3.36 → 1.3.38

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.
Files changed (56) hide show
  1. package/bin/index.js +167 -241
  2. package/bin/mcp.js +190 -267
  3. package/bin/serve.js +264 -341
  4. package/connectors/bluesky/README.md +34 -0
  5. package/connectors/bluesky/package.json +41 -0
  6. package/connectors/bluesky/src/api/client.test.ts +122 -0
  7. package/connectors/bluesky/src/api/client.ts +194 -0
  8. package/connectors/bluesky/src/api/index.ts +108 -0
  9. package/connectors/bluesky/src/index.ts +17 -0
  10. package/connectors/bluesky/src/types/index.ts +46 -0
  11. package/connectors/bluesky/tsconfig.json +16 -0
  12. package/connectors/x/src/api/media.ts +4 -3
  13. package/dashboard/dist/assets/index-CUbp3qRv.js +284 -0
  14. package/dashboard/dist/assets/index-ClBXkNbL.css +1 -0
  15. package/dashboard/dist/index.html +2 -2
  16. package/dist/.types/connectors/bluesky/src/api/client.d.ts +73 -0
  17. package/dist/.types/connectors/bluesky/src/api/index.d.ts +76 -0
  18. package/dist/.types/connectors/bluesky/src/index.d.ts +11 -0
  19. package/dist/.types/connectors/bluesky/src/types/index.d.ts +33 -0
  20. package/dist/.types/connectors/x/src/api/client.d.ts +96 -0
  21. package/dist/.types/connectors/x/src/api/index.d.ts +80 -0
  22. package/dist/.types/connectors/x/src/api/media.d.ts +54 -0
  23. package/dist/.types/connectors/x/src/api/oauth.d.ts +82 -0
  24. package/dist/.types/connectors/x/src/api/oauth1.d.ts +68 -0
  25. package/dist/.types/connectors/x/src/api/tweets.d.ts +175 -0
  26. package/dist/.types/connectors/x/src/api/users.d.ts +127 -0
  27. package/dist/.types/connectors/x/src/cli/index.d.ts +2 -0
  28. package/dist/.types/connectors/x/src/index.d.ts +6 -0
  29. package/dist/.types/connectors/x/src/types/index.d.ts +156 -0
  30. package/dist/.types/connectors/x/src/utils/config.d.ts +99 -0
  31. package/dist/.types/connectors/x/src/utils/output.d.ts +8 -0
  32. package/dist/.types/src/social/bluesky.d.ts +65 -0
  33. package/dist/.types/src/social/errors.d.ts +9 -0
  34. package/dist/.types/src/social/googlebusinessprofile.d.ts +40 -0
  35. package/dist/.types/src/social/http.d.ts +37 -0
  36. package/dist/.types/src/social/index.d.ts +16 -0
  37. package/dist/.types/src/social/linkedin.d.ts +29 -0
  38. package/dist/.types/src/social/mastodon.d.ts +32 -0
  39. package/dist/.types/src/social/pinterest.d.ts +31 -0
  40. package/dist/.types/src/social/reddit.d.ts +31 -0
  41. package/dist/.types/src/social/tiktok.d.ts +24 -0
  42. package/dist/.types/src/social/types.d.ts +79 -0
  43. package/dist/.types/src/social/util.d.ts +5 -0
  44. package/dist/.types/src/social/x.d.ts +82 -0
  45. package/dist/.types/src/social/youtube.d.ts +29 -0
  46. package/dist/cli/components/App.d.ts +1 -2
  47. package/dist/cli/components/CategorySelect.d.ts +1 -2
  48. package/dist/cli/components/ConnectorSelect.d.ts +1 -2
  49. package/dist/cli/components/Header.d.ts +1 -2
  50. package/dist/cli/components/InstallProgress.d.ts +1 -2
  51. package/dist/cli/components/SearchView.d.ts +1 -2
  52. package/dist/index.js +122 -109
  53. package/dist/social/index.js +2303 -0
  54. package/package.json +7 -3
  55. package/dashboard/dist/assets/index-DJjxFlTl.css +0 -1
  56. package/dashboard/dist/assets/index-DNPZ58_i.js +0 -284
@@ -0,0 +1,2303 @@
1
+ // @bun
2
+ var __require = import.meta.require;
3
+
4
+ // src/social/errors.ts
5
+ class ConnectorOperationNotSupported extends Error {
6
+ connector;
7
+ operation;
8
+ constructor(connector, operation) {
9
+ super(`Connector "${connector}" does not support operation "${operation}"`);
10
+ this.name = "ConnectorOperationNotSupported";
11
+ this.connector = connector;
12
+ this.operation = operation;
13
+ }
14
+ }
15
+
16
+ // connectors/x/src/types/index.ts
17
+ class XApiError extends Error {
18
+ statusCode;
19
+ errors;
20
+ constructor(message, statusCode, errors) {
21
+ super(message);
22
+ this.name = "XApiError";
23
+ this.statusCode = statusCode;
24
+ this.errors = errors;
25
+ }
26
+ }
27
+
28
+ // connectors/x/src/api/oauth.ts
29
+ var TOKEN_URL = "https://api.twitter.com/2/oauth2/token";
30
+ async function tokenRequest(url, body, config) {
31
+ body.append("client_id", config.clientId);
32
+ if (config.clientSecret) {
33
+ body.append("client_secret", config.clientSecret);
34
+ }
35
+ return fetch(url, {
36
+ method: "POST",
37
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
38
+ body: body.toString()
39
+ });
40
+ }
41
+ async function refreshAccessToken(config, refreshToken) {
42
+ const body = new URLSearchParams({
43
+ grant_type: "refresh_token",
44
+ refresh_token: refreshToken
45
+ });
46
+ const response = await tokenRequest(TOKEN_URL, body, config);
47
+ if (!response.ok) {
48
+ const errorText = await response.text();
49
+ throw new Error(`Token refresh failed: ${errorText}`);
50
+ }
51
+ const data = await response.json();
52
+ return {
53
+ accessToken: data.access_token,
54
+ refreshToken: data.refresh_token || refreshToken,
55
+ expiresIn: data.expires_in,
56
+ expiresAt: Date.now() + data.expires_in * 1000,
57
+ scope: data.scope,
58
+ tokenType: data.token_type
59
+ };
60
+ }
61
+
62
+ // connectors/x/src/api/oauth1.ts
63
+ import { createHmac, randomBytes } from "crypto";
64
+ function generateNonce() {
65
+ return randomBytes(16).toString("hex");
66
+ }
67
+ function generateTimestamp() {
68
+ return Math.floor(Date.now() / 1000).toString();
69
+ }
70
+ function percentEncode(str) {
71
+ return encodeURIComponent(str).replace(/!/g, "%21").replace(/\*/g, "%2A").replace(/'/g, "%27").replace(/\(/g, "%28").replace(/\)/g, "%29");
72
+ }
73
+ function createSignatureBaseString(method, url, params) {
74
+ const sortedParams = Object.keys(params).sort().map((key) => `${percentEncode(key)}=${percentEncode(params[key])}`).join("&");
75
+ const baseUrl = url.split("?")[0];
76
+ return `${method.toUpperCase()}&${percentEncode(baseUrl)}&${percentEncode(sortedParams)}`;
77
+ }
78
+ function createSignature(baseString, consumerSecret, tokenSecret = "") {
79
+ const signingKey = `${percentEncode(consumerSecret)}&${percentEncode(tokenSecret)}`;
80
+ return createHmac("sha1", signingKey).update(baseString).digest("base64");
81
+ }
82
+ function buildOAuth1Header(config, method, url, additionalParams = {}) {
83
+ const oauthParams = {
84
+ oauth_consumer_key: config.consumerKey,
85
+ oauth_nonce: generateNonce(),
86
+ oauth_signature_method: "HMAC-SHA1",
87
+ oauth_timestamp: generateTimestamp(),
88
+ oauth_version: "1.0"
89
+ };
90
+ if (config.accessToken) {
91
+ oauthParams["oauth_token"] = config.accessToken;
92
+ }
93
+ const allParams = { ...oauthParams, ...additionalParams };
94
+ const baseString = createSignatureBaseString(method, url, allParams);
95
+ const signature = createSignature(baseString, config.consumerSecret, config.accessTokenSecret);
96
+ oauthParams["oauth_signature"] = signature;
97
+ const headerParams = Object.keys(oauthParams).sort().map((key) => `${percentEncode(key)}="${percentEncode(oauthParams[key])}"`).join(", ");
98
+ return `OAuth ${headerParams}`;
99
+ }
100
+ async function oauth1Request(config, method, url, body, additionalHeaders) {
101
+ let bodyParams = {};
102
+ let requestBody;
103
+ const headers = { ...additionalHeaders };
104
+ if (body) {
105
+ if (body instanceof FormData) {
106
+ requestBody = body;
107
+ } else if (typeof body === "string") {
108
+ requestBody = body;
109
+ } else {
110
+ bodyParams = body;
111
+ requestBody = new URLSearchParams(body).toString();
112
+ headers["Content-Type"] = "application/x-www-form-urlencoded";
113
+ }
114
+ }
115
+ const authHeader = buildOAuth1Header(config, method, url, bodyParams);
116
+ headers["Authorization"] = authHeader;
117
+ const response = await fetch(url, {
118
+ method,
119
+ headers,
120
+ body: requestBody
121
+ });
122
+ if (!response.ok) {
123
+ const errorText = await response.text();
124
+ throw new Error(`OAuth 1.0a request failed: ${response.status} - ${errorText}`);
125
+ }
126
+ const contentType = response.headers.get("content-type") || "";
127
+ if (contentType.includes("application/json")) {
128
+ return await response.json();
129
+ }
130
+ return await response.text();
131
+ }
132
+
133
+ class OAuth1Client {
134
+ config;
135
+ constructor(config) {
136
+ if (!config.consumerKey || !config.consumerSecret) {
137
+ throw new Error("Consumer key and secret are required");
138
+ }
139
+ this.config = config;
140
+ }
141
+ hasUserTokens() {
142
+ return !!(this.config.accessToken && this.config.accessTokenSecret);
143
+ }
144
+ setTokens(oauthToken, oauthTokenSecret) {
145
+ this.config.accessToken = oauthToken;
146
+ this.config.accessTokenSecret = oauthTokenSecret;
147
+ }
148
+ async get(url, params) {
149
+ const urlWithParams = params ? `${url}?${new URLSearchParams(params).toString()}` : url;
150
+ return oauth1Request(this.config, "GET", urlWithParams);
151
+ }
152
+ async post(url, body, headers) {
153
+ return oauth1Request(this.config, "POST", url, body, headers);
154
+ }
155
+ buildAuthHeader(method, url, params) {
156
+ return buildOAuth1Header(this.config, method, url, params);
157
+ }
158
+ }
159
+
160
+ // connectors/x/src/api/client.ts
161
+ var DEFAULT_BASE_URL = "https://api.twitter.com";
162
+
163
+ class XClient {
164
+ apiKey;
165
+ apiSecret;
166
+ bearerToken;
167
+ baseUrl;
168
+ accessToken;
169
+ refreshToken;
170
+ tokenExpiresAt;
171
+ clientId;
172
+ clientSecret;
173
+ oauth1Client;
174
+ onTokenRefresh;
175
+ constructor(config) {
176
+ if (!config.apiKey || !config.apiSecret) {
177
+ throw new Error("API key and API secret are required");
178
+ }
179
+ this.apiKey = config.apiKey;
180
+ this.apiSecret = config.apiSecret;
181
+ this.bearerToken = config.bearerToken;
182
+ this.baseUrl = config.baseUrl || DEFAULT_BASE_URL;
183
+ this.accessToken = config.accessToken;
184
+ this.refreshToken = config.refreshToken;
185
+ this.tokenExpiresAt = config.tokenExpiresAt;
186
+ this.clientId = config.clientId;
187
+ this.clientSecret = config.clientSecret;
188
+ if (config.oauth1AccessToken && config.oauth1AccessTokenSecret) {
189
+ this.oauth1Client = new OAuth1Client({
190
+ consumerKey: config.apiKey,
191
+ consumerSecret: config.apiSecret,
192
+ accessToken: config.oauth1AccessToken,
193
+ accessTokenSecret: config.oauth1AccessTokenSecret
194
+ });
195
+ }
196
+ }
197
+ setTokenRefreshCallback(callback) {
198
+ this.onTokenRefresh = callback;
199
+ }
200
+ async getAppBearerToken() {
201
+ if (this.bearerToken) {
202
+ return this.bearerToken;
203
+ }
204
+ const credentials = Buffer.from(`${this.apiKey}:${this.apiSecret}`).toString("base64");
205
+ const response = await fetch(`${this.baseUrl}/oauth2/token`, {
206
+ method: "POST",
207
+ headers: {
208
+ Authorization: `Basic ${credentials}`,
209
+ "Content-Type": "application/x-www-form-urlencoded"
210
+ },
211
+ body: "grant_type=client_credentials"
212
+ });
213
+ if (!response.ok) {
214
+ const errorText = await response.text();
215
+ throw new XApiError(`Failed to get bearer token: ${errorText}`, response.status);
216
+ }
217
+ const data = await response.json();
218
+ this.bearerToken = data.access_token;
219
+ return this.bearerToken;
220
+ }
221
+ isTokenExpired() {
222
+ if (!this.tokenExpiresAt)
223
+ return true;
224
+ return Date.now() > this.tokenExpiresAt - 5 * 60 * 1000;
225
+ }
226
+ async refreshUserToken() {
227
+ if (!this.refreshToken || !this.clientId) {
228
+ throw new Error("Cannot refresh token: missing refresh token or client ID");
229
+ }
230
+ const oauth2Config = {
231
+ clientId: this.clientId,
232
+ clientSecret: this.clientSecret,
233
+ redirectUri: "http://localhost:3000/callback"
234
+ };
235
+ const tokens = await refreshAccessToken(oauth2Config, this.refreshToken);
236
+ this.accessToken = tokens.accessToken;
237
+ this.refreshToken = tokens.refreshToken || this.refreshToken;
238
+ this.tokenExpiresAt = tokens.expiresAt;
239
+ if (this.onTokenRefresh) {
240
+ this.onTokenRefresh({
241
+ accessToken: tokens.accessToken,
242
+ refreshToken: tokens.refreshToken,
243
+ expiresAt: tokens.expiresAt
244
+ });
245
+ }
246
+ return this.accessToken;
247
+ }
248
+ async getAccessToken(preferUser = true) {
249
+ if (preferUser && this.accessToken) {
250
+ if (this.isTokenExpired() && this.refreshToken && this.clientId) {
251
+ const token2 = await this.refreshUserToken();
252
+ return { token: token2, isUserContext: true };
253
+ }
254
+ if (!this.isTokenExpired()) {
255
+ return { token: this.accessToken, isUserContext: true };
256
+ }
257
+ }
258
+ const token = await this.getAppBearerToken();
259
+ return { token, isUserContext: false };
260
+ }
261
+ getAuthMethod() {
262
+ if (this.accessToken && !this.isTokenExpired()) {
263
+ return "oauth2-user";
264
+ }
265
+ if (this.oauth1Client?.hasUserTokens()) {
266
+ return "oauth1-user";
267
+ }
268
+ return "app-only";
269
+ }
270
+ getAuthStatus() {
271
+ const method = this.getAuthMethod();
272
+ return {
273
+ method,
274
+ isAuthenticated: method !== "app-only",
275
+ expiresAt: this.tokenExpiresAt,
276
+ hasOAuth1: this.oauth1Client?.hasUserTokens()
277
+ };
278
+ }
279
+ hasUserContext() {
280
+ return !!(this.accessToken && !this.isTokenExpired());
281
+ }
282
+ hasOAuth1() {
283
+ return this.oauth1Client?.hasUserTokens() ?? false;
284
+ }
285
+ getOAuth1Client() {
286
+ return this.oauth1Client;
287
+ }
288
+ setUserTokens(tokens) {
289
+ this.accessToken = tokens.accessToken;
290
+ this.refreshToken = tokens.refreshToken;
291
+ this.tokenExpiresAt = tokens.expiresAt;
292
+ }
293
+ setOAuth1Tokens(oauthToken, oauthTokenSecret) {
294
+ this.oauth1Client = new OAuth1Client({
295
+ consumerKey: this.apiKey,
296
+ consumerSecret: this.apiSecret,
297
+ accessToken: oauthToken,
298
+ accessTokenSecret: oauthTokenSecret
299
+ });
300
+ }
301
+ buildUrl(path, params) {
302
+ const url = new URL(`${this.baseUrl}${path}`);
303
+ if (params) {
304
+ Object.entries(params).forEach(([key, value]) => {
305
+ if (value !== undefined && value !== null && value !== "") {
306
+ if (Array.isArray(value)) {
307
+ url.searchParams.append(key, value.join(","));
308
+ } else {
309
+ url.searchParams.append(key, String(value));
310
+ }
311
+ }
312
+ });
313
+ }
314
+ return url.toString();
315
+ }
316
+ async request(path, options = {}) {
317
+ const { method = "GET", params, body, headers = {}, authMethod } = options;
318
+ const preferUser = authMethod === "oauth2-user" || authMethod === "oauth1-user" || ["POST", "PUT", "PATCH", "DELETE"].includes(method);
319
+ const url = this.buildUrl(path, params);
320
+ const isWriteOp = ["POST", "PUT", "PATCH", "DELETE"].includes(method);
321
+ const useOAuth1 = authMethod === "oauth1-user" || isWriteOp && !this.accessToken && this.oauth1Client?.hasUserTokens();
322
+ let requestHeaders;
323
+ if (useOAuth1 && this.oauth1Client) {
324
+ const oauth1Header = this.oauth1Client.buildAuthHeader(method, url);
325
+ requestHeaders = {
326
+ Authorization: oauth1Header,
327
+ Accept: "application/json",
328
+ ...headers
329
+ };
330
+ } else {
331
+ const { token } = await this.getAccessToken(preferUser);
332
+ requestHeaders = {
333
+ Authorization: `Bearer ${token}`,
334
+ Accept: "application/json",
335
+ ...headers
336
+ };
337
+ }
338
+ if (body && ["POST", "PUT", "PATCH"].includes(method)) {
339
+ requestHeaders["Content-Type"] = "application/json";
340
+ }
341
+ const fetchOptions = {
342
+ method,
343
+ headers: requestHeaders
344
+ };
345
+ if (body && ["POST", "PUT", "PATCH"].includes(method)) {
346
+ fetchOptions.body = typeof body === "string" ? body : JSON.stringify(body);
347
+ }
348
+ const response = await fetch(url, fetchOptions);
349
+ if (response.status === 204) {
350
+ return {};
351
+ }
352
+ let data;
353
+ const contentType = response.headers.get("content-type") || "";
354
+ if (contentType.includes("application/json")) {
355
+ const text = await response.text();
356
+ if (text) {
357
+ try {
358
+ data = JSON.parse(text);
359
+ } catch {
360
+ data = text;
361
+ }
362
+ }
363
+ } else {
364
+ data = await response.text();
365
+ }
366
+ if (!response.ok) {
367
+ const errorMessage = typeof data === "object" && data !== null ? JSON.stringify(data) : String(data || response.statusText);
368
+ throw new XApiError(errorMessage, response.status);
369
+ }
370
+ return data;
371
+ }
372
+ async get(path, params) {
373
+ return this.request(path, { method: "GET", params });
374
+ }
375
+ async post(path, body, params) {
376
+ return this.request(path, { method: "POST", body, params });
377
+ }
378
+ async delete(path, params) {
379
+ return this.request(path, { method: "DELETE", params });
380
+ }
381
+ getApiKeyPreview() {
382
+ if (this.apiKey.length > 10) {
383
+ return `${this.apiKey.substring(0, 6)}...${this.apiKey.substring(this.apiKey.length - 4)}`;
384
+ }
385
+ return "***";
386
+ }
387
+ hasBearerToken() {
388
+ return !!this.bearerToken;
389
+ }
390
+ }
391
+
392
+ // connectors/x/src/api/tweets.ts
393
+ class TweetsApi {
394
+ client;
395
+ constructor(client) {
396
+ this.client = client;
397
+ }
398
+ async get(id, options) {
399
+ const params = {};
400
+ if (options?.tweetFields?.length) {
401
+ params["tweet.fields"] = options.tweetFields.join(",");
402
+ }
403
+ if (options?.userFields?.length) {
404
+ params["user.fields"] = options.userFields.join(",");
405
+ }
406
+ if (options?.expansions?.length) {
407
+ params["expansions"] = options.expansions.join(",");
408
+ }
409
+ if (options?.mediaFields?.length) {
410
+ params["media.fields"] = options.mediaFields.join(",");
411
+ }
412
+ return this.client.get(`/2/tweets/${id}`, params);
413
+ }
414
+ async getMany(ids, options) {
415
+ const params = {
416
+ ids: ids.join(",")
417
+ };
418
+ if (options?.tweetFields?.length) {
419
+ params["tweet.fields"] = options.tweetFields.join(",");
420
+ }
421
+ if (options?.userFields?.length) {
422
+ params["user.fields"] = options.userFields.join(",");
423
+ }
424
+ if (options?.expansions?.length) {
425
+ params["expansions"] = options.expansions.join(",");
426
+ }
427
+ if (options?.mediaFields?.length) {
428
+ params["media.fields"] = options.mediaFields.join(",");
429
+ }
430
+ return this.client.get("/2/tweets", params);
431
+ }
432
+ async searchRecent(options) {
433
+ const params = {
434
+ query: options.query,
435
+ max_results: options.maxResults || 10,
436
+ next_token: options.nextToken,
437
+ start_time: options.startTime,
438
+ end_time: options.endTime,
439
+ since_id: options.sinceId,
440
+ until_id: options.untilId,
441
+ sort_order: options.sortOrder,
442
+ "tweet.fields": "id,text,author_id,created_at,public_metrics,source,lang",
443
+ expansions: "author_id",
444
+ "user.fields": "id,name,username,profile_image_url"
445
+ };
446
+ return this.client.get("/2/tweets/search/recent", params);
447
+ }
448
+ async countRecent(query, options) {
449
+ const params = {
450
+ query,
451
+ start_time: options?.startTime,
452
+ end_time: options?.endTime,
453
+ granularity: options?.granularity || "hour"
454
+ };
455
+ return this.client.get("/2/tweets/counts/recent", params);
456
+ }
457
+ async getUserTimeline(userId, options) {
458
+ const exclude = [];
459
+ if (options?.excludeReplies)
460
+ exclude.push("replies");
461
+ if (options?.excludeRetweets)
462
+ exclude.push("retweets");
463
+ const params = {
464
+ max_results: options?.maxResults || 10,
465
+ pagination_token: options?.paginationToken,
466
+ start_time: options?.startTime,
467
+ end_time: options?.endTime,
468
+ since_id: options?.sinceId,
469
+ until_id: options?.untilId,
470
+ exclude: exclude.length ? exclude.join(",") : undefined,
471
+ "tweet.fields": "id,text,author_id,created_at,public_metrics,source,lang",
472
+ expansions: "author_id",
473
+ "user.fields": "id,name,username,profile_image_url"
474
+ };
475
+ return this.client.get(`/2/users/${userId}/tweets`, params);
476
+ }
477
+ async getUserMentions(userId, options) {
478
+ const params = {
479
+ max_results: options?.maxResults || 10,
480
+ pagination_token: options?.paginationToken,
481
+ start_time: options?.startTime,
482
+ end_time: options?.endTime,
483
+ since_id: options?.sinceId,
484
+ until_id: options?.untilId,
485
+ "tweet.fields": "id,text,author_id,created_at,public_metrics,source,lang",
486
+ expansions: "author_id",
487
+ "user.fields": "id,name,username,profile_image_url"
488
+ };
489
+ return this.client.get(`/2/users/${userId}/mentions`, params);
490
+ }
491
+ async getLikingUsers(tweetId, options) {
492
+ const params = {
493
+ max_results: options?.maxResults || 100,
494
+ pagination_token: options?.paginationToken,
495
+ "user.fields": "id,name,username,profile_image_url,public_metrics"
496
+ };
497
+ return this.client.get(`/2/tweets/${tweetId}/liking_users`, params);
498
+ }
499
+ async getRetweetedBy(tweetId, options) {
500
+ const params = {
501
+ max_results: options?.maxResults || 100,
502
+ pagination_token: options?.paginationToken,
503
+ "user.fields": "id,name,username,profile_image_url,public_metrics"
504
+ };
505
+ return this.client.get(`/2/tweets/${tweetId}/retweeted_by`, params);
506
+ }
507
+ async getQuoteTweets(tweetId, options) {
508
+ const params = {
509
+ max_results: options?.maxResults || 10,
510
+ pagination_token: options?.paginationToken,
511
+ "tweet.fields": "id,text,author_id,created_at,public_metrics,source,lang",
512
+ expansions: "author_id",
513
+ "user.fields": "id,name,username,profile_image_url"
514
+ };
515
+ return this.client.get(`/2/tweets/${tweetId}/quote_tweets`, params);
516
+ }
517
+ async create(options) {
518
+ const body = {
519
+ text: options.text
520
+ };
521
+ if (options.mediaIds?.length) {
522
+ body.media = { media_ids: options.mediaIds };
523
+ }
524
+ if (options.replyToTweetId) {
525
+ body.reply = { in_reply_to_tweet_id: options.replyToTweetId };
526
+ }
527
+ if (options.quoteTweetId) {
528
+ body.quote_tweet_id = options.quoteTweetId;
529
+ }
530
+ if (options.pollOptions?.length) {
531
+ body.poll = {
532
+ options: options.pollOptions,
533
+ duration_minutes: options.pollDurationMinutes || 1440
534
+ };
535
+ }
536
+ return this.client.post("/2/tweets", body);
537
+ }
538
+ async delete(tweetId) {
539
+ return this.client.delete(`/2/tweets/${tweetId}`);
540
+ }
541
+ async like(userId, tweetId) {
542
+ return this.client.post(`/2/users/${userId}/likes`, {
543
+ tweet_id: tweetId
544
+ });
545
+ }
546
+ async unlike(userId, tweetId) {
547
+ return this.client.delete(`/2/users/${userId}/likes/${tweetId}`);
548
+ }
549
+ async retweet(userId, tweetId) {
550
+ return this.client.post(`/2/users/${userId}/retweets`, {
551
+ tweet_id: tweetId
552
+ });
553
+ }
554
+ async unretweet(userId, tweetId) {
555
+ return this.client.delete(`/2/users/${userId}/retweets/${tweetId}`);
556
+ }
557
+ async getBookmarks(userId, options) {
558
+ return this.client.request(`/2/users/${userId}/bookmarks`, {
559
+ method: "GET",
560
+ params: {
561
+ max_results: options?.maxResults || 10,
562
+ pagination_token: options?.paginationToken,
563
+ "tweet.fields": "id,text,author_id,created_at,public_metrics,source,lang",
564
+ expansions: "author_id",
565
+ "user.fields": "id,name,username,profile_image_url"
566
+ },
567
+ authMethod: "oauth2-user"
568
+ });
569
+ }
570
+ async bookmark(userId, tweetId) {
571
+ return this.client.post(`/2/users/${userId}/bookmarks`, {
572
+ tweet_id: tweetId
573
+ });
574
+ }
575
+ async removeBookmark(userId, tweetId) {
576
+ return this.client.delete(`/2/users/${userId}/bookmarks/${tweetId}`);
577
+ }
578
+ }
579
+
580
+ // connectors/x/src/api/users.ts
581
+ class UsersApi {
582
+ client;
583
+ constructor(client) {
584
+ this.client = client;
585
+ }
586
+ async getById(id, options) {
587
+ const params = {};
588
+ if (options?.userFields?.length) {
589
+ params["user.fields"] = options.userFields.join(",");
590
+ }
591
+ if (options?.tweetFields?.length) {
592
+ params["tweet.fields"] = options.tweetFields.join(",");
593
+ }
594
+ if (options?.expansions?.length) {
595
+ params["expansions"] = options.expansions.join(",");
596
+ }
597
+ return this.client.get(`/2/users/${id}`, params);
598
+ }
599
+ async getByUsername(username, options) {
600
+ const params = {
601
+ "user.fields": "id,name,username,created_at,description,profile_image_url,public_metrics,verified,protected,location,url"
602
+ };
603
+ if (options?.userFields?.length) {
604
+ params["user.fields"] = options.userFields.join(",");
605
+ }
606
+ if (options?.tweetFields?.length) {
607
+ params["tweet.fields"] = options.tweetFields.join(",");
608
+ }
609
+ if (options?.expansions?.length) {
610
+ params["expansions"] = options.expansions.join(",");
611
+ }
612
+ return this.client.get(`/2/users/by/username/${username}`, params);
613
+ }
614
+ async getMany(ids, options) {
615
+ const params = {
616
+ ids: ids.join(","),
617
+ "user.fields": "id,name,username,created_at,description,profile_image_url,public_metrics,verified,protected"
618
+ };
619
+ if (options?.userFields?.length) {
620
+ params["user.fields"] = options.userFields.join(",");
621
+ }
622
+ if (options?.tweetFields?.length) {
623
+ params["tweet.fields"] = options.tweetFields.join(",");
624
+ }
625
+ if (options?.expansions?.length) {
626
+ params["expansions"] = options.expansions.join(",");
627
+ }
628
+ return this.client.get("/2/users", params);
629
+ }
630
+ async getManyByUsernames(usernames, options) {
631
+ const params = {
632
+ usernames: usernames.join(","),
633
+ "user.fields": "id,name,username,created_at,description,profile_image_url,public_metrics,verified,protected"
634
+ };
635
+ if (options?.userFields?.length) {
636
+ params["user.fields"] = options.userFields.join(",");
637
+ }
638
+ if (options?.tweetFields?.length) {
639
+ params["tweet.fields"] = options.tweetFields.join(",");
640
+ }
641
+ if (options?.expansions?.length) {
642
+ params["expansions"] = options.expansions.join(",");
643
+ }
644
+ return this.client.get("/2/users/by", params);
645
+ }
646
+ async getFollowers(userId, options) {
647
+ const params = {
648
+ max_results: options?.maxResults || 100,
649
+ pagination_token: options?.paginationToken,
650
+ "user.fields": "id,name,username,created_at,description,profile_image_url,public_metrics,verified"
651
+ };
652
+ return this.client.get(`/2/users/${userId}/followers`, params);
653
+ }
654
+ async getFollowing(userId, options) {
655
+ const params = {
656
+ max_results: options?.maxResults || 100,
657
+ pagination_token: options?.paginationToken,
658
+ "user.fields": "id,name,username,created_at,description,profile_image_url,public_metrics,verified"
659
+ };
660
+ return this.client.get(`/2/users/${userId}/following`, params);
661
+ }
662
+ async getLikedTweets(userId, options) {
663
+ return this.client.request(`/2/users/${userId}/liked_tweets`, {
664
+ method: "GET",
665
+ params: {
666
+ max_results: options?.maxResults || 10,
667
+ pagination_token: options?.paginationToken,
668
+ "tweet.fields": "id,text,author_id,created_at,public_metrics,source,lang",
669
+ expansions: "author_id",
670
+ "user.fields": "id,name,username,profile_image_url"
671
+ },
672
+ authMethod: "oauth2-user"
673
+ });
674
+ }
675
+ async getOwnedLists(userId, options) {
676
+ const params = {
677
+ max_results: options?.maxResults || 100,
678
+ pagination_token: options?.paginationToken,
679
+ "list.fields": "id,name,description,follower_count,member_count,private,owner_id,created_at"
680
+ };
681
+ return this.client.get(`/2/users/${userId}/owned_lists`, params);
682
+ }
683
+ async getListMemberships(userId, options) {
684
+ const params = {
685
+ max_results: options?.maxResults || 100,
686
+ pagination_token: options?.paginationToken,
687
+ "list.fields": "id,name,description,follower_count,member_count,private,owner_id,created_at"
688
+ };
689
+ return this.client.get(`/2/users/${userId}/list_memberships`, params);
690
+ }
691
+ async me() {
692
+ return this.client.request("/2/users/me", {
693
+ method: "GET",
694
+ params: {
695
+ "user.fields": "id,name,username,created_at,description,profile_image_url,public_metrics,verified,protected,location,url"
696
+ },
697
+ authMethod: "oauth2-user"
698
+ });
699
+ }
700
+ async follow(userId, targetUserId) {
701
+ return this.client.post(`/2/users/${userId}/following`, { target_user_id: targetUserId });
702
+ }
703
+ async unfollow(userId, targetUserId) {
704
+ return this.client.delete(`/2/users/${userId}/following/${targetUserId}`);
705
+ }
706
+ async block(userId, targetUserId) {
707
+ return this.client.post(`/2/users/${userId}/blocking`, { target_user_id: targetUserId });
708
+ }
709
+ async unblock(userId, targetUserId) {
710
+ return this.client.delete(`/2/users/${userId}/blocking/${targetUserId}`);
711
+ }
712
+ async mute(userId, targetUserId) {
713
+ return this.client.post(`/2/users/${userId}/muting`, { target_user_id: targetUserId });
714
+ }
715
+ async unmute(userId, targetUserId) {
716
+ return this.client.delete(`/2/users/${userId}/muting/${targetUserId}`);
717
+ }
718
+ }
719
+
720
+ // connectors/x/src/api/media.ts
721
+ var UPLOAD_URL = "https://upload.twitter.com/1.1/media/upload.json";
722
+ var MIME_TYPES = {
723
+ ".jpg": "image/jpeg",
724
+ ".jpeg": "image/jpeg",
725
+ ".png": "image/png",
726
+ ".gif": "image/gif",
727
+ ".webp": "image/webp",
728
+ ".mp4": "video/mp4",
729
+ ".mov": "video/quicktime"
730
+ };
731
+ var MAX_SIZES = {
732
+ image: 5 * 1024 * 1024,
733
+ gif: 15 * 1024 * 1024,
734
+ video: 512 * 1024 * 1024
735
+ };
736
+ var CHUNK_SIZE = 5 * 1024 * 1024;
737
+ function getMimeType(filePath) {
738
+ const ext = filePath.toLowerCase().match(/\.[^.]+$/)?.[0] || "";
739
+ return MIME_TYPES[ext] || "application/octet-stream";
740
+ }
741
+ function getMediaCategory(mimeType) {
742
+ if (mimeType.startsWith("video/")) {
743
+ return "tweet_video";
744
+ }
745
+ if (mimeType === "image/gif") {
746
+ return "tweet_gif";
747
+ }
748
+ return "tweet_image";
749
+ }
750
+ function requiresChunkedUpload(mimeType, fileSize) {
751
+ if (mimeType.startsWith("video/")) {
752
+ return true;
753
+ }
754
+ if (mimeType === "image/gif" && fileSize > 5 * 1024 * 1024) {
755
+ return true;
756
+ }
757
+ return false;
758
+ }
759
+ async function simpleUpload(oauth1Client, fileData, mimeType) {
760
+ const base64Data = fileData.toString("base64");
761
+ const result = await oauth1Client.post(UPLOAD_URL, {
762
+ media_data: base64Data
763
+ });
764
+ return result;
765
+ }
766
+ async function chunkedUpload(oauth1Client, fileData, mimeType, category) {
767
+ const totalBytes = fileData.length;
768
+ const initResult = await oauth1Client.post(UPLOAD_URL, {
769
+ command: "INIT",
770
+ total_bytes: totalBytes.toString(),
771
+ media_type: mimeType,
772
+ media_category: category
773
+ });
774
+ const mediaId = initResult.media_id_string;
775
+ let segmentIndex = 0;
776
+ let offset = 0;
777
+ while (offset < totalBytes) {
778
+ const chunk = fileData.subarray(offset, Math.min(offset + CHUNK_SIZE, totalBytes));
779
+ const chunkBase64 = chunk.toString("base64");
780
+ await oauth1Client.post(UPLOAD_URL, {
781
+ command: "APPEND",
782
+ media_id: mediaId,
783
+ media_data: chunkBase64,
784
+ segment_index: segmentIndex.toString()
785
+ });
786
+ offset += CHUNK_SIZE;
787
+ segmentIndex++;
788
+ }
789
+ const finalizeResult = await oauth1Client.post(UPLOAD_URL, {
790
+ command: "FINALIZE",
791
+ media_id: mediaId
792
+ });
793
+ return finalizeResult;
794
+ }
795
+ async function waitForProcessing(oauth1Client, mediaId, maxWaitMs = 60000) {
796
+ const startTime = Date.now();
797
+ while (Date.now() - startTime < maxWaitMs) {
798
+ const status = await oauth1Client.get(UPLOAD_URL, {
799
+ command: "STATUS",
800
+ media_id: mediaId
801
+ });
802
+ if (!status.processing_info) {
803
+ return status;
804
+ }
805
+ const { state, check_after_secs, error } = status.processing_info;
806
+ if (state === "succeeded") {
807
+ return status;
808
+ }
809
+ if (state === "failed") {
810
+ throw new Error(`Media processing failed: ${error?.message || "Unknown error"}`);
811
+ }
812
+ const waitMs = (check_after_secs || 5) * 1000;
813
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
814
+ }
815
+ throw new Error("Media processing timed out");
816
+ }
817
+ async function addAltText(oauth1Client, mediaId, altText) {
818
+ const url = "https://upload.twitter.com/1.1/media/metadata/create.json";
819
+ await oauth1Client.post(url, JSON.stringify({
820
+ media_id: mediaId,
821
+ alt_text: { text: altText }
822
+ }), { "Content-Type": "application/json" });
823
+ }
824
+
825
+ class MediaApi {
826
+ oauth1Client;
827
+ constructor(oauth1Client) {
828
+ this.oauth1Client = oauth1Client;
829
+ }
830
+ async uploadFile(filePath, options = {}) {
831
+ const { readFileSync } = await import("fs");
832
+ const fileData = readFileSync(filePath);
833
+ const mimeType = getMimeType(filePath);
834
+ return this.uploadBuffer(fileData, mimeType, {
835
+ ...options,
836
+ category: options.category || getMediaCategory(mimeType)
837
+ });
838
+ }
839
+ async uploadBuffer(data, mimeType, options = {}) {
840
+ const fileSize = data.length;
841
+ const category = options.category || getMediaCategory(mimeType);
842
+ const maxSize = mimeType.startsWith("video/") ? MAX_SIZES.video : mimeType === "image/gif" ? MAX_SIZES.gif : MAX_SIZES.image;
843
+ if (fileSize > maxSize) {
844
+ throw new Error(`File size ${fileSize} exceeds maximum ${maxSize} bytes for ${mimeType}`);
845
+ }
846
+ let result;
847
+ if (requiresChunkedUpload(mimeType, fileSize)) {
848
+ result = await chunkedUpload(this.oauth1Client, data, mimeType, category);
849
+ if (result.processing_info) {
850
+ result = await waitForProcessing(this.oauth1Client, result.media_id_string);
851
+ }
852
+ } else {
853
+ result = await simpleUpload(this.oauth1Client, data, mimeType);
854
+ }
855
+ if (options.altText) {
856
+ await addAltText(this.oauth1Client, result.media_id_string, options.altText);
857
+ }
858
+ return result;
859
+ }
860
+ async uploadFromUrl(url, options = {}) {
861
+ const response = await fetch(url);
862
+ if (!response.ok) {
863
+ throw new Error(`Failed to fetch media from URL: ${response.statusText}`);
864
+ }
865
+ const buffer = Buffer.from(await response.arrayBuffer());
866
+ const contentType = response.headers.get("content-type") || "application/octet-stream";
867
+ return this.uploadBuffer(buffer, contentType, options);
868
+ }
869
+ }
870
+
871
+ // connectors/x/src/api/index.ts
872
+ class X {
873
+ client;
874
+ mediaApi;
875
+ tweets;
876
+ users;
877
+ constructor(config) {
878
+ this.client = new XClient(config);
879
+ this.tweets = new TweetsApi(this.client);
880
+ this.users = new UsersApi(this.client);
881
+ if (config.oauth1AccessToken && config.oauth1AccessTokenSecret) {
882
+ const oauth1Client = new OAuth1Client({
883
+ consumerKey: config.apiKey,
884
+ consumerSecret: config.apiSecret,
885
+ accessToken: config.oauth1AccessToken,
886
+ accessTokenSecret: config.oauth1AccessTokenSecret
887
+ });
888
+ this.mediaApi = new MediaApi(oauth1Client);
889
+ }
890
+ }
891
+ get media() {
892
+ return this.mediaApi;
893
+ }
894
+ hasMediaUpload() {
895
+ return !!this.mediaApi;
896
+ }
897
+ static fromEnv() {
898
+ const apiKey = process.env.X_API_KEY;
899
+ const apiSecret = process.env.X_API_SECRET;
900
+ const bearerToken = process.env.X_BEARER_TOKEN;
901
+ const accessToken = process.env.X_ACCESS_TOKEN;
902
+ const refreshToken = process.env.X_REFRESH_TOKEN;
903
+ const clientId = process.env.X_CLIENT_ID;
904
+ const clientSecret = process.env.X_CLIENT_SECRET;
905
+ const oauth1AccessToken = process.env.X_OAUTH1_ACCESS_TOKEN;
906
+ const oauth1AccessTokenSecret = process.env.X_OAUTH1_ACCESS_TOKEN_SECRET;
907
+ if (!apiKey || !apiSecret) {
908
+ throw new Error("X_API_KEY and X_API_SECRET environment variables are required");
909
+ }
910
+ return new X({
911
+ apiKey,
912
+ apiSecret,
913
+ bearerToken,
914
+ accessToken,
915
+ refreshToken,
916
+ clientId,
917
+ clientSecret,
918
+ oauth1AccessToken,
919
+ oauth1AccessTokenSecret
920
+ });
921
+ }
922
+ getApiKeyPreview() {
923
+ return this.client.getApiKeyPreview();
924
+ }
925
+ hasBearerToken() {
926
+ return this.client.hasBearerToken();
927
+ }
928
+ hasUserContext() {
929
+ return this.client.hasUserContext();
930
+ }
931
+ hasOAuth1() {
932
+ return this.client.hasOAuth1();
933
+ }
934
+ getAuthStatus() {
935
+ return this.client.getAuthStatus();
936
+ }
937
+ setUserTokens(tokens) {
938
+ this.client.setUserTokens(tokens);
939
+ }
940
+ setOAuth1Tokens(oauthToken, oauthTokenSecret) {
941
+ this.client.setOAuth1Tokens(oauthToken, oauthTokenSecret);
942
+ const oauth1Client = this.client.getOAuth1Client();
943
+ if (oauth1Client) {
944
+ this.mediaApi = new MediaApi(oauth1Client);
945
+ }
946
+ }
947
+ setTokenRefreshCallback(callback) {
948
+ this.client.setTokenRefreshCallback(callback);
949
+ }
950
+ getClient() {
951
+ return this.client;
952
+ }
953
+ }
954
+
955
+ // src/social/util.ts
956
+ function decodeBase64(dataBase64) {
957
+ if (typeof dataBase64 !== "string" || dataBase64.length === 0) {
958
+ throw new Error("media.upload requires a non-empty base64 string in `dataBase64`");
959
+ }
960
+ return Buffer.from(dataBase64, "base64");
961
+ }
962
+
963
+ // src/social/x.ts
964
+ class XAdapter {
965
+ x;
966
+ me;
967
+ constructor(x) {
968
+ this.x = x;
969
+ }
970
+ static fromCredentials(creds) {
971
+ if (!creds || !creds.apiKey || !creds.apiSecret) {
972
+ throw new Error("x credentials require `apiKey` and `apiSecret`");
973
+ }
974
+ const x = new X({
975
+ apiKey: creds.apiKey,
976
+ apiSecret: creds.apiSecret,
977
+ bearerToken: creds.bearerToken,
978
+ accessToken: creds.accessToken,
979
+ refreshToken: creds.refreshToken,
980
+ clientId: creds.clientId,
981
+ clientSecret: creds.clientSecret,
982
+ oauth1AccessToken: creds.oauth1AccessToken,
983
+ oauth1AccessTokenSecret: creds.oauth1AccessTokenSecret
984
+ });
985
+ return new XAdapter(x);
986
+ }
987
+ async resolveMe() {
988
+ if (this.me)
989
+ return this.me;
990
+ const res = await this.x.users.me();
991
+ this.me = { id: res.data.id, username: res.data.username, displayName: res.data.name };
992
+ return this.me;
993
+ }
994
+ async accountMe() {
995
+ const me = await this.resolveMe();
996
+ return {
997
+ id: me.id,
998
+ username: me.username,
999
+ displayName: me.displayName,
1000
+ url: `https://x.com/${me.username}`
1001
+ };
1002
+ }
1003
+ async postCreate(input) {
1004
+ const res = await this.x.tweets.create({
1005
+ text: input.text,
1006
+ replyToTweetId: input.replyToId,
1007
+ mediaIds: input.mediaIds
1008
+ });
1009
+ const id = res.data.id;
1010
+ const me = await this.resolveMe();
1011
+ return { id, url: `https://x.com/${me.username}/status/${id}` };
1012
+ }
1013
+ async postDelete(input) {
1014
+ const res = await this.x.tweets.delete(input.id);
1015
+ return { id: input.id, deleted: Boolean(res.data.deleted) };
1016
+ }
1017
+ async mediaUpload(input) {
1018
+ if (!this.x.media) {
1019
+ throw new ConnectorOperationNotSupported("x", "media.upload (requires OAuth 1.0a credentials: oauth1AccessToken + oauth1AccessTokenSecret)");
1020
+ }
1021
+ const buffer = decodeBase64(input.dataBase64);
1022
+ const res = await this.x.media.uploadBuffer(buffer, input.mimeType, {
1023
+ altText: input.altText
1024
+ });
1025
+ return { mediaId: res.media_id_string };
1026
+ }
1027
+ async mentionsList(input = {}) {
1028
+ const me = await this.resolveMe();
1029
+ const res = await this.x.tweets.getUserMentions(me.id, {
1030
+ sinceId: input.sinceId,
1031
+ maxResults: input.limit
1032
+ });
1033
+ const handleById = new Map;
1034
+ for (const u of res.includes?.users ?? []) {
1035
+ handleById.set(u.id, u.username);
1036
+ }
1037
+ const items = (res.data ?? []).map((t) => ({
1038
+ id: t.id,
1039
+ text: t.text,
1040
+ authorId: t.author_id,
1041
+ authorHandle: t.author_id ? handleById.get(t.author_id) : undefined,
1042
+ createdAt: t.created_at
1043
+ }));
1044
+ return { items };
1045
+ }
1046
+ async analyticsPost(input) {
1047
+ const res = await this.x.tweets.get(input.id, {
1048
+ tweetFields: ["public_metrics"]
1049
+ });
1050
+ const metrics = res.data.public_metrics ?? {};
1051
+ return { metrics };
1052
+ }
1053
+ }
1054
+
1055
+ // src/social/mastodon.ts
1056
+ class MastodonAdapter {
1057
+ accessToken;
1058
+ baseUrl;
1059
+ fetchImpl;
1060
+ constructor(creds, fetchImpl) {
1061
+ if (!creds || !creds.accessToken || !creds.baseUrl) {
1062
+ throw new Error("mastodon credentials require `accessToken` and `baseUrl`");
1063
+ }
1064
+ this.accessToken = creds.accessToken;
1065
+ this.baseUrl = creds.baseUrl.replace(/\/+$/, "");
1066
+ this.fetchImpl = fetchImpl ?? globalThis.fetch;
1067
+ }
1068
+ static fromCredentials(creds, fetchImpl) {
1069
+ return new MastodonAdapter(creds, fetchImpl);
1070
+ }
1071
+ async call(path, options = {}) {
1072
+ const { method = "GET", body, query } = options;
1073
+ let url = `${this.baseUrl}${path}`;
1074
+ if (query) {
1075
+ const qs = new URLSearchParams;
1076
+ for (const [k, v] of Object.entries(query)) {
1077
+ if (v !== undefined && v !== null && v !== "")
1078
+ qs.append(k, String(v));
1079
+ }
1080
+ const s = qs.toString();
1081
+ if (s)
1082
+ url += `?${s}`;
1083
+ }
1084
+ const headers = {
1085
+ Authorization: `Bearer ${this.accessToken}`,
1086
+ Accept: "application/json"
1087
+ };
1088
+ let serializedBody;
1089
+ if (body !== undefined && method !== "GET") {
1090
+ headers["Content-Type"] = "application/json";
1091
+ serializedBody = JSON.stringify(body);
1092
+ }
1093
+ const res = await this.fetchImpl(url, { method, headers, body: serializedBody });
1094
+ const text = await res.text();
1095
+ const data = text ? JSON.parse(text) : {};
1096
+ if (!res.ok) {
1097
+ const message = data && (data.error || data.message) || res.statusText || "Mastodon request failed";
1098
+ throw new Error(`Mastodon ${res.status}: ${message}`);
1099
+ }
1100
+ return data;
1101
+ }
1102
+ async accountMe() {
1103
+ const acct = await this.call("/api/v1/accounts/verify_credentials");
1104
+ return {
1105
+ id: acct.id,
1106
+ username: acct.username,
1107
+ displayName: acct.display_name,
1108
+ url: acct.url
1109
+ };
1110
+ }
1111
+ async postCreate(input) {
1112
+ const body = { status: input.text };
1113
+ if (input.replyToId)
1114
+ body.in_reply_to_id = input.replyToId;
1115
+ if (input.mediaIds && input.mediaIds.length > 0)
1116
+ body.media_ids = input.mediaIds;
1117
+ const status = await this.call("/api/v1/statuses", {
1118
+ method: "POST",
1119
+ body
1120
+ });
1121
+ return { id: status.id, url: status.url };
1122
+ }
1123
+ async postDelete(input) {
1124
+ await this.call(`/api/v1/statuses/${input.id}`, { method: "DELETE" });
1125
+ return { id: input.id, deleted: true };
1126
+ }
1127
+ async mediaUpload(input) {
1128
+ const buffer = decodeBase64(input.dataBase64);
1129
+ const form = new FormData;
1130
+ const bytes = Uint8Array.from(buffer);
1131
+ const blob = new Blob([bytes], { type: input.mimeType });
1132
+ form.append("file", blob);
1133
+ if (input.altText)
1134
+ form.append("description", input.altText);
1135
+ const res = await this.fetchImpl(`${this.baseUrl}/api/v2/media`, {
1136
+ method: "POST",
1137
+ headers: { Authorization: `Bearer ${this.accessToken}`, Accept: "application/json" },
1138
+ body: form
1139
+ });
1140
+ const text = await res.text();
1141
+ const data = text ? JSON.parse(text) : {};
1142
+ if (!res.ok) {
1143
+ const message = data && (data.error || data.message) || res.statusText || "media upload failed";
1144
+ throw new Error(`Mastodon ${res.status}: ${message}`);
1145
+ }
1146
+ return { mediaId: String(data.id) };
1147
+ }
1148
+ async mentionsList(input = {}) {
1149
+ const notifications = await this.call("/api/v1/notifications", {
1150
+ query: { types: "mention", since_id: input.sinceId, limit: input.limit }
1151
+ });
1152
+ const items = notifications.filter((n) => n.type === "mention" && n.status).map((n) => ({
1153
+ id: n.status.id,
1154
+ text: stripHtml(n.status.content ?? ""),
1155
+ authorId: n.status.account?.id,
1156
+ authorHandle: n.status.account?.acct,
1157
+ createdAt: n.status.created_at
1158
+ }));
1159
+ return { items };
1160
+ }
1161
+ async analyticsPost(input) {
1162
+ const status = await this.call(`/api/v1/statuses/${input.id}`);
1163
+ return {
1164
+ metrics: {
1165
+ reblogsCount: status.reblogs_count ?? 0,
1166
+ favouritesCount: status.favourites_count ?? 0,
1167
+ repliesCount: status.replies_count ?? 0
1168
+ }
1169
+ };
1170
+ }
1171
+ }
1172
+ function stripHtml(html) {
1173
+ return html.replace(/<[^>]*>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").trim();
1174
+ }
1175
+
1176
+ // connectors/bluesky/src/types/index.ts
1177
+ class BlueskyApiError extends Error {
1178
+ statusCode;
1179
+ errorCode;
1180
+ constructor(message, statusCode, errorCode) {
1181
+ super(message);
1182
+ this.name = "BlueskyApiError";
1183
+ this.statusCode = statusCode;
1184
+ this.errorCode = errorCode;
1185
+ }
1186
+ }
1187
+
1188
+ // connectors/bluesky/src/api/client.ts
1189
+ var DEFAULT_PDS = "https://bsky.social";
1190
+
1191
+ class BlueskyClient {
1192
+ identifier;
1193
+ appPassword;
1194
+ pds;
1195
+ session;
1196
+ constructor(config) {
1197
+ if (!config?.identifier || !config?.appPassword) {
1198
+ throw new Error("bluesky credentials require `identifier` and `appPassword`");
1199
+ }
1200
+ this.identifier = config.identifier;
1201
+ this.appPassword = config.appPassword;
1202
+ this.pds = (config.pds || DEFAULT_PDS).replace(/\/+$/, "");
1203
+ }
1204
+ async xrpc(nsid, options = {}) {
1205
+ const { method = "GET", params, body, auth = true, contentType, rawBody } = options;
1206
+ const url = new URL(`${this.pds}/xrpc/${nsid}`);
1207
+ if (params) {
1208
+ for (const [k, v] of Object.entries(params)) {
1209
+ if (v !== undefined && v !== null && v !== "")
1210
+ url.searchParams.append(k, String(v));
1211
+ }
1212
+ }
1213
+ const headers = { Accept: "application/json" };
1214
+ if (auth) {
1215
+ const session = await this.ensureSession();
1216
+ headers.Authorization = `Bearer ${session.accessJwt}`;
1217
+ }
1218
+ const init = { method, headers };
1219
+ if (rawBody !== undefined) {
1220
+ headers["Content-Type"] = contentType || "application/octet-stream";
1221
+ init.body = rawBody;
1222
+ } else if (body !== undefined && method !== "GET") {
1223
+ headers["Content-Type"] = contentType || "application/json";
1224
+ init.body = JSON.stringify(body);
1225
+ }
1226
+ const response = await fetch(url.toString(), init);
1227
+ if (response.status === 204)
1228
+ return {};
1229
+ let data;
1230
+ const text = await response.text();
1231
+ if (text) {
1232
+ try {
1233
+ data = JSON.parse(text);
1234
+ } catch {
1235
+ data = text;
1236
+ }
1237
+ }
1238
+ if (!response.ok) {
1239
+ const errBody = typeof data === "object" && data !== null ? data : {};
1240
+ const message = errBody.message || errBody.error || response.statusText || "Request failed";
1241
+ throw new BlueskyApiError(message, response.status, errBody.error);
1242
+ }
1243
+ return data;
1244
+ }
1245
+ async ensureSession() {
1246
+ if (this.session)
1247
+ return this.session;
1248
+ const session = await this.xrpc("com.atproto.server.createSession", {
1249
+ method: "POST",
1250
+ auth: false,
1251
+ body: { identifier: this.identifier, password: this.appPassword }
1252
+ });
1253
+ this.session = session;
1254
+ return session;
1255
+ }
1256
+ async createRecord(collection, record) {
1257
+ const session = await this.ensureSession();
1258
+ return this.xrpc("com.atproto.repo.createRecord", {
1259
+ method: "POST",
1260
+ body: { repo: session.did, collection, record }
1261
+ });
1262
+ }
1263
+ async deleteRecord(uri) {
1264
+ const session = await this.ensureSession();
1265
+ const parsed = parseAtUri(uri);
1266
+ await this.xrpc("com.atproto.repo.deleteRecord", {
1267
+ method: "POST",
1268
+ body: { repo: session.did, collection: parsed.collection, rkey: parsed.rkey }
1269
+ });
1270
+ }
1271
+ async uploadBlob(data, mimeType) {
1272
+ return this.xrpc("com.atproto.repo.uploadBlob", {
1273
+ method: "POST",
1274
+ rawBody: data,
1275
+ contentType: mimeType || "application/octet-stream"
1276
+ });
1277
+ }
1278
+ async listNotifications(options) {
1279
+ return this.xrpc("app.bsky.notification.listNotifications", {
1280
+ method: "GET",
1281
+ params: { limit: options?.limit, cursor: options?.cursor }
1282
+ });
1283
+ }
1284
+ async getPosts(uris) {
1285
+ const url = new URL(`${this.pds}/xrpc/app.bsky.feed.getPosts`);
1286
+ for (const u of uris)
1287
+ url.searchParams.append("uris", u);
1288
+ const session = await this.ensureSession();
1289
+ const response = await fetch(url.toString(), {
1290
+ headers: { Accept: "application/json", Authorization: `Bearer ${session.accessJwt}` }
1291
+ });
1292
+ const text = await response.text();
1293
+ const data = text ? JSON.parse(text) : {};
1294
+ if (!response.ok) {
1295
+ throw new BlueskyApiError(data?.message || data?.error || response.statusText, response.status, data?.error);
1296
+ }
1297
+ return data;
1298
+ }
1299
+ getSession() {
1300
+ return this.session;
1301
+ }
1302
+ }
1303
+ function parseAtUri(uri) {
1304
+ const match = /^at:\/\/([^/]+)\/([^/]+)\/(.+)$/.exec(uri);
1305
+ if (!match) {
1306
+ throw new Error(`Invalid at:// URI: ${uri}`);
1307
+ }
1308
+ return { repo: match[1], collection: match[2], rkey: match[3] };
1309
+ }
1310
+
1311
+ // connectors/bluesky/src/api/index.ts
1312
+ var POST_COLLECTION = "app.bsky.feed.post";
1313
+
1314
+ class Bluesky {
1315
+ client;
1316
+ constructor(config) {
1317
+ this.client = new BlueskyClient(config);
1318
+ }
1319
+ static fromEnv() {
1320
+ const identifier = process.env.BLUESKY_IDENTIFIER;
1321
+ const appPassword = process.env.BLUESKY_APP_PASSWORD;
1322
+ const pds = process.env.BLUESKY_PDS;
1323
+ if (!identifier || !appPassword) {
1324
+ throw new Error("BLUESKY_IDENTIFIER and BLUESKY_APP_PASSWORD environment variables are required");
1325
+ }
1326
+ return new Bluesky({ identifier, appPassword, pds });
1327
+ }
1328
+ async me() {
1329
+ return this.client.ensureSession();
1330
+ }
1331
+ async createPost(options) {
1332
+ const record = {
1333
+ $type: POST_COLLECTION,
1334
+ text: options.text,
1335
+ createdAt: options.createdAt || new Date().toISOString()
1336
+ };
1337
+ if (options.embed) {
1338
+ record.embed = options.embed;
1339
+ }
1340
+ if (options.replyToUri) {
1341
+ const { parent, root } = await this.resolveReplyRefs(options.replyToUri);
1342
+ record.reply = { root, parent };
1343
+ }
1344
+ return this.client.createRecord(POST_COLLECTION, record);
1345
+ }
1346
+ async deletePost(uri) {
1347
+ return this.client.deleteRecord(uri);
1348
+ }
1349
+ async uploadBlob(data, mimeType) {
1350
+ const res = await this.client.uploadBlob(data, mimeType);
1351
+ return res.blob;
1352
+ }
1353
+ async listNotifications(options) {
1354
+ return this.client.listNotifications(options);
1355
+ }
1356
+ async getPosts(uris) {
1357
+ return this.client.getPosts(uris);
1358
+ }
1359
+ getClient() {
1360
+ return this.client;
1361
+ }
1362
+ async resolveReplyRefs(parentUri) {
1363
+ parseAtUri(parentUri);
1364
+ const posts = await this.client.getPosts([parentUri]);
1365
+ const match = posts.posts.find((p) => p.uri === parentUri);
1366
+ if (!match || !match.cid) {
1367
+ throw new Error(`Cannot resolve parent post for reply: ${parentUri}`);
1368
+ }
1369
+ const parent = { uri: match.uri, cid: match.cid };
1370
+ const root = match.record?.reply?.root ?? parent;
1371
+ return { parent, root };
1372
+ }
1373
+ }
1374
+
1375
+ // src/social/bluesky.ts
1376
+ function postUrl(handle, uri) {
1377
+ const m = /^at:\/\/[^/]+\/app\.bsky\.feed\.post\/(.+)$/.exec(uri);
1378
+ if (!m)
1379
+ return;
1380
+ return `https://bsky.app/profile/${handle}/post/${m[1]}`;
1381
+ }
1382
+
1383
+ class BlueskyAdapter {
1384
+ bsky;
1385
+ handle;
1386
+ constructor(bsky) {
1387
+ this.bsky = bsky;
1388
+ }
1389
+ static fromCredentials(creds) {
1390
+ if (!creds || !creds.identifier || !creds.appPassword) {
1391
+ throw new Error("bluesky credentials require `identifier` and `appPassword`");
1392
+ }
1393
+ const bsky = new Bluesky({
1394
+ identifier: creds.identifier,
1395
+ appPassword: creds.appPassword,
1396
+ pds: creds.pds
1397
+ });
1398
+ return new BlueskyAdapter(bsky);
1399
+ }
1400
+ async resolveHandle() {
1401
+ if (this.handle)
1402
+ return this.handle;
1403
+ const me = await this.bsky.me();
1404
+ this.handle = me.handle;
1405
+ return this.handle;
1406
+ }
1407
+ async accountMe() {
1408
+ const me = await this.bsky.me();
1409
+ this.handle = me.handle;
1410
+ return {
1411
+ id: me.did,
1412
+ username: me.handle,
1413
+ displayName: me.handle,
1414
+ url: `https://bsky.app/profile/${me.handle}`
1415
+ };
1416
+ }
1417
+ async postCreate(input) {
1418
+ let embed;
1419
+ if (input.mediaIds && input.mediaIds.length > 0) {
1420
+ const images = input.mediaIds.map((m) => {
1421
+ const blob = JSON.parse(m);
1422
+ return { alt: "", image: blob };
1423
+ });
1424
+ embed = { $type: "app.bsky.embed.images", images };
1425
+ }
1426
+ const res = await this.bsky.createPost({
1427
+ text: input.text,
1428
+ replyToUri: input.replyToId,
1429
+ embed
1430
+ });
1431
+ const handle = await this.resolveHandle();
1432
+ return { id: res.uri, url: postUrl(handle, res.uri) };
1433
+ }
1434
+ async postDelete(input) {
1435
+ await this.bsky.deletePost(input.id);
1436
+ return { id: input.id, deleted: true };
1437
+ }
1438
+ async mediaUpload(input) {
1439
+ const buffer = decodeBase64(input.dataBase64);
1440
+ const blob = await this.bsky.uploadBlob(buffer, input.mimeType);
1441
+ return { mediaId: JSON.stringify(blob) };
1442
+ }
1443
+ async mentionsList(input = {}) {
1444
+ const res = await this.bsky.listNotifications({ limit: input.limit });
1445
+ const items = res.notifications.filter((n) => n.reason === "mention" || n.reason === "reply").map((n) => ({
1446
+ id: n.uri,
1447
+ text: n.record?.text ?? "",
1448
+ authorId: n.author?.did,
1449
+ authorHandle: n.author?.handle,
1450
+ createdAt: n.record?.createdAt ?? n.indexedAt
1451
+ }));
1452
+ return { items };
1453
+ }
1454
+ async analyticsPost(input) {
1455
+ const res = await this.bsky.getPosts([input.id]);
1456
+ const post = res.posts.find((p) => p.uri === input.id) ?? res.posts[0];
1457
+ if (!post) {
1458
+ throw new ConnectorOperationNotSupported("bluesky", `analytics.post (post not found: ${input.id})`);
1459
+ }
1460
+ const metrics = {
1461
+ likeCount: post.likeCount ?? 0,
1462
+ repostCount: post.repostCount ?? 0,
1463
+ replyCount: post.replyCount ?? 0,
1464
+ quoteCount: post.quoteCount ?? 0
1465
+ };
1466
+ return { metrics };
1467
+ }
1468
+ }
1469
+
1470
+ // src/social/http.ts
1471
+ function resolveFetch(fetchImpl) {
1472
+ return fetchImpl ?? globalThis.fetch;
1473
+ }
1474
+ function appendQuery(url, query) {
1475
+ if (!query)
1476
+ return url;
1477
+ const qs = new URLSearchParams;
1478
+ for (const [k, v] of Object.entries(query)) {
1479
+ if (v !== undefined && v !== null && v !== "")
1480
+ qs.append(k, String(v));
1481
+ }
1482
+ const s = qs.toString();
1483
+ return s ? `${url}${url.includes("?") ? "&" : "?"}${s}` : url;
1484
+ }
1485
+ function extractError(data, fallback) {
1486
+ if (data && typeof data === "object") {
1487
+ const d = data;
1488
+ const candidates = [
1489
+ d.error_description,
1490
+ typeof d.error === "string" ? d.error : undefined,
1491
+ d.message,
1492
+ d.error && typeof d.error === "object" ? d.error.message : undefined
1493
+ ];
1494
+ for (const c of candidates) {
1495
+ if (typeof c === "string" && c)
1496
+ return c;
1497
+ }
1498
+ }
1499
+ return fallback;
1500
+ }
1501
+ async function jsonRequest(fetchImpl, url, options = {}) {
1502
+ const { method = "GET", query, body } = options;
1503
+ const finalUrl = appendQuery(url, query);
1504
+ const headers = { Accept: "application/json", ...options.headers ?? {} };
1505
+ let serialized;
1506
+ if (body !== undefined && method !== "GET" && method !== "HEAD") {
1507
+ if (typeof body === "string" || body instanceof FormData) {
1508
+ serialized = body;
1509
+ } else {
1510
+ if (!headers["Content-Type"])
1511
+ headers["Content-Type"] = "application/json";
1512
+ serialized = JSON.stringify(body);
1513
+ }
1514
+ }
1515
+ const res = await fetchImpl(finalUrl, { method, headers, body: serialized });
1516
+ const text = await res.text();
1517
+ const data = text ? safeJsonParse(text) : {};
1518
+ if (!res.ok) {
1519
+ const label = options.errorLabel ?? "request";
1520
+ throw new Error(`${label} ${res.status}: ${extractError(data, res.statusText || "failed")}`);
1521
+ }
1522
+ return data;
1523
+ }
1524
+ function safeJsonParse(text) {
1525
+ try {
1526
+ return JSON.parse(text);
1527
+ } catch {
1528
+ return { raw: text };
1529
+ }
1530
+ }
1531
+
1532
+ // src/social/linkedin.ts
1533
+ var DEFAULT_BASE = "https://api.linkedin.com";
1534
+
1535
+ class LinkedInAdapter {
1536
+ accessToken;
1537
+ baseUrl;
1538
+ authorUrn;
1539
+ fetchImpl;
1540
+ constructor(creds, fetchImpl) {
1541
+ if (!creds || !creds.accessToken) {
1542
+ throw new Error("linkedin credentials require `accessToken`");
1543
+ }
1544
+ this.accessToken = creds.accessToken;
1545
+ this.authorUrn = creds.authorUrn;
1546
+ this.baseUrl = (creds.baseUrl ?? DEFAULT_BASE).replace(/\/+$/, "");
1547
+ this.fetchImpl = resolveFetch(fetchImpl);
1548
+ }
1549
+ static fromCredentials(creds, fetchImpl) {
1550
+ return new LinkedInAdapter(creds, fetchImpl);
1551
+ }
1552
+ headers(extra) {
1553
+ return {
1554
+ Authorization: `Bearer ${this.accessToken}`,
1555
+ "X-Restli-Protocol-Version": "2.0.0",
1556
+ ...extra ?? {}
1557
+ };
1558
+ }
1559
+ async resolveAuthorUrn() {
1560
+ if (this.authorUrn)
1561
+ return this.authorUrn;
1562
+ const me = await jsonRequest(this.fetchImpl, `${this.baseUrl}/v2/me`, {
1563
+ headers: this.headers(),
1564
+ errorLabel: "LinkedIn"
1565
+ });
1566
+ this.authorUrn = `urn:li:person:${me.id}`;
1567
+ return this.authorUrn;
1568
+ }
1569
+ async accountMe() {
1570
+ const me = await jsonRequest(this.fetchImpl, `${this.baseUrl}/v2/me`, { headers: this.headers(), errorLabel: "LinkedIn" });
1571
+ this.authorUrn = `urn:li:person:${me.id}`;
1572
+ const displayName = [me.localizedFirstName, me.localizedLastName].filter(Boolean).join(" ") || undefined;
1573
+ return {
1574
+ id: me.id,
1575
+ username: me.vanityName ?? me.id,
1576
+ displayName,
1577
+ url: me.vanityName ? `https://www.linkedin.com/in/${me.vanityName}` : undefined
1578
+ };
1579
+ }
1580
+ async postCreate(input) {
1581
+ const author = await this.resolveAuthorUrn();
1582
+ const visibility = input.visibility ?? "PUBLIC";
1583
+ const hasMedia = Boolean(input.mediaIds && input.mediaIds.length > 0);
1584
+ const shareContent = {
1585
+ shareCommentary: { text: input.text },
1586
+ shareMediaCategory: hasMedia ? "IMAGE" : "NONE"
1587
+ };
1588
+ if (hasMedia) {
1589
+ shareContent.media = input.mediaIds.map((m) => ({ status: "READY", media: m }));
1590
+ }
1591
+ const body = {
1592
+ author,
1593
+ lifecycleState: "PUBLISHED",
1594
+ specificContent: { "com.linkedin.ugc.ShareContent": shareContent },
1595
+ visibility: { "com.linkedin.ugc.MemberNetworkVisibility": visibility }
1596
+ };
1597
+ const res = await jsonRequest(this.fetchImpl, `${this.baseUrl}/v2/ugcPosts`, {
1598
+ method: "POST",
1599
+ headers: this.headers(),
1600
+ body,
1601
+ errorLabel: "LinkedIn"
1602
+ });
1603
+ return {
1604
+ id: res.id,
1605
+ url: `https://www.linkedin.com/feed/update/${res.id}`
1606
+ };
1607
+ }
1608
+ async postDelete(input) {
1609
+ await jsonRequest(this.fetchImpl, `${this.baseUrl}/v2/ugcPosts/${encodeURIComponent(input.id)}`, {
1610
+ method: "DELETE",
1611
+ headers: this.headers(),
1612
+ errorLabel: "LinkedIn"
1613
+ });
1614
+ return { id: input.id, deleted: true };
1615
+ }
1616
+ async mediaUpload(input) {
1617
+ const author = await this.resolveAuthorUrn();
1618
+ const register = await jsonRequest(this.fetchImpl, `${this.baseUrl}/v2/assets?action=registerUpload`, {
1619
+ method: "POST",
1620
+ headers: this.headers(),
1621
+ body: {
1622
+ registerUploadRequest: {
1623
+ owner: author,
1624
+ recipes: ["urn:li:digitalmediaRecipe:feedshare-image"],
1625
+ serviceRelationships: [
1626
+ { relationshipType: "OWNER", identifier: "urn:li:userGeneratedContent" }
1627
+ ]
1628
+ }
1629
+ },
1630
+ errorLabel: "LinkedIn"
1631
+ });
1632
+ const asset = register.value.asset;
1633
+ const uploadUrl = register.value.uploadMechanism["com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest"].uploadUrl;
1634
+ const buffer = decodeBase64(input.dataBase64);
1635
+ const bytes = Uint8Array.from(buffer);
1636
+ const put = await this.fetchImpl(uploadUrl, {
1637
+ method: "PUT",
1638
+ headers: { Authorization: `Bearer ${this.accessToken}`, "Content-Type": input.mimeType },
1639
+ body: bytes
1640
+ });
1641
+ if (!put.ok) {
1642
+ throw new Error(`LinkedIn ${put.status}: media upload failed`);
1643
+ }
1644
+ return { mediaId: asset };
1645
+ }
1646
+ async mentionsList(_input = {}) {
1647
+ throw new ConnectorOperationNotSupported("linkedin", "mentions.list (no public mentions/notifications endpoint)");
1648
+ }
1649
+ async analyticsPost(_input) {
1650
+ throw new ConnectorOperationNotSupported("linkedin", "analytics.post (requires organization socialActions scope; not available for member shares)");
1651
+ }
1652
+ }
1653
+
1654
+ // src/social/reddit.ts
1655
+ var BASE = "https://oauth.reddit.com";
1656
+ function fullname(id) {
1657
+ return id.startsWith("t3_") ? id : `t3_${id}`;
1658
+ }
1659
+
1660
+ class RedditAdapter {
1661
+ accessToken;
1662
+ userAgent;
1663
+ fetchImpl;
1664
+ constructor(creds, fetchImpl) {
1665
+ if (!creds || !creds.accessToken) {
1666
+ throw new Error("reddit credentials require `accessToken`");
1667
+ }
1668
+ this.accessToken = creds.accessToken;
1669
+ this.userAgent = creds.userAgent ?? "hasna-connectors/social";
1670
+ this.fetchImpl = resolveFetch(fetchImpl);
1671
+ }
1672
+ static fromCredentials(creds, fetchImpl) {
1673
+ return new RedditAdapter(creds, fetchImpl);
1674
+ }
1675
+ headers(extra) {
1676
+ return {
1677
+ Authorization: `Bearer ${this.accessToken}`,
1678
+ "User-Agent": this.userAgent,
1679
+ ...extra ?? {}
1680
+ };
1681
+ }
1682
+ form(params) {
1683
+ const qs = new URLSearchParams;
1684
+ for (const [k, v] of Object.entries(params)) {
1685
+ if (v !== undefined)
1686
+ qs.append(k, v);
1687
+ }
1688
+ return qs.toString();
1689
+ }
1690
+ async accountMe() {
1691
+ const me = await jsonRequest(this.fetchImpl, `${BASE}/api/v1/me`, { headers: this.headers(), errorLabel: "Reddit" });
1692
+ return {
1693
+ id: me.id,
1694
+ username: me.name,
1695
+ displayName: me.name,
1696
+ url: `https://www.reddit.com/user/${me.name}`
1697
+ };
1698
+ }
1699
+ async postCreate(input) {
1700
+ if (!input.title) {
1701
+ throw new Error("reddit post.create requires `title`");
1702
+ }
1703
+ if (!input.subreddit) {
1704
+ throw new Error("reddit post.create requires `subreddit`");
1705
+ }
1706
+ const kind = input.kind ?? "self";
1707
+ if (kind === "link" && !input.url) {
1708
+ throw new Error('reddit post.create with kind "link" requires `url`');
1709
+ }
1710
+ const body = this.form({
1711
+ sr: input.subreddit,
1712
+ title: input.title,
1713
+ kind,
1714
+ text: kind === "self" ? input.text : undefined,
1715
+ url: kind === "link" ? input.url : undefined,
1716
+ api_type: "json"
1717
+ });
1718
+ const res = await jsonRequest(this.fetchImpl, `${BASE}/api/submit`, {
1719
+ method: "POST",
1720
+ headers: this.headers({ "Content-Type": "application/x-www-form-urlencoded" }),
1721
+ body,
1722
+ errorLabel: "Reddit"
1723
+ });
1724
+ const errors = res.json?.errors ?? [];
1725
+ if (errors.length > 0) {
1726
+ throw new Error(`Reddit submit failed: ${JSON.stringify(errors)}`);
1727
+ }
1728
+ const data = res.json?.data ?? {};
1729
+ return { id: data.id ?? data.name ?? "", url: data.url };
1730
+ }
1731
+ async postDelete(input) {
1732
+ await jsonRequest(this.fetchImpl, `${BASE}/api/del`, {
1733
+ method: "POST",
1734
+ headers: this.headers({ "Content-Type": "application/x-www-form-urlencoded" }),
1735
+ body: this.form({ id: fullname(input.id) }),
1736
+ errorLabel: "Reddit"
1737
+ });
1738
+ return { id: input.id, deleted: true };
1739
+ }
1740
+ async mediaUpload(_input) {
1741
+ throw new ConnectorOperationNotSupported("reddit", "media.upload (image uploads require the media asset-lease flow; not supported by this SDK)");
1742
+ }
1743
+ async mentionsList(input = {}) {
1744
+ const res = await jsonRequest(this.fetchImpl, `${BASE}/message/inbox`, {
1745
+ headers: this.headers(),
1746
+ query: { limit: input.limit, after: input.sinceId },
1747
+ errorLabel: "Reddit"
1748
+ });
1749
+ const items = (res.data?.children ?? []).map((c) => ({
1750
+ id: c.data.name ?? c.data.id,
1751
+ text: c.data.body ?? "",
1752
+ authorId: c.data.author_fullname,
1753
+ authorHandle: c.data.author,
1754
+ createdAt: c.data.created_utc ? new Date(c.data.created_utc * 1000).toISOString() : undefined
1755
+ }));
1756
+ return { items };
1757
+ }
1758
+ async analyticsPost(input) {
1759
+ const res = await jsonRequest(this.fetchImpl, `${BASE}/api/info`, {
1760
+ headers: this.headers(),
1761
+ query: { id: fullname(input.id) },
1762
+ errorLabel: "Reddit"
1763
+ });
1764
+ const post = res.data?.children?.[0]?.data;
1765
+ if (!post) {
1766
+ throw new ConnectorOperationNotSupported("reddit", `analytics.post (post not found: ${input.id})`);
1767
+ }
1768
+ return {
1769
+ metrics: {
1770
+ ups: post.ups ?? 0,
1771
+ downs: post.downs ?? 0,
1772
+ score: post.score ?? 0,
1773
+ numComments: post.num_comments ?? 0,
1774
+ upvoteRatio: post.upvote_ratio ?? 0
1775
+ }
1776
+ };
1777
+ }
1778
+ }
1779
+
1780
+ // src/social/tiktok.ts
1781
+ var BASE2 = "https://open.tiktokapis.com";
1782
+
1783
+ class TikTokAdapter {
1784
+ accessToken;
1785
+ fetchImpl;
1786
+ constructor(creds, fetchImpl) {
1787
+ if (!creds || !creds.accessToken) {
1788
+ throw new Error("tiktok credentials require `accessToken`");
1789
+ }
1790
+ this.accessToken = creds.accessToken;
1791
+ this.fetchImpl = resolveFetch(fetchImpl);
1792
+ }
1793
+ static fromCredentials(creds, fetchImpl) {
1794
+ return new TikTokAdapter(creds, fetchImpl);
1795
+ }
1796
+ headers(extra) {
1797
+ return { Authorization: `Bearer ${this.accessToken}`, ...extra ?? {} };
1798
+ }
1799
+ async accountMe() {
1800
+ const res = await jsonRequest(this.fetchImpl, `${BASE2}/v2/user/info/`, {
1801
+ headers: this.headers(),
1802
+ query: { fields: "open_id,union_id,display_name,profile_deep_link" },
1803
+ errorLabel: "TikTok"
1804
+ });
1805
+ const user = res.data?.user ?? {};
1806
+ return {
1807
+ id: user.open_id ?? user.union_id ?? "",
1808
+ username: user.display_name,
1809
+ displayName: user.display_name,
1810
+ url: user.profile_deep_link
1811
+ };
1812
+ }
1813
+ async postCreate(input) {
1814
+ const videoId = input.mediaIds?.[0];
1815
+ if (!videoId) {
1816
+ throw new ConnectorOperationNotSupported("tiktok", "post.create (TikTok is video-only: provide a video media id in `mediaIds` via media.upload)");
1817
+ }
1818
+ const body = {
1819
+ post_info: {
1820
+ title: input.text ?? "",
1821
+ privacy_level: input.privacyLevel ?? "PUBLIC_TO_EVERYONE"
1822
+ },
1823
+ source_info: {
1824
+ source: "FILE_UPLOAD",
1825
+ video_id: videoId
1826
+ }
1827
+ };
1828
+ const res = await jsonRequest(this.fetchImpl, `${BASE2}/v2/post/publish/video/init/`, { method: "POST", headers: this.headers({ "Content-Type": "application/json" }), body, errorLabel: "TikTok" });
1829
+ return { id: res.data?.publish_id ?? videoId };
1830
+ }
1831
+ async postDelete(_input) {
1832
+ throw new ConnectorOperationNotSupported("tiktok", "post.delete (the Content Posting API does not expose post deletion)");
1833
+ }
1834
+ async mediaUpload(input) {
1835
+ const buffer = decodeBase64(input.dataBase64);
1836
+ const size = buffer.byteLength;
1837
+ const init = await jsonRequest(this.fetchImpl, `${BASE2}/v2/post/publish/inbox/video/init/`, {
1838
+ method: "POST",
1839
+ headers: this.headers({ "Content-Type": "application/json" }),
1840
+ body: {
1841
+ source_info: {
1842
+ source: "FILE_UPLOAD",
1843
+ video_size: size,
1844
+ chunk_size: size,
1845
+ total_chunk_count: 1
1846
+ }
1847
+ },
1848
+ errorLabel: "TikTok"
1849
+ });
1850
+ const uploadUrl = init.data?.upload_url;
1851
+ const publishId = init.data?.publish_id;
1852
+ if (!uploadUrl || !publishId) {
1853
+ throw new Error("TikTok media.upload: init did not return an upload_url/publish_id");
1854
+ }
1855
+ const bytes = Uint8Array.from(buffer);
1856
+ const put = await this.fetchImpl(uploadUrl, {
1857
+ method: "PUT",
1858
+ headers: {
1859
+ "Content-Type": input.mimeType,
1860
+ "Content-Range": `bytes 0-${size - 1}/${size}`
1861
+ },
1862
+ body: bytes
1863
+ });
1864
+ if (!put.ok) {
1865
+ throw new Error(`TikTok ${put.status}: video upload failed`);
1866
+ }
1867
+ return { mediaId: publishId };
1868
+ }
1869
+ async mentionsList(_input = {}) {
1870
+ throw new ConnectorOperationNotSupported("tiktok", "mentions.list (no mentions endpoint in the Content Posting / Display API)");
1871
+ }
1872
+ async analyticsPost(_input) {
1873
+ throw new ConnectorOperationNotSupported("tiktok", "analytics.post (per-post metrics require the Research/Business API, not available here)");
1874
+ }
1875
+ }
1876
+
1877
+ // src/social/youtube.ts
1878
+ var API = "https://www.googleapis.com/youtube/v3";
1879
+ var UPLOAD = "https://www.googleapis.com/upload/youtube/v3";
1880
+
1881
+ class YouTubeAdapter {
1882
+ accessToken;
1883
+ fetchImpl;
1884
+ constructor(creds, fetchImpl) {
1885
+ if (!creds || !creds.accessToken) {
1886
+ throw new Error("youtube credentials require `accessToken`");
1887
+ }
1888
+ this.accessToken = creds.accessToken;
1889
+ this.fetchImpl = resolveFetch(fetchImpl);
1890
+ }
1891
+ static fromCredentials(creds, fetchImpl) {
1892
+ return new YouTubeAdapter(creds, fetchImpl);
1893
+ }
1894
+ headers(extra) {
1895
+ return { Authorization: `Bearer ${this.accessToken}`, ...extra ?? {} };
1896
+ }
1897
+ async accountMe() {
1898
+ const res = await jsonRequest(this.fetchImpl, `${API}/channels`, {
1899
+ headers: this.headers(),
1900
+ query: { part: "snippet", mine: "true" },
1901
+ errorLabel: "YouTube"
1902
+ });
1903
+ const channel = res.items?.[0];
1904
+ if (!channel) {
1905
+ throw new Error("YouTube account.me: no channel found for the authenticated user");
1906
+ }
1907
+ return {
1908
+ id: channel.id,
1909
+ username: channel.snippet?.customUrl ?? channel.snippet?.title,
1910
+ displayName: channel.snippet?.title,
1911
+ url: `https://www.youtube.com/channel/${channel.id}`
1912
+ };
1913
+ }
1914
+ async postCreate(input) {
1915
+ const videoId = input.mediaIds?.[0];
1916
+ if (!videoId) {
1917
+ throw new ConnectorOperationNotSupported("youtube", "post.create (YouTube is video-only: upload a video via media.upload and pass its id in `mediaIds`)");
1918
+ }
1919
+ const title = input.title ?? input.text ?? "";
1920
+ const description = input.description ?? input.text ?? "";
1921
+ await jsonRequest(this.fetchImpl, `${API}/videos`, {
1922
+ method: "PUT",
1923
+ headers: this.headers({ "Content-Type": "application/json" }),
1924
+ query: { part: "snippet" },
1925
+ body: {
1926
+ id: videoId,
1927
+ snippet: { title, description, categoryId: input.categoryId ?? "22" }
1928
+ },
1929
+ errorLabel: "YouTube"
1930
+ });
1931
+ return { id: videoId, url: `https://www.youtube.com/watch?v=${videoId}` };
1932
+ }
1933
+ async postDelete(input) {
1934
+ await jsonRequest(this.fetchImpl, `${API}/videos`, {
1935
+ method: "DELETE",
1936
+ headers: this.headers(),
1937
+ query: { id: input.id },
1938
+ errorLabel: "YouTube"
1939
+ });
1940
+ return { id: input.id, deleted: true };
1941
+ }
1942
+ async mediaUpload(input) {
1943
+ const buffer = decodeBase64(input.dataBase64);
1944
+ const size = buffer.byteLength;
1945
+ const start = await this.fetchImpl(`${UPLOAD}/videos?uploadType=resumable&part=snippet,status`, {
1946
+ method: "POST",
1947
+ headers: this.headers({
1948
+ "Content-Type": "application/json",
1949
+ "X-Upload-Content-Type": input.mimeType,
1950
+ "X-Upload-Content-Length": String(size)
1951
+ }),
1952
+ body: JSON.stringify({
1953
+ snippet: { title: input.title ?? input.altText ?? "Untitled", description: input.description ?? "" },
1954
+ status: { privacyStatus: input.privacyStatus ?? "private" }
1955
+ })
1956
+ });
1957
+ if (!start.ok) {
1958
+ throw new Error(`YouTube ${start.status}: resumable upload init failed`);
1959
+ }
1960
+ const location = start.headers?.get("location");
1961
+ if (!location) {
1962
+ throw new Error("YouTube media.upload: resumable session did not return a Location header");
1963
+ }
1964
+ const bytes = Uint8Array.from(buffer);
1965
+ const put = await this.fetchImpl(location, {
1966
+ method: "PUT",
1967
+ headers: { "Content-Type": input.mimeType, "Content-Length": String(size) },
1968
+ body: bytes
1969
+ });
1970
+ const text = await put.text();
1971
+ const data = text ? JSON.parse(text) : {};
1972
+ if (!put.ok) {
1973
+ throw new Error(`YouTube ${put.status}: video upload failed`);
1974
+ }
1975
+ return { mediaId: String(data.id) };
1976
+ }
1977
+ async mentionsList(_input = {}) {
1978
+ throw new ConnectorOperationNotSupported("youtube", "mentions.list (no mentions endpoint in the YouTube Data API)");
1979
+ }
1980
+ async analyticsPost(input) {
1981
+ const res = await jsonRequest(this.fetchImpl, `${API}/videos`, {
1982
+ headers: this.headers(),
1983
+ query: { part: "statistics", id: input.id },
1984
+ errorLabel: "YouTube"
1985
+ });
1986
+ const stats = res.items?.[0]?.statistics;
1987
+ if (!stats) {
1988
+ throw new ConnectorOperationNotSupported("youtube", `analytics.post (video not found: ${input.id})`);
1989
+ }
1990
+ return {
1991
+ metrics: {
1992
+ viewCount: Number(stats.viewCount ?? 0),
1993
+ likeCount: Number(stats.likeCount ?? 0),
1994
+ commentCount: Number(stats.commentCount ?? 0),
1995
+ favoriteCount: Number(stats.favoriteCount ?? 0)
1996
+ }
1997
+ };
1998
+ }
1999
+ }
2000
+
2001
+ // src/social/pinterest.ts
2002
+ var BASE3 = "https://api.pinterest.com";
2003
+
2004
+ class PinterestAdapter {
2005
+ accessToken;
2006
+ defaultBoardId;
2007
+ fetchImpl;
2008
+ constructor(creds, fetchImpl) {
2009
+ if (!creds || !creds.accessToken) {
2010
+ throw new Error("pinterest credentials require `accessToken`");
2011
+ }
2012
+ this.accessToken = creds.accessToken;
2013
+ this.defaultBoardId = creds.boardId;
2014
+ this.fetchImpl = resolveFetch(fetchImpl);
2015
+ }
2016
+ static fromCredentials(creds, fetchImpl) {
2017
+ return new PinterestAdapter(creds, fetchImpl);
2018
+ }
2019
+ headers(extra) {
2020
+ return { Authorization: `Bearer ${this.accessToken}`, ...extra ?? {} };
2021
+ }
2022
+ async accountMe() {
2023
+ const acct = await jsonRequest(this.fetchImpl, `${BASE3}/v5/user_account`, { headers: this.headers(), errorLabel: "Pinterest" });
2024
+ return {
2025
+ id: acct.username ?? "",
2026
+ username: acct.username,
2027
+ displayName: acct.username,
2028
+ url: acct.username ? `https://www.pinterest.com/${acct.username}/` : undefined
2029
+ };
2030
+ }
2031
+ async postCreate(input) {
2032
+ const boardId = input.boardId ?? this.defaultBoardId;
2033
+ if (!boardId) {
2034
+ throw new Error("pinterest post.create requires `boardId` (in input or credentials)");
2035
+ }
2036
+ const mediaId = input.mediaIds?.[0];
2037
+ let mediaSource;
2038
+ if (mediaId) {
2039
+ mediaSource = { source_type: "image_upload", media_id: mediaId };
2040
+ } else if (input.imageUrl) {
2041
+ mediaSource = { source_type: "image_url", url: input.imageUrl };
2042
+ } else {
2043
+ throw new Error("pinterest post.create requires an image: provide a `mediaIds` entry or `imageUrl`");
2044
+ }
2045
+ const body = {
2046
+ board_id: boardId,
2047
+ description: input.text,
2048
+ media_source: mediaSource
2049
+ };
2050
+ if (input.title)
2051
+ body.title = input.title;
2052
+ if (input.link)
2053
+ body.link = input.link;
2054
+ const pin = await jsonRequest(this.fetchImpl, `${BASE3}/v5/pins`, {
2055
+ method: "POST",
2056
+ headers: this.headers({ "Content-Type": "application/json" }),
2057
+ body,
2058
+ errorLabel: "Pinterest"
2059
+ });
2060
+ return { id: pin.id, url: `https://www.pinterest.com/pin/${pin.id}/` };
2061
+ }
2062
+ async postDelete(input) {
2063
+ await jsonRequest(this.fetchImpl, `${BASE3}/v5/pins/${encodeURIComponent(input.id)}`, {
2064
+ method: "DELETE",
2065
+ headers: this.headers(),
2066
+ errorLabel: "Pinterest"
2067
+ });
2068
+ return { id: input.id, deleted: true };
2069
+ }
2070
+ async mediaUpload(input) {
2071
+ const register = await jsonRequest(this.fetchImpl, `${BASE3}/v5/media`, {
2072
+ method: "POST",
2073
+ headers: this.headers({ "Content-Type": "application/json" }),
2074
+ body: { media_type: "image" },
2075
+ errorLabel: "Pinterest"
2076
+ });
2077
+ const buffer = decodeBase64(input.dataBase64);
2078
+ const bytes = Uint8Array.from(buffer);
2079
+ const form = new FormData;
2080
+ for (const [k, v] of Object.entries(register.upload_parameters ?? {})) {
2081
+ form.append(k, v);
2082
+ }
2083
+ form.append("file", new Blob([bytes], { type: input.mimeType }));
2084
+ const put = await this.fetchImpl(register.upload_url, { method: "POST", body: form });
2085
+ if (!put.ok) {
2086
+ throw new Error(`Pinterest ${put.status}: media upload failed`);
2087
+ }
2088
+ return { mediaId: register.media_id };
2089
+ }
2090
+ async mentionsList(_input = {}) {
2091
+ throw new ConnectorOperationNotSupported("pinterest", "mentions.list (no mentions endpoint in the Pinterest API)");
2092
+ }
2093
+ async analyticsPost(input) {
2094
+ const res = await jsonRequest(this.fetchImpl, `${BASE3}/v5/pins/${encodeURIComponent(input.id)}/analytics`, {
2095
+ headers: this.headers(),
2096
+ query: { metric_types: "IMPRESSION,PIN_CLICK,SAVE,OUTBOUND_CLICK" },
2097
+ errorLabel: "Pinterest"
2098
+ });
2099
+ const summary = res.all?.["DAILY"]?.summary_metrics ?? res.all?.["TOTAL"]?.summary_metrics ?? {};
2100
+ const metrics = {};
2101
+ for (const [k, v] of Object.entries(summary)) {
2102
+ metrics[k] = typeof v === "number" ? v : Number(v) || 0;
2103
+ }
2104
+ return { metrics };
2105
+ }
2106
+ }
2107
+
2108
+ // src/social/googlebusinessprofile.ts
2109
+ var ACCT_MGMT = "https://mybusinessaccountmanagement.googleapis.com/v1";
2110
+ var V4 = "https://mybusiness.googleapis.com/v4";
2111
+
2112
+ class GoogleBusinessProfileAdapter {
2113
+ accessToken;
2114
+ defaultAccountId;
2115
+ defaultLocationId;
2116
+ fetchImpl;
2117
+ constructor(creds, fetchImpl) {
2118
+ if (!creds || !creds.accessToken) {
2119
+ throw new Error("googlebusinessprofile credentials require `accessToken`");
2120
+ }
2121
+ this.accessToken = creds.accessToken;
2122
+ this.defaultAccountId = creds.accountId;
2123
+ this.defaultLocationId = creds.locationId;
2124
+ this.fetchImpl = resolveFetch(fetchImpl);
2125
+ }
2126
+ static fromCredentials(creds, fetchImpl) {
2127
+ return new GoogleBusinessProfileAdapter(creds, fetchImpl);
2128
+ }
2129
+ headers(extra) {
2130
+ return { Authorization: `Bearer ${this.accessToken}`, ...extra ?? {} };
2131
+ }
2132
+ resolveIds(input) {
2133
+ const accountId = input.accountId ?? this.defaultAccountId;
2134
+ const locationId = input.locationId ?? this.defaultLocationId;
2135
+ if (!accountId)
2136
+ throw new Error("googlebusinessprofile requires `accountId` (in input or credentials)");
2137
+ if (!locationId)
2138
+ throw new Error("googlebusinessprofile requires `locationId` (in input or credentials)");
2139
+ return { accountId, locationId };
2140
+ }
2141
+ async accountMe() {
2142
+ const res = await jsonRequest(this.fetchImpl, `${ACCT_MGMT}/accounts`, { headers: this.headers(), errorLabel: "GoogleBusinessProfile" });
2143
+ const account = res.accounts?.[0];
2144
+ if (!account) {
2145
+ throw new Error("googlebusinessprofile account.me: no accounts found for the authenticated user");
2146
+ }
2147
+ const id = (account.name ?? "").replace(/^accounts\//, "");
2148
+ return {
2149
+ id,
2150
+ username: account.accountName,
2151
+ displayName: account.accountName,
2152
+ url: undefined
2153
+ };
2154
+ }
2155
+ async postCreate(input) {
2156
+ const { accountId, locationId } = this.resolveIds(input);
2157
+ const parent = `accounts/${accountId}/locations/${locationId}`;
2158
+ const body = {
2159
+ languageCode: "en-US",
2160
+ summary: input.text,
2161
+ topicType: "STANDARD"
2162
+ };
2163
+ if (input.cta) {
2164
+ body.callToAction = { actionType: input.cta.actionType, url: input.cta.url };
2165
+ }
2166
+ if (input.mediaIds && input.mediaIds.length > 0) {
2167
+ body.media = input.mediaIds.map((m) => ({ mediaFormat: "PHOTO", sourceUrl: m }));
2168
+ }
2169
+ const post = await jsonRequest(this.fetchImpl, `${V4}/${parent}/localPosts`, { method: "POST", headers: this.headers({ "Content-Type": "application/json" }), body, errorLabel: "GoogleBusinessProfile" });
2170
+ return { id: post.name, url: post.searchUrl };
2171
+ }
2172
+ async postDelete(input) {
2173
+ await jsonRequest(this.fetchImpl, `${V4}/${input.id}`, {
2174
+ method: "DELETE",
2175
+ headers: this.headers(),
2176
+ errorLabel: "GoogleBusinessProfile"
2177
+ });
2178
+ return { id: input.id, deleted: true };
2179
+ }
2180
+ async mediaUpload(input) {
2181
+ if (!input.sourceUrl) {
2182
+ throw new ConnectorOperationNotSupported("googlebusinessprofile", "media.upload (provide a public `sourceUrl`; raw binary upload uses a separate resumable service)");
2183
+ }
2184
+ const { accountId, locationId } = this.resolveIds(input);
2185
+ const parent = `accounts/${accountId}/locations/${locationId}`;
2186
+ const media = await jsonRequest(this.fetchImpl, `${V4}/${parent}/media`, {
2187
+ method: "POST",
2188
+ headers: this.headers({ "Content-Type": "application/json" }),
2189
+ body: {
2190
+ mediaFormat: "PHOTO",
2191
+ locationAssociation: { category: "ADDITIONAL" },
2192
+ sourceUrl: input.sourceUrl
2193
+ },
2194
+ errorLabel: "GoogleBusinessProfile"
2195
+ });
2196
+ return { mediaId: media.name };
2197
+ }
2198
+ async mentionsList(_input = {}) {
2199
+ throw new ConnectorOperationNotSupported("googlebusinessprofile", "mentions.list (no mentions concept; reviews are a separate surface)");
2200
+ }
2201
+ async analyticsPost(_input) {
2202
+ throw new ConnectorOperationNotSupported("googlebusinessprofile", "analytics.post (per-localPost insights are not exposed; use location-level Performance API)");
2203
+ }
2204
+ }
2205
+
2206
+ // src/social/index.ts
2207
+ var SUPPORTED_CONNECTORS = [
2208
+ "x",
2209
+ "mastodon",
2210
+ "bluesky",
2211
+ "linkedin",
2212
+ "reddit",
2213
+ "tiktok",
2214
+ "youtube",
2215
+ "pinterest",
2216
+ "googlebusinessprofile"
2217
+ ];
2218
+ var ALL_OPS = [
2219
+ "account.me",
2220
+ "post.create",
2221
+ "post.delete",
2222
+ "media.upload",
2223
+ "mentions.list",
2224
+ "analytics.post"
2225
+ ];
2226
+ var SUPPORTED_OPERATIONS = {
2227
+ x: [...ALL_OPS],
2228
+ mastodon: [...ALL_OPS],
2229
+ bluesky: [...ALL_OPS],
2230
+ linkedin: ["account.me", "post.create", "post.delete", "media.upload"],
2231
+ reddit: ["account.me", "post.create", "post.delete", "mentions.list", "analytics.post"],
2232
+ tiktok: ["account.me", "post.create", "media.upload"],
2233
+ youtube: ["account.me", "post.create", "post.delete", "media.upload", "analytics.post"],
2234
+ pinterest: ["account.me", "post.create", "post.delete", "media.upload", "analytics.post"],
2235
+ googlebusinessprofile: ["account.me", "post.create", "post.delete", "media.upload"]
2236
+ };
2237
+ function listSocialConnectors() {
2238
+ return [...SUPPORTED_CONNECTORS];
2239
+ }
2240
+ function getSocialOperations(connector) {
2241
+ const ops = SUPPORTED_OPERATIONS[connector];
2242
+ if (!ops) {
2243
+ throw new ConnectorOperationNotSupported(connector, "*");
2244
+ }
2245
+ return [...ops];
2246
+ }
2247
+ function buildAdapter(connector, credentials) {
2248
+ switch (connector) {
2249
+ case "x":
2250
+ return XAdapter.fromCredentials(credentials);
2251
+ case "mastodon":
2252
+ return MastodonAdapter.fromCredentials(credentials);
2253
+ case "bluesky":
2254
+ return BlueskyAdapter.fromCredentials(credentials);
2255
+ case "linkedin":
2256
+ return LinkedInAdapter.fromCredentials(credentials);
2257
+ case "reddit":
2258
+ return RedditAdapter.fromCredentials(credentials);
2259
+ case "tiktok":
2260
+ return TikTokAdapter.fromCredentials(credentials);
2261
+ case "youtube":
2262
+ return YouTubeAdapter.fromCredentials(credentials);
2263
+ case "pinterest":
2264
+ return PinterestAdapter.fromCredentials(credentials);
2265
+ case "googlebusinessprofile":
2266
+ return GoogleBusinessProfileAdapter.fromCredentials(credentials);
2267
+ default:
2268
+ throw new ConnectorOperationNotSupported(connector, "*");
2269
+ }
2270
+ }
2271
+ async function runSocialOperation(args) {
2272
+ const { connector, operation, input = {}, credentials = {} } = args;
2273
+ if (!SUPPORTED_CONNECTORS.includes(connector)) {
2274
+ throw new ConnectorOperationNotSupported(connector, operation);
2275
+ }
2276
+ const supported = SUPPORTED_OPERATIONS[connector] ?? [];
2277
+ if (!supported.includes(operation)) {
2278
+ throw new ConnectorOperationNotSupported(connector, operation);
2279
+ }
2280
+ const adapter = buildAdapter(connector, credentials);
2281
+ switch (operation) {
2282
+ case "account.me":
2283
+ return adapter.accountMe(input);
2284
+ case "post.create":
2285
+ return adapter.postCreate(input);
2286
+ case "post.delete":
2287
+ return adapter.postDelete(input);
2288
+ case "media.upload":
2289
+ return adapter.mediaUpload(input);
2290
+ case "mentions.list":
2291
+ return adapter.mentionsList(input);
2292
+ case "analytics.post":
2293
+ return adapter.analyticsPost(input);
2294
+ default:
2295
+ throw new ConnectorOperationNotSupported(connector, operation);
2296
+ }
2297
+ }
2298
+ export {
2299
+ runSocialOperation,
2300
+ listSocialConnectors,
2301
+ getSocialOperations,
2302
+ ConnectorOperationNotSupported
2303
+ };