@opencoredev/social-sdk 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/cli-request.d.ts +15 -0
  2. package/dist/cli-request.js +193 -0
  3. package/dist/cli.d.ts +4 -3
  4. package/dist/cli.js +19 -21
  5. package/dist/cloud/common.d.ts +7 -6
  6. package/dist/cloud/common.js +35 -54
  7. package/dist/cloud/lifecycle.js +31 -35
  8. package/dist/cloud/media.d.ts +2 -2
  9. package/dist/cloud/media.js +13 -3
  10. package/dist/cloud/outcomes.d.ts +4 -3
  11. package/dist/cloud/outcomes.js +8 -15
  12. package/dist/cloud/post-for-me.js +41 -49
  13. package/dist/cloud/zernio.js +58 -98
  14. package/dist/core/client.js +79 -99
  15. package/dist/core/fields.d.ts +14 -0
  16. package/dist/core/fields.js +14 -0
  17. package/dist/core/idempotency.d.ts +7 -2
  18. package/dist/core/idempotency.js +37 -20
  19. package/dist/core/pagination.js +8 -7
  20. package/dist/core/types.d.ts +3 -2
  21. package/dist/platforms/bluesky.d.ts +65 -1
  22. package/dist/platforms/bluesky.js +675 -276
  23. package/dist/platforms/instagram.d.ts +2 -0
  24. package/dist/platforms/instagram.js +130 -105
  25. package/dist/platforms/linkedin.d.ts +58 -1
  26. package/dist/platforms/linkedin.js +877 -107
  27. package/dist/platforms/threads.d.ts +13 -1
  28. package/dist/platforms/threads.js +204 -302
  29. package/dist/platforms/tiktok.d.ts +4 -0
  30. package/dist/platforms/tiktok.js +140 -124
  31. package/dist/platforms/webhook-adapter.d.ts +9 -0
  32. package/dist/platforms/webhook-adapter.js +24 -0
  33. package/dist/platforms/x-engagement.js +7 -12
  34. package/dist/platforms/x-stream.d.ts +83 -0
  35. package/dist/platforms/x-stream.js +350 -0
  36. package/dist/platforms/x.d.ts +72 -0
  37. package/dist/platforms/x.js +328 -119
  38. package/dist/platforms/youtube-upload.d.ts +1 -1
  39. package/dist/platforms/youtube-upload.js +6 -2
  40. package/dist/platforms/youtube.d.ts +28 -4
  41. package/dist/platforms/youtube.js +291 -133
  42. package/dist/server/bluesky-oauth.d.ts +177 -0
  43. package/dist/server/bluesky-oauth.js +1229 -0
  44. package/dist/server/connections.d.ts +14 -0
  45. package/dist/server/connections.js +10 -2
  46. package/dist/server/egress.d.ts +14 -0
  47. package/dist/server/egress.js +115 -0
  48. package/dist/server/oauth-internal.d.ts +6 -0
  49. package/dist/server/oauth-internal.js +66 -0
  50. package/dist/server/oauth.d.ts +1 -1
  51. package/dist/server/oauth.js +46 -99
  52. package/dist/server/webhooks.d.ts +136 -3
  53. package/dist/server/webhooks.js +639 -25
  54. package/dist/testing/index.js +14 -28
  55. package/dist/transport/http.d.ts +1 -1
  56. package/dist/transport/http.js +0 -1
  57. package/dist/transport/json.d.ts +7 -0
  58. package/dist/transport/json.js +32 -4
  59. package/dist/transport/upload.d.ts +1 -1
  60. package/dist/transport/upload.js +46 -38
  61. package/dist/transport/validation.d.ts +16 -5
  62. package/dist/transport/validation.js +29 -7
  63. package/package.json +2 -2
@@ -1,10 +1,10 @@
1
- /* oxlint-disable anti-slop/no-runtime-typeof -- validated external boundary or fixture contract. */
2
1
  import { managedLifecycle } from "./lifecycle.js";
3
2
  import { managedMedia } from "./media.js";
4
3
  export { MemoryManagedMediaStore, } from "./media.js";
5
4
  import { defineAdapter } from "../core/adapter.js";
6
5
  import { SocialError } from "../core/errors.js";
