@opencoredev/social-sdk 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/cli-request.d.ts +15 -0
  2. package/dist/cli-request.js +193 -0
  3. package/dist/cli.d.ts +4 -3
  4. package/dist/cli.js +19 -21
  5. package/dist/cloud/common.d.ts +7 -6
  6. package/dist/cloud/common.js +35 -54
  7. package/dist/cloud/lifecycle.js +31 -35
  8. package/dist/cloud/media.d.ts +2 -2
  9. package/dist/cloud/media.js +13 -3
  10. package/dist/cloud/outcomes.d.ts +4 -3
  11. package/dist/cloud/outcomes.js +8 -15
  12. package/dist/cloud/post-for-me.js +41 -49
  13. package/dist/cloud/zernio.js +58 -98
  14. package/dist/core/client.js +79 -99
  15. package/dist/core/fields.d.ts +14 -0
  16. package/dist/core/fields.js +14 -0
  17. package/dist/core/idempotency.d.ts +7 -2
  18. package/dist/core/idempotency.js +37 -20
  19. package/dist/core/pagination.js +8 -7
  20. package/dist/core/types.d.ts +3 -2
  21. package/dist/platforms/bluesky.d.ts +65 -1
  22. package/dist/platforms/bluesky.js +675 -276
  23. package/dist/platforms/instagram.d.ts +2 -0
  24. package/dist/platforms/instagram.js +130 -105
  25. package/dist/platforms/linkedin.d.ts +58 -1
  26. package/dist/platforms/linkedin.js +877 -107
  27. package/dist/platforms/threads.d.ts +13 -1
  28. package/dist/platforms/threads.js +204 -302
  29. package/dist/platforms/tiktok.d.ts +4 -0
  30. package/dist/platforms/tiktok.js +140 -124
  31. package/dist/platforms/webhook-adapter.d.ts +9 -0
  32. package/dist/platforms/webhook-adapter.js +24 -0
  33. package/dist/platforms/x-engagement.js +7 -12
  34. package/dist/platforms/x-stream.d.ts +83 -0
  35. package/dist/platforms/x-stream.js +350 -0
  36. package/dist/platforms/x.d.ts +87 -0
  37. package/dist/platforms/x.js +648 -120
  38. package/dist/platforms/youtube-upload.d.ts +1 -1
  39. package/dist/platforms/youtube-upload.js +6 -2
  40. package/dist/platforms/youtube.d.ts +28 -4
  41. package/dist/platforms/youtube.js +291 -133
  42. package/dist/server/bluesky-oauth.d.ts +177 -0
  43. package/dist/server/bluesky-oauth.js +1229 -0
  44. package/dist/server/connections.d.ts +14 -0
  45. package/dist/server/connections.js +10 -2
  46. package/dist/server/egress.d.ts +14 -0
  47. package/dist/server/egress.js +115 -0
  48. package/dist/server/oauth-internal.d.ts +6 -0
  49. package/dist/server/oauth-internal.js +66 -0
  50. package/dist/server/oauth.d.ts +1 -1
  51. package/dist/server/oauth.js +46 -99
  52. package/dist/server/webhooks.d.ts +136 -3
  53. package/dist/server/webhooks.js +639 -25
  54. package/dist/testing/index.js +14 -28
  55. package/dist/transport/http.d.ts +1 -1
  56. package/dist/transport/http.js +0 -1
  57. package/dist/transport/json.d.ts +7 -0
  58. package/dist/transport/json.js +32 -4
  59. package/dist/transport/upload.d.ts +1 -1
  60. package/dist/transport/upload.js +46 -38
  61. package/dist/transport/validation.d.ts +16 -5
  62. package/dist/transport/validation.js +29 -7
  63. package/package.json +2 -2
@@ -9,6 +9,8 @@ export interface InstagramOptions {
9
9
  readonly fetch?: typeof globalThis.fetch;
10
10
  readonly clock?: () => Date;
11
11
  readonly workflowStore?: InstagramWorkflowStore;
12
+ /** App secret that Meta uses to sign webhook deliveries (`X-Hub-Signature-256`). */
13
+ readonly webhookSecret?: string;
12
14
  }
