@opencoredev/social-sdk 0.1.1 → 0.2.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 (117) hide show
  1. package/README.md +10 -2
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +1 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/cloud/common.d.ts.map +1 -1
  6. package/dist/cloud/common.js +48 -20
  7. package/dist/cloud/common.js.map +1 -1
  8. package/dist/cloud/lifecycle.d.ts.map +1 -1
  9. package/dist/cloud/lifecycle.js +5 -0
  10. package/dist/cloud/lifecycle.js.map +1 -1
  11. package/dist/cloud/media.d.ts.map +1 -1
  12. package/dist/cloud/media.js +12 -8
  13. package/dist/cloud/media.js.map +1 -1
  14. package/dist/cloud/outcomes.d.ts.map +1 -1
  15. package/dist/cloud/outcomes.js +17 -9
  16. package/dist/cloud/outcomes.js.map +1 -1
  17. package/dist/cloud/post-for-me.d.ts.map +1 -1
  18. package/dist/cloud/post-for-me.js +99 -28
  19. package/dist/cloud/post-for-me.js.map +1 -1
  20. package/dist/cloud/zernio.d.ts.map +1 -1
  21. package/dist/cloud/zernio.js +100 -9
  22. package/dist/cloud/zernio.js.map +1 -1
  23. package/dist/core/adapter.d.ts +42 -1
  24. package/dist/core/adapter.d.ts.map +1 -1
  25. package/dist/core/adapter.js.map +1 -1
  26. package/dist/core/client.d.ts +35 -1
  27. package/dist/core/client.d.ts.map +1 -1
  28. package/dist/core/client.js +503 -110
  29. package/dist/core/client.js.map +1 -1
  30. package/dist/core/concurrency.d.ts.map +1 -1
  31. package/dist/core/concurrency.js +5 -3
  32. package/dist/core/concurrency.js.map +1 -1
  33. package/dist/core/errors.d.ts.map +1 -1
  34. package/dist/core/errors.js +14 -7
  35. package/dist/core/errors.js.map +1 -1
  36. package/dist/core/idempotency.d.ts.map +1 -1
  37. package/dist/core/idempotency.js.map +1 -1
  38. package/dist/core/index.d.ts.map +1 -1
  39. package/dist/core/index.js.map +1 -1
  40. package/dist/core/pagination.d.ts.map +1 -1
  41. package/dist/core/pagination.js +1 -0
  42. package/dist/core/pagination.js.map +1 -1
  43. package/dist/core/types.d.ts +70 -2
  44. package/dist/core/types.d.ts.map +1 -1
  45. package/dist/core/types.js +10 -0
  46. package/dist/core/types.js.map +1 -1
  47. package/dist/platforms/bluesky.d.ts +131 -1
  48. package/dist/platforms/bluesky.d.ts.map +1 -1
  49. package/dist/platforms/bluesky.js +772 -44
  50. package/dist/platforms/bluesky.js.map +1 -1
  51. package/dist/platforms/instagram.d.ts +70 -3
  52. package/dist/platforms/instagram.d.ts.map +1 -1
  53. package/dist/platforms/instagram.js +403 -47
  54. package/dist/platforms/instagram.js.map +1 -1
  55. package/dist/platforms/linkedin.d.ts +55 -0
  56. package/dist/platforms/linkedin.d.ts.map +1 -1
  57. package/dist/platforms/linkedin.js +350 -39
  58. package/dist/platforms/linkedin.js.map +1 -1
  59. package/dist/platforms/threads.d.ts +37 -4
  60. package/dist/platforms/threads.d.ts.map +1 -1
  61. package/dist/platforms/threads.js +419 -44
  62. package/dist/platforms/threads.js.map +1 -1
  63. package/dist/platforms/tiktok.d.ts.map +1 -1
  64. package/dist/platforms/tiktok.js +109 -26
  65. package/dist/platforms/tiktok.js.map +1 -1
  66. package/dist/platforms/x-engagement.d.ts.map +1 -1
  67. package/dist/platforms/x-engagement.js +4 -0
  68. package/dist/platforms/x-engagement.js.map +1 -1
  69. package/dist/platforms/x.d.ts +227 -9
  70. package/dist/platforms/x.d.ts.map +1 -1
  71. package/dist/platforms/x.js +826 -49
  72. package/dist/platforms/x.js.map +1 -1
  73. package/dist/platforms/youtube-upload.d.ts.map +1 -1
  74. package/dist/platforms/youtube-upload.js +25 -9
  75. package/dist/platforms/youtube-upload.js.map +1 -1
  76. package/dist/platforms/youtube.d.ts +56 -2
  77. package/dist/platforms/youtube.d.ts.map +1 -1
  78. package/dist/platforms/youtube.js +571 -31
  79. package/dist/platforms/youtube.js.map +1 -1
  80. package/dist/server/connections.d.ts.map +1 -1
  81. package/dist/server/connections.js +4 -2
  82. package/dist/server/connections.js.map +1 -1
  83. package/dist/server/credentials.d.ts.map +1 -1
  84. package/dist/server/credentials.js +10 -16
  85. package/dist/server/credentials.js.map +1 -1
  86. package/dist/server/index.d.ts.map +1 -1
  87. package/dist/server/index.js.map +1 -1
  88. package/dist/server/oauth.d.ts +5 -0
  89. package/dist/server/oauth.d.ts.map +1 -1
  90. package/dist/server/oauth.js +118 -47
  91. package/dist/server/oauth.js.map +1 -1
  92. package/dist/server/webhooks.d.ts.map +1 -1
  93. package/dist/server/webhooks.js +11 -4
  94. package/dist/server/webhooks.js.map +1 -1
  95. package/dist/testing/index.d.ts.map +1 -1
  96. package/dist/testing/index.js +153 -1
  97. package/dist/testing/index.js.map +1 -1
  98. package/dist/transport/binary.d.ts.map +1 -1
  99. package/dist/transport/binary.js.map +1 -1
  100. package/dist/transport/budget.d.ts.map +1 -1
  101. package/dist/transport/budget.js.map +1 -1
  102. package/dist/transport/http.d.ts +2 -1
  103. package/dist/transport/http.d.ts.map +1 -1
  104. package/dist/transport/http.js +42 -12
  105. package/dist/transport/http.js.map +1 -1
  106. package/dist/transport/json.d.ts +7 -1
  107. package/dist/transport/json.d.ts.map +1 -1
  108. package/dist/transport/json.js +7 -1
  109. package/dist/transport/json.js.map +1 -1
  110. package/dist/transport/upload.d.ts +1 -0
  111. package/dist/transport/upload.d.ts.map +1 -1
  112. package/dist/transport/upload.js +38 -17
  113. package/dist/transport/upload.js.map +1 -1
  114. package/dist/transport/validation.d.ts.map +1 -1
  115. package/dist/transport/validation.js +1 -0
  116. package/dist/transport/validation.js.map +1 -1
  117. package/package.json +2 -1
