@opencoredev/social-sdk 0.2.0 → 0.3.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 (157) hide show
  1. package/dist/cli.d.ts +0 -1
  2. package/dist/cli.js +0 -1
  3. package/dist/cloud/common.d.ts +0 -1
  4. package/dist/cloud/common.js +0 -1
  5. package/dist/cloud/lifecycle.d.ts +0 -1
  6. package/dist/cloud/lifecycle.js +0 -1
  7. package/dist/cloud/media.d.ts +0 -1
  8. package/dist/cloud/media.js +0 -1
  9. package/dist/cloud/outcomes.d.ts +0 -1
  10. package/dist/cloud/outcomes.js +0 -1
  11. package/dist/cloud/post-for-me.d.ts +0 -1
  12. package/dist/cloud/post-for-me.js +0 -1
  13. package/dist/cloud/zernio.d.ts +0 -1
  14. package/dist/cloud/zernio.js +0 -1
  15. package/dist/core/adapter.d.ts +0 -1
  16. package/dist/core/adapter.js +0 -1
  17. package/dist/core/client.d.ts +0 -1
  18. package/dist/core/client.js +0 -1
  19. package/dist/core/concurrency.d.ts +0 -1
  20. package/dist/core/concurrency.js +0 -1
  21. package/dist/core/errors.d.ts +0 -1
  22. package/dist/core/errors.js +0 -1
  23. package/dist/core/idempotency.d.ts +0 -1
  24. package/dist/core/idempotency.js +0 -1
  25. package/dist/core/index.d.ts +0 -1
  26. package/dist/core/index.js +0 -1
  27. package/dist/core/pagination.d.ts +0 -1
  28. package/dist/core/pagination.js +0 -1
  29. package/dist/core/types.d.ts +0 -1
  30. package/dist/core/types.js +0 -1
  31. package/dist/index.d.ts +0 -1
  32. package/dist/index.js +0 -1
  33. package/dist/platforms/bluesky.d.ts +0 -1
  34. package/dist/platforms/bluesky.js +0 -1
  35. package/dist/platforms/instagram.d.ts +0 -1
  36. package/dist/platforms/instagram.js +0 -1
  37. package/dist/platforms/linkedin.d.ts +0 -1
  38. package/dist/platforms/linkedin.js +0 -1
  39. package/dist/platforms/threads.d.ts +0 -1
  40. package/dist/platforms/threads.js +0 -1
  41. package/dist/platforms/tiktok.d.ts +0 -1
  42. package/dist/platforms/tiktok.js +0 -1
  43. package/dist/platforms/x-engagement.d.ts +0 -1
  44. package/dist/platforms/x-engagement.js +0 -1
  45. package/dist/platforms/x-text.d.ts +2 -0
  46. package/dist/platforms/x-text.js +123 -0
  47. package/dist/platforms/x-tlds.d.ts +1 -0
  48. package/dist/platforms/x-tlds.js +2 -0
  49. package/dist/platforms/x.d.ts +15 -1
  50. package/dist/platforms/x.js +339 -21
  51. package/dist/platforms/youtube-upload.d.ts +0 -1
  52. package/dist/platforms/youtube-upload.js +0 -1
  53. package/dist/platforms/youtube.d.ts +0 -1
  54. package/dist/platforms/youtube.js +0 -1
  55. package/dist/server/connections.d.ts +0 -1
  56. package/dist/server/connections.js +0 -1
  57. package/dist/server/credentials.d.ts +0 -1
  58. package/dist/server/credentials.js +0 -1
  59. package/dist/server/index.d.ts +0 -1
  60. package/dist/server/index.js +0 -1
  61. package/dist/server/oauth.d.ts +0 -1
  62. package/dist/server/oauth.js +0 -1
  63. package/dist/server/webhooks.d.ts +0 -1
  64. package/dist/server/webhooks.js +0 -1
  65. package/dist/testing/index.d.ts +0 -1
  66. package/dist/testing/index.js +0 -1
  67. package/dist/testing.d.ts +0 -1
  68. package/dist/testing.js +0 -1
  69. package/dist/transport/binary.d.ts +0 -1
  70. package/dist/transport/binary.js +0 -1
  71. package/dist/transport/budget.d.ts +0 -1
  72. package/dist/transport/budget.js +0 -1
  73. package/dist/transport/http.d.ts +0 -1
  74. package/dist/transport/http.js +0 -1
  75. package/dist/transport/json.d.ts +0 -1
  76. package/dist/transport/json.js +0 -1
  77. package/dist/transport/upload.d.ts +0 -1
  78. package/dist/transport/upload.js +0 -1
  79. package/dist/transport/validation.d.ts +0 -1
  80. package/dist/transport/validation.js +0 -1
  81. package/package.json +1 -7
  82. package/dist/cli.d.ts.map +0 -1
  83. package/dist/cli.js.map +0 -1
  84. package/dist/cloud/common.d.ts.map +0 -1
  85. package/dist/cloud/common.js.map +0 -1
  86. package/dist/cloud/lifecycle.d.ts.map +0 -1
  87. package/dist/cloud/lifecycle.js.map +0 -1
  88. package/dist/cloud/media.d.ts.map +0 -1
  89. package/dist/cloud/media.js.map +0 -1
  90. package/dist/cloud/outcomes.d.ts.map +0 -1
  91. package/dist/cloud/outcomes.js.map +0 -1
  92. package/dist/cloud/post-for-me.d.ts.map +0 -1
  93. package/dist/cloud/post-for-me.js.map +0 -1
  94. package/dist/cloud/zernio.d.ts.map +0 -1
  95. package/dist/cloud/zernio.js.map +0 -1
  96. package/dist/core/adapter.d.ts.map +0 -1
  97. package/dist/core/adapter.js.map +0 -1
  98. package/dist/core/client.d.ts.map +0 -1
  99. package/dist/core/client.js.map +0 -1
  100. package/dist/core/concurrency.d.ts.map +0 -1
  101. package/dist/core/concurrency.js.map +0 -1
  102. package/dist/core/errors.d.ts.map +0 -1
  103. package/dist/core/errors.js.map +0 -1
  104. package/dist/core/idempotency.d.ts.map +0 -1
  105. package/dist/core/idempotency.js.map +0 -1
  106. package/dist/core/index.d.ts.map +0 -1
  107. package/dist/core/index.js.map +0 -1
  108. package/dist/core/pagination.d.ts.map +0 -1
  109. package/dist/core/pagination.js.map +0 -1
  110. package/dist/core/types.d.ts.map +0 -1
  111. package/dist/core/types.js.map +0 -1
  112. package/dist/index.d.ts.map +0 -1
  113. package/dist/index.js.map +0 -1
  114. package/dist/platforms/bluesky.d.ts.map +0 -1
  115. package/dist/platforms/bluesky.js.map +0 -1
  116. package/dist/platforms/instagram.d.ts.map +0 -1
  117. package/dist/platforms/instagram.js.map +0 -1
  118. package/dist/platforms/linkedin.d.ts.map +0 -1
  119. package/dist/platforms/linkedin.js.map +0 -1
  120. package/dist/platforms/threads.d.ts.map +0 -1
  121. package/dist/platforms/threads.js.map +0 -1
  122. package/dist/platforms/tiktok.d.ts.map +0 -1
  123. package/dist/platforms/tiktok.js.map +0 -1
  124. package/dist/platforms/x-engagement.d.ts.map +0 -1
  125. package/dist/platforms/x-engagement.js.map +0 -1
  126. package/dist/platforms/x.d.ts.map +0 -1
  127. package/dist/platforms/x.js.map +0 -1
  128. package/dist/platforms/youtube-upload.d.ts.map +0 -1
  129. package/dist/platforms/youtube-upload.js.map +0 -1
  130. package/dist/platforms/youtube.d.ts.map +0 -1
  131. package/dist/platforms/youtube.js.map +0 -1
  132. package/dist/server/connections.d.ts.map +0 -1
  133. package/dist/server/connections.js.map +0 -1
  134. package/dist/server/credentials.d.ts.map +0 -1
  135. package/dist/server/credentials.js.map +0 -1
  136. package/dist/server/index.d.ts.map +0 -1
  137. package/dist/server/index.js.map +0 -1
  138. package/dist/server/oauth.d.ts.map +0 -1
  139. package/dist/server/oauth.js.map +0 -1
  140. package/dist/server/webhooks.d.ts.map +0 -1
  141. package/dist/server/webhooks.js.map +0 -1
  142. package/dist/testing/index.d.ts.map +0 -1
  143. package/dist/testing/index.js.map +0 -1
  144. package/dist/testing.d.ts.map +0 -1
  145. package/dist/testing.js.map +0 -1
  146. package/dist/transport/binary.d.ts.map +0 -1
  147. package/dist/transport/binary.js.map +0 -1
  148. package/dist/transport/budget.d.ts.map +0 -1
  149. package/dist/transport/budget.js.map +0 -1
  150. package/dist/transport/http.d.ts.map +0 -1
  151. package/dist/transport/http.js.map +0 -1
  152. package/dist/transport/json.d.ts.map +0 -1
  153. package/dist/transport/json.js.map +0 -1
  154. package/dist/transport/upload.d.ts.map +0 -1
  155. package/dist/transport/upload.js.map +0 -1
  156. package/dist/transport/validation.d.ts.map +0 -1
  157. package/dist/transport/validation.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  /* oxlint-disable anti-slop/no-conditional-empty-object-spread, anti-slop/no-runtime-typeof, anti-slop/require-readable-spacing, anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract. */
