@opencoredev/social-sdk 0.1.2 → 0.2.1

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 (82) hide show
  1. package/README.md +6 -0
  2. package/dist/cli.d.ts +13 -0
  3. package/dist/cli.js +234 -0
  4. package/dist/cloud/common.d.ts +25 -0
  5. package/dist/cloud/common.js +334 -0
  6. package/dist/cloud/lifecycle.d.ts +10 -0
  7. package/dist/cloud/lifecycle.js +112 -0
  8. package/dist/cloud/media.d.ts +23 -0
  9. package/dist/cloud/media.js +100 -0
  10. package/dist/cloud/outcomes.d.ts +9 -0
  11. package/dist/cloud/outcomes.js +195 -0
  12. package/dist/cloud/post-for-me.d.ts +69 -0
  13. package/dist/cloud/post-for-me.js +396 -0
  14. package/dist/cloud/zernio.d.ts +111 -0
  15. package/dist/cloud/zernio.js +632 -0
  16. package/dist/core/adapter.d.ts +158 -0
  17. package/dist/core/adapter.js +3 -0
  18. package/dist/core/client.d.ts +141 -0
  19. package/dist/core/client.js +1285 -0
  20. package/dist/core/concurrency.d.ts +9 -0
  21. package/dist/core/concurrency.js +79 -0
  22. package/dist/core/errors.d.ts +44 -0
  23. package/dist/core/errors.js +50 -0
  24. package/dist/core/idempotency.d.ts +33 -0
  25. package/dist/core/idempotency.js +58 -0
  26. package/dist/core/index.d.ts +6 -0
  27. package/dist/core/index.js +6 -0
  28. package/dist/core/pagination.d.ts +11 -0
  29. package/dist/core/pagination.js +81 -0
  30. package/dist/core/types.d.ts +396 -0
  31. package/dist/core/types.js +16 -0
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +1 -0
  34. package/dist/platforms/bluesky.d.ts +261 -0
  35. package/dist/platforms/bluesky.js +1776 -0
  36. package/dist/platforms/instagram.d.ts +129 -0
  37. package/dist/platforms/instagram.js +1031 -0
  38. package/dist/platforms/linkedin.d.ts +108 -0
  39. package/dist/platforms/linkedin.js +890 -0
  40. package/dist/platforms/threads.d.ts +134 -0
  41. package/dist/platforms/threads.js +945 -0
  42. package/dist/platforms/tiktok.d.ts +31 -0
  43. package/dist/platforms/tiktok.js +593 -0
  44. package/dist/platforms/x-engagement.d.ts +16 -0
  45. package/dist/platforms/x-engagement.js +50 -0
  46. package/dist/platforms/x-text.d.ts +2 -0
  47. package/dist/platforms/x-text.js +123 -0
  48. package/dist/platforms/x-tlds.d.ts +1 -0
  49. package/dist/platforms/x-tlds.js +2 -0
  50. package/dist/platforms/x.d.ts +299 -0
  51. package/dist/platforms/x.js +1287 -0
  52. package/dist/platforms/youtube-upload.d.ts +32 -0
  53. package/dist/platforms/youtube-upload.js +296 -0
  54. package/dist/platforms/youtube.d.ts +108 -0
  55. package/dist/platforms/youtube.js +1100 -0
  56. package/dist/server/connections.d.ts +123 -0
  57. package/dist/server/connections.js +335 -0
  58. package/dist/server/credentials.d.ts +51 -0
  59. package/dist/server/credentials.js +108 -0
  60. package/dist/server/index.d.ts +4 -0
  61. package/dist/server/index.js +4 -0
  62. package/dist/server/oauth.d.ts +52 -0
  63. package/dist/server/oauth.js +553 -0
  64. package/dist/server/webhooks.d.ts +57 -0
  65. package/dist/server/webhooks.js +204 -0
  66. package/dist/testing/index.d.ts +46 -0
  67. package/dist/testing/index.js +564 -0
  68. package/dist/testing.d.ts +1 -0
  69. package/dist/testing.js +1 -0
  70. package/dist/transport/binary.d.ts +2 -0
  71. package/dist/transport/binary.js +29 -0
  72. package/dist/transport/budget.d.ts +3 -0
  73. package/dist/transport/budget.js +26 -0
  74. package/dist/transport/http.d.ts +44 -0
  75. package/dist/transport/http.js +259 -0
  76. package/dist/transport/json.d.ts +8 -0
  77. package/dist/transport/json.js +16 -0
  78. package/dist/transport/upload.d.ts +25 -0
  79. package/dist/transport/upload.js +153 -0
  80. package/dist/transport/validation.d.ts +5 -0
  81. package/dist/transport/validation.js +24 -0
  82. package/package.json +2 -7
