@opencoredev/social-sdk 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +10 -2
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +1 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/cloud/common.d.ts.map +1 -1
  6. package/dist/cloud/common.js +48 -20
  7. package/dist/cloud/common.js.map +1 -1
  8. package/dist/cloud/lifecycle.d.ts.map +1 -1
  9. package/dist/cloud/lifecycle.js +5 -0
  10. package/dist/cloud/lifecycle.js.map +1 -1
  11. package/dist/cloud/media.d.ts.map +1 -1
  12. package/dist/cloud/media.js +12 -8
  13. package/dist/cloud/media.js.map +1 -1
  14. package/dist/cloud/outcomes.d.ts.map +1 -1
  15. package/dist/cloud/outcomes.js +17 -9
  16. package/dist/cloud/outcomes.js.map +1 -1
  17. package/dist/cloud/post-for-me.d.ts.map +1 -1
  18. package/dist/cloud/post-for-me.js +99 -28
  19. package/dist/cloud/post-for-me.js.map +1 -1
  20. package/dist/cloud/zernio.d.ts.map +1 -1
  21. package/dist/cloud/zernio.js +100 -9
  22. package/dist/cloud/zernio.js.map +1 -1
  23. package/dist/core/adapter.d.ts +42 -1
  24. package/dist/core/adapter.d.ts.map +1 -1
  25. package/dist/core/adapter.js.map +1 -1
  26. package/dist/core/client.d.ts +35 -1
  27. package/dist/core/client.d.ts.map +1 -1
  28. package/dist/core/client.js +503 -110
  29. package/dist/core/client.js.map +1 -1
  30. package/dist/core/concurrency.d.ts.map +1 -1
  31. package/dist/core/concurrency.js +5 -3
  32. package/dist/core/concurrency.js.map +1 -1
  33. package/dist/core/errors.d.ts.map +1 -1
  34. package/dist/core/errors.js +14 -7
  35. package/dist/core/errors.js.map +1 -1
  36. package/dist/core/idempotency.d.ts.map +1 -1
  37. package/dist/core/idempotency.js.map +1 -1
  38. package/dist/core/index.d.ts.map +1 -1
  39. package/dist/core/index.js.map +1 -1
  40. package/dist/core/pagination.d.ts.map +1 -1
  41. package/dist/core/pagination.js +1 -0
  42. package/dist/core/pagination.js.map +1 -1
  43. package/dist/core/types.d.ts +70 -2
  44. package/dist/core/types.d.ts.map +1 -1
  45. package/dist/core/types.js +10 -0
  46. package/dist/core/types.js.map +1 -1
  47. package/dist/platforms/bluesky.d.ts +131 -1
  48. package/dist/platforms/bluesky.d.ts.map +1 -1
  49. package/dist/platforms/bluesky.js +772 -44
  50. package/dist/platforms/bluesky.js.map +1 -1
  51. package/dist/platforms/instagram.d.ts +70 -3
  52. package/dist/platforms/instagram.d.ts.map +1 -1
  53. package/dist/platforms/instagram.js +403 -47
  54. package/dist/platforms/instagram.js.map +1 -1
  55. package/dist/platforms/linkedin.d.ts +55 -0
  56. package/dist/platforms/linkedin.d.ts.map +1 -1
  57. package/dist/platforms/linkedin.js +350 -39
  58. package/dist/platforms/linkedin.js.map +1 -1
  59. package/dist/platforms/threads.d.ts +37 -4
  60. package/dist/platforms/threads.d.ts.map +1 -1
  61. package/dist/platforms/threads.js +419 -44
  62. package/dist/platforms/threads.js.map +1 -1
  63. package/dist/platforms/tiktok.d.ts.map +1 -1
  64. package/dist/platforms/tiktok.js +109 -26
  65. package/dist/platforms/tiktok.js.map +1 -1
  66. package/dist/platforms/x-engagement.d.ts.map +1 -1
  67. package/dist/platforms/x-engagement.js +4 -0
  68. package/dist/platforms/x-engagement.js.map +1 -1
  69. package/dist/platforms/x.d.ts +227 -9
  70. package/dist/platforms/x.d.ts.map +1 -1
  71. package/dist/platforms/x.js +826 -49
  72. package/dist/platforms/x.js.map +1 -1
  73. package/dist/platforms/youtube-upload.d.ts.map +1 -1
  74. package/dist/platforms/youtube-upload.js +25 -9
  75. package/dist/platforms/youtube-upload.js.map +1 -1
  76. package/dist/platforms/youtube.d.ts +56 -2
  77. package/dist/platforms/youtube.d.ts.map +1 -1
  78. package/dist/platforms/youtube.js +571 -31
  79. package/dist/platforms/youtube.js.map +1 -1
  80. package/dist/server/connections.d.ts.map +1 -1
  81. package/dist/server/connections.js +4 -2
  82. package/dist/server/connections.js.map +1 -1
  83. package/dist/server/credentials.d.ts.map +1 -1
  84. package/dist/server/credentials.js +10 -16
  85. package/dist/server/credentials.js.map +1 -1
  86. package/dist/server/index.d.ts.map +1 -1
  87. package/dist/server/index.js.map +1 -1
  88. package/dist/server/oauth.d.ts +5 -0
  89. package/dist/server/oauth.d.ts.map +1 -1
  90. package/dist/server/oauth.js +118 -47
  91. package/dist/server/oauth.js.map +1 -1
  92. package/dist/server/webhooks.d.ts.map +1 -1
  93. package/dist/server/webhooks.js +11 -4
  94. package/dist/server/webhooks.js.map +1 -1
  95. package/dist/testing/index.d.ts.map +1 -1
  96. package/dist/testing/index.js +153 -1
  97. package/dist/testing/index.js.map +1 -1
  98. package/dist/transport/binary.d.ts.map +1 -1
  99. package/dist/transport/binary.js.map +1 -1
  100. package/dist/transport/budget.d.ts.map +1 -1
  101. package/dist/transport/budget.js.map +1 -1
  102. package/dist/transport/http.d.ts +2 -1
  103. package/dist/transport/http.d.ts.map +1 -1
  104. package/dist/transport/http.js +42 -12
  105. package/dist/transport/http.js.map +1 -1
  106. package/dist/transport/json.d.ts +7 -1
  107. package/dist/transport/json.d.ts.map +1 -1
  108. package/dist/transport/json.js +7 -1
  109. package/dist/transport/json.js.map +1 -1
  110. package/dist/transport/upload.d.ts +1 -0
  111. package/dist/transport/upload.d.ts.map +1 -1
  112. package/dist/transport/upload.js +38 -17
  113. package/dist/transport/upload.js.map +1 -1
  114. package/dist/transport/validation.d.ts.map +1 -1
  115. package/dist/transport/validation.js +1 -0
  116. package/dist/transport/validation.js.map +1 -1
  117. package/package.json +2 -1