7
- import { array, object, optionalNumber, optionalString, string } from "../transport/validation.js";
6
+ import { definedFields } from "../core/fields.js";
7
+ import { array, isBoolean, isFiniteNumber, isJsonArray, isJsonObject, isString, object, optionalNumber, optionalString, string, } from "../transport/validation.js";
8
8
  import { decodeWebhook, verifyZernioWebhook } from "../server/webhooks.js";
9
9
  import { accountMatches, capabilityManifest, managedHttp, managedPreparation, managedOptionIssues, optionsObject, platform, publicFields, } from "./common.js";
10
10
  import { zernioOutcome } from "./outcomes.js";
@@ -12,7 +12,6 @@ export function zernio(options) {
12
12
  const request = managedHttp("https://zernio.com/api", options);
13
13
  const mediaPipeline = managedMedia("zernio", options, (body, context) => request("/v1/media/presign", context, body));
14
14
  const now = () => (options.clock?.() ?? new Date()).toISOString();
15
- // oxlint-disable-next-line anti-slop/no-unknown-parameters -- provider payload is validated at this adapter boundary.
16
15
  const account = (value, backend) => {
17
16
  const row = object(value);
18
17
  const handle = optionalString(row["username"]);
@@ -25,8 +24,7 @@ export function zernio(options) {
25
24
  accountId: string(row["_id"]),
26
25
  },
27
26
  displayName: optionalString(row["displayName"]) ?? handle ?? string(row["_id"]),
28
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
29
- ...(handle ? { handle } : {}),
27
+ ...definedFields({ handle: handle || undefined }),
30
28
  status: row["isActive"] === true
31
29
  ? "connected"
32
30
  : row["isActive"] === false
@@ -34,7 +32,6 @@ export function zernio(options) {
34
32
  : "unknown",
35
33
  };
36
34
  };
37
- // oxlint-disable-next-line anti-slop/no-unknown-parameters -- validated by account at this boundary.
38
35
  const supportedAccount = (value, backend) => {
39
36
  try {
40
37
  return account(value, backend);
@@ -127,8 +124,9 @@ export function zernio(options) {
127
124
  const parsed = supportedAccount(value, context.backendInstance);
128
125
  return parsed ? [parsed] : [];
129
126
  }),
130
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
131
- ...(pages !== undefined && page < pages ? { nextCursor: String(page + 1) } : {}),
127
+ ...definedFields({
128
+ nextCursor: pages !== undefined && page < pages ? String(page + 1) : undefined,
129
+ }),
132
130
  };
133
131
  },