2
2
  import { remainingBudget } from "../transport/budget.js";
3
- import twitterText from "twitter-text";
3
+ import { isValidXText } from "./x-text.js";
4
4
  import { defineAdapter } from "../core/adapter.js";
5
5
  import { connectedAccountRef, profileRef } from "../core/types.js";
6
6
  import { SocialError } from "../core/errors.js";
@@ -396,6 +396,267 @@ export function x(options) {
396
396
  });
397
397
  return string(data["id"]);
398
398
  }
399
+ const xChunkBytes = 1024 * 1024;
400
+ const xMaxVideoBytes = 512 * 1024 * 1024;
401
+ const xMaxGifBytes = 15 * 1024 * 1024;
402
+ const xMaxStatusPolls = 30;
403
+ function chunkedCategory(media) {
404
+ if (media.source.kind !== "blob")
405
+ return undefined;
406
+ if (media.kind === "video" && media.mimeType === "video/mp4")
407
+ return "tweet_video";
408
+ if (media.kind === "image" && media.mimeType === "image/gif")
409
+ return "tweet_gif";
410
+ return undefined;
411
+ }
412
+ function chunkedLimits(media, category) {
413
+ const size = media.source.kind === "blob" ? media.source.blob.size : 0;
414
+ const maxBytes = category === "tweet_video" ? xMaxVideoBytes : xMaxGifBytes;
415
+ if (size <= 0 || size > maxBytes)
416
+ throw new SocialError({
417
+ code: "invalid_input",
418
+ operation: "media.upload",
419
+ message: category === "tweet_video"
420
+ ? "X video uploads require a non-empty MP4 Blob up to 512 MiB."
421
+ : "X GIF uploads require a non-empty GIF Blob up to 15 MiB.",
422
+ });
423
+ }
424
+ function mediaHttpError(error) {
425
+ if (error.kind === "cancelled")
426
+ return new SocialError({
427
+ code: "cancelled",
428
+ operation: "media.upload",
429
+ message: "X media upload was cancelled before completion.",
430
+ upstreamStatus: error.status,
431
+ });
432
+ if (error.kind === "timeout")
433
+ return new SocialError({
434
+ code: "timeout",
435
+ operation: "media.upload",
436
+ message: "X media upload exceeded its elapsed budget. Reconcile before retrying.",
437
+ upstreamStatus: error.status,
438
+ retryDisposition: { kind: "never" },
439
+ });
440
+ if (error.status === 401)
441
+ return new SocialError({
442
+ code: "reconnect_required",
443
+ operation: "media.upload",
444
+ message: "X rejected the media upload credentials. Reconnect before retrying.",
445
+ upstreamStatus: error.status,
446
+ retryDisposition: { kind: "after-reconnect" },
447
+ });
448
+ if (error.status === 429)
449
+ return new SocialError({
450
+ code: "rate_limited",
451
+ operation: "media.upload",
452
+ message: "X rate-limited the media upload.",
453
+ upstreamStatus: error.status,
454
+ retryDisposition: error.retryAfterMs === undefined
455
+ ? { kind: "never" }
456
+ : { kind: "after-delay", delayMs: error.retryAfterMs },
457
+ });
458
+ if (error.status === 413)
459
+ return new SocialError({
460
+ code: "media_error",
461
+ operation: "media.upload",
462
+ message: "Media chunk rejected by X (payload too large).",
463
+ upstreamStatus: error.status,
464
+ retryDisposition: { kind: "never" },
465
+ });
466
+ return new SocialError({
467
+ code: "media_error",
468
+ operation: "media.upload",
469
+ message: error.message,
470
+ upstreamStatus: error.status,
471
+ retryDisposition: { kind: "never" },
472
+ });
473
+ }
474
+ async function chunkedPost(path, body, context) {
475
+ requireUserToken("media.upload");
476
+ let result;
477
+ try {
478
+ result = await http({
479
+ url: new URL(`https://api.x.com${path}`),
480
+ method: "POST",
481
+ headers: body === undefined
482
+ ? { Authorization: `Bearer ${options.auth.accessToken}` }
483
+ : {
484
+ Authorization: `Bearer ${options.auth.accessToken}`,
485
+ "Content-Type": "application/json",
486
+ },
487
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- FINALIZE carries no body.
488
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
489
+ timeoutMs: remainingBudget(context),
490
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
491
+ ...(context.signal ? { signal: context.signal } : {}),
492
+ });
493
+ }
494
+ catch (error) {
495
+ if (!(error instanceof HttpError))
496
+ throw error;
497
+ throw mediaHttpError(error);
498
+ }
499
+ return object(object(result)["data"]);
500
+ }
501
+ async function appendChunk(mediaId, segmentIndex, chunk, context) {
502
+ requireUserToken("media.upload");
503
+ const body = new FormData();
504
+ body.set("segment_index", String(segmentIndex));
505
+ body.set("media", chunk, `chunk-${segmentIndex}`);
506
+ try {
507
+ await http({
508
+ url: new URL(`https://api.x.com/2/media/upload/${encodeURIComponent(mediaId)}/append`),
509
+ method: "POST",
510
+ headers: { Authorization: `Bearer ${options.auth.accessToken}` },
511
+ body,
512
+ timeoutMs: remainingBudget(context),
513
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
514
+ ...(context.signal ? { signal: context.signal } : {}),
515
+ });
516
+ }
517
+ catch (error) {
518
+ if (!(error instanceof HttpError))
519
+ throw error;
520
+ throw mediaHttpError(error);
521
+ }
522
+ }
523
+ async function readMediaStatus(mediaId, context) {
524
+ requireUserToken("media.upload");
525
+ let result;
526
+ try {
527
+ result = await http({
528
+ url: new URL(`https://api.x.com/2/media/upload?command=STATUS&media_id=${encodeURIComponent(mediaId)}`),
529
+ method: "GET",
530
+ headers: { Authorization: `Bearer ${options.auth.accessToken}` },
531
+ timeoutMs: remainingBudget(context),
532
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
533
+ ...(context.signal ? { signal: context.signal } : {}),
534
+ });
535
+ }
536
+ catch (error) {
537
+ if (!(error instanceof HttpError))
538
+ throw error;
539
+ throw mediaHttpError(error);
540
+ }
541
+ const data = object(object(result)["data"]);
542
+ const processing = data["processing_info"];
543
+ if (processing === undefined)
544
+ return { state: "succeeded", checkAfterSecs: 0 };
545
+ return processingState(object(processing));
546
+ }
547
+ function processingState(info) {
548
+ const checkAfter = optionalNumber(info["check_after_secs"]);
549
+ return {
550
+ state: optionalString(info["state"]) ?? "pending",
551
+ // X always sends check_after_secs while processing; a missing value must not spin the poll.
552
+ checkAfterSecs: checkAfter !== undefined && Number.isFinite(checkAfter) && checkAfter >= 0 ? checkAfter : 1,
553
+ };
554
+ }
555
+ function uploadCancelled() {
556
+ return new SocialError({
557
+ code: "cancelled",
558
+ operation: "media.upload",
559
+ message: "X media upload was cancelled before completion.",
560
+ });
561
+ }
562
+ function throwIfUploadCancelled(context) {
563
+ if (context.signal?.aborted)
564
+ throw uploadCancelled();
565
+ }
566
+ /** Waits for X's processing hint without outliving the shared operation budget. */
567
+ function processingWait(milliseconds, context) {
568
+ throwIfUploadCancelled(context);
569
+ if (milliseconds <= 0)
570
+ return Promise.resolve();
571
+ if (milliseconds >= remainingBudget(context))
572
+ throw new SocialError({
573
+ code: "timeout",
574
+ operation: "media.upload",
575
+ message: "X media processing needs longer than the remaining elapsed budget. No post was created.",
576
+ retryDisposition: { kind: "never" },
577
+ });
578
+ return new Promise((resolve, reject) => {
579
+ const onAbort = () => {
580
+ clearTimeout(timer);
581
+ reject(uploadCancelled());
582
+ };
583
+ const timer = setTimeout(() => {
584
+ context.signal?.removeEventListener("abort", onAbort);
585
+ resolve();
586
+ }, milliseconds);
587
+ context.signal?.addEventListener("abort", onAbort, { once: true });
588
+ });
589
+ }
590
+ async function uploadVideoOrGif(media, context) {
591
+ try {
592
+ return await chunkedUpload(media, context);
593
+ }
594
+ catch (error) {
595
+ // Malformed upload responses fail before any post request, so they are definite failures.
596
+ if (error instanceof HttpError)
597
+ throw mediaHttpError(error);
598
+ throw error;
599
+ }
600
+ }
601
+ async function chunkedUpload(media, context) {
602
+ requireUserToken("media.upload");
603
+ const category = chunkedCategory(media);
604
+ if (category === undefined || media.source.kind !== "blob")
605
+ throw new SocialError({
606
+ code: "invalid_input",
607
+ operation: "media.upload",
608
+ message: "X chunked upload requires a video/mp4 or image/gif Blob.",
609
+ });
610
+ chunkedLimits(media, category);
611
+ const blob = media.source.blob;
612
+ const totalBytes = blob.size;
613
+ const initialized = await chunkedPost("/2/media/upload/initialize", {
614
+ media_category: category,
615
+ media_type: category === "tweet_video" ? "video/mp4" : "image/gif",
616
+ total_bytes: totalBytes,
617
+ }, context);
618
+ const mediaId = string(initialized["id"]);
619
+ const segmentCount = Math.ceil(totalBytes / xChunkBytes);
620
+ for (let segmentIndex = 0; segmentIndex < segmentCount; segmentIndex++) {
621
+ throwIfUploadCancelled(context);
622
+ const start = segmentIndex * xChunkBytes;
623
+ const end = Math.min(start + xChunkBytes, totalBytes);
624
+ const chunk = blob.slice(start, end, media.mimeType ?? "");
625
+ await appendChunk(mediaId, segmentIndex, chunk, context);
626
+ }
627
+ const finalized = await chunkedPost(`/2/media/upload/${encodeURIComponent(mediaId)}/finalize`, undefined, context);
628
+ const finalizedId = optionalString(finalized["id"]) ?? mediaId;
629
+ const processing = finalized["processing_info"];
630
+ if (processing === undefined)
631
+ return finalizedId;
632
+ let { state, checkAfterSecs } = processingState(object(processing));
633
+ for (let poll = 0;; poll++) {
634
+ if (state === "succeeded")
635
+ return finalizedId;
636
+ if (state === "failed")
637
+ throw new SocialError({
638
+ code: "media_error",
639
+ operation: "media.upload",
640
+ message: "X failed to process the uploaded media. No post was created.",
641
+ retryDisposition: { kind: "never" },
642
+ });
643
+ if (poll >= xMaxStatusPolls)
644
+ break;
645
+ await processingWait(checkAfterSecs * 1000, context);
646
+ ({ state, checkAfterSecs } = await readMediaStatus(finalizedId, context));
647
+ }
648
+ throw new SocialError({
649
+ code: "timeout",
650
+ operation: "media.upload",
651
+ message: "X media processing did not complete in time. Reconcile before retrying.",
652
+ retryDisposition: { kind: "never" },
653
+ });
654
+ }
655
+ async function uploadXMedia(media, context) {
656
+ if (chunkedCategory(media) !== undefined)
657
+ return uploadVideoOrGif(media, context);
658
+ return uploadImage(media, context);
659
+ }
399
660
  async function createPost(account, text, context, extra = {}, targetIndex = 0) {
400
661
  authorize(account, context);
401
662
  const result = object(await request("/2/tweets", context, { text, ...extra }));
@@ -435,9 +696,9 @@ export function x(options) {
435
696
  platform: "x",
436
697
  operation: "posts.publish",
437
698
  availability: "available",
438
- formats: ["text", "image"],
699
+ formats: ["text", "image", "video"],
439
700
  requiredScopes: ["tweet.read", "tweet.write", "users.read", "media.write"],
440
- notes: "User-context OAuth2 token; current X API access/billing required. Up to four static JPEG/PNG image Blobs, each at most 5 MiB. Video/GIF processing is outside this slice.",
701
+ notes: "User-context OAuth2 token; current X API access/billing required. Up to four static JPEG/PNG image Blobs, each at most 5 MiB, or one MP4 video up to 512 MiB / one GIF up to 15 MiB via 1 MiB chunked upload with bounded processing poll. Post attach can still reject over-duration video with 403.",
441
702
  },
442
703
  ...[
443
704
  "accounts.read",
@@ -570,13 +831,17 @@ export function x(options) {
570
831
  {
571
832
  platform: "x",
572
833
  operation: "media.video",
573
- availability: "not-implemented-by-adapter",
834
+ availability: "available",
574
835
  formats: ["video"],
836
+ requiredScopes: ["tweet.read", "tweet.write", "users.read", "media.write"],
837
+ notes: "MP4 Blob chunked INIT/APPEND/FINALIZE with 1 MiB segments and bounded STATUS poll honoring check_after_secs.",
575
838
  },
576
839
  {
577
840
  platform: "x",
578
841
  operation: "media.gif",
579
- availability: "not-implemented-by-adapter",
842
+ availability: "available",
843
+ requiredScopes: ["tweet.read", "tweet.write", "users.read", "media.write"],
844
+ notes: "GIF Blob chunked upload; large GIFs process asynchronously before attach.",
580
845
  },
581
846
  {
582
847
  platform: "x",
@@ -745,7 +1010,7 @@ export function x(options) {
745
1010
  if (target.account.platform !== "x" || target.account.accountId !== options.auth.userId)
746
1011
  fail("x.account", "Select the configured X user.");
747
1012
  const text = target.content.text ?? "";
748
- if (text && !twitterText.parseTweet(text).valid)
1013
+ if (text && !isValidXText(text))
749
1014
  fail("x.text", "Text exceeds X's weighted 280-character limit or contains invalid characters.");
750
1015
  if (target.schedule || target.content.link)
751
1016
  fail("x.operation", "Scheduling needs an application runner; place URLs explicitly in text.");
@@ -761,10 +1026,28 @@ export function x(options) {
761
1026
  !["everyone", "following", "mentionedUsers"].includes(String(settings["replySettings"]))))
762
1027
  fail("x.options", "Provide only a supported replySettings value.");
763
1028
  const media = target.content.media ?? [];
764
- if (media.length > 4)
1029
+ const hasChunked = media.some((item) => chunkedCategory(item) !== undefined);
1030
+ if (hasChunked && media.length !== 1)
1031
+ fail("x.media_count", "Attach a single video or GIF per post.");
1032
+ else if (!hasChunked && media.length > 4)
765
1033
  fail("x.media_count", "Attach up to four images.");
766
1034
  for (const item of media) {
767
- if (item.kind !== "image" ||
1035
+ const category = chunkedCategory(item);
1036
+ if (category === "tweet_video") {
1037
+ if (item.source.kind !== "blob" || item.source.blob.size === 0)
1038
+ fail("x.video_size", "Video must be a non-empty MP4 Blob.");
1039
+ else if (item.source.blob.size > xMaxVideoBytes)
1040
+ fail("x.video_size", "Video exceeds the 512 MiB limit.");
1041
+ }
1042
+ else if (category === "tweet_gif") {
1043
+ if (item.source.kind !== "blob" || item.source.blob.size === 0)
1044
+ fail("x.gif_size", "GIF must be a non-empty Blob.");
1045
+ else if (item.source.blob.size > xMaxGifBytes)
1046
+ fail("x.gif_size", "GIF exceeds the 15 MiB limit.");
1047
+ }
1048
+ else if (item.kind === "video")
1049
+ fail("x.video", "X video uploads require a video/mp4 Blob.");
1050
+ else if (item.kind !== "image" ||
768
1051
  item.source.kind !== "blob" ||
769
1052
  !["image/jpeg", "image/png"].includes(item.mimeType ?? ""))
770
1053
  fail("x.image", "This slice requires a JPEG or PNG image Blob.");
@@ -779,18 +1062,36 @@ export function x(options) {
779
1062
  authorize(target.account, context);
780
1063
  const ids = [];
781
1064
  for (const media of target.content.media ?? [])
782
- ids.push(await uploadImage(media, context));
1065
+ ids.push(await uploadXMedia(media, context));
783
1066
  const settings = target.options === undefined ? {} : object(target.options);
784
- return createPost(target.account, target.content.text ?? "", context, {
785
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
786
- ...(ids.length ? { media: { media_ids: ids } } : {}),
787
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
788
- ...(target.replyTo ? { reply: { in_reply_to_tweet_id: target.replyTo.postId } } : {}),
789
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
790
- ...(settings["replySettings"] && settings["replySettings"] !== "everyone"
791
- ? { reply_settings: string(settings["replySettings"]) }
792
- : {}),
793
- }, target.targetIndex);
1067
+ const hasVideo = (target.content.media ?? []).some((media) => chunkedCategory(media) === "tweet_video");
1068
+ try {
1069
+ return await createPost(target.account, target.content.text ?? "", context, {
1070
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
1071
+ ...(ids.length ? { media: { media_ids: ids } } : {}),
1072
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
1073
+ ...(target.replyTo ? { reply: { in_reply_to_tweet_id: target.replyTo.postId } } : {}),
1074
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
1075
+ ...(settings["replySettings"] && settings["replySettings"] !== "everyone"
1076
+ ? { reply_settings: string(settings["replySettings"]) }
1077
+ : {}),
1078
+ }, target.targetIndex);
1079
+ }
1080
+ catch (error) {
1081
+ // X checks video duration only at attach time and answers 403. The transport drops the
1082
+ // response body, so the adapter cannot tell a duration limit from a missing permission.
1083
+ if (hasVideo && error instanceof SocialError && error.upstreamStatus === 403)
1084
+ throw new SocialError({
1085
+ code: "missing_permission",
1086
+ operation: error.operation,
1087
+ backend: error.backend,
1088
+ correlationId: error.correlationId,
1089
+ message: "X rejected the post with its attached video (HTTP 403). The video may exceed this account's duration limit, or the token may lack post permission. No post was created.",
1090
+ upstreamStatus: 403,
1091
+ retryDisposition: { kind: "never" },
1092
+ });
1093
+ throw error;
1094
+ }
794
1095
  },
795
1096
  async get(ref, context) {
796
1097
  return publicFields(await readPost(ref, context), [
@@ -824,7 +1125,7 @@ export function x(options) {
824
1125
  },
825
1126
  async reply(ref, content, context) {
826
1127
  await validateReplyParent(ref, context);
827
- if (!twitterText.parseTweet(content.text).valid)
1128
+ if (!isValidXText(content.text))
828
1129
  throw new SocialError({
829
1130
  code: "invalid_input",
830
1131
  operation: "comments.write",
@@ -1003,6 +1304,24 @@ export function x(options) {
1003
1304
  authorize(account, context);
1004
1305
  await request(`/2/tweets/${encodeURIComponent(postId)}`, context, undefined, {}, "DELETE");
1005
1306
  },
1307
+ async uploadVideo({ account, video, context }) {
1308
+ authorize(account, context);
1309
+ const media = {
1310
+ kind: "video",
1311
+ mimeType: "video/mp4",
1312
+ source: { kind: "blob", blob: video, fingerprint: "native-upload" },
1313
+ };
1314
+ return { mediaId: await uploadVideoOrGif(media, context) };
1315
+ },
1316
+ async uploadGif({ account, gif, context }) {
1317
+ authorize(account, context);
1318
+ const media = {
1319
+ kind: "image",
1320
+ mimeType: "image/gif",
1321
+ source: { kind: "blob", blob: gif, fingerprint: "native-upload" },
1322
+ };
1323
+ return { mediaId: await uploadVideoOrGif(media, context) };
1324
+ },
1006
1325
  async createPoll({ account, text, options: pollOptions, durationMinutes, context }) {
1007
1326
  authorize(account, context);
1008
1327
  if (pollOptions.length < 2 ||
@@ -1285,4 +1604,3 @@ export function x(options) {
1285
1604
  });
1286
1605
  return adapter;
1287
1606
  }
1288
- //# sourceMappingURL=x.js.map
@@ -30,4 +30,3 @@ export type YouTubeUploadStatus = {
30
30
  export declare function queryYouTubeUpload(session: YouTubeUploadSession, options: YouTubeUploadOptions): Promise<YouTubeUploadStatus>;
31
31
  /** Explicit resumption reopens the caller's replayable source and skips confirmed bytes. */
32
32
  export declare function sendYouTubeUpload(session: YouTubeUploadSession, media: MediaAttachment, options: YouTubeUploadOptions, startByte?: number): Promise<YouTubeUploadStatus>;
33
- //# sourceMappingURL=youtube-upload.d.ts.map
@@ -294,4 +294,3 @@ export async function sendYouTubeUpload(session, media, options, startByte = 0)
294
294
  reader.releaseLock();
295
295
  }
296
296
  }
297
- //# sourceMappingURL=youtube-upload.js.map
@@ -106,4 +106,3 @@ export interface YouTubeNative {
106
106
  }) => Promise<JsonObject>;
107
107
  }
108
108
  export declare function youtube(options: YouTubeOptions): import("../core/adapter.js").SocialAdapter<YouTubeNative>;
109
- //# sourceMappingURL=youtube.d.ts.map
@@ -1098,4 +1098,3 @@ export function youtube(options) {
1098
1098
  },
1099
1099
  });
1100
1100
  }
1101
- //# sourceMappingURL=youtube.js.map
@@ -121,4 +121,3 @@ export declare class MemoryConnectionStore implements ConnectionStore {
121
121
  }): Promise<readonly ConnectionGrant[]>;
122
122
  grants(): readonly ConnectionGrant[];
123
123
  }
124
- //# sourceMappingURL=connections.d.ts.map
@@ -333,4 +333,3 @@ export class MemoryConnectionStore {
333
333
  return structuredClone(this.#grants);
334
334
  }
335
335
  }
336
- //# sourceMappingURL=connections.js.map
@@ -49,4 +49,3 @@ export declare class CredentialManager {
49
49
  rotate(key: string, refresh: (current: StoredCredential) => Promise<StoredCredential>): Promise<StoredCredential>;
50
50
  delete(key: string): Promise<void>;
51
51
  }
52
- //# sourceMappingURL=credentials.d.ts.map
@@ -106,4 +106,3 @@ export class CredentialManager {
106
106
  return this.store.delete(key);
107
107
  }
108
108
  }
109
- //# sourceMappingURL=credentials.js.map
@@ -2,4 +2,3 @@ export * from "./connections.js";
2
2
  export * from "./credentials.js";
3
3
  export * from "./webhooks.js";
4
4
  export * from "./oauth.js";
5
- //# sourceMappingURL=index.d.ts.map
@@ -2,4 +2,3 @@ export * from "./connections.js";
2
2
  export * from "./credentials.js";
3
3
  export * from "./webhooks.js";
4
4
  export * from "./oauth.js";
5
- //# sourceMappingURL=index.js.map
@@ -50,4 +50,3 @@ export declare const instagramOAuth: (options: OAuthProviderOptions) => Connecti
50
50
  export declare const linkedinOAuth: (options: OAuthProviderOptions) => ConnectionProvider;
51
51
  export declare function refreshOAuthToken(kind: ProviderKind, options: OAuthProviderOptions, current: OAuthTokenSet): Promise<OAuthTokenSet>;
52
52
  export {};
53
- //# sourceMappingURL=oauth.d.ts.map
@@ -551,4 +551,3 @@ export async function refreshOAuthToken(kind, options, current) {
551
551
  }, "oauth.refresh", options), "oauth.refresh");
552
552
  return next.refreshToken ? next : { ...next, refreshToken: current.refreshToken };
553
553
  }
