@opencoredev/social-sdk 0.1.0 → 0.1.2

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 (154) hide show
  1. package/README.md +4 -2
  2. package/package.json +1 -1
  3. package/dist/cli.d.ts +0 -14
  4. package/dist/cli.d.ts.map +0 -1
  5. package/dist/cli.js +0 -223
  6. package/dist/cli.js.map +0 -1
  7. package/dist/cloud/common.d.ts +0 -26
  8. package/dist/cloud/common.d.ts.map +0 -1
  9. package/dist/cloud/common.js +0 -307
  10. package/dist/cloud/common.js.map +0 -1
  11. package/dist/cloud/lifecycle.d.ts +0 -11
  12. package/dist/cloud/lifecycle.d.ts.map +0 -1
  13. package/dist/cloud/lifecycle.js +0 -108
  14. package/dist/cloud/lifecycle.js.map +0 -1
  15. package/dist/cloud/media.d.ts +0 -24
  16. package/dist/cloud/media.d.ts.map +0 -1
  17. package/dist/cloud/media.js +0 -97
  18. package/dist/cloud/media.js.map +0 -1
  19. package/dist/cloud/outcomes.d.ts +0 -10
  20. package/dist/cloud/outcomes.d.ts.map +0 -1
  21. package/dist/cloud/outcomes.js +0 -188
  22. package/dist/cloud/outcomes.js.map +0 -1
  23. package/dist/cloud/post-for-me.d.ts +0 -70
  24. package/dist/cloud/post-for-me.d.ts.map +0 -1
  25. package/dist/cloud/post-for-me.js +0 -326
  26. package/dist/cloud/post-for-me.js.map +0 -1
  27. package/dist/cloud/zernio.d.ts +0 -112
  28. package/dist/cloud/zernio.d.ts.map +0 -1
  29. package/dist/cloud/zernio.js +0 -542
  30. package/dist/cloud/zernio.js.map +0 -1
  31. package/dist/core/adapter.d.ts +0 -118
  32. package/dist/core/adapter.d.ts.map +0 -1
  33. package/dist/core/adapter.js +0 -4
  34. package/dist/core/adapter.js.map +0 -1
  35. package/dist/core/client.d.ts +0 -108
  36. package/dist/core/client.d.ts.map +0 -1
  37. package/dist/core/client.js +0 -893
  38. package/dist/core/client.js.map +0 -1
  39. package/dist/core/concurrency.d.ts +0 -10
  40. package/dist/core/concurrency.d.ts.map +0 -1
  41. package/dist/core/concurrency.js +0 -78
  42. package/dist/core/concurrency.js.map +0 -1
  43. package/dist/core/errors.d.ts +0 -45
  44. package/dist/core/errors.d.ts.map +0 -1
  45. package/dist/core/errors.js +0 -44
  46. package/dist/core/errors.js.map +0 -1
  47. package/dist/core/idempotency.d.ts +0 -34
  48. package/dist/core/idempotency.d.ts.map +0 -1
  49. package/dist/core/idempotency.js +0 -59
  50. package/dist/core/idempotency.js.map +0 -1
  51. package/dist/core/index.d.ts +0 -7
  52. package/dist/core/index.d.ts.map +0 -1
  53. package/dist/core/index.js +0 -7
  54. package/dist/core/index.js.map +0 -1
  55. package/dist/core/pagination.d.ts +0 -12
  56. package/dist/core/pagination.d.ts.map +0 -1
  57. package/dist/core/pagination.js +0 -81
  58. package/dist/core/pagination.js.map +0 -1
  59. package/dist/core/types.d.ts +0 -329
  60. package/dist/core/types.d.ts.map +0 -1
  61. package/dist/core/types.js +0 -7
  62. package/dist/core/types.js.map +0 -1
  63. package/dist/index.d.ts +0 -2
  64. package/dist/index.d.ts.map +0 -1
  65. package/dist/index.js +0 -2
  66. package/dist/index.js.map +0 -1
  67. package/dist/platforms/bluesky.d.ts +0 -132
  68. package/dist/platforms/bluesky.d.ts.map +0 -1
  69. package/dist/platforms/bluesky.js +0 -1049
  70. package/dist/platforms/bluesky.js.map +0 -1
  71. package/dist/platforms/instagram.d.ts +0 -63
  72. package/dist/platforms/instagram.d.ts.map +0 -1
  73. package/dist/platforms/instagram.js +0 -676
  74. package/dist/platforms/instagram.js.map +0 -1
  75. package/dist/platforms/linkedin.d.ts +0 -54
  76. package/dist/platforms/linkedin.d.ts.map +0 -1
  77. package/dist/platforms/linkedin.js +0 -580
  78. package/dist/platforms/linkedin.js.map +0 -1
  79. package/dist/platforms/threads.d.ts +0 -102
  80. package/dist/platforms/threads.d.ts.map +0 -1
  81. package/dist/platforms/threads.js +0 -571
  82. package/dist/platforms/threads.js.map +0 -1
  83. package/dist/platforms/tiktok.d.ts +0 -32
  84. package/dist/platforms/tiktok.d.ts.map +0 -1
  85. package/dist/platforms/tiktok.js +0 -511
  86. package/dist/platforms/tiktok.js.map +0 -1
  87. package/dist/platforms/x-engagement.d.ts +0 -17
  88. package/dist/platforms/x-engagement.d.ts.map +0 -1
  89. package/dist/platforms/x-engagement.js +0 -47
  90. package/dist/platforms/x-engagement.js.map +0 -1
  91. package/dist/platforms/x.d.ts +0 -82
  92. package/dist/platforms/x.d.ts.map +0 -1
  93. package/dist/platforms/x.js +0 -511
  94. package/dist/platforms/x.js.map +0 -1
  95. package/dist/platforms/youtube-upload.d.ts +0 -33
  96. package/dist/platforms/youtube-upload.d.ts.map +0 -1
  97. package/dist/platforms/youtube-upload.js +0 -281
  98. package/dist/platforms/youtube-upload.js.map +0 -1
  99. package/dist/platforms/youtube.d.ts +0 -55
  100. package/dist/platforms/youtube.d.ts.map +0 -1
  101. package/dist/platforms/youtube.js +0 -561
  102. package/dist/platforms/youtube.js.map +0 -1
  103. package/dist/server/connections.d.ts +0 -124
  104. package/dist/server/connections.d.ts.map +0 -1
  105. package/dist/server/connections.js +0 -334
  106. package/dist/server/connections.js.map +0 -1
  107. package/dist/server/credentials.d.ts +0 -52
  108. package/dist/server/credentials.d.ts.map +0 -1
  109. package/dist/server/credentials.js +0 -115
  110. package/dist/server/credentials.js.map +0 -1
  111. package/dist/server/index.d.ts +0 -5
  112. package/dist/server/index.d.ts.map +0 -1
  113. package/dist/server/index.js +0 -5
  114. package/dist/server/index.js.map +0 -1
  115. package/dist/server/oauth.d.ts +0 -48
  116. package/dist/server/oauth.d.ts.map +0 -1
  117. package/dist/server/oauth.js +0 -483
  118. package/dist/server/oauth.js.map +0 -1
  119. package/dist/server/webhooks.d.ts +0 -58
  120. package/dist/server/webhooks.d.ts.map +0 -1
  121. package/dist/server/webhooks.js +0 -198
  122. package/dist/server/webhooks.js.map +0 -1
  123. package/dist/testing/index.d.ts +0 -47
  124. package/dist/testing/index.d.ts.map +0 -1
  125. package/dist/testing/index.js +0 -413
  126. package/dist/testing/index.js.map +0 -1
  127. package/dist/testing.d.ts +0 -2
  128. package/dist/testing.d.ts.map +0 -1
  129. package/dist/testing.js +0 -2
  130. package/dist/testing.js.map +0 -1
  131. package/dist/transport/binary.d.ts +0 -3
  132. package/dist/transport/binary.d.ts.map +0 -1
  133. package/dist/transport/binary.js +0 -30
  134. package/dist/transport/binary.js.map +0 -1
  135. package/dist/transport/budget.d.ts +0 -4
  136. package/dist/transport/budget.d.ts.map +0 -1
  137. package/dist/transport/budget.js +0 -27
  138. package/dist/transport/budget.js.map +0 -1
  139. package/dist/transport/http.d.ts +0 -44
  140. package/dist/transport/http.d.ts.map +0 -1
  141. package/dist/transport/http.js +0 -230
  142. package/dist/transport/http.js.map +0 -1
  143. package/dist/transport/json.d.ts +0 -3
  144. package/dist/transport/json.d.ts.map +0 -1
  145. package/dist/transport/json.js +0 -11
  146. package/dist/transport/json.js.map +0 -1
  147. package/dist/transport/upload.d.ts +0 -25
  148. package/dist/transport/upload.d.ts.map +0 -1
  149. package/dist/transport/upload.js +0 -133
  150. package/dist/transport/upload.js.map +0 -1
  151. package/dist/transport/validation.d.ts +0 -6
  152. package/dist/transport/validation.d.ts.map +0 -1
  153. package/dist/transport/validation.js +0 -24
  154. package/dist/transport/validation.js.map +0 -1