@@ -1,3 +1,4 @@
1
+ /* oxlint-disable anti-slop/require-readable-spacing, anti-slop/no-conditional-empty-object-spread, anti-slop/require-safety-comment-for-type-assertion -- facade dispatch keeps capability-specific branches together. */
1
2
  import { decodeCursor, encodeCursor, iterateItems } from "./pagination.js";
2
3
  import { createConcurrencyLimiter } from "./concurrency.js";
3
4
  import { SocialError } from "./errors.js";
@@ -44,12 +45,10 @@ function mediaFingerprintView(content) {
44
45
  };
45
46
  }
46
47
  function preparationIssue(code, message, targetIndex) {
47
- return {
48
- code,
49
- message,
50
- severity: "error",
51
- ...(targetIndex === undefined ? {} : { targetIndex }),
52
- };
48
+ const issue = { code, message, severity: "error" };
49
+ if (targetIndex !== undefined)
50
+ Object.assign(issue, { targetIndex });
51
+ return issue;
53
52
  }
54
53
  function publicationStatus(outcomes) {
55
54
  const pending = new Set(["scheduled", "accepted", "processing"]);
@@ -76,16 +75,18 @@ async function mapConcurrent(count, concurrency, execute) {
76
75
  return results;
77
76
  }
78
77
  function makeContext(backendInstance, correlationId, options, targetIdempotencyKey) {
79
- return {
78
+ const context = {
80
79
  backendInstance,
81
80
  correlationId,
82
81
  retryBudget: options?.retryBudget ?? { maxAttempts: 1, maxElapsedMs: 30_000 },
83
- ...(options?.signal === undefined ? {} : { signal: options.signal }),
84
- ...(options?.authorization === undefined ? {} : { authorization: options.authorization }),
85
- ...(targetIdempotencyKey === undefined
86
- ? {}
87
- : { idempotencyKey: targetIdempotencyKey, targetIdempotencyKey }),
88
82
  };
83
+ if (options?.signal !== undefined)
84
+ Object.assign(context, { signal: options.signal });
85
+ if (options?.authorization !== undefined)
86
+ Object.assign(context, { authorization: options.authorization });
87
+ if (targetIdempotencyKey !== undefined)
88
+ Object.assign(context, { idempotencyKey: targetIdempotencyKey, targetIdempotencyKey });
89
+ return context;
89
90
  }
90
91
  function unsupported(operation, backend) {
91
92
  throw new SocialError({
@@ -95,7 +96,9 @@ function unsupported(operation, backend) {
95
96
  message: `${backend} does not implement ${operation}`,
96
97
  });
97
98
  }
98
- function outcomeFromError(error, target, observedAt) {
99
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Adapter rejections enter the error boundary here.
100
+ function outcomeFromError(error, // Transport and adapter rejections can be arbitrary JavaScript values.
101
+ target, observedAt) {
99
102
  if (error instanceof SocialError) {
100
103
  if (error.code === "cancelled") {
101
104
  return {
@@ -183,7 +186,17 @@ export function createSocial(config) {
183
186
  message: `Unknown backend instance: ${backend}`,
184
187
  });
185
188
  }
186
- return limiter(signal, operation, work);
189
+ return limiter(signal, operation, work).catch((error) => {
190
+ if (error instanceof SocialError)
191
+ throw error;
192
+ throw new SocialError({
193
+ code: "upstream_failure",
194
+ operation,
195
+ backend,
196
+ message: "The adapter failed while processing the request",
197
+ cause: error,
198
+ });
199
+ });
187
200
  }
188
201
  const clock = config.clock ?? (() => new Date());
189
202
  let correlationSequence = 0;
@@ -250,10 +263,13 @@ export function createSocial(config) {
250
263
  targetKey: key,
251
264
  account: target.account,
252
265
  content: mergeContent(request.content, target.content),
253
- ...(target.options === undefined ? {} : { options: target.options }),
254
- ...(request.schedule === undefined ? {} : { schedule: request.schedule }),
255
- ...(replyTo === undefined ? {} : { replyTo }),
256
266
  };
267
+ if (target.options !== undefined)
268
+ Object.assign(prepared, { options: target.options });
269
+ if (request.schedule !== undefined)
270
+ Object.assign(prepared, { schedule: request.schedule });
271
+ if (replyTo !== undefined)
272
+ Object.assign(prepared, { replyTo });
257
273
  targets.push(prepared);
258
274
  try {
259
275
  issues.push(...adapter.posts.prepareTarget(prepared));
@@ -284,6 +300,41 @@ export function createSocial(config) {
284
300
  });
285
301
  }
286
302
  }
303
+ async function authorizeRefWithFallback(operation, fallback, account, options, correlationId) {
304
+ try {
305
+ await authorizeRef(operation, account, options, correlationId);
306
+ }
307
+ catch (error) {
308
+ if (!(error instanceof SocialError) || error.code !== "unauthorized")
309
+ throw error;
310
+ await authorizeRef(fallback, account, options, correlationId);
311
+ }
312
+ }
313
+ function validateAccountRef(account, operation) {
314
+ if (account === null ||
315
+ typeof account !== "object" ||
316
+ account.kind !== "connected-account" ||
317
+ account.version !== 1 ||
318
+ typeof account.backend !== "string" ||
319
+ !account.backend.trim() ||
320
+ !account.platform ||
321
+ typeof account.accountId !== "string" ||
322
+ !account.accountId.trim())
323
+ throw new SocialError({
324
+ code: "invalid_input",
325
+ operation,
326
+ message: "A valid connected account reference is required",
327
+ });
328
+ }
329
+ function validDateOnly(value) {
330
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
331
+ return false;
332
+ const parsed = new Date(`${value}T00:00:00Z`);
333
+ return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
334
+ }
335
+ function validTimestamp(value) {
336
+ return typeof value === "string" && value.trim() !== "" && !Number.isNaN(Date.parse(value));
337
+ }
287
338
  function selected(ref, operation) {
288
339
  if (ref.kind !== undefined && (ref.kind === "" || ref.version !== 1)) {
289
340
  throw new SocialError({
@@ -497,18 +548,8 @@ export function createSocial(config) {
497
548
  callOptions?.authorization?.tenantId ?? null,
498
549
  callOptions?.limit ?? null,
499
550
  ]);
500
- const received = await dispatch(first[0], callOptions?.signal, "accounts.read", () => first[1].accounts.list({
501
- ...(callOptions?.cursor === undefined
502
- ? {}
503
- : { cursor: decodeCursor(cursorScope, callOptions.cursor) }),
504
- ...(callOptions?.limit === undefined ? {} : { limit: callOptions.limit }),
505
- }, makeContext(first[0], correlationId, callOptions)));
506
- const page = {
507
- ...received,
508
- ...(received.nextCursor === undefined
509
- ? {}
510
- : { nextCursor: encodeCursor(cursorScope, received.nextCursor) }),
511
- };
551
+ const received = await dispatch(first[0], callOptions?.signal, "accounts.read", () => first[1].accounts.list(decodePageOptions(cursorScope, callOptions), makeContext(first[0], correlationId, callOptions)));
552
+ const page = encodePage(cursorScope, received);
512
553
  if (config.authorization === undefined)
513
554
  return page;
514
555
  const decisions = await config.authorization.authorizeTargets({
@@ -517,14 +558,17 @@ export function createSocial(config) {
517
558
  context: makeContext(first[0], correlationId, callOptions),
518
559
  });
519
560
  const allowed = new Set(decisions.filter((item) => item.allowed).map((item) => targetKey(item.account)));
520
- return {
561
+ const filtered = {
521
562
  items: page.items.filter((item) => allowed.has(targetKey(item.ref))),
522
- ...(page.nextCursor === undefined ? {} : { nextCursor: page.nextCursor }),
523
- ...(page.metadata === undefined ? {} : { metadata: page.metadata }),
524
563
  };
564
+ if (page.nextCursor !== undefined)
565
+ Object.assign(filtered, { nextCursor: page.nextCursor });
566
+ if (page.metadata !== undefined)
567
+ Object.assign(filtered, { metadata: page.metadata });
568
+ return filtered;
525
569
  },
526
570
  iterate(callOptions) {
527
- return iterateItems((cursor) => accountsFacade.list({ ...callOptions, ...(cursor === undefined ? {} : { cursor }) }), callOptions);
571
+ return iterateItems((cursor) => accountsFacade.list(iterationPageOptions(callOptions, cursor)), callOptions);
528
572
  },
529
573
  async get(ref, callOptions) {
530
574
  const correlationId = `social-${++correlationSequence}`;
@@ -536,6 +580,137 @@ export function createSocial(config) {
536
580
  return dispatch(ref.backend, callOptions?.signal, "accounts.read", () => adapter.accounts.get(ref, makeContext(ref.backend, correlationId, callOptions)));
537
581
  },
538
582
  };
583
+ const graphFacade = {
584
+ async getProfile(account, input, callOptions) {
585
+ validateAccountRef(account, "profiles.read");
586
+ if (input === null ||
587
+ typeof input !== "object" ||
588
+ (input.profileId !== undefined &&
589
+ input.handle !== undefined &&
590
+ typeof input.profileId !== "string" &&
591
+ typeof input.handle !== "string") ||
592
+ (input.profileId !== undefined &&
593
+ (typeof input.profileId !== "string" || input.profileId.trim() === "")) ||
594
+ (input.handle !== undefined &&
595
+ (typeof input.handle !== "string" || input.handle.trim() === "")))
596
+ throw new SocialError({
597
+ code: "invalid_input",
598
+ operation: "profiles.read",
599
+ message: "Profile selector fields must be nonempty strings when provided",
600
+ });
601
+ const correlationId = `social-${++correlationSequence}`;
602
+ await authorizeRef("profiles.read", account, callOptions, correlationId);
603
+ const adapter = selected(account, "profiles.read");
604
+ requireCapability(adapter, "profiles.read", account.platform, account.backend);
605
+ if (adapter.graph?.getProfile === undefined)
606
+ unsupported("profiles.read", account.backend);
607
+ return dispatch(account.backend, callOptions?.signal, "profiles.read", () => adapter.graph.getProfile(account, input, makeContext(account.backend, correlationId, callOptions)));
608
+ },
609
+ async listRelationships(account, input, callOptions) {
610
+ validateAccountRef(account, "graph.read");
611
+ if (input === null ||
612
+ typeof input !== "object" ||
613
+ !["following", "followers", "blocked", "muted"].includes(input.kind) ||
614
+ (input.limit !== undefined && (!Number.isSafeInteger(input.limit) || input.limit < 1)))
615
+ throw new SocialError({
616
+ code: "invalid_input",
617
+ operation: "graph.read",
618
+ message: !["following", "followers", "blocked", "muted"].includes(input?.kind)
619
+ ? "Relationship kind must be following, followers, blocked, or muted"
620
+ : "Relationship page limit must be a positive integer",
621
+ });
622
+ const correlationId = `social-${++correlationSequence}`;
623
+ await authorizeRef("graph.read", account, callOptions, correlationId);
624
+ const adapter = selected(account, "graph.read");
625
+ requireCapability(adapter, "graph.read", account.platform, account.backend);
626
+ if (adapter.graph?.listRelationships === undefined)
627
+ unsupported("graph.read", account.backend);
628
+ const scope = JSON.stringify([
629
+ account.backend,
630
+ "graph.read",
631
+ callOptions?.authorization?.tenantId ?? null,
632
+ account.platform,
633
+ account.accountId,
634
+ input.kind,
635
+ input.limit ?? null,
636
+ ]);
637
+ const page = await dispatch(account.backend, callOptions?.signal, "graph.read", () => adapter.graph.listRelationships(account, {
638
+ ...input,
639
+ ...(input.cursor === undefined ? {} : { cursor: decodeCursor(scope, input.cursor) }),
640
+ }, makeContext(account.backend, correlationId, callOptions)));
641
+ return encodePage(scope, page);
642
+ },
643
+ follow: (target, options) => graphMutation(target, "graph.follow", options),
644
+ unfollow: (target, options) => graphMutation(target, "graph.unfollow", options),
645
+ block: (target, options) => graphMutation(target, "graph.block", options),
646
+ unblock: (target, options) => graphMutation(target, "graph.unblock", options),
647
+ mute: (target, options) => graphMutation(target, "graph.mute", options),
648
+ unmute: (target, options) => graphMutation(target, "graph.unmute", options),
649
+ };
650
+ async function graphMutation(target, operation, callOptions) {
651
+ if (target === null ||
652
+ typeof target !== "object" ||
653
+ target.kind !== "profile" ||
654
+ target.version !== 1 ||
655
+ typeof target.backend !== "string" ||
656
+ target.backend.trim() === "" ||
657
+ typeof target.platform !== "string" ||
658
+ target.platform.trim() === "" ||
659
+ typeof target.accountId !== "string" ||
660
+ target.accountId.trim() === "" ||
661
+ typeof target.profileId !== "string" ||
662
+ target.profileId.trim() === "")
663
+ throw new SocialError({
664
+ code: "invalid_input",
665
+ operation,
666
+ message: "A valid profile reference is required",
667
+ });
668
+ const account = {
669
+ kind: "connected-account",
670
+ version: 1,
671
+ backend: target.backend,
672
+ platform: target.platform,
673
+ accountId: target.accountId,
674
+ };
675
+ const correlationId = `social-${++correlationSequence}`;
676
+ await authorizeRef(operation, account, callOptions, correlationId);
677
+ const adapter = selected(target, operation);
678
+ requireCapability(adapter, operation, target.platform, target.backend);
679
+ if (adapter.graph === undefined)
680
+ unsupported(operation, target.backend);
681
+ const context = makeContext(target.backend, correlationId, callOptions);
682
+ return dispatch(target.backend, callOptions?.signal, operation, async () => {
683
+ switch (operation) {
684
+ case "graph.follow":
685
+ if (adapter.graph.follow === undefined)
686
+ unsupported(operation, target.backend);
687
+ return await adapter.graph.follow(target, context);
688
+ case "graph.unfollow":
689
+ if (adapter.graph.unfollow === undefined)
690
+ unsupported(operation, target.backend);
691
+ await adapter.graph.unfollow(target, context);
692
+ return undefined;
693
+ case "graph.block":
694
+ if (adapter.graph.block === undefined)
695
+ unsupported(operation, target.backend);
696
+ return await adapter.graph.block(target, context);
697
+ case "graph.unblock":
698
+ if (adapter.graph.unblock === undefined)
699
+ unsupported(operation, target.backend);
700
+ await adapter.graph.unblock(target, context);
701
+ return undefined;
702
+ case "graph.mute":
703
+ if (adapter.graph.mute === undefined)
704
+ unsupported(operation, target.backend);
705
+ return await adapter.graph.mute(target, context);
706
+ case "graph.unmute":
707
+ if (adapter.graph.unmute === undefined)
708
+ unsupported(operation, target.backend);
709
+ await adapter.graph.unmute(target, context);
710
+ return undefined;
711
+ }
712
+ });
713
+ }
539
714
  async function lifecycle(ref, expectedKind, operation, callOptions) {
540
715
  if (ref.kind !== expectedKind || ref.version !== 1 || !ref.accountId || !ref.platform)
541
716
  throw new SocialError({
@@ -571,25 +746,12 @@ export function createSocial(config) {
571
746
  account.accountId,
572
747
  callOptions?.limit ?? null,
573
748
  ]);
574
- const input = {
575
- ...(callOptions?.cursor === undefined
576
- ? {}
577
- : { cursor: decodeCursor(scope, callOptions.cursor) }),
578
- ...(callOptions?.limit === undefined ? {} : { limit: callOptions.limit }),
579
- };
749
+ const input = decodePageOptions(scope, callOptions);
580
750
  const page = await dispatch(account.backend, callOptions?.signal, "posts.list", () => adapter.posts.list(account, input, makeContext(account.backend, correlationId, callOptions)));
581
- return {
582
- ...page,
583
- ...(page.nextCursor === undefined
584
- ? {}
585
- : { nextCursor: encodeCursor(scope, page.nextCursor) }),
586
- };
751
+ return encodePage(scope, page);
587
752
  },
588
753
  iterate(account, callOptions) {
589
- return iterateItems((cursor) => postsFacade.list(account, {
590
- ...callOptions,
591
- ...(cursor === undefined ? {} : { cursor }),
592
- }), callOptions);
754
+ return iterateItems((cursor) => postsFacade.list(account, iterationPageOptions(callOptions, cursor)), callOptions);
593
755
  },
594
756
  async cancelScheduled(ref, callOptions) {
595
757
  const { adapter, context } = await lifecycle(ref, "scheduled-job", "posts.cancelScheduled", callOptions);
@@ -618,25 +780,105 @@ export function createSocial(config) {
618
780
  operation: "posts.publishSequence",
619
781
  message: "A sequence requires at least one item",
620
782
  });
621
- const results = [];
783
+ if (typeof request.idempotencyKey !== "string" || request.idempotencyKey.trim() === "")
784
+ throw new SocialError({
785
+ code: "invalid_input",
786
+ operation: "posts.publishSequence",
787
+ message: "A nonempty idempotencyKey is required",
788
+ });
789
+ const prepared = [];
622
790
  for (const [index, item] of request.items.entries()) {
623
- const result = await publish({
791
+ const publishRequest = {
624
792
  targets: item.targets,
625
793
  content: item.content,
626
794
  idempotencyKey: `${request.idempotencyKey}:${index}`,
627
- ...(item.replyTo === undefined ? {} : { replyTo: item.replyTo }),
628
- }, callOptions);
795
+ };
796
+ if (item.replyTo !== undefined)
797
+ Object.assign(publishRequest, { replyTo: item.replyTo });
798
+ const chained = request.replyToPrevious === true && index > 0;
799
+ if (chained) {
800
+ const account = item.targets[0]?.account;
801
+ const parent = request.items[index - 1]?.targets[0]?.account;
802
+ // A chain replies to the single post the previous item created, so every link
803
+ // must publish to one target on the same account.
804
+ if (item.targets.length !== 1 ||
805
+ request.items[index - 1]?.targets.length !== 1 ||
806
+ item.replyTo !== undefined ||
807
+ item.targets[0]?.replyTo !== undefined ||
808
+ account === undefined ||
809
+ parent === undefined ||
810
+ account.backend !== parent.backend ||
811
+ account.platform !== parent.platform ||
812
+ account.accountId !== parent.accountId)
813
+ throw new SocialError({
814
+ code: "invalid_input",
815
+ operation: "posts.publishSequence",
816
+ message: "replyToPrevious requires every item to publish to the same single account without its own replyTo",
817
+ });
818
+ }
819
+ const plan = prepare(chained
820
+ ? {
821
+ ...publishRequest,
822
+ // Placeholder parent so adapters validate reply support before anything is sent.
823
+ replyTo: {
824
+ kind: "platform-post",
825
+ version: 1,
826
+ backend: item.targets[0].account.backend,
827
+ platform: item.targets[0].account.platform,
828
+ accountId: item.targets[0].account.accountId,
829
+ postId: "preflight",
830
+ },
831
+ }
832
+ : publishRequest);
833
+ if (!plan.ok)
834
+ throw new SocialError({
835
+ code: "invalid_input",
836
+ operation: "posts.publishSequence",
837
+ message: "Publication preparation failed; no targets were dispatched",
838
+ issues: plan.issues,
839
+ });
840
+ for (const target of plan.targets)
841
+ await authorizeRef("posts.publish", target.account, callOptions, `social-${++correlationSequence}`);
842
+ prepared.push(publishRequest);
843
+ }
844
+ const results = [];
845
+ const failures = [];
846
+ let previous;
847
+ for (const [index, publishRequest] of prepared.entries()) {
848
+ const chained = request.replyToPrevious === true && index > 0;
849
+ // Without a published parent the item would post as an unrelated root post.
850
+ if (chained && previous === undefined)
851
+ break;
852
+ const nextRequest = chained && previous !== undefined
853
+ ? { ...publishRequest, replyTo: previous }
854
+ : publishRequest;
855
+ let result;
856
+ previous = undefined;
857
+ try {
858
+ result = await publish(nextRequest, callOptions);
859
+ }
860
+ catch (error) {
861
+ if (error instanceof SocialError) {
862
+ failures.push({ index, code: error.code, message: error.message });
863
+ if (request.stopOnFailure !== false)
864
+ break;
865
+ continue;
866
+ }
867
+ throw error;
868
+ }
629
869
  results.push(result);
630
- if (request.stopOnFailure !== false && result.status !== "complete")
870
+ const published = result.outcomes.find((outcome) => outcome.state === "published");
871
+ if (published?.post !== undefined)
872
+ previous = published.post;
873
+ if (request.stopOnFailure !== false && result.status === "partial")
631
874
  break;
632
875
  }
633
- const status = results.length === request.items.length &&
634
- results.every((item) => item.status === "complete")
635
- ? "complete"
636
- : results.some((item) => item.status === "partial") || results.length > 0
637
- ? "partial"
876
+ const status = results.length < request.items.length || results.some((item) => item.status === "partial")
877
+ ? "partial"
878
+ : results.every((item) => item.status === "complete")
879
+ ? "complete"
638
880
  : "pending";
639
- return { status, items: results };
881
+ return { status, items: results, failures };
640
882
  },
641
883
  async get(ref, callOptions) {
642
884
  const correlationId = `social-${++correlationSequence}`;
@@ -666,10 +908,61 @@ export function createSocial(config) {
666
908
  const adapter = selected(ref, "posts.getDelivery");
667
909
  requireCapability(adapter, "posts.status", ref.platform, ref.backend);
668
910
  if (adapter.posts?.getDelivery === undefined)
669
- unsupported("posts.read", ref.backend);
911
+ unsupported("posts.status", ref.backend);
670
912
  return dispatch(ref.backend, callOptions?.signal, "posts.status", () => adapter.posts.getDelivery(ref, makeContext(ref.backend, correlationId, callOptions)));
671
913
  },
672
914
  };
915
+ const searchFacade = {
916
+ async posts(account, input, callOptions) {
917
+ validateAccountRef(account, "search.posts");
918
+ if (input === null ||
919
+ input === undefined ||
920
+ typeof input.query !== "string" ||
921
+ !input.query.trim() ||
922
+ (input.limit !== undefined && (!Number.isSafeInteger(input.limit) || input.limit < 1)) ||
923
+ (input.startTime !== undefined &&
924
+ (typeof input.startTime !== "string" || !validTimestamp(input.startTime))) ||
925
+ (input.endTime !== undefined &&
926
+ (typeof input.endTime !== "string" || !validTimestamp(input.endTime))) ||
927
+ (input.startTime !== undefined &&
928
+ input.endTime !== undefined &&
929
+ Date.parse(input.startTime) > Date.parse(input.endTime)) ||
930
+ (input.scope !== undefined && input.scope !== "recent" && input.scope !== "all"))
931
+ throw new SocialError({
932
+ code: "invalid_input",
933
+ operation: "search.posts",
934
+ message: "Search requires an account reference, a nonempty query, and a positive page limit.",
935
+ });
936
+ const correlationId = `social-${++correlationSequence}`;
937
+ await authorizeRef("search.posts", account, callOptions, correlationId);
938
+ const adapter = selected(account, "search.posts");
939
+ requireCapability(adapter, "search.posts", account.platform, account.backend);
940
+ const search = adapter.search;
941
+ if (search === undefined)
942
+ unsupported("search.posts", account.backend);
943
+ const scope = await fingerprint([
944
+ account.backend,
945
+ "search.posts",
946
+ callOptions?.authorization?.tenantId ?? null,
947
+ account.platform,
948
+ account.accountId,
949
+ input.query,
950
+ input.limit ?? null,
951
+ input.startTime ?? null,
952
+ input.endTime ?? null,
953
+ input.scope ?? "recent",
954
+ ]);
955
+ const providerInput = { ...input };
956
+ if (input.cursor !== undefined) {
957
+ Object.assign(providerInput, { cursor: decodeCursor(scope, input.cursor) });
958
+ }
959
+ const page = await dispatch(account.backend, callOptions?.signal, "search.posts", () => search.posts(account, providerInput, makeContext(account.backend, correlationId, callOptions)));
960
+ return encodePage(scope, page);
961
+ },
962
+ iteratePosts(account, input, callOptions) {
963
+ return iterateItems((cursor) => searchFacade.posts(account, { ...input, ...iterationPageOptions(undefined, cursor) }, callOptions), callOptions);
964
+ },
965
+ };
673
966
  const mediaFacade = {
674
967
  async upload(input, account, callOptions) {
675
968
  const correlationId = `social-${++correlationSequence}`;
@@ -683,8 +976,9 @@ export function createSocial(config) {
683
976
  };
684
977
  const analyticsFacade = {
685
978
  async getAccountMetrics(ref, callOptions) {
979
+ validateAccountRef(ref, "analytics.account.read");
686
980
  const correlationId = `social-${++correlationSequence}`;
687
- await authorizeRef("analytics.read", ref, callOptions, correlationId);
981
+ await authorizeRefWithFallback("analytics.account.read", "analytics.read", ref, callOptions, correlationId);
688
982
  const adapter = selected(ref, "analytics.getAccountMetrics");
689
983
  requireCapability(adapter, "analytics.account.read", ref.platform, ref.backend);
690
984
  if (!adapter.analytics?.getAccountMetrics)
@@ -707,10 +1001,47 @@ export function createSocial(config) {
707
1001
  unsupported("analytics.read", ref.backend);
708
1002
  return dispatch(ref.backend, callOptions?.signal, "analytics.read", () => adapter.analytics.getPostMetrics(ref, makeContext(ref.backend, correlationId, callOptions)));
709
1003
  },
1004
+ async getReport(ref, query, callOptions) {
1005
+ validateAccountRef(ref, "analytics.report.read");
1006
+ if (query === undefined ||
1007
+ query === null ||
1008
+ !validDateOnly(query.from) ||
1009
+ !validDateOnly(query.to) ||
1010
+ query.from > query.to ||
1011
+ !Array.isArray(query.metrics) ||
1012
+ query.metrics.length === 0 ||
1013
+ query.metrics.some(
1014
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- runtime query boundary.
1015
+ (metric) => typeof metric !== "string" || !metric.trim()) ||
1016
+ (query.dimensions !== undefined &&
1017
+ (!Array.isArray(query.dimensions) ||
1018
+ query.dimensions.some(
1019
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- runtime query boundary.
1020
+ (dimension) => typeof dimension !== "string" || !dimension.trim()))))
1021
+ throw new SocialError({
1022
+ code: "invalid_input",
1023
+ operation: "analytics.report.read",
1024
+ message: "Reports require an ordered YYYY-MM-DD range, at least one metric, and nonempty dimensions.",
1025
+ });
1026
+ const correlationId = `social-${++correlationSequence}`;
1027
+ await authorizeRef("analytics.report.read", ref, callOptions, correlationId);
1028
+ const adapter = selected(ref, "analytics.getReport");
1029
+ requireCapability(adapter, "analytics.report.read", ref.platform, ref.backend);
1030
+ if (!adapter.analytics?.getReport)
1031
+ unsupported("analytics.report.read", ref.backend);
1032
+ return dispatch(ref.backend, callOptions?.signal, "analytics.report.read", () => adapter.analytics.getReport(ref, query, makeContext(ref.backend, correlationId, callOptions)));
1033
+ },
710
1034
  };
711
1035
  const commentsFacade = {
712
1036
  async list(ref, callOptions) {
713
1037
  const correlationId = `social-${++correlationSequence}`;
1038
+ if (callOptions?.limit !== undefined &&
1039
+ (!Number.isSafeInteger(callOptions.limit) || callOptions.limit < 1))
1040
+ throw new SocialError({
1041
+ code: "invalid_input",
1042
+ operation: "comments.read",
1043
+ message: "Comment page limit must be a positive integer",
1044
+ });
714
1045
  const account = {
715
1046
  kind: "connected-account",
716
1047
  version: 1,
@@ -732,21 +1063,11 @@ export function createSocial(config) {
732
1063
  ref.postId,
733
1064
  callOptions?.limit ?? null,
734
1065
  ]);
735
- const received = await dispatch(ref.backend, callOptions?.signal, "comments.read", () => adapter.comments.list(ref, {
736
- ...(callOptions?.cursor === undefined
737
- ? {}
738
- : { cursor: decodeCursor(cursorScope, callOptions.cursor) }),
739
- ...(callOptions?.limit === undefined ? {} : { limit: callOptions.limit }),
740
- }, makeContext(ref.backend, correlationId, callOptions)));
741
- return {
742
- ...received,
743
- ...(received.nextCursor === undefined
744
- ? {}
745
- : { nextCursor: encodeCursor(cursorScope, received.nextCursor) }),
746
- };
1066
+ const received = await dispatch(ref.backend, callOptions?.signal, "comments.read", () => adapter.comments.list(ref, decodePageOptions(cursorScope, callOptions), makeContext(ref.backend, correlationId, callOptions)));
1067
+ return encodePage(cursorScope, received);
747
1068
  },
748
1069
  iterate(ref, callOptions) {
749
- return iterateItems((cursor) => commentsFacade.list(ref, { ...callOptions, ...(cursor === undefined ? {} : { cursor }) }), callOptions);
1070
+ return iterateItems((cursor) => commentsFacade.list(ref, iterationPageOptions(callOptions, cursor)), callOptions);
750
1071
  },
751
1072
  async reply(ref, content, callOptions) {
752
1073
  const correlationId = `social-${++correlationSequence}`;
@@ -768,6 +1089,13 @@ export function createSocial(config) {
768
1089
  const messagesFacade = {
769
1090
  async listConversations(account, callOptions) {
770
1091
  const correlationId = `social-${++correlationSequence}`;
1092
+ if (callOptions?.limit !== undefined &&
1093
+ (!Number.isSafeInteger(callOptions.limit) || callOptions.limit < 1))
1094
+ throw new SocialError({
1095
+ code: "invalid_input",
1096
+ operation: "messages.read",
1097
+ message: "Message page limit must be a positive integer",
1098
+ });
771
1099
  await authorizeRef("messages.read", account, callOptions, correlationId);
772
1100
  const adapter = selected(account, "messages.listConversations");
773
1101
  requireCapability(adapter, "messages.read", account.platform, account.backend);
@@ -782,27 +1110,21 @@ export function createSocial(config) {
782
1110
  "conversations",
783
1111
  callOptions?.limit ?? null,
784
1112
  ]);
785
- const received = await dispatch(account.backend, callOptions?.signal, "messages.read", () => adapter.messages.listConversations(account, {
786
- ...(callOptions?.cursor === undefined
787
- ? {}
788
- : { cursor: decodeCursor(cursorScope, callOptions.cursor) }),
789
- ...(callOptions?.limit === undefined ? {} : { limit: callOptions.limit }),
790
- }, makeContext(account.backend, correlationId, callOptions)));
791
- return {
792
- ...received,
793
- ...(received.nextCursor === undefined
794
- ? {}
795
- : { nextCursor: encodeCursor(cursorScope, received.nextCursor) }),
796
- };
1113
+ const received = await dispatch(account.backend, callOptions?.signal, "messages.read", () => adapter.messages.listConversations(account, decodePageOptions(cursorScope, callOptions), makeContext(account.backend, correlationId, callOptions)));
1114
+ return encodePage(cursorScope, received);
797
1115
  },
798
1116
  iterateConversations(account, callOptions) {
799
- return iterateItems((cursor) => messagesFacade.listConversations(account, {
800
- ...callOptions,
801
- ...(cursor === undefined ? {} : { cursor }),
802
- }), callOptions);
1117
+ return iterateItems((cursor) => messagesFacade.listConversations(account, iterationPageOptions(callOptions, cursor)), callOptions);
803
1118
  },
804
1119
  async listMessages(ref, callOptions) {
805
1120
  const correlationId = `social-${++correlationSequence}`;
1121
+ if (callOptions?.limit !== undefined &&
1122
+ (!Number.isSafeInteger(callOptions.limit) || callOptions.limit < 1))
1123
+ throw new SocialError({
1124
+ code: "invalid_input",
1125
+ operation: "messages.read",
1126
+ message: "Message page limit must be a positive integer",
1127
+ });
806
1128
  const account = {
807
1129
  kind: "connected-account",
808
1130
  version: 1,
@@ -824,24 +1146,11 @@ export function createSocial(config) {
824
1146
  ref.conversationId,
825
1147
  callOptions?.limit ?? null,
826
1148
  ]);
827
- const received = await dispatch(ref.backend, callOptions?.signal, "messages.read", () => adapter.messages.listMessages(ref, {
828
- ...(callOptions?.cursor === undefined
829
- ? {}
830
- : { cursor: decodeCursor(cursorScope, callOptions.cursor) }),
831
- ...(callOptions?.limit === undefined ? {} : { limit: callOptions.limit }),
832
- }, makeContext(ref.backend, correlationId, callOptions)));
833
- return {
834
- ...received,
835
- ...(received.nextCursor === undefined
836
- ? {}
837
- : { nextCursor: encodeCursor(cursorScope, received.nextCursor) }),
838
- };
1149
+ const received = await dispatch(ref.backend, callOptions?.signal, "messages.read", () => adapter.messages.listMessages(ref, decodePageOptions(cursorScope, callOptions), makeContext(ref.backend, correlationId, callOptions)));
1150
+ return encodePage(cursorScope, received);
839
1151
  },
840
1152
  iterateMessages(ref, callOptions) {
841
- return iterateItems((cursor) => messagesFacade.listMessages(ref, {
842
- ...callOptions,
843
- ...(cursor === undefined ? {} : { cursor }),
844
- }), callOptions);
1153
+ return iterateItems((cursor) => messagesFacade.listMessages(ref, iterationPageOptions(callOptions, cursor)), callOptions);
845
1154
  },
846
1155
  async send(ref, content, callOptions) {
847
1156
  const correlationId = `social-${++correlationSequence}`;
@@ -860,13 +1169,65 @@ export function createSocial(config) {
860
1169
  return dispatch(ref.backend, callOptions?.signal, "messages.write", () => adapter.messages.send(ref, content, makeContext(ref.backend, correlationId, callOptions)));
861
1170
  },
862
1171
  };
1172
+ const notificationsFacade = {
1173
+ async list(account, callOptions) {
1174
+ validateAccountRef(account, "notifications.read");
1175
+ if (callOptions?.limit !== undefined &&
1176
+ (!Number.isSafeInteger(callOptions.limit) || callOptions.limit < 1))
1177
+ throw new SocialError({
1178
+ code: "invalid_input",
1179
+ operation: "notifications.read",
1180
+ message: "Notification page limit must be a positive integer",
1181
+ });
1182
+ const correlationId = `social-${++correlationSequence}`;
1183
+ await authorizeRef("notifications.read", account, callOptions, correlationId);
1184
+ const adapter = selected(account, "notifications.list");
1185
+ requireCapability(adapter, "notifications.read", account.platform, account.backend);
1186
+ if (adapter.notifications === undefined)
1187
+ unsupported("notifications.read", account.backend);
1188
+ const cursorScope = JSON.stringify([
1189
+ account.backend,
1190
+ "notifications.read",
1191
+ callOptions?.authorization?.tenantId ?? null,
1192
+ account.platform,
1193
+ account.accountId,
1194
+ callOptions?.limit ?? null,
1195
+ ]);
1196
+ const received = await dispatch(account.backend, callOptions?.signal, "notifications.read", () => adapter.notifications.list(account, decodePageOptions(cursorScope, callOptions), makeContext(account.backend, correlationId, callOptions)));
1197
+ return encodePage(cursorScope, received);
1198
+ },
1199
+ iterate(account, callOptions) {
1200
+ return iterateItems((cursor) => notificationsFacade.list(account, iterationPageOptions(callOptions, cursor)), callOptions);
1201
+ },
1202
+ async markSeen(account, input = {}, callOptions) {
1203
+ validateAccountRef(account, "notifications.seen");
1204
+ if (input === null ||
1205
+ typeof input !== "object" ||
1206
+ (input.seenAt !== undefined && !validTimestamp(input.seenAt)))
1207
+ throw new SocialError({
1208
+ code: "invalid_input",
1209
+ operation: "notifications.seen",
1210
+ message: "seenAt must be a valid timestamp",
1211
+ });
1212
+ const correlationId = `social-${++correlationSequence}`;
1213
+ await authorizeRef("notifications.seen", account, callOptions, correlationId);
1214
+ const adapter = selected(account, "notifications.markSeen");
1215
+ requireCapability(adapter, "notifications.seen", account.platform, account.backend);
1216
+ if (adapter.notifications === undefined)
1217
+ unsupported("notifications.seen", account.backend);
1218
+ return dispatch(account.backend, callOptions?.signal, "notifications.seen", () => adapter.notifications.markSeen(account, input, makeContext(account.backend, correlationId, callOptions)));
1219
+ },
1220
+ };
863
1221
  return {
864
1222
  accounts: accountsFacade,
1223
+ graph: graphFacade,
865
1224
  posts: postsFacade,
1225
+ search: searchFacade,
866
1226
  media: mediaFacade,
867
1227
  analytics: analyticsFacade,
868
1228
  comments: commentsFacade,
869
1229
  messages: messagesFacade,
1230
+ notifications: notificationsFacade,
870
1231
  capabilities: () => Object.fromEntries(entries.map(([key, adapter]) => [key, adapter.capabilities])),
871
1232
  adapter: (backend, acknowledgement) => {
872
1233
  if (acknowledgement?.acknowledgeUnsafe !== true)
@@ -877,17 +1238,49 @@ export function createSocial(config) {
877
1238
  });
878
1239
  const selected = registry[String(backend)];
879
1240
  if (selected === undefined)
880
- throw new Error(`Unknown backend: ${String(backend)}`);
1241
+ throw new SocialError({
1242
+ code: "invalid_input",
1243
+ operation: "adapter",
1244
+ message: `Unknown backend: ${String(backend)}`,
1245
+ });
881
1246
  return selected;
882
1247
  },
883
1248
  native: (backend, acknowledgement) => {
884
1249
  if (acknowledgement?.acknowledgeUnsafe !== true)
885
- throw new Error("Native access requires acknowledgeUnsafe: true");
1250
+ throw new SocialError({
1251
+ code: "invalid_input",
1252
+ operation: "native",
1253
+ message: "Native access requires acknowledgeUnsafe: true",
1254
+ });
886
1255
  const selected = registry[String(backend)];
887
1256
  if (selected === undefined)
888
- throw new Error(`Unknown backend: ${String(backend)}`);
1257
+ throw new SocialError({
1258
+ code: "invalid_input",
1259
+ operation: "native",
1260
+ message: `Unknown backend: ${String(backend)}`,
1261
+ });
889
1262
  return selected.native;
890
1263
  },
891
1264
  };
892
1265
  }
1266
+ function decodePageOptions(scope, options) {
1267
+ const input = {};
1268
+ if (options?.cursor !== undefined)
1269
+ input.cursor = decodeCursor(scope, options.cursor);
1270
+ if (options?.limit !== undefined)
1271
+ input.limit = options.limit;
1272
+ return input;
1273
+ }
1274
+ function encodePage(scope, page) {
1275
+ const result = { ...page };
1276
+ if (page.nextCursor !== undefined)
1277
+ result.nextCursor = encodeCursor(scope, page.nextCursor);
1278
+ return result;
1279
+ }
1280
+ function iterationPageOptions(options, cursor) {
1281
+ const result = { ...options };
1282
+ if (cursor !== undefined)
1283
+ result.cursor = cursor;
1284
+ return result;
1285
+ }
893
1286
  //# sourceMappingURL=client.js.map