554
- //# sourceMappingURL=oauth.js.map
@@ -55,4 +55,3 @@ export declare function acceptWebhook(input: {
55
55
  state: "accepted" | "duplicate";
56
56
  quarantined: boolean;
57
57
  }>;
58
- //# sourceMappingURL=webhooks.d.ts.map
@@ -202,4 +202,3 @@ export async function acceptWebhook(input) {
202
202
  });
203
203
  return { state, quarantined };
204
204
  }
205
- //# sourceMappingURL=webhooks.js.map
@@ -44,4 +44,3 @@ export declare class MemoryIdempotencyStore implements IdempotencyStore {
44
44
  export declare function mockSharedAccountAuthorization(input: {
45
45
  readonly memberships: Readonly<Record<string, readonly string[]>>;
46
46
  }): AuthorizationPolicy;
47
- //# sourceMappingURL=index.d.ts.map
@@ -562,4 +562,3 @@ export function mockSharedAccountAuthorization(input) {
562
562
  },
563
563
  };
564
564
  }
565
- //# sourceMappingURL=index.js.map
package/dist/testing.d.ts CHANGED
@@ -1,2 +1 @@
1
1
  export * from "./testing/index.js";
2
- //# sourceMappingURL=testing.d.ts.map
package/dist/testing.js CHANGED
@@ -1,2 +1 @@
1
1
  export * from "./testing/index.js";