@@ -1,893 +0,0 @@
1
- import { decodeCursor, encodeCursor, iterateItems } from "./pagination.js";
2
- import { createConcurrencyLimiter } from "./concurrency.js";
3
- import { SocialError } from "./errors.js";
4
- import { deriveTargetIdempotencyKey, fingerprint, } from "./idempotency.js";
5
- function targetKey(account) {
6
- return JSON.stringify([account.backend, account.platform, account.accountId]);
7
- }
8
- function mergeContent(base, override) {
9
- if (override === undefined)
10
- return base;
11
- const result = {};
12
- if (Object.hasOwn(override, "text")) {
13
- if (override.text !== undefined)
14
- Object.assign(result, { text: override.text });
15
- }
16
- else if (base.text !== undefined)
17
- Object.assign(result, { text: base.text });
18
- if (Object.hasOwn(override, "media")) {
19
- if (override.media !== undefined)
20
- Object.assign(result, { media: override.media });
21
- }
22
- else if (base.media !== undefined)
23
- Object.assign(result, { media: base.media });
24
- if (Object.hasOwn(override, "link")) {
25
- if (override.link !== undefined)
26
- Object.assign(result, { link: override.link });
27
- }
28
- else if (base.link !== undefined)
29
- Object.assign(result, { link: base.link });
30
- return result;
31
- }
32
- function mediaFingerprintView(content) {
33
- return {
34
- ...content,
35
- media: content.media?.map((media) => ({
36
- ...media,
37
- source: media.source.kind === "blob" || media.source.kind === "stream"
38
- ? { kind: media.source.kind, fingerprint: media.source.fingerprint }
39
- : media.source,
40
- thumbnail: media.thumbnail?.kind === "blob" || media.thumbnail?.kind === "stream"
41
- ? { kind: media.thumbnail.kind, fingerprint: media.thumbnail.fingerprint }
42
- : media.thumbnail,
43
- })),
44
- };
45
- }
46
- function preparationIssue(code, message, targetIndex) {
47
- return {
48
- code,
49
- message,
50
- severity: "error",
51
- ...(targetIndex === undefined ? {} : { targetIndex }),
52
- };
53
- }
54
- function publicationStatus(outcomes) {
55
- const pending = new Set(["scheduled", "accepted", "processing"]);
56
- const success = new Set(["published"]);
57
- const hasPending = outcomes.some((outcome) => pending.has(outcome.state));
58
- const hasSuccess = outcomes.some((outcome) => success.has(outcome.state));
59
- const hasOther = outcomes.some((outcome) => !pending.has(outcome.state) && !success.has(outcome.state));
60
- if (hasPending && !hasSuccess && !hasOther)
61
- return "pending";
62
- if (!hasPending && !hasOther)
63
- return "complete";
64
- return "partial";
65
- }
66
- async function mapConcurrent(count, concurrency, execute) {
67
- const results = [];
68
- let next = 0;
69
- async function worker() {
70
- while (next < count) {
71
- const index = next++;
72
- results[index] = await execute(index);
73
- }
74
- }
75
- await Promise.all(Array.from({ length: Math.min(count, concurrency) }, () => worker()));
76
- return results;
77
- }
78
- function makeContext(backendInstance, correlationId, options, targetIdempotencyKey) {
79
- return {
80
- backendInstance,
81
- correlationId,
82
- 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
- };
89
- }
90
- function unsupported(operation, backend) {
91
- throw new SocialError({
92
- code: "unsupported_capability",
93
- operation,
94
- backend,
95
- message: `${backend} does not implement ${operation}`,
96
- });
97
- }
98
- function outcomeFromError(error, target, observedAt) {
99
- if (error instanceof SocialError) {
100
- if (error.code === "cancelled") {
101
- return {
102
- state: "unknown",
103
- targetIndex: target.targetIndex,
104
- account: target.account,
105
- observedAt,
106
- reason: "ambiguous-submission",
107
- diagnostic: "Cancellation interrupted a dispatched request; reconcile before retrying",
108
- };
109
- }
110
- if (error.code === "ambiguous_outcome" || error.code === "timeout") {
111
- return {
112
- state: "unknown",
113
- targetIndex: target.targetIndex,
114
- account: target.account,
115
- observedAt,
116
- reason: "ambiguous-submission",
117
- diagnostic: "The request outcome is ambiguous; reconcile with the backend before retrying",
118
- };
119
- }
120
- return {
121
- state: "failed",
122
- targetIndex: target.targetIndex,
123
- account: target.account,
124
- observedAt,
125
- code: error.code,
126
- message: error.message,
127
- retryDisposition: error.retryDisposition,
128
- };
129
- }
130
- return {
131
- state: "unknown",
132
- targetIndex: target.targetIndex,
133
- account: target.account,
134
- observedAt,
135
- reason: "ambiguous-submission",
136
- diagnostic: "Adapter failed after dispatch; reconcile with the backend before retrying",
137
- };
138
- }
139
- function assertValidOutcome(outcome, target) {
140
- return (outcome.targetIndex === target.targetIndex &&
141
- outcome.account.backend === target.account.backend &&
142
- outcome.account.platform === target.account.platform &&
143
- outcome.account.accountId === target.account.accountId);
144
- }
145
- export function createSocial(config) {
146
- const registry = "backend" in config && config.backend !== undefined
147
- ? { default: config.backend }
148
- : config.backends;
149
- const entries = Object.entries(registry);
150
- if (entries.length === 0) {
151
- throw new SocialError({
152
- code: "invalid_config",
153
- operation: "createSocial",
154
- message: "Configure at least one backend",
155
- });
156
- }
157
- const concurrency = config.concurrency ?? 4;
158
- if (!Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > 100) {
159
- throw new SocialError({
160
- code: "invalid_config",
161
- operation: "createSocial",
162
- message: "concurrency must be an integer from 1 through 100",
163
- });
164
- }
165
- const maxQueued = config.maxQueued ?? 100;
166
- if (!Number.isSafeInteger(maxQueued) || maxQueued < 0 || maxQueued > 10_000) {
167
- throw new SocialError({
168
- code: "invalid_config",
169
- operation: "createSocial",
170
- message: "maxQueued must be an integer from 0 through 10000",
171
- });
172
- }
173
- const limiters = new Map(entries.map(([backend]) => [
174
- backend,
175
- createConcurrencyLimiter({ maxActive: concurrency, maxQueued, backend }),
176
- ]));
177
- function dispatch(backend, signal, operation, work) {
178
- const limiter = limiters.get(backend);
179
- if (limiter === undefined) {
180
- throw new SocialError({
181
- code: "invalid_input",
182
- operation,
183
- message: `Unknown backend instance: ${backend}`,
184
- });
185
- }
186
- return limiter(signal, operation, work);
187
- }
188
- const clock = config.clock ?? (() => new Date());
189
- let correlationSequence = 0;
190
- function prepare(request) {
191
- const issues = [];
192
- const targets = [];
193
- if (request.targets.length === 0) {
194
- issues.push(preparationIssue("targets.required", "At least one target is required"));
195
- }
196
- if (request.content.text === undefined && (request.content.media?.length ?? 0) === 0) {
197
- issues.push(preparationIssue("content.required", "Text or media is required"));
198
- }
199
- if (request.schedule !== undefined) {
200
- const scheduledAt = Date.parse(request.schedule.at);
201
- if (!Number.isFinite(scheduledAt)) {
202
- issues.push(preparationIssue("schedule.invalid", "schedule.at must be an ISO timestamp"));
203
- }
204
- else if (scheduledAt <= clock().getTime()) {
205
- issues.push(preparationIssue("schedule.in_past", "A scheduled time must be in the future"));
206
- }
207
- }
208
- const seen = new Set();
209
- for (const [targetIndex, target] of request.targets.entries()) {
210
- if (target.account.kind !== "connected-account" ||
211
- target.account.version !== 1 ||
212
- target.account.backend.length === 0 ||
213
- target.account.platform.length === 0 ||
214
- target.account.accountId.length === 0) {
215
- issues.push(preparationIssue("target.reference_invalid", "Target account reference must be a version 1 connected-account reference", targetIndex));
216
- continue;
217
- }
218
- const key = targetKey(target.account);
219
- if (seen.has(key)) {
220
- issues.push(preparationIssue("target.duplicate", "Duplicate target", targetIndex));
221
- }
222
- seen.add(key);
223
- const adapter = registry[target.account.backend];
224
- if (adapter === undefined) {
225
- issues.push(preparationIssue("target.backend_unknown", `Unknown backend instance: ${target.account.backend}`, targetIndex));
226
- continue;
227
- }
228
- if (adapter.posts === undefined) {
229
- issues.push(preparationIssue("target.posts_unavailable", `Backend ${target.account.backend} does not implement posts`, targetIndex));
230
- continue;
231
- }
232
- const declaration = adapter.capabilities.capabilities.find((candidate) => candidate.operation === "posts.publish" &&
233
- (candidate.platform === "*" || candidate.platform === target.account.platform));
234
- if (declaration === undefined || declaration.availability !== "available") {
235
- issues.push(preparationIssue("capability.unavailable", declaration?.notes ??
236
- `Backend ${target.account.backend} does not declare posts.publish for ${target.account.platform}`, targetIndex));
237
- continue;
238
- }
239
- const replyTo = target.replyTo ?? request.replyTo;
240
- if (replyTo &&
241
- (replyTo.version !== 1 ||
242
- !["platform-post", "comment"].includes(replyTo.kind) ||
243
- replyTo.backend !== target.account.backend ||
244
- replyTo.platform !== target.account.platform ||
245
- replyTo.accountId !== target.account.accountId)) {
246
- issues.push(preparationIssue("reply.reference_mismatch", "Reply references must use the selected account, backend, and platform", targetIndex));
247
- }
248
- const prepared = {
249
- targetIndex,
250
- targetKey: key,
251
- account: target.account,
252
- 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
- };
257
- targets.push(prepared);
258
- try {
259
- issues.push(...adapter.posts.prepareTarget(prepared));
260
- }
261
- catch (error) {
262
- issues.push(preparationIssue("adapter.prepare_failed", error instanceof SocialError
263
- ? error.message
264
- : "The adapter could not validate this target locally", targetIndex));
265
- }
266
- }
267
- return { ok: !issues.some((issue) => issue.severity === "error"), targets, issues };
268
- }
269
- async function authorizeRef(operation, account, options, correlationId) {
270
- if (config.authorization === undefined)
271
- return;
272
- const decision = (await config.authorization.authorizeTargets({
273
- operation,
274
- accounts: [account],
275
- context: makeContext("*", correlationId, options),
276
- }))[0];
277
- if (decision?.allowed !== true || targetKey(decision.account) !== targetKey(account)) {
278
- throw new SocialError({
279
- code: "unauthorized",
280
- operation,
281
- message: decision?.reason ?? "The account is not authorized for this operation",
282
- account,
283
- correlationId,
284
- });
285
- }
286
- }
287
- function selected(ref, operation) {
288
- if (ref.kind !== undefined && (ref.kind === "" || ref.version !== 1)) {
289
- throw new SocialError({
290
- code: "invalid_input",
291
- operation,
292
- message: "Reference kind/version does not match the Social SDK contract",
293
- });
294
- }
295
- const adapter = registry[ref.backend];
296
- if (adapter === undefined) {
297
- throw new SocialError({
298
- code: "invalid_input",
299
- operation,
300
- message: `Unknown backend instance: ${ref.backend}`,
301
- });
302
- }
303
- return adapter;
304
- }
305
- function requireCapability(adapter, operation, platform, backend) {
306
- const declaration = adapter.capabilities.capabilities.find((candidate) => candidate.operation === operation &&
307
- (platform === "*" || candidate.platform === "*" || candidate.platform === platform));
308
- if (declaration?.availability !== "available")
309
- unsupported(operation, backend);
310
- }
311
- async function publish(request, options) {
312
- const correlationId = request.correlationId ?? `social-${++correlationSequence}`;
313
- const authContext = makeContext("*", correlationId, options);
314
- if (options?.signal?.aborted) {
315
- throw new SocialError({
316
- code: "cancelled",
317
- operation: "posts.publish",
318
- message: "Publishing was cancelled before authorization",
319
- correlationId,
320
- });
321
- }
322
- if (config.authorization !== undefined) {
323
- const decisions = await config.authorization.authorizeTargets({
324
- operation: "posts.publish",
325
- accounts: request.targets.map((target) => target.account),
326
- context: authContext,
327
- });
328
- for (const target of request.targets) {
329
- const decision = decisions.find((candidate) => targetKey(candidate.account) === targetKey(target.account));
330
- if (decision?.allowed !== true) {
331
- throw new SocialError({
332
- code: "unauthorized",
333
- operation: "posts.publish",
334
- message: decision?.reason ?? "A target is not authorized for this operation",
335
- account: target.account,
336
- correlationId,
337
- });
338
- }
339
- }
340
- }
341
- const plan = prepare(request);
342
- if (!plan.ok) {
343
- throw new SocialError({
344
- code: "invalid_input",
345
- operation: "posts.publish",
346
- message: "Publication preparation failed; no targets were dispatched",
347
- correlationId,
348
- issues: plan.issues,
349
- });
350
- }
351
- const targetViews = plan.targets.map((target) => ({
352
- account: target.account,
353
- content: mediaFingerprintView(target.content),
354
- options: target.options,
355
- replyTo: target.replyTo,
356
- schedule: target.schedule,
357
- }));
358
- const payloadFingerprint = await fingerprint(targetViews);
359
- const scope = JSON.stringify([
360
- options?.authorization?.tenantId ?? "credential-ready",
361
- "posts.publish",
362
- ]);
363
- let claim;
364
- if (request.idempotencyKey !== undefined && config.idempotencyStore !== undefined) {
365
- claim = await config.idempotencyStore.claim({
366
- scope,
367
- key: request.idempotencyKey,
368
- fingerprint: payloadFingerprint,
369
- targetKeys: plan.targets.map((target) => target.targetKey),
370
- });
371
- if (claim.kind === "conflict") {
372
- throw new SocialError({
373
- code: "idempotency_conflict",
374
- operation: "posts.publish",
375
- message: "The idempotency key was already used with a different target or payload",
376
- correlationId,
377
- });
378
- }
379
- }
380
- const observedAt = () => clock().toISOString();
381
- const outcomes = await mapConcurrent(plan.targets.length, Math.max(1, plan.targets.length), async (index) => {
382
- const target = plan.targets[index];
383
- if (target === undefined)
384
- throw new Error("Prepared target index was lost");
385
- if (claim !== undefined && claim.kind === "existing") {
386
- const saved = claim.outcomes[target.targetKey];
387
- if (saved !== undefined)
388
- return saved;
389
- return {
390
- state: "unknown",
391
- targetIndex: target.targetIndex,
392
- account: target.account,
393
- observedAt: observedAt(),
394
- reason: "ambiguous-submission",
395
- diagnostic: "A previous execution claimed this target without recording an outcome",
396
- };
397
- }
398
- const adapter = registry[target.account.backend];
399
- if (adapter?.posts === undefined)
400
- throw new Error("Prepared adapter disappeared");
401
- const targetIdempotencyKey = request.idempotencyKey === undefined
402
- ? undefined
403
- : await deriveTargetIdempotencyKey({
404
- logicalKey: request.idempotencyKey,
405
- scope,
406
- backend: target.account.backend,
407
- targetKey: target.targetKey,
408
- payloadFingerprint,
409
- });
410
- let outcome;
411
- let enteredAdapter = false;
412
- try {
413
- const candidate = await dispatch(target.account.backend, options?.signal, "posts.publish", () => {
414
- enteredAdapter = true;
415
- return adapter.posts.publishTarget(target, makeContext(target.account.backend, correlationId, options, targetIdempotencyKey));
416
- });
417
- outcome = assertValidOutcome(candidate, target)
418
- ? candidate
419
- : {
420
- state: "unknown",
421
- targetIndex: target.targetIndex,
422
- account: target.account,
423
- observedAt: observedAt(),
424
- reason: "unmapped-state",
425
- diagnostic: "Adapter returned an outcome for a different target",
426
- };
427
- }
428
- catch (error) {
429
- outcome =
430
- !enteredAdapter && error instanceof SocialError && error.code === "cancelled"
431
- ? {
432
- state: "cancelled",
433
- targetIndex: target.targetIndex,
434
- account: target.account,
435
- observedAt: observedAt(),
436
- reason: "before-submission",
437
- }
438
- : !enteredAdapter && error instanceof SocialError && error.code === "rate_limited"
439
- ? {
440
- state: "not-submitted",
441
- targetIndex: target.targetIndex,
442
- account: target.account,
443
- observedAt: observedAt(),
444
- reason: "capacity",
445
- }
446
- : outcomeFromError(error, target, observedAt());
447
- }
448
- if (claim?.kind === "new" && config.idempotencyStore !== undefined) {
449
- try {
450
- await config.idempotencyStore.saveOutcome({
451
- claimId: claim.claimId,
452
- targetKey: target.targetKey,
453
- outcome,
454
- });
455
- }
456
- catch {
457
- // Return the complete in-memory result even if durable persistence is unavailable.
458
- }
459
- }
460
- return outcome;
461
- });
462
- outcomes.sort((left, right) => left.targetIndex - right.targetIndex);
463
- const publicationBackend = new Set(plan.targets.map((target) => target.account.backend)).size === 1
464
- ? (plan.targets[0]?.account.backend ?? "unknown")
465
- : "multiple";
466
- const publicationId = await fingerprint({
467
- scope,
468
- key: request.idempotencyKey ?? correlationId,
469
- payloadFingerprint,
470
- });
471
- const publication = {
472
- kind: "publication",
473
- version: 1,
474
- backend: publicationBackend,
475
- publicationId,
476
- };
477
- return { status: publicationStatus(outcomes), publication, outcomes };
478
- }
479
- const accountsFacade = {
480
- async list(callOptions) {
481
- const correlationId = `social-${++correlationSequence}`;
482
- if (entries.length > 1 && callOptions?.backend === undefined)
483
- throw new SocialError({
484
- code: "invalid_input",
485
- operation: "accounts.read",
486
- message: "Select a backend instance when listing accounts from a mixed registry.",
487
- });
488
- const first = callOptions?.backend === undefined
489
- ? entries[0]
490
- : entries.find(([key]) => key === callOptions.backend);
491
- if (first === undefined || first[1].accounts === undefined)
492
- unsupported("accounts.read", first?.[0] ?? "unknown");
493
- requireCapability(first[1], "accounts.read", "*", first[0]);
494
- const cursorScope = JSON.stringify([
495
- first[0],
496
- "accounts.read",
497
- callOptions?.authorization?.tenantId ?? null,
498
- callOptions?.limit ?? null,
499
- ]);
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
- };
512
- if (config.authorization === undefined)
513
- return page;
514
- const decisions = await config.authorization.authorizeTargets({
515
- operation: "accounts.read",
516
- accounts: page.items.map((item) => item.ref),
517
- context: makeContext(first[0], correlationId, callOptions),
518
- });
519
- const allowed = new Set(decisions.filter((item) => item.allowed).map((item) => targetKey(item.account)));
520
- return {
521
- 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
- };
525
- },
526
- iterate(callOptions) {
527
- return iterateItems((cursor) => accountsFacade.list({ ...callOptions, ...(cursor === undefined ? {} : { cursor }) }), callOptions);
528
- },
529
- async get(ref, callOptions) {
530
- const correlationId = `social-${++correlationSequence}`;
531
- await authorizeRef("accounts.read", ref, callOptions, correlationId);
532
- const adapter = selected(ref, "accounts.get");
533
- requireCapability(adapter, "accounts.read", ref.platform, ref.backend);
534
- if (adapter.accounts === undefined)
535
- unsupported("accounts.read", ref.backend);
536
- return dispatch(ref.backend, callOptions?.signal, "accounts.read", () => adapter.accounts.get(ref, makeContext(ref.backend, correlationId, callOptions)));
537
- },
538
- };
539
- async function lifecycle(ref, expectedKind, operation, callOptions) {
540
- if (ref.kind !== expectedKind || ref.version !== 1 || !ref.accountId || !ref.platform)
541
- throw new SocialError({
542
- code: "invalid_input",
543
- operation,
544
- message: "Use the account-scoped reference returned for this resource kind.",
545
- });
546
- const correlationId = `social-${++correlationSequence}`;
547
- await authorizeRef(operation, {
548
- kind: "connected-account",
549
- version: 1,
550
- backend: ref.backend,
551
- platform: ref.platform,
552
- accountId: ref.accountId,
553
- }, callOptions, correlationId);
554
- const adapter = selected(ref, operation);
555
- requireCapability(adapter, operation, ref.platform, ref.backend);
556
- return { adapter, context: makeContext(ref.backend, correlationId, callOptions) };
557
- }
558
- const postsFacade = {
559
- async list(account, callOptions) {
560
- const correlationId = `social-${++correlationSequence}`;
561
- await authorizeRef("posts.read", account, callOptions, correlationId);
562
- const adapter = selected(account, "posts.list");
563
- requireCapability(adapter, "posts.list", account.platform, account.backend);
564
- if (!adapter.posts?.list)
565
- unsupported("posts.list", account.backend);
566
- const scope = JSON.stringify([
567
- account.backend,
568
- "posts.list",
569
- callOptions?.authorization?.tenantId ?? null,
570
- account.platform,
571
- account.accountId,
572
- callOptions?.limit ?? null,
573
- ]);
574
- const input = {
575
- ...(callOptions?.cursor === undefined
576
- ? {}
577
- : { cursor: decodeCursor(scope, callOptions.cursor) }),
578
- ...(callOptions?.limit === undefined ? {} : { limit: callOptions.limit }),
579
- };
580
- 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
- };
587
- },
588
- iterate(account, callOptions) {
589
- return iterateItems((cursor) => postsFacade.list(account, {
590
- ...callOptions,
591
- ...(cursor === undefined ? {} : { cursor }),
592
- }), callOptions);
593
- },
594
- async cancelScheduled(ref, callOptions) {
595
- const { adapter, context } = await lifecycle(ref, "scheduled-job", "posts.cancelScheduled", callOptions);
596
- if (!adapter.posts?.cancelScheduled)
597
- unsupported("posts.cancelScheduled", ref.backend);
598
- return dispatch(ref.backend, callOptions?.signal, "posts.cancelScheduled", () => adapter.posts.cancelScheduled(ref, context));
599
- },
600
- async deleteBackendRecord(ref, callOptions) {
601
- const { adapter, context } = await lifecycle(ref, "backend-post", "posts.deleteBackendRecord", callOptions);
602
- if (!adapter.posts?.deleteBackendRecord)
603
- unsupported("posts.deleteBackendRecord", ref.backend);
604
- return dispatch(ref.backend, callOptions?.signal, "posts.deleteBackendRecord", () => adapter.posts.deleteBackendRecord(ref, context));
605
- },
606
- async removeFromPlatform(ref, callOptions) {
607
- const { adapter, context } = await lifecycle(ref, "platform-post", "posts.removeFromPlatform", callOptions);
608
- if (!adapter.posts?.removeFromPlatform)
609
- unsupported("posts.removeFromPlatform", ref.backend);
610
- return dispatch(ref.backend, callOptions?.signal, "posts.removeFromPlatform", () => adapter.posts.removeFromPlatform(ref, context));
611
- },
612
- prepare,
613
- publish,
614
- async publishSequence(request, callOptions) {
615
- if (request.items.length === 0)
616
- throw new SocialError({
617
- code: "invalid_input",
618
- operation: "posts.publishSequence",
619
- message: "A sequence requires at least one item",
620
- });
621
- const results = [];
622
- for (const [index, item] of request.items.entries()) {
623
- const result = await publish({
624
- targets: item.targets,
625
- content: item.content,
626
- idempotencyKey: `${request.idempotencyKey}:${index}`,
627
- ...(item.replyTo === undefined ? {} : { replyTo: item.replyTo }),
628
- }, callOptions);
629
- results.push(result);
630
- if (request.stopOnFailure !== false && result.status !== "complete")
631
- break;
632
- }
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"
638
- : "pending";
639
- return { status, items: results };
640
- },
641
- async get(ref, callOptions) {
642
- const correlationId = `social-${++correlationSequence}`;
643
- await authorizeRef("posts.read", {
644
- kind: "connected-account",
645
- version: 1,
646
- backend: ref.backend,
647
- platform: ref.platform,
648
- accountId: ref.accountId,
649
- }, callOptions, correlationId);
650
- const adapter = selected(ref, "posts.get");
651
- requireCapability(adapter, "posts.read", ref.platform, ref.backend);
652
- if (adapter.posts?.get === undefined)
653
- unsupported("posts.read", ref.backend);
654
- return dispatch(ref.backend, callOptions?.signal, "posts.read", () => adapter.posts.get(ref, makeContext(ref.backend, correlationId, callOptions)));
655
- },
656
- async getDelivery(ref, callOptions) {
657
- const correlationId = `social-${++correlationSequence}`;
658
- const account = {
659
- kind: "connected-account",
660
- version: 1,
661
- backend: ref.backend,
662
- platform: ref.platform,
663
- accountId: ref.accountId,
664
- };
665
- await authorizeRef("posts.read", account, callOptions, correlationId);
666
- const adapter = selected(ref, "posts.getDelivery");
667
- requireCapability(adapter, "posts.status", ref.platform, ref.backend);
668
- if (adapter.posts?.getDelivery === undefined)
669
- unsupported("posts.read", ref.backend);
670
- return dispatch(ref.backend, callOptions?.signal, "posts.status", () => adapter.posts.getDelivery(ref, makeContext(ref.backend, correlationId, callOptions)));
671
- },
672
- };
673
- const mediaFacade = {
674
- async upload(input, account, callOptions) {
675
- const correlationId = `social-${++correlationSequence}`;
676
- await authorizeRef("posts.publish", account, callOptions, correlationId);
677
- const adapter = selected(account, "media.upload");
678
- requireCapability(adapter, "media.upload", account.platform, account.backend);
679
- if (adapter.media === undefined)
680
- unsupported("media.upload", account.backend);
681
- return dispatch(account.backend, callOptions?.signal, "media.upload", () => adapter.media.upload(input, account, makeContext(account.backend, correlationId, callOptions)));
682
- },
683
- };
684
- const analyticsFacade = {
685
- async getAccountMetrics(ref, callOptions) {
686
- const correlationId = `social-${++correlationSequence}`;
687
- await authorizeRef("analytics.read", ref, callOptions, correlationId);
688
- const adapter = selected(ref, "analytics.getAccountMetrics");
689
- requireCapability(adapter, "analytics.account.read", ref.platform, ref.backend);
690
- if (!adapter.analytics?.getAccountMetrics)
691
- unsupported("analytics.account.read", ref.backend);
692
- return dispatch(ref.backend, callOptions?.signal, "analytics.account.read", () => adapter.analytics.getAccountMetrics(ref, makeContext(ref.backend, correlationId, callOptions)));
693
- },
694
- async getPostMetrics(ref, callOptions) {
695
- const correlationId = `social-${++correlationSequence}`;
696
- const account = {
697
- kind: "connected-account",
698
- version: 1,
699
- backend: ref.backend,
700
- platform: ref.platform,
701
- accountId: ref.accountId,
702
- };
703
- await authorizeRef("analytics.read", account, callOptions, correlationId);
704
- const adapter = selected(ref, "analytics.getPostMetrics");
705
- requireCapability(adapter, "analytics.read", ref.platform, ref.backend);
706
- if (adapter.analytics === undefined)
707
- unsupported("analytics.read", ref.backend);
708
- return dispatch(ref.backend, callOptions?.signal, "analytics.read", () => adapter.analytics.getPostMetrics(ref, makeContext(ref.backend, correlationId, callOptions)));
709
- },
710
- };
711
- const commentsFacade = {
712
- async list(ref, callOptions) {
713
- const correlationId = `social-${++correlationSequence}`;
714
- const account = {
715
- kind: "connected-account",
716
- version: 1,
717
- backend: ref.backend,
718
- platform: ref.platform,
719
- accountId: ref.accountId,
720
- };
721
- await authorizeRef("comments.read", account, callOptions, correlationId);
722
- const adapter = selected(ref, "comments.list");
723
- requireCapability(adapter, "comments.read", ref.platform, ref.backend);
724
- if (adapter.comments === undefined)
725
- unsupported("comments.read", ref.backend);
726
- const cursorScope = JSON.stringify([
727
- ref.backend,
728
- "comments.read",
729
- callOptions?.authorization?.tenantId ?? null,
730
- ref.platform,
731
- ref.accountId,
732
- ref.postId,
733
- callOptions?.limit ?? null,
734
- ]);
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
- };
747
- },
748
- iterate(ref, callOptions) {
749
- return iterateItems((cursor) => commentsFacade.list(ref, { ...callOptions, ...(cursor === undefined ? {} : { cursor }) }), callOptions);
750
- },
751
- async reply(ref, content, callOptions) {
752
- const correlationId = `social-${++correlationSequence}`;
753
- const account = {
754
- kind: "connected-account",
755
- version: 1,
756
- backend: ref.backend,
757
- platform: ref.platform,
758
- accountId: ref.accountId,
759
- };
760
- await authorizeRef("comments.write", account, callOptions, correlationId);
761
- const adapter = selected(ref, "comments.reply");
762
- requireCapability(adapter, "comments.write", ref.platform, ref.backend);
763
- if (adapter.comments === undefined)
764
- unsupported("comments.write", ref.backend);
765
- return dispatch(ref.backend, callOptions?.signal, "comments.write", () => adapter.comments.reply(ref, content, makeContext(ref.backend, correlationId, callOptions)));
766
- },
767
- };
768
- const messagesFacade = {
769
- async listConversations(account, callOptions) {
770
- const correlationId = `social-${++correlationSequence}`;
771
- await authorizeRef("messages.read", account, callOptions, correlationId);
772
- const adapter = selected(account, "messages.listConversations");
773
- requireCapability(adapter, "messages.read", account.platform, account.backend);
774
- if (adapter.messages === undefined)
775
- unsupported("messages.read", account.backend);
776
- const cursorScope = JSON.stringify([
777
- account.backend,
778
- "messages.read",
779
- callOptions?.authorization?.tenantId ?? null,
780
- account.platform,
781
- account.accountId,
782
- "conversations",
783
- callOptions?.limit ?? null,
784
- ]);
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
- };
797
- },
798
- iterateConversations(account, callOptions) {
799
- return iterateItems((cursor) => messagesFacade.listConversations(account, {
800
- ...callOptions,
801
- ...(cursor === undefined ? {} : { cursor }),
802
- }), callOptions);
803
- },
804
- async listMessages(ref, callOptions) {
805
- const correlationId = `social-${++correlationSequence}`;
806
- const account = {
807
- kind: "connected-account",
808
- version: 1,
809
- backend: ref.backend,
810
- platform: ref.platform,
811
- accountId: ref.accountId,
812
- };
813
- await authorizeRef("messages.read", account, callOptions, correlationId);
814
- const adapter = selected(ref, "messages.listMessages");
815
- requireCapability(adapter, "messages.read", ref.platform, ref.backend);
816
- if (adapter.messages === undefined)
817
- unsupported("messages.read", ref.backend);
818
- const cursorScope = JSON.stringify([
819
- ref.backend,
820
- "messages.read",
821
- callOptions?.authorization?.tenantId ?? null,
822
- ref.platform,
823
- ref.accountId,
824
- ref.conversationId,
825
- callOptions?.limit ?? null,
826
- ]);
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
- };
839
- },
840
- iterateMessages(ref, callOptions) {
841
- return iterateItems((cursor) => messagesFacade.listMessages(ref, {
842
- ...callOptions,
843
- ...(cursor === undefined ? {} : { cursor }),
844
- }), callOptions);
845
- },
846
- async send(ref, content, callOptions) {
847
- const correlationId = `social-${++correlationSequence}`;
848
- const account = {
849
- kind: "connected-account",
850
- version: 1,
851
- backend: ref.backend,
852
- platform: ref.platform,
853
- accountId: ref.accountId,
854
- };
855
- await authorizeRef("messages.write", account, callOptions, correlationId);
856
- const adapter = selected(ref, "messages.send");
857
- requireCapability(adapter, "messages.write", ref.platform, ref.backend);
858
- if (adapter.messages === undefined)
859
- unsupported("messages.write", ref.backend);
860
- return dispatch(ref.backend, callOptions?.signal, "messages.write", () => adapter.messages.send(ref, content, makeContext(ref.backend, correlationId, callOptions)));
861
- },
862
- };
863
- return {
864
- accounts: accountsFacade,
865
- posts: postsFacade,
866
- media: mediaFacade,
867
- analytics: analyticsFacade,
868
- comments: commentsFacade,
869
- messages: messagesFacade,
870
- capabilities: () => Object.fromEntries(entries.map(([key, adapter]) => [key, adapter.capabilities])),
871
- adapter: (backend, acknowledgement) => {
872
- if (acknowledgement?.acknowledgeUnsafe !== true)
873
- throw new SocialError({
874
- code: "invalid_input",
875
- operation: "native",
876
- message: "Raw adapter access bypasses client authorization. Explicit acknowledgeUnsafe: true is required.",
877
- });
878
- const selected = registry[String(backend)];
879
- if (selected === undefined)
880
- throw new Error(`Unknown backend: ${String(backend)}`);
881
- return selected;
882
- },
883
- native: (backend, acknowledgement) => {
884
- if (acknowledgement?.acknowledgeUnsafe !== true)
885
- throw new Error("Native access requires acknowledgeUnsafe: true");
886
- const selected = registry[String(backend)];
887
- if (selected === undefined)
888
- throw new Error(`Unknown backend: ${String(backend)}`);
889
- return selected.native;
890
- },
891
- };
892
- }
893
- //# sourceMappingURL=client.js.map