@@ -0,0 +1,1285 @@
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. */
2
+ import { decodeCursor, encodeCursor, iterateItems } from "./pagination.js";
3
+ import { createConcurrencyLimiter } from "./concurrency.js";
4
+ import { SocialError } from "./errors.js";
5
+ import { deriveTargetIdempotencyKey, fingerprint, } from "./idempotency.js";
6
+ function targetKey(account) {
7
+ return JSON.stringify([account.backend, account.platform, account.accountId]);
8
+ }
9
+ function mergeContent(base, override) {
10
+ if (override === undefined)
11
+ return base;
12
+ const result = {};
13
+ if (Object.hasOwn(override, "text")) {
14
+ if (override.text !== undefined)
15
+ Object.assign(result, { text: override.text });
16
+ }
17
+ else if (base.text !== undefined)
18
+ Object.assign(result, { text: base.text });
19
+ if (Object.hasOwn(override, "media")) {
20
+ if (override.media !== undefined)
21
+ Object.assign(result, { media: override.media });
22
+ }
23
+ else if (base.media !== undefined)
24
+ Object.assign(result, { media: base.media });
25
+ if (Object.hasOwn(override, "link")) {
26
+ if (override.link !== undefined)
27
+ Object.assign(result, { link: override.link });
28
+ }
29
+ else if (base.link !== undefined)
30
+ Object.assign(result, { link: base.link });
31
+ return result;
32
+ }
33
+ function mediaFingerprintView(content) {
34
+ return {
35
+ ...content,
36
+ media: content.media?.map((media) => ({
37
+ ...media,
38
+ source: media.source.kind === "blob" || media.source.kind === "stream"
39
+ ? { kind: media.source.kind, fingerprint: media.source.fingerprint }
40
+ : media.source,
41
+ thumbnail: media.thumbnail?.kind === "blob" || media.thumbnail?.kind === "stream"
42
+ ? { kind: media.thumbnail.kind, fingerprint: media.thumbnail.fingerprint }
43
+ : media.thumbnail,
44
+ })),
45
+ };
46
+ }
47
+ function preparationIssue(code, message, targetIndex) {
48
+ const issue = { code, message, severity: "error" };
49
+ if (targetIndex !== undefined)
50
+ Object.assign(issue, { targetIndex });
51
+ return issue;
52
+ }
53
+ function publicationStatus(outcomes) {
54
+ const pending = new Set(["scheduled", "accepted", "processing"]);
55
+ const success = new Set(["published"]);
56
+ const hasPending = outcomes.some((outcome) => pending.has(outcome.state));
57
+ const hasSuccess = outcomes.some((outcome) => success.has(outcome.state));
58
+ const hasOther = outcomes.some((outcome) => !pending.has(outcome.state) && !success.has(outcome.state));
59
+ if (hasPending && !hasSuccess && !hasOther)
60
+ return "pending";
61
+ if (!hasPending && !hasOther)
62
+ return "complete";
63
+ return "partial";
64
+ }
65
+ async function mapConcurrent(count, concurrency, execute) {
66
+ const results = [];
67
+ let next = 0;
68
+ async function worker() {
69
+ while (next < count) {
70
+ const index = next++;
71
+ results[index] = await execute(index);
72
+ }
73
+ }
74
+ await Promise.all(Array.from({ length: Math.min(count, concurrency) }, () => worker()));
75
+ return results;
76
+ }
77
+ function makeContext(backendInstance, correlationId, options, targetIdempotencyKey) {
78
+ const context = {
79
+ backendInstance,
80
+ correlationId,
81
+ retryBudget: options?.retryBudget ?? { maxAttempts: 1, maxElapsedMs: 30_000 },
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;
90
+ }
91
+ function unsupported(operation, backend) {
92
+ throw new SocialError({
93
+ code: "unsupported_capability",
94
+ operation,
95
+ backend,
96
+ message: `${backend} does not implement ${operation}`,
97
+ });
98
+ }
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) {
102
+ if (error instanceof SocialError) {
103
+ if (error.code === "cancelled") {
104
+ return {
105
+ state: "unknown",
106
+ targetIndex: target.targetIndex,
107
+ account: target.account,
108
+ observedAt,
109
+ reason: "ambiguous-submission",
110
+ diagnostic: "Cancellation interrupted a dispatched request; reconcile before retrying",
111
+ };
112
+ }
113
+ if (error.code === "ambiguous_outcome" || error.code === "timeout") {
114
+ return {
115
+ state: "unknown",
116
+ targetIndex: target.targetIndex,
117
+ account: target.account,
118
+ observedAt,
119
+ reason: "ambiguous-submission",
120
+ diagnostic: "The request outcome is ambiguous; reconcile with the backend before retrying",
121
+ };
122
+ }
123
+ return {
124
+ state: "failed",
125
+ targetIndex: target.targetIndex,
126
+ account: target.account,
127
+ observedAt,
128
+ code: error.code,
129
+ message: error.message,
130
+ retryDisposition: error.retryDisposition,
131
+ };
132
+ }
133
+ return {
134
+ state: "unknown",
135
+ targetIndex: target.targetIndex,
136
+ account: target.account,
137
+ observedAt,
138
+ reason: "ambiguous-submission",
139
+ diagnostic: "Adapter failed after dispatch; reconcile with the backend before retrying",
140
+ };
141
+ }
142
+ function assertValidOutcome(outcome, target) {
143
+ return (outcome.targetIndex === target.targetIndex &&
144
+ outcome.account.backend === target.account.backend &&
145
+ outcome.account.platform === target.account.platform &&
146
+ outcome.account.accountId === target.account.accountId);
147
+ }
148
+ export function createSocial(config) {
149
+ const registry = "backend" in config && config.backend !== undefined
150
+ ? { default: config.backend }
151
+ : config.backends;
152
+ const entries = Object.entries(registry);
153
+ if (entries.length === 0) {
154
+ throw new SocialError({
155
+ code: "invalid_config",
156
+ operation: "createSocial",
157
+ message: "Configure at least one backend",
158
+ });
159
+ }
160
+ const concurrency = config.concurrency ?? 4;
161
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > 100) {
162
+ throw new SocialError({
163
+ code: "invalid_config",
164
+ operation: "createSocial",
165
+ message: "concurrency must be an integer from 1 through 100",
166
+ });
167
+ }
168
+ const maxQueued = config.maxQueued ?? 100;
169
+ if (!Number.isSafeInteger(maxQueued) || maxQueued < 0 || maxQueued > 10_000) {
170
+ throw new SocialError({
171
+ code: "invalid_config",
172
+ operation: "createSocial",
173
+ message: "maxQueued must be an integer from 0 through 10000",
174
+ });
175
+ }
176
+ const limiters = new Map(entries.map(([backend]) => [
177
+ backend,
178
+ createConcurrencyLimiter({ maxActive: concurrency, maxQueued, backend }),
179
+ ]));
180
+ function dispatch(backend, signal, operation, work) {
181
+ const limiter = limiters.get(backend);
182
+ if (limiter === undefined) {
183
+ throw new SocialError({
184
+ code: "invalid_input",
185
+ operation,
186
+ message: `Unknown backend instance: ${backend}`,
187
+ });
188
+ }
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
+ });
200
+ }
201
+ const clock = config.clock ?? (() => new Date());
202
+ let correlationSequence = 0;
203
+ function prepare(request) {
204
+ const issues = [];
205
+ const targets = [];
206
+ if (request.targets.length === 0) {
207
+ issues.push(preparationIssue("targets.required", "At least one target is required"));
208
+ }
209
+ if (request.content.text === undefined && (request.content.media?.length ?? 0) === 0) {
210
+ issues.push(preparationIssue("content.required", "Text or media is required"));
211
+ }
212
+ if (request.schedule !== undefined) {
213
+ const scheduledAt = Date.parse(request.schedule.at);
214
+ if (!Number.isFinite(scheduledAt)) {
215
+ issues.push(preparationIssue("schedule.invalid", "schedule.at must be an ISO timestamp"));
216
+ }
217
+ else if (scheduledAt <= clock().getTime()) {
218
+ issues.push(preparationIssue("schedule.in_past", "A scheduled time must be in the future"));
219
+ }
220
+ }
221
+ const seen = new Set();
222
+ for (const [targetIndex, target] of request.targets.entries()) {
223
+ if (target.account.kind !== "connected-account" ||
224
+ target.account.version !== 1 ||
225
+ target.account.backend.length === 0 ||
226
+ target.account.platform.length === 0 ||
227
+ target.account.accountId.length === 0) {
228
+ issues.push(preparationIssue("target.reference_invalid", "Target account reference must be a version 1 connected-account reference", targetIndex));
229
+ continue;
230
+ }
231
+ const key = targetKey(target.account);
232
+ if (seen.has(key)) {
233
+ issues.push(preparationIssue("target.duplicate", "Duplicate target", targetIndex));
234
+ }
235
+ seen.add(key);
236
+ const adapter = registry[target.account.backend];
237
+ if (adapter === undefined) {
238
+ issues.push(preparationIssue("target.backend_unknown", `Unknown backend instance: ${target.account.backend}`, targetIndex));
239
+ continue;
240
+ }
241
+ if (adapter.posts === undefined) {
242
+ issues.push(preparationIssue("target.posts_unavailable", `Backend ${target.account.backend} does not implement posts`, targetIndex));
243
+ continue;
244
+ }
245
+ const declaration = adapter.capabilities.capabilities.find((candidate) => candidate.operation === "posts.publish" &&
246
+ (candidate.platform === "*" || candidate.platform === target.account.platform));
247
+ if (declaration === undefined || declaration.availability !== "available") {
248
+ issues.push(preparationIssue("capability.unavailable", declaration?.notes ??
249
+ `Backend ${target.account.backend} does not declare posts.publish for ${target.account.platform}`, targetIndex));
250
+ continue;
251
+ }
252
+ const replyTo = target.replyTo ?? request.replyTo;
253
+ if (replyTo &&
254
+ (replyTo.version !== 1 ||
255
+ !["platform-post", "comment"].includes(replyTo.kind) ||
256
+ replyTo.backend !== target.account.backend ||
257
+ replyTo.platform !== target.account.platform ||
258
+ replyTo.accountId !== target.account.accountId)) {
259
+ issues.push(preparationIssue("reply.reference_mismatch", "Reply references must use the selected account, backend, and platform", targetIndex));
260
+ }
261
+ const prepared = {
262
+ targetIndex,
263
+ targetKey: key,
264
+ account: target.account,
265
+ content: mergeContent(request.content, target.content),
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 });
273
+ targets.push(prepared);
274
+ try {
275
+ issues.push(...adapter.posts.prepareTarget(prepared));
276
+ }
277
+ catch (error) {
278
+ issues.push(preparationIssue("adapter.prepare_failed", error instanceof SocialError
279
+ ? error.message
280
+ : "The adapter could not validate this target locally", targetIndex));
281
+ }
282
+ }
283
+ return { ok: !issues.some((issue) => issue.severity === "error"), targets, issues };
284
+ }
285
+ async function authorizeRef(operation, account, options, correlationId) {
286
+ if (config.authorization === undefined)
287
+ return;
288
+ const decision = (await config.authorization.authorizeTargets({
289
+ operation,
290
+ accounts: [account],
291
+ context: makeContext("*", correlationId, options),
292
+ }))[0];
293
+ if (decision?.allowed !== true || targetKey(decision.account) !== targetKey(account)) {
294
+ throw new SocialError({
295
+ code: "unauthorized",
296
+ operation,
297
+ message: decision?.reason ?? "The account is not authorized for this operation",
298
+ account,
299
+ correlationId,
300
+ });
301
+ }
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
+ }
338
+ function selected(ref, operation) {
339
+ if (ref.kind !== undefined && (ref.kind === "" || ref.version !== 1)) {
340
+ throw new SocialError({
341
+ code: "invalid_input",
342
+ operation,
343
+ message: "Reference kind/version does not match the Social SDK contract",
344
+ });
345
+ }
346
+ const adapter = registry[ref.backend];
347
+ if (adapter === undefined) {
348
+ throw new SocialError({
349
+ code: "invalid_input",
350
+ operation,
351
+ message: `Unknown backend instance: ${ref.backend}`,
352
+ });
353
+ }
354
+ return adapter;
355
+ }
356
+ function requireCapability(adapter, operation, platform, backend) {
357
+ const declaration = adapter.capabilities.capabilities.find((candidate) => candidate.operation === operation &&
358
+ (platform === "*" || candidate.platform === "*" || candidate.platform === platform));
359
+ if (declaration?.availability !== "available")
360
+ unsupported(operation, backend);
361
+ }
362
+ async function publish(request, options) {
363
+ const correlationId = request.correlationId ?? `social-${++correlationSequence}`;
364
+ const authContext = makeContext("*", correlationId, options);
365
+ if (options?.signal?.aborted) {
366
+ throw new SocialError({
367
+ code: "cancelled",
368
+ operation: "posts.publish",
369
+ message: "Publishing was cancelled before authorization",
370
+ correlationId,
371
+ });
372
+ }
373
+ if (config.authorization !== undefined) {
374
+ const decisions = await config.authorization.authorizeTargets({
375
+ operation: "posts.publish",
376
+ accounts: request.targets.map((target) => target.account),
377
+ context: authContext,
378
+ });
379
+ for (const target of request.targets) {
380
+ const decision = decisions.find((candidate) => targetKey(candidate.account) === targetKey(target.account));
381
+ if (decision?.allowed !== true) {
382
+ throw new SocialError({
383
+ code: "unauthorized",
384
+ operation: "posts.publish",
385
+ message: decision?.reason ?? "A target is not authorized for this operation",
386
+ account: target.account,
387
+ correlationId,
388
+ });
389
+ }
390
+ }
391
+ }
392
+ const plan = prepare(request);
393
+ if (!plan.ok) {
394
+ throw new SocialError({
395
+ code: "invalid_input",
396
+ operation: "posts.publish",
397
+ message: "Publication preparation failed; no targets were dispatched",
398
+ correlationId,
399
+ issues: plan.issues,
400
+ });
401
+ }
402
+ const targetViews = plan.targets.map((target) => ({
403
+ account: target.account,
404
+ content: mediaFingerprintView(target.content),
405
+ options: target.options,
406
+ replyTo: target.replyTo,
407
+ schedule: target.schedule,
408
+ }));
409
+ const payloadFingerprint = await fingerprint(targetViews);
410
+ const scope = JSON.stringify([
411
+ options?.authorization?.tenantId ?? "credential-ready",
412
+ "posts.publish",
413
+ ]);
414
+ let claim;
415
+ if (request.idempotencyKey !== undefined && config.idempotencyStore !== undefined) {
416
+ claim = await config.idempotencyStore.claim({
417
+ scope,
418
+ key: request.idempotencyKey,
419
+ fingerprint: payloadFingerprint,
420
+ targetKeys: plan.targets.map((target) => target.targetKey),
421
+ });
422
+ if (claim.kind === "conflict") {
423
+ throw new SocialError({
424
+ code: "idempotency_conflict",
425
+ operation: "posts.publish",
426
+ message: "The idempotency key was already used with a different target or payload",
427
+ correlationId,
428
+ });
429
+ }
430
+ }
431
+ const observedAt = () => clock().toISOString();
432
+ const outcomes = await mapConcurrent(plan.targets.length, Math.max(1, plan.targets.length), async (index) => {
433
+ const target = plan.targets[index];
434
+ if (target === undefined)
435
+ throw new Error("Prepared target index was lost");
436
+ if (claim !== undefined && claim.kind === "existing") {
437
+ const saved = claim.outcomes[target.targetKey];
438
+ if (saved !== undefined)
439
+ return saved;
440
+ return {
441
+ state: "unknown",
442
+ targetIndex: target.targetIndex,
443
+ account: target.account,
444
+ observedAt: observedAt(),
445
+ reason: "ambiguous-submission",
446
+ diagnostic: "A previous execution claimed this target without recording an outcome",
447
+ };
448
+ }
449
+ const adapter = registry[target.account.backend];
450
+ if (adapter?.posts === undefined)
451
+ throw new Error("Prepared adapter disappeared");
452
+ const targetIdempotencyKey = request.idempotencyKey === undefined
453
+ ? undefined
454
+ : await deriveTargetIdempotencyKey({
455
+ logicalKey: request.idempotencyKey,
456
+ scope,
457
+ backend: target.account.backend,
458
+ targetKey: target.targetKey,
459
+ payloadFingerprint,
460
+ });
461
+ let outcome;
462
+ let enteredAdapter = false;
463
+ try {
464
+ const candidate = await dispatch(target.account.backend, options?.signal, "posts.publish", () => {
465
+ enteredAdapter = true;
466
+ return adapter.posts.publishTarget(target, makeContext(target.account.backend, correlationId, options, targetIdempotencyKey));
467
+ });
468
+ outcome = assertValidOutcome(candidate, target)
469
+ ? candidate
470
+ : {
471
+ state: "unknown",
472
+ targetIndex: target.targetIndex,
473
+ account: target.account,
474
+ observedAt: observedAt(),
475
+ reason: "unmapped-state",
476
+ diagnostic: "Adapter returned an outcome for a different target",
477
+ };
478
+ }
479
+ catch (error) {
480
+ outcome =
481
+ !enteredAdapter && error instanceof SocialError && error.code === "cancelled"
482
+ ? {
483
+ state: "cancelled",
484
+ targetIndex: target.targetIndex,
485
+ account: target.account,
486
+ observedAt: observedAt(),
487
+ reason: "before-submission",
488
+ }
489
+ : !enteredAdapter && error instanceof SocialError && error.code === "rate_limited"
490
+ ? {
491
+ state: "not-submitted",
492
+ targetIndex: target.targetIndex,
493
+ account: target.account,
494
+ observedAt: observedAt(),
495
+ reason: "capacity",
496
+ }
497
+ : outcomeFromError(error, target, observedAt());
498
+ }
499
+ if (claim?.kind === "new" && config.idempotencyStore !== undefined) {
500
+ try {
501
+ await config.idempotencyStore.saveOutcome({
502
+ claimId: claim.claimId,
503
+ targetKey: target.targetKey,
504
+ outcome,
505
+ });
506
+ }
507
+ catch {
508
+ // Return the complete in-memory result even if durable persistence is unavailable.
509
+ }
510
+ }
511
+ return outcome;
512
+ });
513
+ outcomes.sort((left, right) => left.targetIndex - right.targetIndex);
514
+ const publicationBackend = new Set(plan.targets.map((target) => target.account.backend)).size === 1
515
+ ? (plan.targets[0]?.account.backend ?? "unknown")
516
+ : "multiple";
517
+ const publicationId = await fingerprint({
518
+ scope,
519
+ key: request.idempotencyKey ?? correlationId,
520
+ payloadFingerprint,
521
+ });
522
+ const publication = {
523
+ kind: "publication",
524
+ version: 1,
525
+ backend: publicationBackend,
526
+ publicationId,
527
+ };
528
+ return { status: publicationStatus(outcomes), publication, outcomes };
529
+ }
530
+ const accountsFacade = {
531
+ async list(callOptions) {
532
+ const correlationId = `social-${++correlationSequence}`;
533
+ if (entries.length > 1 && callOptions?.backend === undefined)
534
+ throw new SocialError({
535
+ code: "invalid_input",
536
+ operation: "accounts.read",
537
+ message: "Select a backend instance when listing accounts from a mixed registry.",
538
+ });
539
+ const first = callOptions?.backend === undefined
540
+ ? entries[0]
541
+ : entries.find(([key]) => key === callOptions.backend);
542
+ if (first === undefined || first[1].accounts === undefined)
543
+ unsupported("accounts.read", first?.[0] ?? "unknown");
544
+ requireCapability(first[1], "accounts.read", "*", first[0]);
545
+ const cursorScope = JSON.stringify([
546
+ first[0],
547
+ "accounts.read",
548
+ callOptions?.authorization?.tenantId ?? null,
549
+ callOptions?.limit ?? null,
550
+ ]);
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);
553
+ if (config.authorization === undefined)
554
+ return page;
555
+ const decisions = await config.authorization.authorizeTargets({
556
+ operation: "accounts.read",
557
+ accounts: page.items.map((item) => item.ref),
558
+ context: makeContext(first[0], correlationId, callOptions),
559
+ });
560
+ const allowed = new Set(decisions.filter((item) => item.allowed).map((item) => targetKey(item.account)));
561
+ const filtered = {
562
+ items: page.items.filter((item) => allowed.has(targetKey(item.ref))),
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;
569
+ },
570
+ iterate(callOptions) {
571
+ return iterateItems((cursor) => accountsFacade.list(iterationPageOptions(callOptions, cursor)), callOptions);
572
+ },
573
+ async get(ref, callOptions) {
574
+ const correlationId = `social-${++correlationSequence}`;
575
+ await authorizeRef("accounts.read", ref, callOptions, correlationId);
576
+ const adapter = selected(ref, "accounts.get");
577
+ requireCapability(adapter, "accounts.read", ref.platform, ref.backend);
578
+ if (adapter.accounts === undefined)
579
+ unsupported("accounts.read", ref.backend);
580
+ return dispatch(ref.backend, callOptions?.signal, "accounts.read", () => adapter.accounts.get(ref, makeContext(ref.backend, correlationId, callOptions)));
581
+ },
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
+ }
714
+ async function lifecycle(ref, expectedKind, operation, callOptions) {
715
+ if (ref.kind !== expectedKind || ref.version !== 1 || !ref.accountId || !ref.platform)
716
+ throw new SocialError({
717
+ code: "invalid_input",
718
+ operation,
719
+ message: "Use the account-scoped reference returned for this resource kind.",
720
+ });
721
+ const correlationId = `social-${++correlationSequence}`;
722
+ await authorizeRef(operation, {
723
+ kind: "connected-account",
724
+ version: 1,
725
+ backend: ref.backend,
726
+ platform: ref.platform,
727
+ accountId: ref.accountId,
728
+ }, callOptions, correlationId);
729
+ const adapter = selected(ref, operation);
730
+ requireCapability(adapter, operation, ref.platform, ref.backend);
731
+ return { adapter, context: makeContext(ref.backend, correlationId, callOptions) };
732
+ }
733
+ const postsFacade = {
734
+ async list(account, callOptions) {
735
+ const correlationId = `social-${++correlationSequence}`;
736
+ await authorizeRef("posts.read", account, callOptions, correlationId);
737
+ const adapter = selected(account, "posts.list");
738
+ requireCapability(adapter, "posts.list", account.platform, account.backend);
739
+ if (!adapter.posts?.list)
740
+ unsupported("posts.list", account.backend);
741
+ const scope = JSON.stringify([
742
+ account.backend,
743
+ "posts.list",
744
+ callOptions?.authorization?.tenantId ?? null,
745
+ account.platform,
746
+ account.accountId,
747
+ callOptions?.limit ?? null,
748
+ ]);
749
+ const input = decodePageOptions(scope, callOptions);
750
+ const page = await dispatch(account.backend, callOptions?.signal, "posts.list", () => adapter.posts.list(account, input, makeContext(account.backend, correlationId, callOptions)));
751
+ return encodePage(scope, page);
752
+ },
753
+ iterate(account, callOptions) {
754
+ return iterateItems((cursor) => postsFacade.list(account, iterationPageOptions(callOptions, cursor)), callOptions);
755
+ },
756
+ async cancelScheduled(ref, callOptions) {
757
+ const { adapter, context } = await lifecycle(ref, "scheduled-job", "posts.cancelScheduled", callOptions);
758
+ if (!adapter.posts?.cancelScheduled)
759
+ unsupported("posts.cancelScheduled", ref.backend);
760
+ return dispatch(ref.backend, callOptions?.signal, "posts.cancelScheduled", () => adapter.posts.cancelScheduled(ref, context));
761
+ },
762
+ async deleteBackendRecord(ref, callOptions) {
763
+ const { adapter, context } = await lifecycle(ref, "backend-post", "posts.deleteBackendRecord", callOptions);
764
+ if (!adapter.posts?.deleteBackendRecord)
765
+ unsupported("posts.deleteBackendRecord", ref.backend);
766
+ return dispatch(ref.backend, callOptions?.signal, "posts.deleteBackendRecord", () => adapter.posts.deleteBackendRecord(ref, context));
767
+ },
768
+ async removeFromPlatform(ref, callOptions) {
769
+ const { adapter, context } = await lifecycle(ref, "platform-post", "posts.removeFromPlatform", callOptions);
770
+ if (!adapter.posts?.removeFromPlatform)
771
+ unsupported("posts.removeFromPlatform", ref.backend);
772
+ return dispatch(ref.backend, callOptions?.signal, "posts.removeFromPlatform", () => adapter.posts.removeFromPlatform(ref, context));
773
+ },
774
+ prepare,
775
+ publish,
776
+ async publishSequence(request, callOptions) {
777
+ if (request.items.length === 0)
778
+ throw new SocialError({
779
+ code: "invalid_input",
780
+ operation: "posts.publishSequence",
781
+ message: "A sequence requires at least one item",
782
+ });
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 = [];
790
+ for (const [index, item] of request.items.entries()) {
791
+ const publishRequest = {
792
+ targets: item.targets,
793
+ content: item.content,
794
+ idempotencyKey: `${request.idempotencyKey}:${index}`,
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
+ }
869
+ results.push(result);
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")
874
+ break;
875
+ }
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"
880
+ : "pending";
881
+ return { status, items: results, failures };
882
+ },
883
+ async get(ref, callOptions) {
884
+ const correlationId = `social-${++correlationSequence}`;
885
+ await authorizeRef("posts.read", {
886
+ kind: "connected-account",
887
+ version: 1,
888
+ backend: ref.backend,
889
+ platform: ref.platform,
890
+ accountId: ref.accountId,
891
+ }, callOptions, correlationId);
892
+ const adapter = selected(ref, "posts.get");
893
+ requireCapability(adapter, "posts.read", ref.platform, ref.backend);
894
+ if (adapter.posts?.get === undefined)
895
+ unsupported("posts.read", ref.backend);
896
+ return dispatch(ref.backend, callOptions?.signal, "posts.read", () => adapter.posts.get(ref, makeContext(ref.backend, correlationId, callOptions)));
897
+ },
898
+ async getDelivery(ref, callOptions) {
899
+ const correlationId = `social-${++correlationSequence}`;
900
+ const account = {
901
+ kind: "connected-account",
902
+ version: 1,
903
+ backend: ref.backend,
904
+ platform: ref.platform,
905
+ accountId: ref.accountId,
906
+ };
907
+ await authorizeRef("posts.read", account, callOptions, correlationId);
908
+ const adapter = selected(ref, "posts.getDelivery");
909
+ requireCapability(adapter, "posts.status", ref.platform, ref.backend);
910
+ if (adapter.posts?.getDelivery === undefined)
911
+ unsupported("posts.status", ref.backend);
912
+ return dispatch(ref.backend, callOptions?.signal, "posts.status", () => adapter.posts.getDelivery(ref, makeContext(ref.backend, correlationId, callOptions)));
913
+ },
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
+ };
966
+ const mediaFacade = {
967
+ async upload(input, account, callOptions) {
968
+ const correlationId = `social-${++correlationSequence}`;
969
+ await authorizeRef("posts.publish", account, callOptions, correlationId);
970
+ const adapter = selected(account, "media.upload");
971
+ requireCapability(adapter, "media.upload", account.platform, account.backend);
972
+ if (adapter.media === undefined)
973
+ unsupported("media.upload", account.backend);
974
+ return dispatch(account.backend, callOptions?.signal, "media.upload", () => adapter.media.upload(input, account, makeContext(account.backend, correlationId, callOptions)));
975
+ },
976
+ };
977
+ const analyticsFacade = {
978
+ async getAccountMetrics(ref, callOptions) {
979
+ validateAccountRef(ref, "analytics.account.read");
980
+ const correlationId = `social-${++correlationSequence}`;
981
+ await authorizeRefWithFallback("analytics.account.read", "analytics.read", ref, callOptions, correlationId);
982
+ const adapter = selected(ref, "analytics.getAccountMetrics");
983
+ requireCapability(adapter, "analytics.account.read", ref.platform, ref.backend);
984
+ if (!adapter.analytics?.getAccountMetrics)
985
+ unsupported("analytics.account.read", ref.backend);
986
+ return dispatch(ref.backend, callOptions?.signal, "analytics.account.read", () => adapter.analytics.getAccountMetrics(ref, makeContext(ref.backend, correlationId, callOptions)));
987
+ },
988
+ async getPostMetrics(ref, callOptions) {
989
+ const correlationId = `social-${++correlationSequence}`;
990
+ const account = {
991
+ kind: "connected-account",
992
+ version: 1,
993
+ backend: ref.backend,
994
+ platform: ref.platform,
995
+ accountId: ref.accountId,
996
+ };
997
+ await authorizeRef("analytics.read", account, callOptions, correlationId);
998
+ const adapter = selected(ref, "analytics.getPostMetrics");
999
+ requireCapability(adapter, "analytics.read", ref.platform, ref.backend);
1000
+ if (adapter.analytics === undefined)
1001
+ unsupported("analytics.read", ref.backend);
1002
+ return dispatch(ref.backend, callOptions?.signal, "analytics.read", () => adapter.analytics.getPostMetrics(ref, makeContext(ref.backend, correlationId, callOptions)));
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
+ },
1034
+ };
1035
+ const commentsFacade = {
1036
+ async list(ref, callOptions) {
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
+ });
1045
+ const account = {
1046
+ kind: "connected-account",
1047
+ version: 1,
1048
+ backend: ref.backend,
1049
+ platform: ref.platform,
1050
+ accountId: ref.accountId,
1051
+ };
1052
+ await authorizeRef("comments.read", account, callOptions, correlationId);
1053
+ const adapter = selected(ref, "comments.list");
1054
+ requireCapability(adapter, "comments.read", ref.platform, ref.backend);
1055
+ if (adapter.comments === undefined)
1056
+ unsupported("comments.read", ref.backend);
1057
+ const cursorScope = JSON.stringify([
1058
+ ref.backend,
1059
+ "comments.read",
1060
+ callOptions?.authorization?.tenantId ?? null,
1061
+ ref.platform,
1062
+ ref.accountId,
1063
+ ref.postId,
1064
+ callOptions?.limit ?? null,
1065
+ ]);
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);
1068
+ },
1069
+ iterate(ref, callOptions) {
1070
+ return iterateItems((cursor) => commentsFacade.list(ref, iterationPageOptions(callOptions, cursor)), callOptions);
1071
+ },
1072
+ async reply(ref, content, callOptions) {
1073
+ const correlationId = `social-${++correlationSequence}`;
1074
+ const account = {
1075
+ kind: "connected-account",
1076
+ version: 1,
1077
+ backend: ref.backend,
1078
+ platform: ref.platform,
1079
+ accountId: ref.accountId,
1080
+ };
1081
+ await authorizeRef("comments.write", account, callOptions, correlationId);
1082
+ const adapter = selected(ref, "comments.reply");
1083
+ requireCapability(adapter, "comments.write", ref.platform, ref.backend);
1084
+ if (adapter.comments === undefined)
1085
+ unsupported("comments.write", ref.backend);
1086
+ return dispatch(ref.backend, callOptions?.signal, "comments.write", () => adapter.comments.reply(ref, content, makeContext(ref.backend, correlationId, callOptions)));
1087
+ },
1088
+ };
1089
+ const messagesFacade = {
1090
+ async listConversations(account, callOptions) {
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
+ });
1099
+ await authorizeRef("messages.read", account, callOptions, correlationId);
1100
+ const adapter = selected(account, "messages.listConversations");
1101
+ requireCapability(adapter, "messages.read", account.platform, account.backend);
1102
+ if (adapter.messages === undefined)
1103
+ unsupported("messages.read", account.backend);
1104
+ const cursorScope = JSON.stringify([
1105
+ account.backend,
1106
+ "messages.read",
1107
+ callOptions?.authorization?.tenantId ?? null,
1108
+ account.platform,
1109
+ account.accountId,
1110
+ "conversations",
1111
+ callOptions?.limit ?? null,
1112
+ ]);
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);
1115
+ },
1116
+ iterateConversations(account, callOptions) {
1117
+ return iterateItems((cursor) => messagesFacade.listConversations(account, iterationPageOptions(callOptions, cursor)), callOptions);
1118
+ },
1119
+ async listMessages(ref, callOptions) {
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
+ });
1128
+ const account = {
1129
+ kind: "connected-account",
1130
+ version: 1,
1131
+ backend: ref.backend,
1132
+ platform: ref.platform,
1133
+ accountId: ref.accountId,
1134
+ };
1135
+ await authorizeRef("messages.read", account, callOptions, correlationId);
1136
+ const adapter = selected(ref, "messages.listMessages");
1137
+ requireCapability(adapter, "messages.read", ref.platform, ref.backend);
1138
+ if (adapter.messages === undefined)
1139
+ unsupported("messages.read", ref.backend);
1140
+ const cursorScope = JSON.stringify([
1141
+ ref.backend,
1142
+ "messages.read",
1143
+ callOptions?.authorization?.tenantId ?? null,
1144
+ ref.platform,
1145
+ ref.accountId,
1146
+ ref.conversationId,
1147
+ callOptions?.limit ?? null,
1148
+ ]);
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);
1151
+ },
1152
+ iterateMessages(ref, callOptions) {
1153
+ return iterateItems((cursor) => messagesFacade.listMessages(ref, iterationPageOptions(callOptions, cursor)), callOptions);
1154
+ },
1155
+ async send(ref, content, callOptions) {
1156
+ const correlationId = `social-${++correlationSequence}`;
1157
+ const account = {
1158
+ kind: "connected-account",
1159
+ version: 1,
1160
+ backend: ref.backend,
1161
+ platform: ref.platform,
1162
+ accountId: ref.accountId,
1163
+ };
1164
+ await authorizeRef("messages.write", account, callOptions, correlationId);
1165
+ const adapter = selected(ref, "messages.send");
1166
+ requireCapability(adapter, "messages.write", ref.platform, ref.backend);
1167
+ if (adapter.messages === undefined)
1168
+ unsupported("messages.write", ref.backend);
1169
+ return dispatch(ref.backend, callOptions?.signal, "messages.write", () => adapter.messages.send(ref, content, makeContext(ref.backend, correlationId, callOptions)));
1170
+ },
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
+ };
1221
+ return {
1222
+ accounts: accountsFacade,
1223
+ graph: graphFacade,
1224
+ posts: postsFacade,
1225
+ search: searchFacade,
1226
+ media: mediaFacade,
1227
+ analytics: analyticsFacade,
1228
+ comments: commentsFacade,
1229
+ messages: messagesFacade,
1230
+ notifications: notificationsFacade,
1231
+ capabilities: () => Object.fromEntries(entries.map(([key, adapter]) => [key, adapter.capabilities])),
1232
+ adapter: (backend, acknowledgement) => {
1233
+ if (acknowledgement?.acknowledgeUnsafe !== true)
1234
+ throw new SocialError({
1235
+ code: "invalid_input",
1236
+ operation: "native",
1237
+ message: "Raw adapter access bypasses client authorization. Explicit acknowledgeUnsafe: true is required.",
1238
+ });
1239
+ const selected = registry[String(backend)];
1240
+ if (selected === undefined)
1241
+ throw new SocialError({
1242
+ code: "invalid_input",
1243
+ operation: "adapter",
1244
+ message: `Unknown backend: ${String(backend)}`,
1245
+ });
1246
+ return selected;
1247
+ },
1248
+ native: (backend, acknowledgement) => {
1249
+ if (acknowledgement?.acknowledgeUnsafe !== true)
1250
+ throw new SocialError({
1251
+ code: "invalid_input",
1252
+ operation: "native",
1253
+ message: "Native access requires acknowledgeUnsafe: true",
1254
+ });
1255
+ const selected = registry[String(backend)];
1256
+ if (selected === undefined)
1257
+ throw new SocialError({
1258
+ code: "invalid_input",
1259
+ operation: "native",
1260
+ message: `Unknown backend: ${String(backend)}`,
1261
+ });
1262
+ return selected.native;
1263
+ },
1264
+ };
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
+ }