2
- //# sourceMappingURL=testing.js.map
@@ -1,3 +1,2 @@
1
1
  /** Bounded buffering for APIs that require one small binary image payload. */
2
2
  export declare function readBinary(stream: ReadableStream<Uint8Array>, maxBytes: number, signal: AbortSignal): Promise<Uint8Array>;
3
- //# sourceMappingURL=binary.d.ts.map
@@ -27,4 +27,3 @@ export async function readBinary(stream, maxBytes, signal) {
27
27
  }
28
28
  return result;
29
29
  }
30
- //# sourceMappingURL=binary.js.map
@@ -1,4 +1,3 @@
1
1
  import type { AdapterOperationContext } from "../core/types.js";
2
2
  /** Sequential requests sharing an operation context consume one elapsed budget. */
3
3
  export declare function remainingBudget(context: AdapterOperationContext, now?: number): number;
4
- //# sourceMappingURL=budget.d.ts.map
@@ -24,4 +24,3 @@ export function remainingBudget(context, now = performance.now()) {
24
24
  });
25
25
  return remaining;
26
26
  }
27
- //# sourceMappingURL=budget.js.map
@@ -42,4 +42,3 @@ export declare function retryDelay(value: string | null, now: number): number |
42
42
  export declare function abortable<T>(work: Promise<T>, signal: AbortSignal): Promise<T>;