13
15
  export interface InstagramNative {
14
16
  readonly moderateComment: (input: {
@@ -1,11 +1,13 @@
1
- /* oxlint-disable anti-slop/no-conditional-empty-object-spread, anti-slop/no-known-value-widening, anti-slop/no-runtime-typeof, anti-slop/no-unsafe-dictionary-type, anti-slop/require-readable-spacing -- provider query and response boundaries are validated locally. */
2
1
  import { defineAdapter } from "../core/adapter.js";
3
2
  import { SocialError } from "../core/errors.js";
4
3
  import { profileRef } from "../core/types.js";
5
- import { managedHttp, publicFields } from "../cloud/common.js";
4
+ import { managedHttp, optionsObject, publicFields } from "../cloud/common.js";
6
5
  import { HttpError } from "../transport/http.js";
7
- import { array, object, optionalNumber, optionalString, string } from "../transport/validation.js";
6
+ import { definedFields } from "../core/fields.js";
7
+ import { array, isBoolean, isJsonObject, isString, object, optionalNumber, optionalString, string, } from "../transport/validation.js";
8
8
  import { httpsUrl } from "../transport/upload.js";
9
+ import { verifyMetaWebhook } from "../server/webhooks.js";
10
+ import { directWebhooks, webhookCapability } from "./webhook-adapter.js";
9
11
  class MemoryInstagramWorkflowStore {
10
12
  workflows = new Map();
11
13
  claims = new Set();
@@ -43,12 +45,10 @@ export function instagram(options) {
43
45
  : `https://graph.instagram.com/${apiVersion}`;
44
46
  const request = managedHttp(origin, {
45
47
  apiKey: options.auth.accessToken,
46
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
47
- ...(options.fetch ? { fetch: options.fetch } : {}),
48
+ ...definedFields({ fetch: options.fetch }),
48
49
  });
49
50
  const now = () => (options.clock?.() ?? new Date()).toISOString();
50
51
  const workflows = options.workflowStore ?? new MemoryInstagramWorkflowStore();
51
- // oxlint-disable-next-line anti-slop/no-unknown-parameters -- provider payload is validated here.
52
52
  const validatedObject = (value, operation) => {
53
53
  try {
54
54
  return object(value);
@@ -73,6 +73,47 @@ export function instagram(options) {
73
73
  retryDisposition: { kind: "never" },
74
74
  });
75
75
  };
76
+ // Meta's Instagram Login mentions guide lists only GET /{ig-user-id}/tags and
77
+ // POST /{ig-user-id}/mentions. The mentioned_media and mentioned_comment field
78
+ // expansions are documented for Facebook Login only. Sources, accessed 2026-09-24
79
+ // against Graph API v25.0:
80
+ // https://developers.facebook.com/documentation/instagram-platform/instagram-api-with-instagram-login/mentions
81
+ // https://developers.facebook.com/documentation/instagram-platform/instagram-api-with-facebook-login/mentions
82
+ // https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user/tags
83
+ // https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user/mentioned_media
84
+ const requireFacebookLoginForMentionLookup = (field) => {
85
+ if (flavor !== "facebook-login")
86
+ throw new SocialError({
87
+ code: "unsupported_capability",
88
+ operation: "instagram.mentions.read",
89
+ message: `Instagram ${field} lookups require a Facebook Login Graph API access token. Instagram Login supports only the /{ig-user-id}/tags mentions edge.`,
90
+ retryDisposition: { kind: "never" },
91
+ });
92
+ };
93
+ // Meta documents `DELETE /{ig-media-id}` for Instagram API with Facebook Login only.
94
+ // Source: https://developers.facebook.com/docs/instagram-platform/reference/instagram-media
95
+ // (accessed 2026-09-24, Graph API v25.0).
96
+ const deleteMedia = async (mediaId, context, operation) => {
97
+ if (flavor !== "facebook-login")
98
+ throw new SocialError({
99
+ code: "unsupported_capability",
100
+ operation,
101
+ message: `${operation} is not supported with Instagram Login. Meta supports media deletion only through Instagram API with Facebook Login.`,
102
+ retryDisposition: { kind: "never" },
103
+ });
104
+ const result = await request(`/${encodeURIComponent(mediaId)}`, context, undefined, {}, "DELETE");
105
+ // SAFETY: request validates the Graph response as a JSON field before returning it.
106
+ const confirmed = isJsonObject(result)
107
+ ? result["success"]
108
+ : undefined;
109
+ if (confirmed !== true)
110
+ throw new SocialError({
111
+ code: "ambiguous_outcome",
112
+ operation,
113
+ message: "Instagram did not confirm the media deletion. Reconcile before retrying.",
114
+ retryDisposition: { kind: "reconcile-first" },
115
+ });
116
+ };
76
117
  const pageLimit = (limit, max = 50) => {
77
118
  const value = limit ?? 25;
78
119
  if (!Number.isSafeInteger(value) || value < 1 || value > max)
@@ -88,12 +129,10 @@ export function instagram(options) {
88
129
  const paging = result["paging"] === undefined ? {} : object(result["paging"]);
89
130
  const cursors = paging["cursors"] === undefined ? {} : object(paging["cursors"]);
90
131
  // Graph API omits `paging.next` on the last page even when `cursors.after` is present.
91
- const nextCursor = typeof paging["next"] === "string" &&
92
- typeof cursors["after"] === "string" &&
93
- cursors["after"].length > 0
132
+ const nextCursor = isString(paging["next"]) && isString(cursors["after"]) && cursors["after"].length > 0
94
133
  ? cursors["after"]
95
134
  : undefined;
96
- return { items, ...(nextCursor === undefined ? {} : { nextCursor }) };
135
+ return { items, ...definedFields({ nextCursor }) };
97
136
  };
98
137
  const authorize = (ref, context) => {
99
138
  if (ref.backend !== context.backendInstance ||
@@ -117,14 +156,13 @@ export function instagram(options) {
117
156
  operation: "posts.list",
118
157
  message: "Instagram feed limit must be an integer from 1 through 100.",
119
158
  });
120
- // oxlint-disable-next-line anti-slop/no-known-value-widening -- validated boundary or fixture contract.
121
159
  const query = {
122
160
  fields: "id,caption,media_type,media_product_type,permalink,timestamp,username",
161
+ ...definedFields({
162
+ after: input.cursor,
163
+ limit: input.limit === undefined ? undefined : String(input.limit),
164
+ }),
123
165
  };
124
- if (input.cursor !== undefined)
125
- query["after"] = input.cursor;
126
- if (input.limit !== undefined)
127
- query["limit"] = String(input.limit);
128
166
  const result = object(await request(`/${encodeURIComponent(selected.accountId)}/media`, context, undefined, query));
129
167
  const items = array(result["data"]).map((entry) => publicFields(entry, [
130
168
  "id",
@@ -137,15 +175,11 @@ export function instagram(options) {
137
175
  ]));
138
176
  const paging = result["paging"] === undefined ? {} : object(result["paging"]);
139
177
  const cursors = paging["cursors"] === undefined ? {} : object(paging["cursors"]);
140
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
141
- const hasNext = typeof paging["next"] === "string" && paging["next"].length > 0;
142
- const nextCursor =
143
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
144
- hasNext && typeof cursors["after"] === "string" ? cursors["after"] : undefined;
178
+ const hasNext = isString(paging["next"]) && paging["next"].length > 0;
179
+ const nextCursor = hasNext ? optionalString(cursors["after"]) : undefined;
145
180
  return {
146
181
  items,
147
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
148
- ...(nextCursor === undefined ? {} : { nextCursor }),
182
+ ...definedFields({ nextCursor }),
149
183
  };
150
184
  };
151
185
  const getAccountMetrics = async (selected, context) => {
@@ -241,7 +275,6 @@ export function instagram(options) {
241
275
  await workflows.update(workflowId, { stage: "unknown" });
242
276
  let result;
243
277
  try {
244
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
245
278
  result = object(await request(`/${encodeURIComponent(account.accountId)}/media_publish`, context, {
246
279
  creation_id: containerId,
247
280
  }));
@@ -360,13 +393,12 @@ export function instagram(options) {
360
393
  };
361
394
  const listMentions = async ({ account, cursor, limit, context, }) => {
362
395
  authorize(account, context);
363
- requireFacebookLogin("instagram.mentions.read");
396
+ // Both login flavors expose GET /{ig-user-id}/tags; only the host and scopes differ.
364
397
  const query = {
365
398
  fields: "id,caption,media_type,media_product_type,permalink,timestamp,username",
366
399
  limit: String(pageLimit(limit)),
400
+ ...definedFields({ after: cursor }),
367
401
  };
368
- if (cursor !== undefined)
369
- query["after"] = cursor;
370
402
  const result = validatedObject(await request(`/${encodeURIComponent(account.accountId)}/tags`, context, undefined, query), "instagram.mentions.read");
371
403
  return page(result, [
372
404
  "id",
@@ -407,6 +439,18 @@ export function instagram(options) {
407
439
  : ["instagram_business_basic", "instagram_business_content_publish"],
408
440
  notes: "Professional accounts, public HTTPS media, explicit native continuation for processing containers. Carousels must have matching aspect ratios to avoid upstream cropping.",
409
441
  },
442
+ {
443
+ platform: "instagram",
444
+ operation: "posts.schedule",
445
+ availability: "unsupported-by-platform",
446
+ notes: "The content publishing API has no publish-time parameter; media_publish publishes immediately and unpublished containers expire after 24 hours.",
447
+ },
448
+ {
449
+ platform: "instagram",
450
+ operation: "posts.update",
451
+ availability: "unsupported-by-platform",
452
+ notes: "Instagram Graph API does not provide an edit endpoint for published media.",
453
+ },
410
454
  ...[
411
455
  "accounts.read",
412
456
  "posts.list",
@@ -463,21 +507,24 @@ export function instagram(options) {
463
507
  { platform: "instagram", operation: "stories.publish", availability: "available" },
464
508
  {
465
509
  platform: "instagram",
466
- operation: "posts.delete",
467
- availability: flavor === "facebook-login"
468
- ? "available"
469
- : "not-implemented-by-adapter",
470
- ...(flavor === "facebook-login"
471
- ? { requiredScopes: ["instagram_basic", "pages_read_engagement"] }
472
- : {}),
473
- },
474
- {
475
- platform: "instagram",
476
- operation: "posts.removeFromPlatform",
477
- availability: flavor === "facebook-login"
478
- ? "available"
479
- : "not-implemented-by-adapter",
510
+ operation: "profile.update",
511
+ availability: "unsupported-by-platform",
512
+ notes: "The IG User node supports reads only. Updating it is not supported.",
480
513
  },
514
+ ...["posts.delete", "posts.removeFromPlatform"].map((operation) => flavor === "facebook-login"
515
+ ? {
516
+ platform: "instagram",
517
+ operation,
518
+ availability: "available",
519
+ requiredScopes: ["instagram_basic", "instagram_manage_contents"],
520
+ notes: "Deletes non-ad posts, Stories, Reels, and whole carousel albums through DELETE /{ig-media-id}. Individual carousel children cannot be deleted.",
521
+ }
522
+ : {
523
+ platform: "instagram",
524
+ operation,
525
+ availability: "unsupported-by-platform",
526
+ notes: "Meta documents media deletion for Instagram API with Facebook Login only. See https://developers.facebook.com/docs/instagram-platform/reference/instagram-media",
527
+ }),
481
528
  ...(flavor === "facebook-login"
482
529
  ? [
483
530
  {
@@ -494,27 +541,17 @@ export function instagram(options) {
494
541
  operation: "publishing.limit.read",
495
542
  availability: "available",
496
543
  },
497
- ...(flavor === "facebook-login"
498
- ? [
499
- {
500
- platform: "instagram",
501
- operation: "mentions.read",
502
- availability: "available",
503
- requiredScopes: [
504
- "instagram_basic",
505
- "instagram_manage_comments",
506
- "pages_read_engagement",
507
- ],
508
- notes: "Reads the paginated /{ig-user-id}/tags edge.",
509
- },
510
- ]
511
- : [
512
- {
513
- platform: "instagram",
514
- operation: "mentions.read",
515
- availability: "not-implemented-by-adapter",
516
- },
517
- ]),
544
+ {
545
+ platform: "instagram",
546
+ operation: "mentions.read",
547
+ availability: "available",
548
+ requiredScopes: flavor === "facebook-login"
549
+ ? ["instagram_basic", "instagram_manage_comments", "pages_read_engagement"]
550
+ : ["instagram_business_basic", "instagram_business_manage_comments"],
551
+ notes: flavor === "facebook-login"
552
+ ? "Reads the paginated /{ig-user-id}/tags edge. Native mentionedMedia and mentionedComment look up single @mentions. Story mentions and private media are not returned."
553
+ : "Reads the paginated /{ig-user-id}/tags edge on graph.instagram.com. mentionedMedia and mentionedComment require Facebook Login. Story mentions and private media are not returned.",
554
+ },
518
555
  {
519
556
  platform: "instagram",
520
557
  operation: "product.tagging",
@@ -525,8 +562,16 @@ export function instagram(options) {
525
562
  operation: "messages.read",
526
563
  availability: "approval-dependent",
527
564
  },
565
+ webhookCapability("instagram", "Verifies Meta X-Hub-Signature-256 with the app secret and decodes object=instagram deliveries. Answer the GET handshake with answerMetaWebhookChallenge."),
566
+ {
567
+ platform: "instagram",
568
+ operation: "notifications.read",
569
+ availability: "unsupported-by-platform",
570
+ notes: "The Instagram API has no notifications edge. Use mentions.read, tags, comments, or webhooks instead.",
571
+ },
528
572
  ],
529
573
  },
574
+ webhooks: directWebhooks("instagram", (input) => verifyMetaWebhook({ ...input, secret: options.webhookSecret ?? "" }), now),
530
575
  accounts: {
531
576
  async list(_input, context) {
532
577
  return { items: [await readAccount(context)] };
@@ -558,6 +603,8 @@ export function instagram(options) {
558
603
  catch {
559
604
  fail("instagram.url", "Use public HTTPS media without local hosts or credentials.");
560
605
  }
606
+ if (item.kind === "document")
607
+ fail("instagram.document", "Instagram publishing accepts image and video media only.");
561
608
  if (item.kind === "image" && item.mimeType !== "image/jpeg")
562
609
  fail("instagram.jpeg", "Instagram image publishing requires JPEG media.");
563
610
  if (item.kind === "video" &&
@@ -596,29 +643,21 @@ export function instagram(options) {
596
643
  operation: "posts.publish",
597
644
  message: "Public HTTPS media required.",
598
645
  });
599
- const config = target.options === undefined ? {} : object(target.options);
646
+ const config = optionsObject(target);
647
+ const shareToFeed = config["shareToFeed"];
600
648
  await workflows.update(workflow.id, { stage: "unknown" });
601
649
  const created = object(await request(`/${encodeURIComponent(target.account.accountId)}/media`, context, {
602
650
  ...(item.kind === "image"
603
651
  ? {
604
652
  image_url: item.source.url,
605
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
606
- ...(item.altText === undefined ? {} : { alt_text: item.altText }),
653
+ ...definedFields({ alt_text: item.altText }),
607
654
  }
608
655
  : {
609
656
  video_url: item.source.url,
610
657
  media_type: media.length > 1 ? "VIDEO" : "REELS",
611
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
612
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
613
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
614
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
615
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
616
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
617
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
618
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
619
- ...(typeof config["shareToFeed"] === "boolean"
620
- ? { share_to_feed: config["shareToFeed"] }
621
- : {}),
658
+ ...definedFields({
659
+ share_to_feed: isBoolean(shareToFeed) ? shareToFeed : undefined,
660
+ }),
622
661
  }),
623
662
  ...(media.length > 1
624
663
  ? { is_carousel_item: true }
@@ -739,8 +778,7 @@ export function instagram(options) {
739
778
  },
740
779
  async removeFromPlatform(ref, context) {
741
780
  authorize(ref, context);
742
- requireFacebookLogin("instagram.posts.removeFromPlatform");
743
- await request(`/${encodeURIComponent(ref.postId)}`, context, undefined, {}, "DELETE");
781
+ await deleteMedia(ref.postId, context, "instagram.posts.removeFromPlatform");
744
782
  },
745
783
  },
746
784
  comments: {
@@ -749,9 +787,8 @@ export function instagram(options) {
749
787
  const query = {
750
788
  fields: "id,text,timestamp,username",
751
789
  limit: String(pageLimit(input.limit)),
790
+ ...definedFields({ after: input.cursor }),
752
791
  };
753
- if (input.cursor !== undefined)
754
- query["after"] = input.cursor;
755
792
  const result = validatedObject(await request(`/${encodeURIComponent(ref.postId)}/comments`, context, undefined, query), "instagram.comments.read");
756
793
  return page(result, ["id", "text", "timestamp", "username"]);
757
794
  },
@@ -855,12 +892,12 @@ export function instagram(options) {
855
892
  accountId: account.accountId,
856
893
  profileId: id,
857
894
  }),
858
- ...(typeof profile["name"] === "string" ? { displayName: profile["name"] } : {}),
859
- ...(typeof profile["username"] === "string" ? { handle: profile["username"] } : {}),
860
- ...(typeof profile["profile_picture_url"] === "string"
861
- ? { avatarUrl: profile["profile_picture_url"] }
862
- : {}),
863
- ...(typeof profile["biography"] === "string" ? { bio: profile["biography"] } : {}),
895
+ ...definedFields({
896
+ displayName: optionalString(profile["name"]),
897
+ handle: optionalString(profile["username"]),
898
+ avatarUrl: optionalString(profile["profile_picture_url"]),
899
+ bio: optionalString(profile["biography"]),
900
+ }),
864
901
  native: profile,
865
902
  };
866
903
  },
@@ -883,37 +920,32 @@ export function instagram(options) {
883
920
  const query = {
884
921
  fields: "id,text,timestamp,username",
885
922
  limit: String(pageLimit(limit)),
923
+ ...definedFields({ after: cursor }),
886
924
  };
887
- if (cursor !== undefined)
888
- query["after"] = cursor;
889
925
  return page(validatedObject(await request(`/${encodeURIComponent(commentId)}/replies`, context, undefined, query), "instagram.comments.replies.read"), ["id", "text", "timestamp", "username"]);
890
926
  },
891
927
  listMentions,
892
928
  async mentionedMedia({ account, mediaId, context }) {
893
929
  authorize(account, context);
894
- requireFacebookLogin("instagram.mentions.read");
895
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated provider object boundary.
930
+ requireFacebookLoginForMentionLookup("mentioned_media");
896
931
  return validatedObject(await request(`/${encodeURIComponent(account.accountId)}`, context, undefined, {
897
932
  fields: `mentioned_media.media_id(${encodeURIComponent(mediaId)}){id,caption,media_type,media_url,timestamp,username,comments_count,like_count}`,
898
933
  }), "instagram.mentions.read");
899
934
  },
900
935
  async mentionedComment({ account, commentId, context }) {
901
936
  authorize(account, context);
902
- requireFacebookLogin("instagram.mentions.read");
903
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated provider object boundary.
937
+ requireFacebookLoginForMentionLookup("mentioned_comment");
904
938
  return validatedObject(await request(`/${encodeURIComponent(account.accountId)}`, context, undefined, {
905
939
  fields: `mentioned_comment.comment_id(${encodeURIComponent(commentId)}){id,text,timestamp,like_count,media}`,
906
940
  }), "instagram.mentions.read");
907
941
  },
908
942
  async listTaggedMedia({ account, cursor, limit, context }) {
909
943
  authorize(account, context);
910
- requireFacebookLogin("instagram.mentions.read");
911
944
  const query = {
912
945
  fields: "id,caption,media_type,permalink,timestamp,username",
913
946
  limit: String(pageLimit(limit)),
947
+ ...definedFields({ after: cursor }),
914
948
  };
915
- if (cursor !== undefined)
916
- query["after"] = cursor;
917
949
  return page(validatedObject(await request(`/${encodeURIComponent(account.accountId)}/tags`, context, undefined, query), "instagram.mentions.read"), ["id", "caption", "media_type", "permalink", "timestamp", "username"]);
918
950
  },
919
951
  async hashtagMedia({ account, hashtagId, kind, cursor, limit, context }) {
@@ -922,9 +954,8 @@ export function instagram(options) {
922
954
  const query = {
923
955
  fields: "id,caption,media_type,permalink,timestamp,username",
924
956
  limit: String(pageLimit(limit, 50)),
957
+ ...definedFields({ after: cursor }),
925
958
  };
926
- if (cursor !== undefined)
927
- query["after"] = cursor;
928
959
  return page(validatedObject(await request(`/${encodeURIComponent(hashtagId)}/${kind}_media`, context, undefined, query), "instagram.hashtags.search"), ["id", "caption", "media_type", "permalink", "timestamp", "username"]);
929
960
  },
930
961
  async businessDiscovery({ account, username, fields, context }) {
@@ -938,7 +969,6 @@ export function instagram(options) {
938
969
  });
939
970
  const selectedFields = fields ??
940
971
  `business_discovery.username(${username}){id,username,name,biography,followers_count,media_count,profile_picture_url,media.limit(25){id,caption,media_type,permalink,timestamp}}`;
941
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated provider object boundary.
942
972
  return object(await request(`/${encodeURIComponent(account.accountId)}`, context, undefined, {
943
973
  fields: selectedFields,
944
974
  }));
@@ -973,8 +1003,7 @@ export function instagram(options) {
973
1003
  const created = object(await request(`/${encodeURIComponent(account.accountId)}/media`, context, {
974
1004
  media_type: "REELS",
975
1005
  video_url: videoUrl,
976
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
977
- ...(caption ? { caption } : {}),
1006
+ ...definedFields({ caption: caption === "" ? undefined : caption }),
978
1007
  }));
979
1008
  return publishContainer(account, string(created["id"]), context);
980
1009
  },
@@ -988,8 +1017,7 @@ export function instagram(options) {
988
1017
  },
989
1018
  async deletePost({ account, postId, context }) {
990
1019
  authorize(account, context);
991
- requireFacebookLogin("instagram.posts.delete");
992
- await request(`/${encodeURIComponent(postId)}`, context, undefined, {}, "DELETE");
1020
+ await deleteMedia(postId, context, "instagram.posts.delete");
993
1021
  },
994
1022
  async hashtagSearch({ account, hashtag, context }) {
995
1023
  authorize(account, context);
@@ -1014,15 +1042,12 @@ export function instagram(options) {
1014
1042
  },
1015
1043
  async publishingLimit({ account, context }) {
1016
1044
  authorize(account, context);
1017
- // SAFETY: object() establishes an object response; this native method intentionally
1018
- // preserves Meta's provider-specific config object after validating its outer shape.
1019
1045
  return object(await request(`/${encodeURIComponent(account.accountId)}/content_publishing_limit`, context, undefined, { fields: "config,quota_usage" }));
1020
1046
  },
1021
1047
  async mentions({ account, cursor, limit, context }) {
1022
1048
  return listMentions({
1023
1049
  account,
1024
- ...(cursor === undefined ? {} : { cursor }),
1025
- ...(limit === undefined ? {} : { limit }),
1050
+ ...definedFields({ cursor, limit }),
1026
1051
  context,
1027
1052
  });
1028
1053
  },
@@ -1,4 +1,4 @@
1
- import type { AdapterOperationContext, ConnectedAccountRef, JsonObject, MediaRef } from "../core/types.js";
1
+ import type { AdapterOperationContext, ConnectedAccountRef, JsonObject, MediaRef, Page } from "../core/types.js";
2
2
  export interface LinkedInOptions {
3
3
  readonly auth: {
4
4
  readonly accessToken: string;
@@ -8,6 +8,8 @@ export interface LinkedInOptions {
8
8
  readonly apiVersion: string;
9
9
  readonly fetch?: typeof globalThis.fetch;
10
10
  readonly clock?: () => Date;
11
+ /** App client secret that LinkedIn uses to sign webhook deliveries (`X-LI-Signature`). */
12
+ readonly webhookSecret?: string;
11
13
  }
12
14
  export interface LinkedInTimeInterval {
13
15
  readonly granularity: "DAY" | "WEEK" | "MONTH";
@@ -44,8 +46,45 @@ export interface LinkedInShareStatistics {
44
46
  readonly interval?: LinkedInTimeInterval | undefined;
45
47
  readonly metrics: Readonly<Record<string, number>>;
46
48
  }
49
+ /** One approved organization role returned by LinkedIn's `organizationAcls` roleAssignee finder. */
50
+ export interface LinkedInOrganizationRole {
51
+ readonly organization: `urn:li:organization:${string}`;
52
+ readonly role: "ADMINISTRATOR";
53
+ readonly state: "APPROVED";
54
+ }
47
55
  export interface LinkedInNative {
56
+ /**
57
+ * Lists organizations the authenticated member administers (approved `ADMINISTRATOR` roles).
58
+ * Requires `rw_organization_admin` or `r_organization_admin`. Use the result to configure
59
+ * another adapter instance; this instance still acts only as its configured author.
60
+ */
61
+ readonly listAdministeredOrganizations: (input: {
62
+ readonly account: ConnectedAccountRef;
63
+ readonly cursor?: string;
64
+ readonly limit?: number;
65
+ readonly context: AdapterOperationContext;
66
+ }) => Promise<Page<LinkedInOrganizationRole>>;
48
67
  readonly imageStatus: (ref: MediaRef, context: AdapterOperationContext) => Promise<JsonObject>;
68
+ /**
69
+ * Reads one uploaded video's owner and processing status with a single GET. Call it again later
70
+ * while the status is PROCESSING or WAITING_UPLOAD, or use `waitForVideo`. Other methods never
71
+ * poll on their own.
72
+ */
73
+ readonly videoStatus: (ref: MediaRef, context: AdapterOperationContext) => Promise<JsonObject>;
74
+ /**
75
+ * Opt-in bounded wait for one uploaded video. Reads the status like `videoStatus`, and while it is
76
+ * PROCESSING or WAITING_UPLOAD waits `intervalMs` and reads again, up to `maxChecks` reads. It
77
+ * stops early when the next wait would exceed the context's elapsed budget, and throws
78
+ * `cancelled` when the context signal aborts. Returns the last status it read, which may still be
79
+ * PROCESSING; it never throws for slow processing and never creates a post.
80
+ */
81
+ readonly waitForVideo: (ref: MediaRef, context: AdapterOperationContext, options?: LinkedInVideoWaitOptions) => Promise<JsonObject>;
82
+ /** Reads `id`, `owner` and `status` (`WAITING_UPLOAD`, `PROCESSING`, `AVAILABLE`, `PROCESSING_FAILED`). */
83
+ readonly documentStatus: (ref: MediaRef, context: AdapterOperationContext) => Promise<JsonObject>;
84
+ /**
85
+ * @deprecated Upload video bytes with `social.media.upload` and check processing with
86
+ * `videoStatus`. This method always throws `unsupported_capability`.
87
+ */
49
88
  readonly registerVideo: (input: {
50
89
  readonly account: ConnectedAccountRef;
51
90
  readonly byteSize: number;
@@ -80,6 +119,18 @@ export interface LinkedInNative {
80
119
  readonly postId: string;
81
120
  readonly context: AdapterOperationContext;
82
121
  }) => Promise<void>;
122
+ /**
123
+ * Deletes a comment with the Comments API. `postId` is the share or ugcPost URN, and
124
+ * `commentId` is the complete `commentUrn` from comment reads. Organization authors are sent
125
+ * as the `actor`. LinkedIn does not document which comments an actor may delete, so expect
126
+ * only the configured author's own comments to succeed.
127
+ */
128
+ readonly deleteComment: (input: {
129
+ readonly account: ConnectedAccountRef;
130
+ readonly postId: string;
131
+ readonly commentId: string;
132
+ readonly context: AdapterOperationContext;
133
+ }) => Promise<void>;
83
134
  readonly organizationAnalytics: (input: {
84
135
  readonly account: ConnectedAccountRef;
85
136
  readonly query?: JsonObject;
@@ -105,4 +156,10 @@ export interface LinkedInNative {
105
156
  readonly context: AdapterOperationContext;
106
157
  }) => Promise<number | undefined>;
107
158
  }
159
+ export interface LinkedInVideoWaitOptions {
160
+ /** Delay between status reads. Integer from 1,000 to 60,000 ms; defaults to 5,000. */
161
+ readonly intervalMs?: number;
162
+ /** Maximum status reads, including the first. Integer from 1 to 60; defaults to 12. */
163
+ readonly maxChecks?: number;
164
+ }
108
165
  export declare function linkedin(options: LinkedInOptions): import("../core/adapter.js").SocialAdapter<LinkedInNative>;