@@ -1,4 +1,5 @@
1
- import { connectedAccountRef, defineAdapter, platformPostRef, } from "../core/index.js";
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
+ import { connectedAccountRef, defineAdapter, platformPostRef, profileRef, } from "../core/index.js";
2
3
  import { SocialError } from "../core/errors.js";
3
4
  import { managedHttp, publicFields } from "../cloud/common.js";
4
5
  import { object, array, optionalNumber, optionalString } from "../transport/validation.js";
@@ -46,12 +47,14 @@ export function threads(options) {
46
47
  const backend = options.backend ?? "default", version = options.graphVersion ?? "v1.0", auth = options.auth, store = options.workflowStore ?? new MemoryThreadsWorkflowStore(), now = () => (options.clock?.() ?? new Date()).toISOString();
47
48
  const http = managedHttp(`https://graph.threads.net/${version}`, {
48
49
  apiKey: auth.accessToken,
50
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
49
51
  ...(options.fetch ? { fetch: options.fetch } : {}),
50
52
  });
51
53
  async function request(path, init, operation, context) {
52
54
  const [raw = "", qs] = path.split("?", 2);
53
55
  const query = Object.fromEntries(new URLSearchParams(qs ?? ""));
54
56
  try {
57
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
55
58
  return object(await http(`/${raw.replace(/^\//, "")}`, context, init.method === "POST" ? {} : undefined, query, init.method === "POST" || init.method === "DELETE" || init.method === "PUT"
56
59
  ? init.method
57
60
  : "GET"));
@@ -67,14 +70,37 @@ export function threads(options) {
67
70
  fail(op, "Account reference does not match this Threads authorization.", "unauthorized");
68
71
  }
69
72
  const account = accountRef(backend, auth.userId);
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
+ });
78
+ const paging = result["paging"] === undefined ? {} : object(result["paging"]);
79
+ const cursors = paging["cursors"] === undefined ? {} : object(paging["cursors"]);
80
+ // 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
82
+ ? optionalString(cursors["after"])
83
+ : undefined;
84
+ return { items, ...(nextCursor === undefined ? {} : { nextCursor }) };
85
+ }
70
86
  async function readAccount(context) {
71
- const v = await request("/me?fields=id,username", { method: "GET" }, "threads.accounts.read", context);
87
+ const v = await request("/me?fields=id,username,threads_profile_picture_url,threads_biography", { method: "GET" }, "threads.accounts.read", context);
72
88
  const id = v["id"], username = v["username"];
89
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
73
90
  if (typeof id !== "string" || id !== auth.userId)
74
91
  fail("threads.accounts.read", "Threads returned an unauthorized account.", "unauthorized");
75
92
  return {
76
93
  ref: account,
94
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
77
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.
78
104
  ...(typeof username === "string" ? { handle: username } : {}),
79
105
  status: "connected",
80
106
  };
@@ -95,10 +121,14 @@ export function threads(options) {
95
121
  const items = array(result["data"]).map((entry) => publicFields(entry, ["id", "text", "username", "media_type", "permalink", "timestamp"]));
96
122
  const paging = result["paging"] === undefined ? {} : object(result["paging"]);
97
123
  const cursors = paging["cursors"] === undefined ? {} : object(paging["cursors"]);
124
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
98
125
  const hasNext = typeof paging["next"] === "string" && paging["next"].length > 0;
99
- const nextCursor = hasNext && typeof cursors["after"] === "string" ? cursors["after"] : undefined;
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;
100
129
  return {
101
130
  items,
131
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
102
132
  ...(nextCursor === undefined ? {} : { nextCursor }),
103
133
  };
104
134
  }
@@ -114,9 +144,10 @@ export function threads(options) {
114
144
  "followers_count",
115
145
  ];
116
146
  const result = await request(`${encodeURIComponent(selected.accountId)}/threads_insights?metric=${metricNames.join(",")}`, { method: "GET" }, "threads.analytics", context);
117
- const allowed = new Set(metricNames);
147
+ const allowed = new Set([...metricNames, "link_total_values"]);
118
148
  return array(result["data"]).flatMap((entry) => {
119
149
  const row = object(entry);
150
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
120
151
  const name = typeof row["name"] === "string" ? row["name"] : undefined;
121
152
  if (name === undefined || !allowed.has(name))
122
153
  return [];
@@ -144,10 +175,11 @@ export function threads(options) {
144
175
  : "unknown";
145
176
  return [
146
177
  {
147
- name,
178
+ name: name === "link_total_values" ? "clicks" : name,
148
179
  value,
149
180
  unit: "count",
150
181
  period,
182
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
151
183
  ...(totalValue === undefined && latestEnd !== undefined ? { measuredAt: latestEnd } : {}),
152
184
  fetchedAt: now(),
153
185
  freshness: "unknown",
@@ -157,13 +189,25 @@ export function threads(options) {
157
189
  });
158
190
  }
159
191
  async function status(id, c) {
160
- return request(`${encodeURIComponent(id)}?fields=id,status,error_message`, { method: "GET", ...(c.signal ? { signal: c.signal } : {}) }, "threads.container.status", 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);
161
195
  }
162
- const base = (a, id, state, backendState) => ({
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
+ ({
163
206
  state,
164
207
  targetIndex: 0,
165
208
  account: a,
166
209
  observedAt: now(),
210
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
167
211
  ...(backendState ? { backendState } : {}),
168
212
  delivery: {
169
213
  kind: "delivery",
@@ -174,7 +218,9 @@ export function threads(options) {
174
218
  deliveryId: id,
175
219
  },
176
220
  });
177
- const terminalFailure = (a, id, state) => ({
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
+ ({
178
224
  ...base(a, id, "failed", state),
179
225
  code: "media_error",
180
226
  message: "Threads container failed or expired before publication.",
@@ -182,6 +228,7 @@ export function threads(options) {
182
228
  });
183
229
  async function resume(w, a, c) {
184
230
  if (w.nativeId)
231
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
185
232
  return {
186
233
  ...base(a, w.id, "published", "PUBLISHED"),
187
234
  post: platformPostRef({
@@ -192,6 +239,7 @@ export function threads(options) {
192
239
  }),
193
240
  };
194
241
  if (w.stage === "unknown")
242
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
195
243
  return {
196
244
  ...base(a, w.id, "unknown", w.backendState),
197
245
  reason: "ambiguous-submission",
@@ -207,10 +255,21 @@ export function threads(options) {
207
255
  ...(item["kind"] === "video"
208
256
  ? { video_url: String(item["url"]) }
209
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.
210
266
  ...(typeof item["altText"] === "string" ? { alt_text: item["altText"] } : {}),
211
267
  });
212
268
  await store.update(cur.id, { stage: "unknown", backendState: "CREATING_CHILD" });
213
- const child = await request(`${a.accountId}/threads?${p}`, { method: "POST", ...(c.signal ? { signal: c.signal } : {}) }, "threads.container.child", c);
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.
214
273
  if (typeof child["id"] !== "string")
215
274
  fail("threads.container.child", "Threads did not return a child container ID.");
216
275
  cur = await store.update(cur.id, {
@@ -220,6 +279,7 @@ export function threads(options) {
220
279
  }
221
280
  for (const id of cur.childIds) {
222
281
  const s = await status(id, c);
282
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
223
283
  const st = typeof s["status"] === "string" ? s["status"] : "";
224
284
  if (st === "ERROR" || st === "EXPIRED")
225
285
  return terminalFailure(a, w.id, st);
@@ -237,19 +297,59 @@ export function threads(options) {
237
297
  : singleKind === "image"
238
298
  ? "IMAGE"
239
299
  : "TEXT",
300
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
240
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.
241
310
  ...(typeof singleUrl === "string"
242
311
  ? singleKind === "video"
243
312
  ? { video_url: singleUrl }
244
313
  : { image_url: singleUrl }
245
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.
246
323
  ...(typeof cur.options["_altText"] === "string"
247
324
  ? { alt_text: cur.options["_altText"] }
248
325
  : {}),
326
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
249
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.
250
336
  ...(typeof cur.options["replyControl"] === "string"
251
- ? { reply_control: cur.options["replyControl"] }
337
+ ? {
338
+ reply_control: cur.options["replyControl"] === "accountsYouFollow"
339
+ ? "accounts_you_follow"
340
+ : cur.options["replyControl"] === "mentionedOnly"
341
+ ? "mentioned_only"
342
+ : "everyone",
343
+ }
252
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.
253
353
  ...(typeof cur.options["_replyToId"] === "string"
254
354
  ? { reply_to_id: cur.options["_replyToId"] }
255
355
  : {}),
@@ -257,8 +357,11 @@ export function threads(options) {
257
357
  // Persist ambiguity before dispatch so a process crash cannot recreate a
258
358
  // possibly accepted container under the same continuation handle.
259
359
  await store.update(cur.id, { stage: "unknown", backendState: "CREATING_PARENT" });
260
- const parent = await request(`${a.accountId}/threads?${p}`, { method: "POST", ...(c.signal ? { signal: c.signal } : {}) }, "threads.container.create", c);
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);
261
363
  const parentId = parent["id"];
364
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
262
365
  if (typeof parentId !== "string")
263
366
  fail("threads.container.create", "Threads did not return a container ID.");
264
367
  cur = await store.update(cur.id, { parentId, stage: "parent" });
@@ -267,11 +370,13 @@ export function threads(options) {
267
370
  if (!parentId)
268
371
  fail("threads.container.create", "Missing parent container ID.");
269
372
  const ps = await status(parentId, c);
373
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
270
374
  const pst = typeof ps["status"] === "string" ? ps["status"] : "";
271
375
  if (pst === "ERROR" || pst === "EXPIRED")
272
376
  return terminalFailure(a, cur.id, pst);
273
377
  if (pst === "PUBLISHED") {
274
378
  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.
275
380
  return {
276
381
  ...base(a, cur.id, "unknown", pst),
277
382
  reason: "ambiguous-submission",
@@ -281,10 +386,14 @@ export function threads(options) {
281
386
  return base(a, cur.id, "processing", pst || "PROCESSING");
282
387
  await store.update(cur.id, { stage: "unknown", backendState: "PUBLISHING" });
283
388
  try {
284
- const pub = await request(`${a.accountId}/threads_publish?creation_id=${encodeURIComponent(parentId)}`, { method: "POST", ...(c.signal ? { signal: c.signal } : {}) }, "threads.publish", c);
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);
285
392
  const nativeId = pub["id"];
393
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
286
394
  if (typeof nativeId !== "string") {
287
395
  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.
288
397
  return {
289
398
  ...base(a, cur.id, "unknown", "MISSING_ID"),
290
399
  reason: "ambiguous-submission",
@@ -292,6 +401,7 @@ export function threads(options) {
292
401
  };
293
402
  }
294
403
  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.
295
405
  return {
296
406
  ...base(a, cur.id, "published", "PUBLISHED"),
297
407
  post: platformPostRef({
@@ -305,6 +415,7 @@ export function threads(options) {
305
415
  catch (e) {
306
416
  await store.update(cur.id, { stage: "unknown", backendState: "AMBIGUOUS" });
307
417
  if (e instanceof SocialError)
418
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
308
419
  return {
309
420
  ...base(a, cur.id, "unknown", "AMBIGUOUS"),
310
421
  reason: "ambiguous-submission",
@@ -325,19 +436,93 @@ export function threads(options) {
325
436
  platform: "threads",
326
437
  availability: "available",
327
438
  formats: ["text", "image", "video", "carousel"],
439
+ requiredScopes: ["threads_basic", "threads_content_publish"],
328
440
  },
329
441
  { operation: "posts.read", platform: "threads", availability: "available" },
330
442
  { operation: "posts.list", platform: "threads", availability: "available" },
331
443
  { operation: "posts.status", platform: "threads", availability: "available" },
332
- { operation: "analytics.read", platform: "threads", availability: "available" },
333
- { operation: "comments.read", platform: "threads", availability: "available" },
334
- { operation: "comments.write", platform: "threads", availability: "available" },
335
- { operation: "posts.quote", platform: "threads", availability: "available" },
336
- { operation: "posts.repost", platform: "threads", availability: "available" },
337
- { operation: "posts.delete", platform: "threads", availability: "available" },
444
+ {
445
+ operation: "analytics.read",
446
+ platform: "threads",
447
+ availability: "available",
448
+ requiredScopes: ["threads_basic", "threads_manage_insights"],
449
+ },
450
+ {
451
+ operation: "analytics.account.read",
452
+ platform: "threads",
453
+ availability: "available",
454
+ requiredScopes: ["threads_basic", "threads_manage_insights"],
455
+ },
456
+ {
457
+ operation: "comments.read",
458
+ platform: "threads",
459
+ availability: "available",
460
+ requiredScopes: ["threads_basic", "threads_read_replies"],
461
+ },
462
+ {
463
+ operation: "comments.write",
464
+ platform: "threads",
465
+ availability: "available",
466
+ requiredScopes: ["threads_basic", "threads_manage_replies"],
467
+ },
468
+ {
469
+ operation: "comments.moderate",
470
+ platform: "threads",
471
+ availability: "available",
472
+ requiredScopes: ["threads_manage_replies"],
473
+ notes: "Hiding replies and pending-reply moderation require Threads reply-management permissions.",
474
+ },
475
+ {
476
+ operation: "posts.quote",
477
+ platform: "threads",
478
+ availability: "available",
479
+ requiredScopes: ["threads_basic", "threads_content_publish"],
480
+ },
481
+ {
482
+ operation: "posts.repost",
483
+ platform: "threads",
484
+ availability: "available",
485
+ requiredScopes: ["threads_basic", "threads_content_publish"],
486
+ },
487
+ {
488
+ operation: "posts.delete",
489
+ platform: "threads",
490
+ availability: "available",
491
+ requiredScopes: ["threads_basic", "threads_delete"],
492
+ },
493
+ {
494
+ operation: "posts.removeFromPlatform",
495
+ platform: "threads",
496
+ availability: "available",
497
+ requiredScopes: ["threads_basic", "threads_delete"],
498
+ },
338
499
  { operation: "profile.read", platform: "threads", availability: "available" },
339
- { operation: "search.keyword", platform: "threads", availability: "available" },
340
- { operation: "mentions.read", platform: "threads", availability: "available" },
500
+ {
501
+ operation: "search.keyword",
502
+ platform: "threads",
503
+ availability: "available",
504
+ requiredScopes: ["threads_basic", "threads_keyword_search"],
505
+ },
506
+ {
507
+ operation: "profiles.read",
508
+ platform: "threads",
509
+ availability: "available",
510
+ requiredScopes: ["threads_basic"],
511
+ notes: "The profile endpoint reads the authorized app-scoped user; profile lookup requires threads_profile_discovery.",
512
+ },
513
+ {
514
+ operation: "search.posts",
515
+ platform: "threads",
516
+ availability: "available",
517
+ requiredScopes: ["threads_basic", "threads_keyword_search"],
518
+ notes: "Without threads_keyword_search approval, results are limited to the authorized user's posts.",
519
+ },
520
+ {
521
+ operation: "mentions.read",
522
+ platform: "threads",
523
+ availability: "available",
524
+ requiredScopes: ["threads_basic", "threads_manage_mentions"],
525
+ },
341
526
  {
342
527
  operation: "messages.read",
343
528
  platform: "threads",
@@ -347,7 +532,9 @@ export function threads(options) {
347
532
  ],
348
533
  };
349
534
  const native = {
350
- getPost: (id, c) => request(`${encodeURIComponent(id)}?fields=id,text,username,media_type,permalink`, { method: "GET", ...(c.signal ? { signal: c.signal } : {}) }, "threads.posts.get", c),
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),
351
538
  getContainer: (id, c) => status(id, c),
352
539
  resumePublication: async (a, id, c) => {
353
540
  authorize(a, "threads.posts.resume");
@@ -365,27 +552,79 @@ export function threads(options) {
365
552
  },
366
553
  async quotePost({ account, postId, text, context }) {
367
554
  authorize(account, "threads.posts.quote");
368
- return request(`${encodeURIComponent(account.accountId)}/threads?media_type=TEXT&text=${encodeURIComponent(text)}&quote_id=${encodeURIComponent(postId)}`, { method: "POST" }, "threads.posts.quote", context);
555
+ const container = await request(`${encodeURIComponent(account.accountId)}/threads?media_type=TEXT&text=${encodeURIComponent(text)}&quote_post_id=${encodeURIComponent(postId)}`, { method: "POST" }, "threads.posts.quote", context);
556
+ const creationId = optionalString(container["id"]);
557
+ if (creationId === undefined)
558
+ fail("threads.posts.quote", "Threads did not return a quote container ID.");
559
+ return request(`${encodeURIComponent(account.accountId)}/threads_publish?creation_id=${encodeURIComponent(creationId)}`, { method: "POST" }, "threads.posts.quote", context);
369
560
  },
370
561
  async repost({ account, postId, context }) {
371
562
  authorize(account, "threads.posts.repost");
372
- return request(`${encodeURIComponent(account.accountId)}/threads?media_type=TEXT&repost_id=${encodeURIComponent(postId)}`, { method: "POST" }, "threads.posts.repost", context);
563
+ return request(`${encodeURIComponent(postId)}/repost`, { method: "POST" }, "threads.posts.repost", context);
373
564
  },
374
565
  async deletePost({ account, postId, context }) {
375
566
  authorize(account, "threads.posts.delete");
376
567
  await request(encodeURIComponent(postId), { method: "DELETE" }, "threads.posts.delete", context);
377
568
  },
378
- async search({ account, query, cursor, context }) {
569
+ async search({ account, query, cursor, searchType, searchMode, mediaType, since, until, limit, authorUsername, context, }) {
379
570
  authorize(account, "threads.search");
380
- return request(`keyword_search?query=${encodeURIComponent(query)}${cursor ? `&after=${encodeURIComponent(cursor)}` : ""}`, { method: "GET" }, "threads.search", context);
571
+ const q = query.trim();
572
+ if (!q)
573
+ fail("threads.search", "A search query is required.", "invalid_input");
574
+ if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1 || limit > 100))
575
+ fail("threads.search", "Search limit must be an integer from 1 through 100.", "invalid_input");
576
+ const params = new URLSearchParams({
577
+ q,
578
+ fields: "id,text,media_type,media_url,permalink,timestamp,username,shortcode,is_quote_post",
579
+ });
580
+ if (searchType !== undefined)
581
+ params.set("search_type", searchType);
582
+ if (searchMode !== undefined)
583
+ params.set("search_mode", searchMode);
584
+ if (mediaType !== undefined)
585
+ params.set("media_type", mediaType);
586
+ if (since !== undefined)
587
+ params.set("since", since);
588
+ if (until !== undefined)
589
+ params.set("until", until);
590
+ if (limit !== undefined)
591
+ params.set("limit", String(limit));
592
+ if (authorUsername !== undefined)
593
+ params.set("author_username", authorUsername);
594
+ if (cursor !== undefined)
595
+ params.set("after", cursor);
596
+ return request(`keyword_search?${params.toString()}`, { method: "GET" }, "threads.search", context);
381
597
  },
382
598
  async mentions({ account, cursor, context }) {
383
599
  authorize(account, "threads.mentions");
384
- return request(`${encodeURIComponent(account.accountId)}/mentions${cursor ? `?after=${encodeURIComponent(cursor)}` : ""}`, { method: "GET" }, "threads.mentions", context);
600
+ return request(`${encodeURIComponent(account.accountId)}/mentions?fields=id,text,username,media_type,media_url,permalink,timestamp${cursor ? `&after=${encodeURIComponent(cursor)}` : ""}`, { method: "GET" }, "threads.mentions", context);
385
601
  },
386
602
  async getProfile({ account, context }) {
387
603
  authorize(account, "threads.profile.read");
388
- return request(`${encodeURIComponent(account.accountId)}?fields=id,username,name,threads_profile_picture_url`, { method: "GET" }, "threads.profile.read", context);
604
+ return request(`${encodeURIComponent(account.accountId)}?fields=id,username,name,threads_profile_picture_url,threads_biography,is_verified`, { method: "GET" }, "threads.profile.read", context);
605
+ },
606
+ async hideReply({ account, replyId, hide, context }) {
607
+ authorize(account, "threads.comments.moderate");
608
+ return request(`${encodeURIComponent(replyId)}/manage_reply?hide=${String(hide)}`, { method: "POST" }, "threads.comments.moderate", context);
609
+ },
610
+ async listConversation({ account, mediaId, cursor, context }) {
611
+ authorize(account, "threads.comments.conversation");
612
+ 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);
613
+ },
614
+ async listPendingReplies({ account, mediaId, cursor, approvalStatus, context }) {
615
+ authorize(account, "threads.comments.pending");
616
+ const params = new URLSearchParams({
617
+ fields: "id,text,username,timestamp,is_reply,hide_status,reply_approval_status",
618
+ });
619
+ if (cursor !== undefined)
620
+ params.set("after", cursor);
621
+ if (approvalStatus !== undefined)
622
+ params.set("approval_status", approvalStatus);
623
+ return request(`${encodeURIComponent(mediaId)}/pending_replies?${params.toString()}`, { method: "GET" }, "threads.comments.pending", context);
624
+ },
625
+ async managePendingReply({ account, replyId, approve, context }) {
626
+ authorize(account, "threads.comments.moderate");
627
+ return request(`${encodeURIComponent(replyId)}/manage_pending_reply?approve=${String(approve)}`, { method: "POST" }, "threads.comments.moderate", context);
389
628
  },
390
629
  };
391
630
  const adapter = defineAdapter({
@@ -399,6 +638,72 @@ export function threads(options) {
399
638
  return readAccount(context);
400
639
  },
401
640
  },
641
+ graph: {
642
+ async getProfile(account, input, context) {
643
+ authorize(account, "profiles.read");
644
+ let result;
645
+ 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);
647
+ }
648
+ else {
649
+ const profileId = input.profileId ?? account.accountId;
650
+ if (profileId !== account.accountId)
651
+ fail("profiles.read", "Threads profile reads are limited to the authorized app-scoped user.", "unauthorized");
652
+ result = await request(`${encodeURIComponent(profileId)}?fields=id,username,name,threads_profile_picture_url,threads_biography,is_verified`, { method: "GET" }, "profiles.read", context);
653
+ }
654
+ const returnedProfileId = optionalString(result["id"]);
655
+ const profileId = returnedProfileId ??
656
+ input.profileId ??
657
+ (input.handle ? `lookup:${input.handle}` : undefined);
658
+ if (profileId === undefined)
659
+ fail("profiles.read", "Threads profile response did not return a profile ID.");
660
+ const handle = optionalString(result["username"]) ?? input.handle;
661
+ const displayName = optionalString(result["name"]);
662
+ const avatarUrl = optionalString(result["profile_picture_url"]) ??
663
+ optionalString(result["threads_profile_picture_url"]);
664
+ const bio = optionalString(result["biography"]) ?? optionalString(result["threads_biography"]);
665
+ return {
666
+ ref: profileRef({
667
+ backend,
668
+ platform: "threads",
669
+ accountId: account.accountId,
670
+ profileId,
671
+ }),
672
+ ...(displayName === undefined ? {} : { displayName }),
673
+ ...(handle === undefined ? {} : { handle }),
674
+ ...(avatarUrl === undefined ? {} : { avatarUrl }),
675
+ ...(bio === undefined ? {} : { bio }),
676
+ native: returnedProfileId === undefined ? { ...result, _profileIdUnavailable: true } : result,
677
+ };
678
+ },
679
+ },
680
+ search: {
681
+ async posts(account, input, context) {
682
+ authorize(account, "search.posts");
683
+ if (input.scope === "all")
684
+ fail("search.posts", "Threads search does not support scope 'all'.", "invalid_input");
685
+ const query = input.query.trim();
686
+ if (!query)
687
+ fail("search.posts", "A search query is required.", "invalid_input");
688
+ const params = new URLSearchParams({
689
+ q: query,
690
+ fields: "id,text,media_type,media_url,permalink,timestamp,username,shortcode,is_quote_post",
691
+ });
692
+ if (input.limit !== undefined) {
693
+ if (!Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 100)
694
+ fail("search.posts", "Search limit must be an integer from 1 through 100.", "invalid_input");
695
+ params.set("limit", String(input.limit));
696
+ }
697
+ if (input.cursor !== undefined)
698
+ params.set("after", input.cursor);
699
+ if (input.startTime !== undefined)
700
+ params.set("since", input.startTime);
701
+ if (input.endTime !== undefined)
702
+ params.set("until", input.endTime);
703
+ const result = await request(`keyword_search?${params.toString()}`, { method: "GET" }, "search.posts", context);
704
+ return pageFrom(result);
705
+ },
706
+ },
402
707
  posts: {
403
708
  list: listPosts,
404
709
  prepareTarget(target) {
@@ -447,26 +752,33 @@ export function threads(options) {
447
752
  add("schedule.unsupported", "Threads scheduling requires an application-owned runner.");
448
753
  return issues;
449
754
  },
450
- async publishTarget(target) {
755
+ async publishTarget(target, context) {
451
756
  authorize(target.account, "threads.posts.publish");
452
757
  const media = target.content.media ?? [];
453
758
  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.
454
760
  const opts = {
761
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
455
762
  ...object(target.options ?? {}),
763
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
456
764
  ...(target.replyTo ? { _replyToId: target.replyTo.postId } : {}),
765
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
457
766
  ...(media.length > 1
458
767
  ? {
459
768
  _mediaItems: media.map((entry) => ({
460
769
  kind: entry.kind,
461
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.
462
772
  ...(entry.altText ? { altText: entry.altText } : {}),
463
773
  })),
464
774
  }
465
775
  : {}),
776
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
466
777
  ...(item?.source.kind === "https-url"
467
778
  ? {
468
779
  _mediaKind: item.kind,
469
780
  _mediaUrl: item.source.url,
781
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
470
782
  ...(item.altText ? { _altText: item.altText } : {}),
471
783
  }
472
784
  : {}),
@@ -479,21 +791,48 @@ export function threads(options) {
479
791
  options: opts,
480
792
  stage: "children",
481
793
  });
482
- return {
483
- ...base(target.account, w.id, "processing", "CREATED"),
484
- targetIndex: target.targetIndex,
485
- };
794
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
795
+ if (!(await store.claim(w.id)))
796
+ return base(target.account, w.id, "processing", "CLAIMED");
797
+ try {
798
+ try {
799
+ const outcome = await resume((await store.get(w.id)) ?? w, target.account, context);
800
+ return { ...outcome, targetIndex: target.targetIndex };
801
+ }
802
+ catch (error) {
803
+ if (error instanceof SocialError && error.code === "ambiguous_outcome")
804
+ return {
805
+ ...base(target.account, w.id, "unknown", "AMBIGUOUS"),
806
+ targetIndex: target.targetIndex,
807
+ reason: "ambiguous-submission",
808
+ diagnostic: error.message,
809
+ };
810
+ throw error;
811
+ }
812
+ }
813
+ finally {
814
+ await store.release?.(w.id);
815
+ }
486
816
  },
487
817
  async get(ref, c) {
488
818
  authorize(ref, "threads.posts.get");
489
819
  return native.getPost(ref.postId, c);
490
820
  },
491
- async getDelivery(ref, _c) {
821
+ async removeFromPlatform(ref, c) {
822
+ authorize(ref, "threads.posts.delete");
823
+ return native.deletePost({
824
+ account: accountRef(backend, ref.accountId),
825
+ postId: ref.postId,
826
+ context: c,
827
+ });
828
+ },
829
+ async getDelivery(ref, c) {
492
830
  authorize(ref, "threads.posts.status");
493
831
  const w = await store.get(ref.deliveryId);
494
832
  if (!w || w.backend !== backend || w.accountId !== ref.accountId)
495
833
  fail("threads.posts.status", "Workflow handle is not authorized for this account.", "unauthorized");
496
834
  if (w.nativeId)
835
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
497
836
  return {
498
837
  ...base(accountRef(backend, ref.accountId), w.id, "published", "PUBLISHED"),
499
838
  post: platformPostRef({
@@ -504,25 +843,54 @@ export function threads(options) {
504
843
  }),
505
844
  };
506
845
  if (w.stage === "unknown")
846
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
507
847
  return {
508
848
  ...base(accountRef(backend, ref.accountId), w.id, "unknown", w.backendState),
509
849
  reason: "ambiguous-submission",
510
850
  diagnostic: "Explicit resumePublication is required.",
511
851
  };
512
- return base(accountRef(backend, ref.accountId), w.id, "processing", w.stage.toUpperCase());
852
+ if (!(await store.claim(w.id)))
853
+ return base(accountRef(backend, ref.accountId), w.id, "processing", "CLAIMED");
854
+ try {
855
+ return await resume((await store.get(w.id)) ?? w, accountRef(backend, ref.accountId), c);
856
+ }
857
+ finally {
858
+ await store.release?.(w.id);
859
+ }
513
860
  },
514
861
  },
515
862
  comments: {
516
863
  async list(post, input, context) {
517
864
  authorize(post, "threads.comments.list");
518
865
  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.
519
867
  const rows = Array.isArray(result["data"]) ? result["data"] : [];
520
868
  return {
521
869
  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.
522
878
  ...(typeof result["paging"] === "object" &&
523
879
  result["paging"] &&
524
- typeof result["paging"]["next"] === "string"
525
- ? { nextCursor: String(result["paging"]["next"]) }
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
+ }
526
894
  : {}),
527
895
  };
528
896
  },
@@ -530,25 +898,32 @@ export function threads(options) {
530
898
  authorize(comment, "threads.comments.reply");
531
899
  if (!content.text.trim())
532
900
  fail("threads.comments.reply", "Comment text is required.", "invalid_input");
533
- const result = await request(`${encodeURIComponent(comment.postId)}/replies`, { method: "POST" }, "threads.comments.reply", context);
534
- return {
535
- ...comment,
536
- commentId: typeof result["id"] === "string" ? result["id"] : comment.commentId,
537
- };
901
+ const container = await request(`${encodeURIComponent(comment.accountId)}/threads?media_type=TEXT&text=${encodeURIComponent(content.text)}&reply_to_id=${encodeURIComponent(comment.commentId)}`, { method: "POST" }, "threads.comments.reply", context);
902
+ const creationId = optionalString(container["id"]);
903
+ if (creationId === undefined)
904
+ fail("threads.comments.reply", "Threads did not return a reply container ID.");
905
+ const result = await request(`${encodeURIComponent(comment.accountId)}/threads_publish?creation_id=${encodeURIComponent(creationId)}`, { method: "POST" }, "threads.comments.reply", context);
906
+ return { ...comment, commentId: optionalString(result["id"]) ?? comment.commentId };
538
907
  },
539
908
  },
540
909
  analytics: {
541
910
  getAccountMetrics,
542
911
  async getPostMetrics(post, c) {
543
912
  authorize(post, "threads.analytics");
544
- const r = await request(`${encodeURIComponent(post.postId)}/insights?metric=views,likes,replies,reposts,quotes,shares`, { method: "GET", ...(c.signal ? { signal: c.signal } : {}) }, "threads.analytics", c);
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);
545
916
  return array(r["data"]).flatMap((x) => {
546
917
  const row = object(x);
547
918
  const first = Array.isArray(row["values"]) ? row["values"][0] : undefined;
548
- const value = first && typeof first === "object"
919
+ const value =
920
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
921
+ first && typeof first === "object"
549
922
  ? optionalNumber(object(first)["value"])
550
923
  : optionalNumber(row["value"]);
551
- const name = row["name"];
924
+ const rawName = row["name"];
925
+ const name = rawName === "link_total_values" ? "clicks" : rawName;
926
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
552
927
  return value === undefined || typeof name !== "string"
553
928
  ? []
554
929
  : [