@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,9 +1,11 @@
1
- /* oxlint-disable anti-slop/no-chained-type-assertions, anti-slop/no-conditional-empty-object-spread, anti-slop/no-runtime-typeof, anti-slop/require-readable-spacing, anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract. */
2
1
  import { connectedAccountRef, defineAdapter, platformPostRef, profileRef, } from "../core/index.js";
3
2
  import { SocialError } from "../core/errors.js";
4
- import { managedHttp, publicFields } from "../cloud/common.js";
5
- import { object, array, optionalNumber, optionalString } from "../transport/validation.js";
3
+ import { managedHttp, optionsObject, publicFields } from "../cloud/common.js";
4
+ import { definedFields } from "../core/fields.js";
5
+ import { object, array, isJsonArray, isJsonObject, isString, optionalArray, optionalNumber, optionalObject, optionalString, } from "../transport/validation.js";
6
6
  import { httpsUrl } from "../transport/upload.js";
7
+ import { verifyMetaWebhook } from "../server/webhooks.js";
8
+ import { directWebhooks, webhookCapability } from "./webhook-adapter.js";
7
9
  export class MemoryThreadsWorkflowStore {
8
10
  rows = new Map();
9
11
  claims = new Set();
@@ -47,14 +49,12 @@ export function threads(options) {
47
49
  const backend = options.backend ?? "default", version = options.graphVersion ?? "v1.0", auth = options.auth, store = options.workflowStore ?? new MemoryThreadsWorkflowStore(), now = () => (options.clock?.() ?? new Date()).toISOString();
48
50
  const http = managedHttp(`https://graph.threads.net/${version}`, {
49
51
  apiKey: auth.accessToken,
50
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
51
- ...(options.fetch ? { fetch: options.fetch } : {}),
52
+ ...definedFields({ fetch: options.fetch }),
52
53
  });
53
54
  async function request(path, init, operation, context) {
54
55
  const [raw = "", qs] = path.split("?", 2);
55
56
  const query = Object.fromEntries(new URLSearchParams(qs ?? ""));
56
57
  try {
57
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
58
58
  return object(await http(`/${raw.replace(/^\//, "")}`, context, init.method === "POST" ? {} : undefined, query, init.method === "POST" || init.method === "DELETE" || init.method === "PUT"
59
59
  ? init.method
60
60
  : "GET"));
@@ -71,37 +71,24 @@ export function threads(options) {
71
71
  }
72
72
  const account = accountRef(backend, auth.userId);
73
73
  function pageFrom(result) {
74
- const items = array(result["data"]).map((entry) => {
75
- // SAFETY: object() validates each provider data entry as a JSON object.
76
- return object(entry);
77
- });
74
+ const items = array(result["data"]).map((entry) => object(entry));
78
75
  const paging = result["paging"] === undefined ? {} : object(result["paging"]);
79
76
  const cursors = paging["cursors"] === undefined ? {} : object(paging["cursors"]);
80
77
  // Graph omits paging.next on the last page even when cursors.after is present.
81
- const nextCursor = typeof paging["next"] === "string" && paging["next"].length > 0
78
+ const nextCursor = isString(paging["next"]) && paging["next"].length > 0
82
79
  ? optionalString(cursors["after"])
83
80
  : undefined;
84
- return { items, ...(nextCursor === undefined ? {} : { nextCursor }) };
81
+ return { items, ...definedFields({ nextCursor }) };
85
82
  }
86
83
  async function readAccount(context) {
87
84
  const v = await request("/me?fields=id,username,threads_profile_picture_url,threads_biography", { method: "GET" }, "threads.accounts.read", context);
88
85
  const id = v["id"], username = v["username"];
89
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
90
- if (typeof id !== "string" || id !== auth.userId)
86
+ if (!isString(id) || id !== auth.userId)
91
87
  fail("threads.accounts.read", "Threads returned an unauthorized account.", "unauthorized");
92
88
  return {
93
89
  ref: account,
94
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
95
- displayName: typeof username === "string" ? username : (auth.handle ?? auth.userId),
96
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
97
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
98
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
99
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
100
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
101
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
102
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
103
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
104
- ...(typeof username === "string" ? { handle: username } : {}),
90
+ displayName: optionalString(username) ?? auth.handle ?? auth.userId,
91
+ ...definedFields({ handle: optionalString(username) }),
105
92
  status: "connected",
106
93
  };
107
94
  }
@@ -121,15 +108,11 @@ export function threads(options) {
121
108
  const items = array(result["data"]).map((entry) => publicFields(entry, ["id", "text", "username", "media_type", "permalink", "timestamp"]));
122
109
  const paging = result["paging"] === undefined ? {} : object(result["paging"]);
123
110
  const cursors = paging["cursors"] === undefined ? {} : object(paging["cursors"]);
124
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
125
- const hasNext = typeof paging["next"] === "string" && paging["next"].length > 0;
126
- const nextCursor =
127
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
128
- hasNext && typeof cursors["after"] === "string" ? cursors["after"] : undefined;
111
+ const hasNext = isString(paging["next"]) && paging["next"].length > 0;
112
+ const nextCursor = hasNext ? optionalString(cursors["after"]) : undefined;
129
113
  return {
130
114
  items,
131
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
132
- ...(nextCursor === undefined ? {} : { nextCursor }),
115
+ ...definedFields({ nextCursor }),
133
116
  };
134
117
  }
135
118
  async function getAccountMetrics(selected, context) {
@@ -147,8 +130,7 @@ export function threads(options) {
147
130
  const allowed = new Set([...metricNames, "link_total_values"]);
148
131
  return array(result["data"]).flatMap((entry) => {
149
132
  const row = object(entry);
150
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
151
- const name = typeof row["name"] === "string" ? row["name"] : undefined;
133
+ const name = optionalString(row["name"]);
152
134
  if (name === undefined || !allowed.has(name))
153
135
  return [];
154
136
  const total = row["total_value"] === undefined ? undefined : object(row["total_value"]);
@@ -179,8 +161,7 @@ export function threads(options) {
179
161
  value,
180
162
  unit: "count",
181
163
  period,
182
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
183
- ...(totalValue === undefined && latestEnd !== undefined ? { measuredAt: latestEnd } : {}),
164
+ ...definedFields({ measuredAt: totalValue === undefined ? latestEnd : undefined }),
184
165
  fetchedAt: now(),
185
166
  freshness: "unknown",
186
167
  source: `threads:${version}`,
@@ -189,180 +170,123 @@ export function threads(options) {
189
170
  });
190
171
  }
191
172
  async function status(id, c) {
192
- return request(`${encodeURIComponent(id)}?fields=id,status,error_message`,
193
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
194
- { method: "GET", ...(c.signal ? { signal: c.signal } : {}) }, "threads.container.status", c);
173
+ return request(`${encodeURIComponent(id)}?fields=id,status,error_message`, { method: "GET", ...definedFields({ signal: c.signal }) }, "threads.container.status", c);
195
174
  }
196
- const base = (a, id, state, backendState) =>
197
- // oxlint-disable-next-line anti-slop/no-chained-type-assertions -- validated boundary or fixture contract.
198
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
199
- // oxlint-disable-next-line anti-slop/no-chained-type-assertions -- provider payload is validated at this adapter boundary.
200
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract.
201
- // oxlint-disable-next-line anti-slop/no-chained-type-assertions -- validated external boundary or fixture contract.
202
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract.
203
- // oxlint-disable-next-line anti-slop/no-chained-type-assertions -- validated external boundary or fixture contract.
204
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract.
205
- ({
206
- state,
207
- targetIndex: 0,
208
- account: a,
209
- observedAt: now(),
210
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
211
- ...(backendState ? { backendState } : {}),
212
- delivery: {
175
+ function outcomeFields(a, id, backendState) {
176
+ const delivery = {
213
177
  kind: "delivery",
214
178
  version: 1,
215
179
  backend: a.backend,
216
180
  platform: "threads",
217
181
  accountId: a.accountId,
218
182
  deliveryId: id,
219
- },
183
+ };
184
+ return {
185
+ targetIndex: 0,
186
+ account: a,
187
+ observedAt: now(),
188
+ ...definedFields({ backendState: backendState === "" ? undefined : backendState }),
189
+ delivery,
190
+ };
191
+ }
192
+ const processing = (a, id, backendState) => ({
193
+ state: "processing",
194
+ ...outcomeFields(a, id, backendState),
195
+ });
196
+ const published = (a, id, postId) => ({
197
+ state: "published",
198
+ ...outcomeFields(a, id, "PUBLISHED"),
199
+ post: platformPostRef({ backend, platform: "threads", accountId: a.accountId, postId }),
220
200
  });
221
- const terminalFailure = (a, id, state) =>
222
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
223
- ({
224
- ...base(a, id, "failed", state),
201
+ const ambiguous = (a, id, backendState, diagnostic) => ({
202
+ state: "unknown",
203
+ ...outcomeFields(a, id, backendState),
204
+ reason: "ambiguous-submission",
205
+ ...definedFields({ diagnostic }),
206
+ });
207
+ const terminalFailure = (a, id, state) => ({
208
+ state: "failed",
209
+ ...outcomeFields(a, id, state),
225
210
  code: "media_error",
226
211
  message: "Threads container failed or expired before publication.",
227
212
  retryDisposition: { kind: "never" },
228
213
  });
214
+ function replyControlParam(value) {
215
+ if (value === "accountsYouFollow")
216
+ return "accounts_you_follow";
217
+ if (value === "mentionedOnly")
218
+ return "mentioned_only";
219
+ return "everyone";
220
+ }
221
+ function parentMediaType(childCount, singleKind) {
222
+ if (childCount)
223
+ return "CAROUSEL";
224
+ if (singleKind === "video")
225
+ return "VIDEO";
226
+ if (singleKind === "image")
227
+ return "IMAGE";
228
+ return "TEXT";
229
+ }
229
230
  async function resume(w, a, c) {
230
231
  if (w.nativeId)
231
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
232
- return {
233
- ...base(a, w.id, "published", "PUBLISHED"),
234
- post: platformPostRef({
235
- backend,
236
- platform: "threads",
237
- accountId: a.accountId,
238
- postId: w.nativeId,
239
- }),
240
- };
232
+ return published(a, w.id, w.nativeId);
241
233
  if (w.stage === "unknown")
242
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
243
- return {
244
- ...base(a, w.id, "unknown", w.backendState),
245
- reason: "ambiguous-submission",
246
- diagnostic: "Threads publish acceptance is unknown; no replay was attempted.",
247
- };
234
+ return ambiguous(a, w.id, w.backendState, "Threads publish acceptance is unknown; no replay was attempted.");
248
235
  let cur = w;
249
- const items = Array.isArray(cur.options["_mediaItems"]) ? cur.options["_mediaItems"] : [];
236
+ const items = optionalArray(cur.options["_mediaItems"]) ?? [];
250
237
  while (cur.childIds.length < items.length) {
251
238
  const item = object(items[cur.childIds.length]);
239
+ const url = String(item["url"]);
240
+ const isVideo = item["kind"] === "video";
252
241
  const p = new URLSearchParams({
253
- media_type: item["kind"] === "video" ? "VIDEO" : "IMAGE",
242
+ media_type: isVideo ? "VIDEO" : "IMAGE",
254
243
  is_carousel_item: "true",
255
- ...(item["kind"] === "video"
256
- ? { video_url: String(item["url"]) }
257
- : { image_url: String(item["url"]) }),
258
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
259
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
260
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
261
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
262
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
263
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
264
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
265
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
266
- ...(typeof item["altText"] === "string" ? { alt_text: item["altText"] } : {}),
244
+ ...definedFields({
245
+ video_url: isVideo ? url : undefined,
246
+ image_url: isVideo ? undefined : url,
247
+ alt_text: optionalString(item["altText"]),
248
+ }),
267
249
  });
268
250
  await store.update(cur.id, { stage: "unknown", backendState: "CREATING_CHILD" });
269
- const child = await request(`${a.accountId}/threads?${p}`,
270
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
271
- { method: "POST", ...(c.signal ? { signal: c.signal } : {}) }, "threads.container.child", c);
272
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
273
- if (typeof child["id"] !== "string")
251
+ const child = await request(`${a.accountId}/threads?${p}`, { method: "POST", ...definedFields({ signal: c.signal }) }, "threads.container.child", c);
252
+ const childId = optionalString(child["id"]);
253
+ if (childId === undefined)
274
254
  fail("threads.container.child", "Threads did not return a child container ID.");
275
255
  cur = await store.update(cur.id, {
276
- childIds: [...cur.childIds, child["id"]],
256
+ childIds: [...cur.childIds, childId],
277
257
  stage: "children",
278
258
  });
279
259
  }
280
260
  for (const id of cur.childIds) {
281
261
  const s = await status(id, c);
282
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
283
- const st = typeof s["status"] === "string" ? s["status"] : "";
262
+ const st = optionalString(s["status"]) ?? "";
284
263
  if (st === "ERROR" || st === "EXPIRED")
285
264
  return terminalFailure(a, w.id, st);
286
265
  if (st !== "FINISHED" && st !== "PUBLISHED")
287
- return base(a, w.id, "processing", st || "PROCESSING");
266
+ return processing(a, w.id, st || "PROCESSING");
288
267
  }
289
268
  if (!cur.parentId) {
290
269
  const singleKind = cur.options["_mediaKind"];
291
- const singleUrl = cur.options["_mediaUrl"];
270
+ const singleUrl = optionalString(cur.options["_mediaUrl"]);
271
+ const replyControl = optionalString(cur.options["replyControl"]);
292
272
  const p = new URLSearchParams({
293
- media_type: cur.childIds.length
294
- ? "CAROUSEL"
295
- : singleKind === "video"
296
- ? "VIDEO"
297
- : singleKind === "image"
298
- ? "IMAGE"
299
- : "TEXT",
300
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
301
- ...(cur.childIds.length ? { children: cur.childIds.join(",") } : {}),
302
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
303
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
304
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
305
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
306
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
307
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
308
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
309
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
310
- ...(typeof singleUrl === "string"
311
- ? singleKind === "video"
312
- ? { video_url: singleUrl }
313
- : { image_url: singleUrl }
314
- : {}),
315
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
316
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
317
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
318
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
319
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
320
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
321
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
322
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
323
- ...(typeof cur.options["_altText"] === "string"
324
- ? { alt_text: cur.options["_altText"] }
325
- : {}),
326
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
327
- ...(cur.caption ? { text: cur.caption } : {}),
328
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
329
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
330
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
331
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
332
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
333
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
334
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
335
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
336
- ...(typeof cur.options["replyControl"] === "string"
337
- ? {
338
- reply_control: cur.options["replyControl"] === "accountsYouFollow"
339
- ? "accounts_you_follow"
340
- : cur.options["replyControl"] === "mentionedOnly"
341
- ? "mentioned_only"
342
- : "everyone",
343
- }
344
- : {}),
345
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
346
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
347
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
348
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
349
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
350
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
351
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
352
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
353
- ...(typeof cur.options["_replyToId"] === "string"
354
- ? { reply_to_id: cur.options["_replyToId"] }
355
- : {}),
273
+ media_type: parentMediaType(cur.childIds.length, singleKind),
274
+ ...definedFields({
275
+ children: cur.childIds.length ? cur.childIds.join(",") : undefined,
276
+ video_url: singleKind === "video" ? singleUrl : undefined,
277
+ image_url: singleKind === "video" ? undefined : singleUrl,
278
+ alt_text: optionalString(cur.options["_altText"]),
279
+ text: cur.caption === "" ? undefined : cur.caption,
280
+ reply_control: replyControl === undefined ? undefined : replyControlParam(replyControl),
281
+ reply_to_id: optionalString(cur.options["_replyToId"]),
282
+ }),
356
283
  });
357
284
  // Persist ambiguity before dispatch so a process crash cannot recreate a
358
285
  // possibly accepted container under the same continuation handle.
359
286
  await store.update(cur.id, { stage: "unknown", backendState: "CREATING_PARENT" });
360
- const parent = await request(`${a.accountId}/threads?${p}`,
361
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
362
- { method: "POST", ...(c.signal ? { signal: c.signal } : {}) }, "threads.container.create", c);
363
- const parentId = parent["id"];
364
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
365
- if (typeof parentId !== "string")
287
+ const parent = await request(`${a.accountId}/threads?${p}`, { method: "POST", ...definedFields({ signal: c.signal }) }, "threads.container.create", c);
288
+ const parentId = optionalString(parent["id"]);
289
+ if (parentId === undefined)
366
290
  fail("threads.container.create", "Threads did not return a container ID.");
367
291
  cur = await store.update(cur.id, { parentId, stage: "parent" });
368
292
  }
@@ -370,57 +294,30 @@ export function threads(options) {
370
294
  if (!parentId)
371
295
  fail("threads.container.create", "Missing parent container ID.");
372
296
  const ps = await status(parentId, c);
373
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
374
- const pst = typeof ps["status"] === "string" ? ps["status"] : "";
297
+ const pst = optionalString(ps["status"]) ?? "";
375
298
  if (pst === "ERROR" || pst === "EXPIRED")
376
299
  return terminalFailure(a, cur.id, pst);
377
300
  if (pst === "PUBLISHED") {
378
301
  await store.update(cur.id, { stage: "unknown", backendState: "PUBLISHED_WITHOUT_NATIVE_ID" });
379
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
380
- return {
381
- ...base(a, cur.id, "unknown", pst),
382
- reason: "ambiguous-submission",
383
- };
302
+ return ambiguous(a, cur.id, pst);
384
303
  }
385
304
  if (pst !== "FINISHED")
386
- return base(a, cur.id, "processing", pst || "PROCESSING");
305
+ return processing(a, cur.id, pst || "PROCESSING");
387
306
  await store.update(cur.id, { stage: "unknown", backendState: "PUBLISHING" });
388
307
  try {
389
- const pub = await request(`${a.accountId}/threads_publish?creation_id=${encodeURIComponent(parentId)}`,
390
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
391
- { method: "POST", ...(c.signal ? { signal: c.signal } : {}) }, "threads.publish", c);
392
- const nativeId = pub["id"];
393
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
394
- if (typeof nativeId !== "string") {
308
+ const pub = await request(`${a.accountId}/threads_publish?creation_id=${encodeURIComponent(parentId)}`, { method: "POST", ...definedFields({ signal: c.signal }) }, "threads.publish", c);
309
+ const nativeId = optionalString(pub["id"]);
310
+ if (nativeId === undefined) {
395
311
  await store.update(cur.id, { stage: "unknown", backendState: "MISSING_ID" });
396
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
397
- return {
398
- ...base(a, cur.id, "unknown", "MISSING_ID"),
399
- reason: "ambiguous-submission",
400
- diagnostic: "Threads accepted publish without a native post ID.",
401
- };
312
+ return ambiguous(a, cur.id, "MISSING_ID", "Threads accepted publish without a native post ID.");
402
313
  }
403
314
  await store.update(cur.id, { nativeId, stage: "published", backendState: "PUBLISHED" });
404
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
405
- return {
406
- ...base(a, cur.id, "published", "PUBLISHED"),
407
- post: platformPostRef({
408
- backend,
409
- platform: "threads",
410
- accountId: a.accountId,
411
- postId: nativeId,
412
- }),
413
- };
315
+ return published(a, cur.id, nativeId);
414
316
  }
415
317
  catch (e) {
416
318
  await store.update(cur.id, { stage: "unknown", backendState: "AMBIGUOUS" });
417
319
  if (e instanceof SocialError)
418
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
419
- return {
420
- ...base(a, cur.id, "unknown", "AMBIGUOUS"),
421
- reason: "ambiguous-submission",
422
- diagnostic: e.message,
423
- };
320
+ return ambiguous(a, cur.id, "AMBIGUOUS", e.message);
424
321
  throw e;
425
322
  }
426
323
  }
@@ -431,6 +328,7 @@ export function threads(options) {
431
328
  runtime: ["node>=22.12", "bun"],
432
329
  capabilities: [
433
330
  { operation: "accounts.read", platform: "threads", availability: "available" },
331
+ webhookCapability("threads", "Verifies Meta X-Hub-Signature-256 with the app secret and decodes replies, mentions, publish, and delete deliveries. Answer the GET handshake with answerMetaWebhookChallenge."),
434
332
  {
435
333
  operation: "posts.publish",
436
334
  platform: "threads",
@@ -438,6 +336,12 @@ export function threads(options) {
438
336
  formats: ["text", "image", "video", "carousel"],
439
337
  requiredScopes: ["threads_basic", "threads_content_publish"],
440
338
  },
339
+ {
340
+ operation: "posts.schedule",
341
+ platform: "threads",
342
+ availability: "unsupported-by-platform",
343
+ notes: "Threads publishing endpoints have no publish-time parameter; threads_publish publishes immediately.",
344
+ },
441
345
  { operation: "posts.read", platform: "threads", availability: "available" },
442
346
  { operation: "posts.list", platform: "threads", availability: "available" },
443
347
  { operation: "posts.status", platform: "threads", availability: "available" },
@@ -472,6 +376,13 @@ export function threads(options) {
472
376
  requiredScopes: ["threads_manage_replies"],
473
377
  notes: "Hiding replies and pending-reply moderation require Threads reply-management permissions.",
474
378
  },
379
+ {
380
+ operation: "comments.delete",
381
+ platform: "threads",
382
+ availability: "available",
383
+ requiredScopes: ["threads_basic", "threads_delete"],
384
+ notes: "Deletes only replies published by the authenticated user. Threads allows 100 deletions per account per day. Use hideReply for other users' replies.",
385
+ },
475
386
  {
476
387
  operation: "posts.quote",
477
388
  platform: "threads",
@@ -496,7 +407,20 @@ export function threads(options) {
496
407
  availability: "available",
497
408
  requiredScopes: ["threads_basic", "threads_delete"],
498
409
  },
410
+ // Source, accessed 2026-09-24: https://developers.facebook.com/docs/threads/posts
411
+ {
412
+ operation: "posts.update",
413
+ platform: "threads",
414
+ availability: "unsupported-by-platform",
415
+ notes: "The Threads API has no endpoint to edit a published post; text and attachments are set when the container is created.",
416
+ },
499
417
  { operation: "profile.read", platform: "threads", availability: "available" },
418
+ {
419
+ operation: "profile.update",
420
+ platform: "threads",
421
+ availability: "unsupported-by-platform",
422
+ notes: "The Threads API exposes profile reads only. It has no profile write endpoint.",
423
+ },
500
424
  {
501
425
  operation: "search.keyword",
502
426
  platform: "threads",
@@ -510,6 +434,24 @@ export function threads(options) {
510
434
  requiredScopes: ["threads_basic"],
511
435
  notes: "The profile endpoint reads the authorized app-scoped user; profile lookup requires threads_profile_discovery.",
512
436
  },
437
+ {
438
+ // https://developers.facebook.com/docs/threads/threads-profiles and
439
+ // https://developers.facebook.com/docs/threads/reference/user (accessed 2026-09-24):
440
+ // profile_lookup requires an exact username match; there is no profile search endpoint.
441
+ operation: "profiles.search",
442
+ platform: "threads",
443
+ availability: "unsupported-by-platform",
444
+ notes: "Threads has no profile search endpoint. Profile discovery only looks up an exact username, which profiles.read covers.",
445
+ },
446
+ {
447
+ // https://developers.facebook.com/docs/threads/reference/user and
448
+ // https://developers.facebook.com/docs/threads/insights (accessed 2026-09-24): the user
449
+ // node has no followers or following edge; follower data is aggregate counts only.
450
+ operation: "graph.read",
451
+ platform: "threads",
452
+ availability: "unsupported-by-platform",
453
+ notes: "Threads does not expose follower or following lists. The followers_count metric is available through analytics.account.read.",
454
+ },
513
455
  {
514
456
  operation: "search.posts",
515
457
  platform: "threads",
@@ -529,12 +471,16 @@ export function threads(options) {
529
471
  availability: "unsupported-by-platform",
530
472
  notes: "Threads does not expose messaging APIs to standard apps.",
531
473
  },
474
+ {
475
+ operation: "notifications.read",
476
+ platform: "threads",
477
+ availability: "unsupported-by-platform",
478
+ notes: "The Threads API has no notifications endpoint. Use mentions.read, reply reads, or webhooks instead.",
479
+ },
532
480
  ],
533
481
  };
534
482
  const native = {
535
- getPost: (id, c) => request(`${encodeURIComponent(id)}?fields=id,text,username,media_type,permalink`,
536
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
537
- { method: "GET", ...(c.signal ? { signal: c.signal } : {}) }, "threads.posts.get", c),
483
+ getPost: (id, c) => request(`${encodeURIComponent(id)}?fields=id,text,username,media_type,permalink`, { method: "GET", ...definedFields({ signal: c.signal }) }, "threads.posts.get", c),
538
484
  getContainer: (id, c) => status(id, c),
539
485
  resumePublication: async (a, id, c) => {
540
486
  authorize(a, "threads.posts.resume");
@@ -542,7 +488,7 @@ export function threads(options) {
542
488
  if (!w || w.backend !== backend || w.accountId !== a.accountId)
543
489
  fail("threads.posts.resume", "Workflow handle is not authorized for this account.", "unauthorized");
544
490
  if (!(await store.claim(id)))
545
- return base(a, id, "processing", "CLAIMED");
491
+ return processing(a, id, "CLAIMED");
546
492
  try {
547
493
  return await resume((await store.get(id)) ?? w, a, c);
548
494
  }
@@ -607,6 +553,16 @@ export function threads(options) {
607
553
  authorize(account, "threads.comments.moderate");
608
554
  return request(`${encodeURIComponent(replyId)}/manage_reply?hide=${String(hide)}`, { method: "POST" }, "threads.comments.moderate", context);
609
555
  },
556
+ async deleteComment({ account, commentId, context }) {
557
+ // Sources (accessed 2026-09-24): https://developers.facebook.com/docs/threads/posts/delete-posts
558
+ // and https://developers.facebook.com/docs/threads/retrieve-and-manage-replies/create-replies
559
+ authorize(account, "threads.comments.delete");
560
+ if (!/^\d+$/.test(commentId))
561
+ fail("threads.comments.delete", "Provide the numeric Threads reply media ID.", "invalid_input");
562
+ const result = await request(encodeURIComponent(commentId), { method: "DELETE" }, "threads.comments.delete", context);
563
+ if (result["success"] !== true)
564
+ fail("threads.comments.delete", "Threads did not confirm the reply deletion.");
565
+ },
610
566
  async listConversation({ account, mediaId, cursor, context }) {
611
567
  authorize(account, "threads.comments.conversation");
612
568
  return request(`${encodeURIComponent(mediaId)}/conversation?fields=id,text,username,permalink,timestamp,is_reply,hide_status${cursor ? `&after=${encodeURIComponent(cursor)}` : ""}`, { method: "GET" }, "threads.comments.conversation", context);
@@ -631,6 +587,7 @@ export function threads(options) {
631
587
  id: backend,
632
588
  capabilities,
633
589
  native,
590
+ webhooks: directWebhooks("threads", (input) => verifyMetaWebhook({ ...input, secret: options.webhookSecret ?? "" }), now),
634
591
  accounts: {
635
592
  list: async (_input, context) => ({ items: [await readAccount(context)] }),
636
593
  get: async (ref, context) => {
@@ -643,7 +600,7 @@ export function threads(options) {
643
600
  authorize(account, "profiles.read");
644
601
  let result;
645
602
  if (input.handle !== undefined) {
646
- result = await request(`profile_lookup?username=${encodeURIComponent(input.handle)}&fields=id,username,name,profile_picture_url,biography,is_verified`, { method: "GET" }, "profiles.read", context);
603
+ result = await request(`profile_lookup?username=${encodeURIComponent(input.handle)}&fields=username,name,profile_picture_url,biography,is_verified`, { method: "GET" }, "profiles.read", context);
647
604
  }
648
605
  else {
649
606
  const profileId = input.profileId ?? account.accountId;
@@ -651,7 +608,7 @@ export function threads(options) {
651
608
  fail("profiles.read", "Threads profile reads are limited to the authorized app-scoped user.", "unauthorized");
652
609
  result = await request(`${encodeURIComponent(profileId)}?fields=id,username,name,threads_profile_picture_url,threads_biography,is_verified`, { method: "GET" }, "profiles.read", context);
653
610
  }
654
- const returnedProfileId = optionalString(result["id"]);
611
+ const returnedProfileId = input.handle === undefined ? optionalString(result["id"]) : undefined;
655
612
  const profileId = returnedProfileId ??
656
613
  input.profileId ??
657
614
  (input.handle ? `lookup:${input.handle}` : undefined);
@@ -669,10 +626,7 @@ export function threads(options) {
669
626
  accountId: account.accountId,
670
627
  profileId,
671
628
  }),
672
- ...(displayName === undefined ? {} : { displayName }),
673
- ...(handle === undefined ? {} : { handle }),
674
- ...(avatarUrl === undefined ? {} : { avatarUrl }),
675
- ...(bio === undefined ? {} : { bio }),
629
+ ...definedFields({ displayName, handle, avatarUrl, bio }),
676
630
  native: returnedProfileId === undefined ? { ...result, _profileIdUnavailable: true } : result,
677
631
  };
678
632
  },
@@ -734,11 +688,13 @@ export function threads(options) {
734
688
  add("media.format", "Threads images must be JPEG or PNG.");
735
689
  if (item.kind === "video" && item.mimeType && item.mimeType !== "video/mp4")
736
690
  add("media.format", "Threads videos must be MP4.");
691
+ if (item.kind === "document")
692
+ add("media.format", "Threads publishing accepts image and video media only.");
737
693
  if (item.altText && item.altText.length > 1000)
738
694
  add("media.alt_text", "Alt text is limited to 1,000 characters.");
739
695
  }
740
696
  if (target.options !== undefined) {
741
- const o = object(target.options);
697
+ const o = optionsObject(target);
742
698
  if (Object.keys(o).some((key) => key !== "replyControl"))
743
699
  add("options.unmapped", "A supplied option has no Threads mapping.");
744
700
  const replyControl = o["replyControl"];
@@ -756,32 +712,23 @@ export function threads(options) {
756
712
  authorize(target.account, "threads.posts.publish");
757
713
  const media = target.content.media ?? [];
758
714
  const item = media.length === 1 ? media[0] : undefined;
759
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
715
+ const mediaItems = media.length > 1
716
+ ? media.map((entry) => ({
717
+ kind: entry.kind,
718
+ url: entry.source.kind === "https-url" ? entry.source.url : "",
719
+ ...definedFields({ altText: entry.altText === "" ? undefined : entry.altText }),
720
+ }))
721
+ : undefined;
722
+ const singleSource = item?.source.kind === "https-url" ? item.source : undefined;
760
723
  const opts = {
761
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
762
- ...object(target.options ?? {}),
763
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
764
- ...(target.replyTo ? { _replyToId: target.replyTo.postId } : {}),
765
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
766
- ...(media.length > 1
767
- ? {
768
- _mediaItems: media.map((entry) => ({
769
- kind: entry.kind,
770
- url: entry.source.kind === "https-url" ? entry.source.url : "",
771
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
772
- ...(entry.altText ? { altText: entry.altText } : {}),
773
- })),
774
- }
775
- : {}),
776
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
777
- ...(item?.source.kind === "https-url"
778
- ? {
779
- _mediaKind: item.kind,
780
- _mediaUrl: item.source.url,
781
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
782
- ...(item.altText ? { _altText: item.altText } : {}),
783
- }
784
- : {}),
724
+ ...optionsObject(target),
725
+ ...definedFields({
726
+ _replyToId: target.replyTo?.postId,
727
+ _mediaItems: mediaItems,
728
+ _mediaKind: singleSource === undefined ? undefined : item?.kind,
729
+ _mediaUrl: singleSource?.url,
730
+ _altText: singleSource === undefined || item?.altText === "" ? undefined : item?.altText,
731
+ }),
785
732
  };
786
733
  const w = await store.create({
787
734
  backend,
@@ -791,9 +738,8 @@ export function threads(options) {
791
738
  options: opts,
792
739
  stage: "children",
793
740
  });
794
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
795
741
  if (!(await store.claim(w.id)))
796
- return base(target.account, w.id, "processing", "CLAIMED");
742
+ return processing(target.account, w.id, "CLAIMED");
797
743
  try {
798
744
  try {
799
745
  const outcome = await resume((await store.get(w.id)) ?? w, target.account, context);
@@ -802,10 +748,8 @@ export function threads(options) {
802
748
  catch (error) {
803
749
  if (error instanceof SocialError && error.code === "ambiguous_outcome")
804
750
  return {
805
- ...base(target.account, w.id, "unknown", "AMBIGUOUS"),
751
+ ...ambiguous(target.account, w.id, "AMBIGUOUS", error.message),
806
752
  targetIndex: target.targetIndex,
807
- reason: "ambiguous-submission",
808
- diagnostic: error.message,
809
753
  };
810
754
  throw error;
811
755
  }
@@ -831,28 +775,15 @@ export function threads(options) {
831
775
  const w = await store.get(ref.deliveryId);
832
776
  if (!w || w.backend !== backend || w.accountId !== ref.accountId)
833
777
  fail("threads.posts.status", "Workflow handle is not authorized for this account.", "unauthorized");
778
+ const owner = accountRef(backend, ref.accountId);
834
779
  if (w.nativeId)
835
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
836
- return {
837
- ...base(accountRef(backend, ref.accountId), w.id, "published", "PUBLISHED"),
838
- post: platformPostRef({
839
- backend,
840
- platform: "threads",
841
- accountId: ref.accountId,
842
- postId: w.nativeId,
843
- }),
844
- };
780
+ return published(owner, w.id, w.nativeId);
845
781
  if (w.stage === "unknown")
846
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
847
- return {
848
- ...base(accountRef(backend, ref.accountId), w.id, "unknown", w.backendState),
849
- reason: "ambiguous-submission",
850
- diagnostic: "Explicit resumePublication is required.",
851
- };
782
+ return ambiguous(owner, w.id, w.backendState, "Explicit resumePublication is required.");
852
783
  if (!(await store.claim(w.id)))
853
- return base(accountRef(backend, ref.accountId), w.id, "processing", "CLAIMED");
784
+ return processing(owner, w.id, "CLAIMED");
854
785
  try {
855
- return await resume((await store.get(w.id)) ?? w, accountRef(backend, ref.accountId), c);
786
+ return await resume((await store.get(w.id)) ?? w, owner, c);
856
787
  }
857
788
  finally {
858
789
  await store.release?.(w.id);
@@ -863,35 +794,11 @@ export function threads(options) {
863
794
  async list(post, input, context) {
864
795
  authorize(post, "threads.comments.list");
865
796
  const result = await request(`${encodeURIComponent(post.postId)}/replies?fields=id,text,username,timestamp${input.cursor ? `&after=${encodeURIComponent(input.cursor)}` : ""}`, { method: "GET" }, "threads.comments.list", context);
866
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
867
- const rows = Array.isArray(result["data"]) ? result["data"] : [];
797
+ const rows = (optionalArray(result["data"]) ?? []).filter(isJsonObject);
798
+ const cursors = optionalObject(optionalObject(result["paging"])?.["cursors"]);
868
799
  return {
869
800
  items: rows,
870
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
871
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
872
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
873
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
874
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
875
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
876
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
877
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
878
- ...(typeof result["paging"] === "object" &&
879
- result["paging"] &&
880
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
881
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
882
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
883
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract.
884
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
885
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract.
886
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
887
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract.
888
- typeof result["paging"]["cursors"] === "object" &&
889
- result["paging"]["cursors"] !== null &&
890
- typeof result["paging"]["cursors"]["after"] === "string"
891
- ? {
892
- nextCursor: String(result["paging"]["cursors"]["after"]),
893
- }
894
- : {}),
801
+ ...definedFields({ nextCursor: optionalString(cursors?.["after"]) }),
895
802
  };
896
803
  },
897
804
  async reply(comment, content, context) {
@@ -910,21 +817,16 @@ export function threads(options) {
910
817
  getAccountMetrics,
911
818
  async getPostMetrics(post, c) {
912
819
  authorize(post, "threads.analytics");
913
- const r = await request(`${encodeURIComponent(post.postId)}/insights?metric=views,likes,replies,reposts,quotes,shares,link_total_values`,
914
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
915
- { method: "GET", ...(c.signal ? { signal: c.signal } : {}) }, "threads.analytics", c);
820
+ const r = await request(`${encodeURIComponent(post.postId)}/insights?metric=views,likes,replies,reposts,quotes,shares,link_total_values`, { method: "GET", ...definedFields({ signal: c.signal }) }, "threads.analytics", c);
916
821
  return array(r["data"]).flatMap((x) => {
917
822
  const row = object(x);
918
- const first = Array.isArray(row["values"]) ? row["values"][0] : undefined;
919
- const value =
920
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
921
- first && typeof first === "object"
823
+ const first = optionalArray(row["values"])?.[0];
824
+ const value = isJsonObject(first) || isJsonArray(first)
922
825
  ? optionalNumber(object(first)["value"])
923
826
  : optionalNumber(row["value"]);
924
- const rawName = row["name"];
827
+ const rawName = optionalString(row["name"]);
925
828
  const name = rawName === "link_total_values" ? "clicks" : rawName;
926
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
927
- return value === undefined || typeof name !== "string"
829
+ return value === undefined || name === undefined
928
830
  ? []
929
831
  : [
930
832
  {