134
132
  async get(ref, context) {
@@ -193,55 +191,33 @@ export function zernio(options) {
193
191
  const rows = array(result["posts"])
194
192
  .map(object)
195
193
  .flatMap((row) => {
196
- const destinations = Array.isArray(row["platforms"])
194
+ const destinations = isJsonArray(row["platforms"])
197
195
  ? array(row["platforms"]).map(object)
198
196
  : [];
199
197
  const destination = destinations.find((item) => {
200
198
  const value = item["accountId"];
201
- const id =
202
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
203
- typeof value === "string"
199
+ const id = isString(value)
204
200
  ? value
205
- : // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
206
- value && typeof value === "object"
207
- ? object(value)["_id"]
208
- : undefined;
201
+ : isJsonObject(value) || isJsonArray(value)
202
+ ? object(value)["_id"]
203
+ : undefined;
209
204
  return (id === account.accountId &&
210
205
  (item["platform"] === account.platform ||
211
206
  item["platform"] === (account.platform === "x" ? "twitter" : account.platform)));
212
207
  });
213
208
  if (destination === undefined)
214
209
  return [];
215
- const platformPostId =
216
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
217
- typeof destination["platformPostId"] === "string"
218
- ? destination["platformPostId"]
219
- : undefined;
220
210
  return [
221
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- provider payload is validated at this adapter boundary.
222
211
  {
223
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
224
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
225
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
226
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
227
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
228
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
229
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
230
- ...(typeof row["_id"] === "string" ? { backendPostId: row["_id"] } : {}),
231
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
232
- ...(platformPostId === undefined ? {} : { platformPostId }),
212
+ ...definedFields({
213
+ backendPostId: optionalString(row["_id"]),
214
+ platformPostId: optionalString(destination["platformPostId"]),
215
+ }),
233
216
  platform: account.platform,
234
217
  accountId: account.accountId,
235
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
236
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
237
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
238
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
239
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
240
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
241
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
242
- ...(typeof destination["platformPostUrl"] === "string"
243
- ? { platformPostUrl: destination["platformPostUrl"] }
244
- : {}),
218
+ ...definedFields({
219
+ platformPostUrl: optionalString(destination["platformPostUrl"]),
220
+ }),
245
221
  ...publicFields(row, [
246
222
  "content",
247
223
  "caption",
@@ -257,12 +233,10 @@ export function zernio(options) {
257
233
  const pagination = result["pagination"] === undefined ? {} : object(result["pagination"]);
258
234
  const totalPages = optionalNumber(pagination["pages"]) ?? optionalNumber(pagination["totalPages"]);
259
235
  return {
260
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- provider payload is validated at this adapter boundary.
261
236
  items: rows,
262
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
263
- ...(totalPages !== undefined && page < totalPages
264
- ? { nextCursor: String(page + 1) }
265
- : {}),
237
+ ...definedFields({
238
+ nextCursor: totalPages !== undefined && page < totalPages ? String(page + 1) : undefined,
239
+ }),
266
240
  };
267
241
  },
268
242
  prepareTarget(target) {
@@ -291,23 +265,20 @@ export function zernio(options) {
291
265
  media.push({
292
266
  type: item.kind,
293
267
  url: await mediaPipeline.resolve(item, target.account, context),
294
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
295
- ...(item.altText === undefined ? {} : { altText: item.altText }),
296
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
297
- ...(item.mimeType === undefined ? {} : { mimeType: item.mimeType }),
268
+ ...definedFields({ altText: item.altText, mimeType: item.mimeType }),
298
269
  });
299
270
  const config = optionsObject(target);
300
271
  const native = {};
301
272
  if (target.account.platform === "youtube") {
302
273
  native["title"] = string(config["title"]);
303
274
  native["visibility"] = string(config["visibility"]);
304
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
305
- if (typeof config["madeForKids"] === "boolean")
306
- native["madeForKids"] = config["madeForKids"];
275
+ const madeForKids = config["madeForKids"];
276
+ if (isBoolean(madeForKids))
277
+ native["madeForKids"] = madeForKids;
307
278
  }
308
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
309
- if (target.account.platform === "instagram" && typeof config["shareToFeed"] === "boolean")
310
- native["shareToFeed"] = config["shareToFeed"];
279
+ const shareToFeed = config["shareToFeed"];
280
+ if (target.account.platform === "instagram" && isBoolean(shareToFeed))
281
+ native["shareToFeed"] = shareToFeed;
311
282
  if (target.account.platform === "x" &&
312
283
  config["replySettings"] !== undefined &&
313
284
  config["replySettings"] !== "everyone")
@@ -319,21 +290,24 @@ export function zernio(options) {
319
290
  native["autoAddMusic"] = false;
320
291
  native["allowDuet"] = !config["disableDuet"];
321
292
  native["allowStitch"] = !config["disableStitch"];
322
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- provider payload is validated at this adapter boundary.
323
- native["isBrandOrganicPost"] = config["ownBrand"];
324
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- provider payload is validated at this adapter boundary.
325
- native["videoMadeWithAi"] = config["aiGenerated"];
326
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- provider payload is validated at this adapter boundary.
327
- native["draft"] = config["draft"];
328
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
329
- if (typeof config["photoCoverIndex"] === "number")
330
- native["photoCoverIndex"] = config["photoCoverIndex"];
331
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
332
- if (typeof config["disableComments"] === "boolean")
333
- native["allowComment"] = !config["disableComments"];
334
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
335
- if (typeof config["brandedContent"] === "boolean")
336
- native["brandPartnerPromote"] = config["brandedContent"];
293
+ // prepareTarget requires these TikTok choices to be booleans. Skipping an
294
+ // absent value sends the same JSON body, since serialization drops undefined.
295
+ for (const [nativeKey, optionKey] of [
296
+ ["isBrandOrganicPost", "ownBrand"],
297
+ ["videoMadeWithAi", "aiGenerated"],
298
+ ["draft", "draft"],
299
+ ]) {
300
+ const value = config[optionKey];
301
+ if (value !== undefined)
302
+ native[nativeKey] = value;
303
+ }
304
+ const { photoCoverIndex, disableComments, brandedContent } = config;
305
+ if (isFiniteNumber(photoCoverIndex))
306
+ native["photoCoverIndex"] = photoCoverIndex;
307
+ if (isBoolean(disableComments))
308
+ native["allowComment"] = !disableComments;
309
+ if (isBoolean(brandedContent))
310
+ native["brandPartnerPromote"] = brandedContent;
337
311
  }
338
312
  const response = await request("/v1/posts", context, {
339
313
  content: target.content.text ?? "",
@@ -342,15 +316,15 @@ export function zernio(options) {
342
316
  {
343
317
  platform: target.account.platform === "x" ? "twitter" : target.account.platform,
344
318
  accountId: target.account.accountId,
345
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
346
- ...(Object.keys(native).length ? { platformSpecificData: native } : {}),
319
+ ...definedFields({
320
+ platformSpecificData: Object.keys(native).length ? native : undefined,
321
+ }),
347
322
  },
348
323
  ],
349
324
  ...(target.schedule
350
325
  ? {
351
326
  scheduledFor: target.schedule.at,
352
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
353
- ...(target.schedule.timeZone ? { timezone: target.schedule.timeZone } : {}),
327
+ ...definedFields({ timezone: target.schedule.timeZone || undefined }),
354
328
  }
355
329
  : { publishNow: true }),
356
330
  });
@@ -471,8 +445,7 @@ export function zernio(options) {
471
445
  const result = object(await request(`/v1/inbox/comments/${encodeURIComponent(ref.postId)}`, context, undefined, {
472
446
  accountId: ref.accountId,
473
447
  limit: String(limit),
474
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
475
- ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
448
+ ...definedFields({ cursor: input.cursor }),
476
449
  }));
477
450
  const pagination = result["pagination"] ? object(result["pagination"]) : {};
478
451
  const cursor = optionalString(pagination["cursor"]);
@@ -492,8 +465,7 @@ export function zernio(options) {
492
465
  "canDelete",
493
466
  "canHide",
494
467
  ])),
495
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
496
- ...(hasMore === false || !cursor ? {} : { nextCursor: cursor }),
468
+ ...definedFields({ nextCursor: hasMore === false || !cursor ? undefined : cursor }),
497
469
  };
498
470
  },
499
471
  async reply(ref, content, context) {
@@ -525,8 +497,7 @@ export function zernio(options) {
525
497
  const result = object(await request("/v1/inbox/conversations", context, undefined, {
526
498
  accountId: ref.accountId,
527
499
  limit: String(limit),
528
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
529
- ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
500
+ ...definedFields({ cursor: input.cursor }),
530
501
  }));
531
502
  const pagination = result["pagination"] === undefined ? {} : object(result["pagination"]);
532
503
  const cursor = optionalString(pagination["nextCursor"]);
@@ -541,8 +512,7 @@ export function zernio(options) {
541
512
  "updatedTime",
542
513
  "unreadCount",
543
514
  ])),
544
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
545
- ...(cursor ? { nextCursor: cursor } : {}),
515
+ ...definedFields({ nextCursor: cursor || undefined }),
546
516
  };
547
517
  },
548
518
  async listMessages(ref, input, context) {
@@ -557,8 +527,7 @@ export function zernio(options) {
557
527
  const result = object(await request(`/v1/inbox/conversations/${encodeURIComponent(ref.conversationId)}/messages`, context, undefined, {
558
528
  accountId: ref.accountId,
559
529
  limit: String(limit),
560
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
561
- ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
530
+ ...definedFields({ cursor: input.cursor }),
562
531
  }));
563
532
  const pagination = result["pagination"] ? object(result["pagination"]) : {};
564
533
  const cursor = optionalString(pagination["nextCursor"]);
@@ -572,8 +541,7 @@ export function zernio(options) {
572
541
  "createdAt",
573
542
  "deliveryStatus",
574
543
  ])),
575
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
576
- ...(cursor ? { nextCursor: cursor } : {}),
544
+ ...definedFields({ nextCursor: cursor || undefined }),
577
545
  };
578
546
  },
579
547
  async send(ref, content, context) {
@@ -587,8 +555,7 @@ export function zernio(options) {
587
555
  });
588
556
  const data = object(result["data"]);
589
557
  const messageId = optionalString(data["messageId"]);
590
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
591
- return { state: "sent", ...(messageId ? { messageId } : {}) };
558
+ return { state: "sent", ...definedFields({ messageId: messageId || undefined }) };
592
559
  },
593
560
  },
594
561
  webhooks: {
@@ -613,14 +580,7 @@ export function zernio(options) {
613
580
  const response = object(await request(`/v1/connect/${input.platform === "x" ? "twitter" : encodeURIComponent(input.platform)}`, context, undefined, { profileId: input.profileId, redirect_url: input.redirectUrl }));
614
581
  return {
615
582
  url: string(response["authUrl"]),
616
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
617
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
618
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
619
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
620
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
621
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
622
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
623
- ...(typeof response["state"] === "string" ? { providerState: response["state"] } : {}),
583
+ ...definedFields({ providerState: optionalString(response["state"]) }),
624
584
  };
625
585
  },
626
586
  async listProfiles(context) {
@@ -1,7 +1,7 @@
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
1
  import { decodeCursor, encodeCursor, iterateItems } from "./pagination.js";
3
2
  import { createConcurrencyLimiter } from "./concurrency.js";
4
3
  import { SocialError } from "./errors.js";
4
+ import { definedFields } from "./fields.js";
5
5
  import { deriveTargetIdempotencyKey, fingerprint, } from "./idempotency.js";
6
6
  function targetKey(account) {
7
7
  return JSON.stringify([account.backend, account.platform, account.accountId]);
@@ -96,10 +96,19 @@ function unsupported(operation, backend) {
96
96
  message: `${backend} does not implement ${operation}`,
97
97
  });
98
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) {
99
+ /**
100
+ * Runtime guards for JavaScript callers that bypass the facade's static types.
101
+ * They are generic so a typed argument keeps its declared type when narrowed.
102
+ */
103
+ function isObjectInput(value) {
104
+ return typeof value === "object" && value !== null;
105
+ }
106
+ function isNonBlankStringInput(value) {
107
+ return typeof value === "string" && value.trim() !== "";
108
+ }
109
+ /** `error` is the rejection when it was a SocialError, or undefined for any other thrown value. */
110
+ function outcomeFromError(error, target, observedAt) {
111
+ if (error !== undefined) {
103
112
  if (error.code === "cancelled") {
104
113
  return {
105
114
  state: "unknown",
@@ -186,15 +195,15 @@ export function createSocial(config) {
186
195
  message: `Unknown backend instance: ${backend}`,
187
196
  });
188
197
  }
189
- return limiter(signal, operation, work).catch((error) => {
190
- if (error instanceof SocialError)
191
- throw error;
198
+ return limiter(signal, operation, work).catch((cause) => {
199
+ if (cause instanceof SocialError)
200
+ throw cause;
192
201
  throw new SocialError({
193
202
  code: "upstream_failure",
194
203
  operation,
195
204
  backend,
196
205
  message: "The adapter failed while processing the request",
197
- cause: error,
206
+ cause,
198
207
  });
199
208
  });
200
209
  }
@@ -311,15 +320,12 @@ export function createSocial(config) {
311
320
  }
312
321
  }
313
322
  function validateAccountRef(account, operation) {
314
- if (account === null ||
315
- typeof account !== "object" ||
323
+ if (!isObjectInput(account) ||
316
324
  account.kind !== "connected-account" ||
317
325
  account.version !== 1 ||
318
- typeof account.backend !== "string" ||
319
- !account.backend.trim() ||
326
+ !isNonBlankStringInput(account.backend) ||
320
327
  !account.platform ||
321
- typeof account.accountId !== "string" ||
322
- !account.accountId.trim())
328
+ !isNonBlankStringInput(account.accountId))
323
329
  throw new SocialError({
324
330
  code: "invalid_input",
325
331
  operation,
@@ -494,7 +500,7 @@ export function createSocial(config) {
494
500
  observedAt: observedAt(),
495
501
  reason: "capacity",
496
502
  }
497
- : outcomeFromError(error, target, observedAt());
503
+ : outcomeFromError(error instanceof SocialError ? error : undefined, target, observedAt());
498
504
  }
499
505
  if (claim?.kind === "new" && config.idempotencyStore !== undefined) {
500
506
  try {
@@ -583,16 +589,9 @@ export function createSocial(config) {
583
589
  const graphFacade = {
584
590
  async getProfile(account, input, callOptions) {
585
591
  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() === "")))
592
+ if (!isObjectInput(input) ||
593
+ (input.profileId !== undefined && !isNonBlankStringInput(input.profileId)) ||
594
+ (input.handle !== undefined && !isNonBlankStringInput(input.handle)))
596
595
  throw new SocialError({
597
596
  code: "invalid_input",
598
597
  operation: "profiles.read",
@@ -608,8 +607,7 @@ export function createSocial(config) {
608
607
  },
609
608
  async listRelationships(account, input, callOptions) {
610
609
  validateAccountRef(account, "graph.read");
611
- if (input === null ||
612
- typeof input !== "object" ||
610
+ if (!isObjectInput(input) ||
613
611
  !["following", "followers", "blocked", "muted"].includes(input.kind) ||
614
612
  (input.limit !== undefined && (!Number.isSafeInteger(input.limit) || input.limit < 1)))
615
613
  throw new SocialError({
@@ -636,30 +634,52 @@ export function createSocial(config) {
636
634
  ]);
637
635
  const page = await dispatch(account.backend, callOptions?.signal, "graph.read", () => adapter.graph.listRelationships(account, {
638
636
  ...input,
639
- ...(input.cursor === undefined ? {} : { cursor: decodeCursor(scope, input.cursor) }),
637
+ ...definedFields({
638
+ cursor: input.cursor === undefined ? undefined : decodeCursor(scope, input.cursor),
639
+ }),
640
640
  }, makeContext(account.backend, correlationId, callOptions)));
641
641
  return encodePage(scope, page);
642
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),
643
+ follow: (target, options) => graphMutation(target, "graph.follow", options, async (graph, context) => {
644
+ if (graph.follow === undefined)
645
+ unsupported("graph.follow", target.backend);
646
+ return await graph.follow(target, context);
647
+ }),
648
+ unfollow: (target, options) => graphMutation(target, "graph.unfollow", options, async (graph, context) => {
649
+ if (graph.unfollow === undefined)
650
+ unsupported("graph.unfollow", target.backend);
651
+ await graph.unfollow(target, context);
652
+ }),
653
+ block: (target, options) => graphMutation(target, "graph.block", options, async (graph, context) => {
654
+ if (graph.block === undefined)
655
+ unsupported("graph.block", target.backend);
656
+ return await graph.block(target, context);
657
+ }),
658
+ unblock: (target, options) => graphMutation(target, "graph.unblock", options, async (graph, context) => {
659
+ if (graph.unblock === undefined)
660
+ unsupported("graph.unblock", target.backend);
661
+ await graph.unblock(target, context);
662
+ }),
663
+ mute: (target, options) => graphMutation(target, "graph.mute", options, async (graph, context) => {
664
+ if (graph.mute === undefined)
665
+ unsupported("graph.mute", target.backend);
666
+ return await graph.mute(target, context);
667
+ }),
668
+ unmute: (target, options) => graphMutation(target, "graph.unmute", options, async (graph, context) => {
669
+ if (graph.unmute === undefined)
670
+ unsupported("graph.unmute", target.backend);
671
+ await graph.unmute(target, context);
672
+ }),
649
673
  };
650
- async function graphMutation(target, operation, callOptions) {
651
- if (target === null ||
652
- typeof target !== "object" ||
674
+ /** Validates and authorizes a graph mutation, then runs `mutate` inside the backend limiter. */
675
+ async function graphMutation(target, operation, callOptions, mutate) {
676
+ if (!isObjectInput(target) ||
653
677
  target.kind !== "profile" ||
654
678
  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() === "")
679
+ !isNonBlankStringInput(target.backend) ||
680
+ !isNonBlankStringInput(target.platform) ||
681
+ !isNonBlankStringInput(target.accountId) ||
682
+ !isNonBlankStringInput(target.profileId))
663
683
  throw new SocialError({
664
684
  code: "invalid_input",
665
685
  operation,
@@ -679,37 +699,8 @@ export function createSocial(config) {
679
699
  if (adapter.graph === undefined)
680
700
  unsupported(operation, target.backend);
681
701
  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
- });
702
+ const graph = adapter.graph;
703
+ return dispatch(target.backend, callOptions?.signal, operation, () => mutate(graph, context));
713
704
  }
714
705
  async function lifecycle(ref, expectedKind, operation, callOptions) {
715
706
  if (ref.kind !== expectedKind || ref.version !== 1 || !ref.accountId || !ref.platform)
@@ -780,7 +771,7 @@ export function createSocial(config) {
780
771
  operation: "posts.publishSequence",
781
772
  message: "A sequence requires at least one item",
782
773
  });
783
- if (typeof request.idempotencyKey !== "string" || request.idempotencyKey.trim() === "")
774
+ if (!isNonBlankStringInput(request.idempotencyKey))
784
775
  throw new SocialError({
785
776
  code: "invalid_input",
786
777
  operation: "posts.publishSequence",
@@ -917,13 +908,10 @@ export function createSocial(config) {
917
908
  validateAccountRef(account, "search.posts");
918
909
  if (input === null ||
919
910
  input === undefined ||
920
- typeof input.query !== "string" ||
921
- !input.query.trim() ||
911
+ !isNonBlankStringInput(input.query) ||
922
912
  (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))) ||
913
+ (input.startTime !== undefined && !validTimestamp(input.startTime)) ||
914
+ (input.endTime !== undefined && !validTimestamp(input.endTime)) ||
927
915
  (input.startTime !== undefined &&
928
916
  input.endTime !== undefined &&
929
917
  Date.parse(input.startTime) > Date.parse(input.endTime)) ||
@@ -1010,14 +998,10 @@ export function createSocial(config) {
1010
998
  query.from > query.to ||
1011
999
  !Array.isArray(query.metrics) ||
1012
1000
  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()) ||
1001
+ query.metrics.some((metric) => !isNonBlankStringInput(metric)) ||
1016
1002
  (query.dimensions !== undefined &&
1017
1003
  (!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()))))
1004
+ query.dimensions.some((dimension) => !isNonBlankStringInput(dimension)))))
1021
1005
  throw new SocialError({
1022
1006
  code: "invalid_input",
1023
1007
  operation: "analytics.report.read",
@@ -1201,9 +1185,7 @@ export function createSocial(config) {
1201
1185
  },
1202
1186
  async markSeen(account, input = {}, callOptions) {
1203
1187
  validateAccountRef(account, "notifications.seen");
1204
- if (input === null ||
1205
- typeof input !== "object" ||
1206
- (input.seenAt !== undefined && !validTimestamp(input.seenAt)))
1188
+ if (!isObjectInput(input) || (input.seenAt !== undefined && !validTimestamp(input.seenAt)))
1207
1189
  throw new SocialError({
1208
1190
  code: "invalid_input",
1209
1191
  operation: "notifications.seen",
@@ -1264,12 +1246,10 @@ export function createSocial(config) {
1264
1246
  };
1265
1247
  }
1266
1248
  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;
1249
+ return definedFields({
1250
+ cursor: options?.cursor === undefined ? undefined : decodeCursor(scope, options.cursor),
1251
+ limit: options?.limit,
1252
+ });
1273
1253
  }
1274
1254
  function encodePage(scope, page) {
1275
1255
  const result = { ...page };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Fields of `T` with `undefined` removed, each one optional. `T[K] & ({} | null)`
3
+ * is the form TypeScript narrows a generic `T[K]` to after an `!== undefined`
4
+ * check. For concrete types it matches `Exclude<T[K], undefined>`, except that an
5
+ * `unknown` field becomes `{} | null`.
6
+ */
7
+ export type DefinedFields<T> = {
8
+ [K in keyof T]?: T[K] & ({} | null);
9
+ };
10
+ /**
11
+ * Copy only the fields whose value is defined, so optional properties stay
12
+ * absent under `exactOptionalPropertyTypes` instead of being set to `undefined`.
13
+ */
14
+ export declare function definedFields<T extends object>(fields: T): DefinedFields<T>;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Copy only the fields whose value is defined, so optional properties stay
3
+ * absent under `exactOptionalPropertyTypes` instead of being set to `undefined`.
4
+ */
5
+ export function definedFields(fields) {
6
+ const result = {};
7
+ for (const key in fields) {
8
+ const value = fields[key];
9
+ if (value === undefined)
10
+ continue;
11
+ result[key] = value;
12
+ }
13
+ return result;
14
+ }