43
43
  export declare function readJson(response: Response, limit: number, signal?: AbortSignal): Promise<JsonValue>;
44
44
  export declare function createHttp(options?: HttpOptions): (input: HttpRequest) => Promise<unknown>;
45
- //# sourceMappingURL=http.d.ts.map
@@ -257,4 +257,3 @@ export function createHttp(options = {}) {
257
257
  }
258
258
  };
259
259
  }
260
- //# sourceMappingURL=http.js.map
@@ -6,4 +6,3 @@ export declare function parseJson(text: string): JsonValue;
6
6
  export declare function jsonObject(value: JsonValue): {
7
7
  readonly [key: string]: JsonValue;
8
8
  };
9
- //# sourceMappingURL=json.d.ts.map
@@ -14,4 +14,3 @@ export function jsonObject(value) {
14
14
  // SAFETY: Object(value) === value and Array.isArray(value) is false establish a JSON object.
15
15
  return value;
16
16
  }
17
- //# sourceMappingURL=json.js.map
@@ -23,4 +23,3 @@ export declare function upload(options: UploadOptions): Promise<{
23
23
  bytes: number;
24
24
  etag?: string;
25
25
  }>;
26
- //# sourceMappingURL=upload.d.ts.map
@@ -151,4 +151,3 @@ export async function upload(options) {
151
151
  reader?.releaseLock();
152
152
  }
153
153
  }
154
- //# sourceMappingURL=upload.js.map
@@ -3,4 +3,3 @@ export declare function array(value: unknown): unknown[];
3
3
  export declare function string(value: unknown): string;
4
4
  export declare function optionalString(value: unknown): string | undefined;
5
5
  export declare function optionalNumber(value: unknown): number | undefined;
6
- //# sourceMappingURL=validation.d.ts.map
@@ -22,4 +22,3 @@ export function optionalString(value) {
22
22
  export function optionalNumber(value) {
23
23
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
24
24
  }
25
- //# sourceMappingURL=validation.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencoredev/social-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Social platform integrations for TypeScript applications.",
5
5
  "homepage": "https://github.com/opencoredev/social-sdk#readme",
6
6
  "bugs": {
@@ -85,12 +85,6 @@
85
85
  "lint": "oxlint src tests type-tests",
86
86
  "test": "bun test tests"
87
87
  },
88
- "dependencies": {
89
- "twitter-text": "3.1.0"
90
- },
91
- "devDependencies": {
92
- "@types/twitter-text": "^3.1.10"
93
- },
94
88
  "engines": {
95
89
  "node": ">=22.12"
96
90
  }
package/dist/cli.d.ts.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAMA,OAAO,KAAK,EAAkB,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAYrE,QAAA,MAAM,KAAK,qHAWD,CAAC;AAEX,KAAK,WAAW,GAAG,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC;AAmB1C,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,WAAW,EACjB,SAAS,SAAuB,GAC/B,aAAa,CAAC,OAAO,CAAC,CAmDxB;AAED,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC3D,QAAQ,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACtD,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC;AAED,uGAAuG;AACvG,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAmJhF"}