@opencoredev/social-sdk 0.1.2 → 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 (154) hide show
  1. package/README.md +6 -0
  2. package/dist/cli.d.ts +14 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +235 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/cloud/common.d.ts +26 -0
  7. package/dist/cloud/common.d.ts.map +1 -0
  8. package/dist/cloud/common.js +335 -0
  9. package/dist/cloud/common.js.map +1 -0
  10. package/dist/cloud/lifecycle.d.ts +11 -0
  11. package/dist/cloud/lifecycle.d.ts.map +1 -0
  12. package/dist/cloud/lifecycle.js +113 -0
  13. package/dist/cloud/lifecycle.js.map +1 -0
  14. package/dist/cloud/media.d.ts +24 -0
  15. package/dist/cloud/media.d.ts.map +1 -0
  16. package/dist/cloud/media.js +101 -0
  17. package/dist/cloud/media.js.map +1 -0
  18. package/dist/cloud/outcomes.d.ts +10 -0
  19. package/dist/cloud/outcomes.d.ts.map +1 -0
  20. package/dist/cloud/outcomes.js +196 -0
  21. package/dist/cloud/outcomes.js.map +1 -0
  22. package/dist/cloud/post-for-me.d.ts +70 -0
  23. package/dist/cloud/post-for-me.d.ts.map +1 -0
  24. package/dist/cloud/post-for-me.js +397 -0
  25. package/dist/cloud/post-for-me.js.map +1 -0
  26. package/dist/cloud/zernio.d.ts +112 -0
  27. package/dist/cloud/zernio.d.ts.map +1 -0
  28. package/dist/cloud/zernio.js +633 -0
  29. package/dist/cloud/zernio.js.map +1 -0
  30. package/dist/core/adapter.d.ts +159 -0
  31. package/dist/core/adapter.d.ts.map +1 -0
  32. package/dist/core/adapter.js +4 -0
  33. package/dist/core/adapter.js.map +1 -0
  34. package/dist/core/client.d.ts +142 -0
  35. package/dist/core/client.d.ts.map +1 -0
  36. package/dist/core/client.js +1286 -0
  37. package/dist/core/client.js.map +1 -0
  38. package/dist/core/concurrency.d.ts +10 -0
  39. package/dist/core/concurrency.d.ts.map +1 -0
  40. package/dist/core/concurrency.js +80 -0
  41. package/dist/core/concurrency.js.map +1 -0
  42. package/dist/core/errors.d.ts +45 -0
  43. package/dist/core/errors.d.ts.map +1 -0
  44. package/dist/core/errors.js +51 -0
  45. package/dist/core/errors.js.map +1 -0
  46. package/dist/core/idempotency.d.ts +34 -0
  47. package/dist/core/idempotency.d.ts.map +1 -0
  48. package/dist/core/idempotency.js +59 -0
  49. package/dist/core/idempotency.js.map +1 -0
  50. package/dist/core/index.d.ts +7 -0
  51. package/dist/core/index.d.ts.map +1 -0
  52. package/dist/core/index.js +7 -0
  53. package/dist/core/index.js.map +1 -0
  54. package/dist/core/pagination.d.ts +12 -0
  55. package/dist/core/pagination.d.ts.map +1 -0
  56. package/dist/core/pagination.js +82 -0
  57. package/dist/core/pagination.js.map +1 -0
  58. package/dist/core/types.d.ts +397 -0
  59. package/dist/core/types.d.ts.map +1 -0
  60. package/dist/core/types.js +17 -0
  61. package/dist/core/types.js.map +1 -0
  62. package/dist/index.d.ts +2 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +2 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/platforms/bluesky.d.ts +262 -0
  67. package/dist/platforms/bluesky.d.ts.map +1 -0
  68. package/dist/platforms/bluesky.js +1777 -0
  69. package/dist/platforms/bluesky.js.map +1 -0
  70. package/dist/platforms/instagram.d.ts +130 -0
  71. package/dist/platforms/instagram.d.ts.map +1 -0
  72. package/dist/platforms/instagram.js +1032 -0
  73. package/dist/platforms/instagram.js.map +1 -0
  74. package/dist/platforms/linkedin.d.ts +109 -0
  75. package/dist/platforms/linkedin.d.ts.map +1 -0
  76. package/dist/platforms/linkedin.js +891 -0
  77. package/dist/platforms/linkedin.js.map +1 -0
  78. package/dist/platforms/threads.d.ts +135 -0
  79. package/dist/platforms/threads.d.ts.map +1 -0
  80. package/dist/platforms/threads.js +946 -0
  81. package/dist/platforms/threads.js.map +1 -0
  82. package/dist/platforms/tiktok.d.ts +32 -0
  83. package/dist/platforms/tiktok.d.ts.map +1 -0
  84. package/dist/platforms/tiktok.js +594 -0
  85. package/dist/platforms/tiktok.js.map +1 -0
  86. package/dist/platforms/x-engagement.d.ts +17 -0
  87. package/dist/platforms/x-engagement.d.ts.map +1 -0
  88. package/dist/platforms/x-engagement.js +51 -0
  89. package/dist/platforms/x-engagement.js.map +1 -0
  90. package/dist/platforms/x.d.ts +300 -0
  91. package/dist/platforms/x.d.ts.map +1 -0
  92. package/dist/platforms/x.js +1288 -0
  93. package/dist/platforms/x.js.map +1 -0
  94. package/dist/platforms/youtube-upload.d.ts +33 -0
  95. package/dist/platforms/youtube-upload.d.ts.map +1 -0
  96. package/dist/platforms/youtube-upload.js +297 -0
  97. package/dist/platforms/youtube-upload.js.map +1 -0
  98. package/dist/platforms/youtube.d.ts +109 -0
  99. package/dist/platforms/youtube.d.ts.map +1 -0
  100. package/dist/platforms/youtube.js +1101 -0
  101. package/dist/platforms/youtube.js.map +1 -0
  102. package/dist/server/connections.d.ts +124 -0
  103. package/dist/server/connections.d.ts.map +1 -0
  104. package/dist/server/connections.js +336 -0
  105. package/dist/server/connections.js.map +1 -0
  106. package/dist/server/credentials.d.ts +52 -0
  107. package/dist/server/credentials.d.ts.map +1 -0
  108. package/dist/server/credentials.js +109 -0
  109. package/dist/server/credentials.js.map +1 -0
  110. package/dist/server/index.d.ts +5 -0
  111. package/dist/server/index.d.ts.map +1 -0
  112. package/dist/server/index.js +5 -0
  113. package/dist/server/index.js.map +1 -0
  114. package/dist/server/oauth.d.ts +53 -0
  115. package/dist/server/oauth.d.ts.map +1 -0
  116. package/dist/server/oauth.js +554 -0
  117. package/dist/server/oauth.js.map +1 -0
  118. package/dist/server/webhooks.d.ts +58 -0
  119. package/dist/server/webhooks.d.ts.map +1 -0
  120. package/dist/server/webhooks.js +205 -0
  121. package/dist/server/webhooks.js.map +1 -0
  122. package/dist/testing/index.d.ts +47 -0
  123. package/dist/testing/index.d.ts.map +1 -0
  124. package/dist/testing/index.js +565 -0
  125. package/dist/testing/index.js.map +1 -0
  126. package/dist/testing.d.ts +2 -0
  127. package/dist/testing.d.ts.map +1 -0
  128. package/dist/testing.js +2 -0
  129. package/dist/testing.js.map +1 -0
  130. package/dist/transport/binary.d.ts +3 -0
  131. package/dist/transport/binary.d.ts.map +1 -0
  132. package/dist/transport/binary.js +30 -0
  133. package/dist/transport/binary.js.map +1 -0
  134. package/dist/transport/budget.d.ts +4 -0
  135. package/dist/transport/budget.d.ts.map +1 -0
  136. package/dist/transport/budget.js +27 -0
  137. package/dist/transport/budget.js.map +1 -0
  138. package/dist/transport/http.d.ts +45 -0
  139. package/dist/transport/http.d.ts.map +1 -0
  140. package/dist/transport/http.js +260 -0
  141. package/dist/transport/http.js.map +1 -0
  142. package/dist/transport/json.d.ts +9 -0
  143. package/dist/transport/json.d.ts.map +1 -0
  144. package/dist/transport/json.js +17 -0
  145. package/dist/transport/json.js.map +1 -0
  146. package/dist/transport/upload.d.ts +26 -0
  147. package/dist/transport/upload.d.ts.map +1 -0
  148. package/dist/transport/upload.js +154 -0
  149. package/dist/transport/upload.js.map +1 -0
  150. package/dist/transport/validation.d.ts +6 -0
  151. package/dist/transport/validation.d.ts.map +1 -0
  152. package/dist/transport/validation.js +25 -0
  153. package/dist/transport/validation.js.map +1 -0
  154. package/package.json +2 -1
@@ -0,0 +1,946 @@
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";
3
+ import { SocialError } from "../core/errors.js";
4
+ import { managedHttp, publicFields } from "../cloud/common.js";
5
+ import { object, array, optionalNumber, optionalString } from "../transport/validation.js";
6
+ import { httpsUrl } from "../transport/upload.js";
7
+ export class MemoryThreadsWorkflowStore {
8
+ rows = new Map();
9
+ claims = new Set();
10
+ async create(input) {
11
+ const row = { ...input, id: `thwf_${globalThis.crypto.randomUUID()}` };
12
+ this.rows.set(row.id, structuredClone(row));
13
+ return structuredClone(row);
14
+ }
15
+ async get(id) {
16
+ const row = this.rows.get(id);
17
+ return row ? structuredClone(row) : undefined;
18
+ }
19
+ async update(id, update) {
20
+ const row = this.rows.get(id);
21
+ if (!row)
22
+ throw new Error("Threads workflow not found");
23
+ const next = { ...row, ...update, id };
24
+ this.rows.set(id, structuredClone(next));
25
+ return structuredClone(next);
26
+ }
27
+ async claim(id) {
28
+ if (!this.rows.has(id) || this.claims.has(id))
29
+ return false;
30
+ this.claims.add(id);
31
+ return true;
32
+ }
33
+ async release(id) {
34
+ this.claims.delete(id);
35
+ }
36
+ }
37
+ function fail(operation, message, code = "upstream_failure") {
38
+ throw new SocialError({
39
+ code,
40
+ operation,
41
+ message,
42
+ retryDisposition: { kind: code === "invalid_input" ? "never" : "reconcile-first" },
43
+ });
44
+ }
45
+ const accountRef = (backend, accountId) => connectedAccountRef({ backend, platform: "threads", accountId });
46
+ export function threads(options) {
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();
48
+ const http = managedHttp(`https://graph.threads.net/${version}`, {
49
+ 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
+ });
53
+ async function request(path, init, operation, context) {
54
+ const [raw = "", qs] = path.split("?", 2);
55
+ const query = Object.fromEntries(new URLSearchParams(qs ?? ""));
56
+ try {
57
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
58
+ return object(await http(`/${raw.replace(/^\//, "")}`, context, init.method === "POST" ? {} : undefined, query, init.method === "POST" || init.method === "DELETE" || init.method === "PUT"
59
+ ? init.method
60
+ : "GET"));
61
+ }
62
+ catch (e) {
63
+ if (e instanceof SocialError)
64
+ throw e;
65
+ fail(operation, "Threads API request failed.");
66
+ }
67
+ }
68
+ function authorize(ref, op) {
69
+ if (ref.backend !== backend || ref.platform !== "threads" || ref.accountId !== auth.userId)
70
+ fail(op, "Account reference does not match this Threads authorization.", "unauthorized");
71
+ }
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
+ }
86
+ async function readAccount(context) {
87
+ const v = await request("/me?fields=id,username,threads_profile_picture_url,threads_biography", { method: "GET" }, "threads.accounts.read", context);
88
+ 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)
91
+ fail("threads.accounts.read", "Threads returned an unauthorized account.", "unauthorized");
92
+ return {
93
+ 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 } : {}),
105
+ status: "connected",
106
+ };
107
+ }
108
+ async function listPosts(selected, input, context) {
109
+ authorize(selected, "threads.posts.list");
110
+ if (input.limit !== undefined &&
111
+ (!Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 100))
112
+ fail("threads.posts.list", "Threads feed limit must be an integer from 1 through 100.", "invalid_input");
113
+ const query = new URLSearchParams({
114
+ fields: "id,text,username,media_type,permalink,timestamp",
115
+ });
116
+ if (input.cursor !== undefined)
117
+ query.set("after", input.cursor);
118
+ if (input.limit !== undefined)
119
+ query.set("limit", String(input.limit));
120
+ const result = await request(`${encodeURIComponent(selected.accountId)}/threads?${query.toString()}`, { method: "GET" }, "threads.posts.list", context);
121
+ const items = array(result["data"]).map((entry) => publicFields(entry, ["id", "text", "username", "media_type", "permalink", "timestamp"]));
122
+ const paging = result["paging"] === undefined ? {} : object(result["paging"]);
123
+ 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;
129
+ return {
130
+ items,
131
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
132
+ ...(nextCursor === undefined ? {} : { nextCursor }),
133
+ };
134
+ }
135
+ async function getAccountMetrics(selected, context) {
136
+ authorize(selected, "threads.analytics");
137
+ const metricNames = [
138
+ "views",
139
+ "likes",
140
+ "replies",
141
+ "reposts",
142
+ "quotes",
143
+ "clicks",
144
+ "followers_count",
145
+ ];
146
+ const result = await request(`${encodeURIComponent(selected.accountId)}/threads_insights?metric=${metricNames.join(",")}`, { method: "GET" }, "threads.analytics", context);
147
+ const allowed = new Set([...metricNames, "link_total_values"]);
148
+ return array(result["data"]).flatMap((entry) => {
149
+ 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;
152
+ if (name === undefined || !allowed.has(name))
153
+ return [];
154
+ const total = row["total_value"] === undefined ? undefined : object(row["total_value"]);
155
+ const values = row["values"] === undefined ? [] : array(row["values"]);
156
+ const latest = values.at(-1);
157
+ const totalValue = optionalNumber(total?.["value"]);
158
+ const value = totalValue ?? (latest === undefined ? undefined : optionalNumber(object(latest)["value"]));
159
+ if (value === undefined)
160
+ return [];
161
+ const latestEnd = latest === undefined ? undefined : optionalString(object(latest)["end_time"]);
162
+ const totalIsLifetime = totalValue !== undefined && (row["period"] === "lifetime" || name === "followers_count");
163
+ const period = totalValue !== undefined
164
+ ? totalIsLifetime
165
+ ? "lifetime"
166
+ : "unknown"
167
+ : totalValue === undefined &&
168
+ row["period"] === "day" &&
169
+ latestEnd !== undefined &&
170
+ Number.isFinite(Date.parse(latestEnd))
171
+ ? {
172
+ from: new Date(Date.parse(latestEnd) - 86_400_000).toISOString(),
173
+ to: latestEnd,
174
+ }
175
+ : "unknown";
176
+ return [
177
+ {
178
+ name: name === "link_total_values" ? "clicks" : name,
179
+ value,
180
+ unit: "count",
181
+ 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 } : {}),
184
+ fetchedAt: now(),
185
+ freshness: "unknown",
186
+ source: `threads:${version}`,
187
+ },
188
+ ];
189
+ });
190
+ }
191
+ 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);
195
+ }
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: {
213
+ kind: "delivery",
214
+ version: 1,
215
+ backend: a.backend,
216
+ platform: "threads",
217
+ accountId: a.accountId,
218
+ deliveryId: id,
219
+ },
220
+ });
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),
225
+ code: "media_error",
226
+ message: "Threads container failed or expired before publication.",
227
+ retryDisposition: { kind: "never" },
228
+ });
229
+ async function resume(w, a, c) {
230
+ 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
+ };
241
+ 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
+ };
248
+ let cur = w;
249
+ const items = Array.isArray(cur.options["_mediaItems"]) ? cur.options["_mediaItems"] : [];
250
+ while (cur.childIds.length < items.length) {
251
+ const item = object(items[cur.childIds.length]);
252
+ const p = new URLSearchParams({
253
+ media_type: item["kind"] === "video" ? "VIDEO" : "IMAGE",
254
+ 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"] } : {}),
267
+ });
268
+ 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")
274
+ fail("threads.container.child", "Threads did not return a child container ID.");
275
+ cur = await store.update(cur.id, {
276
+ childIds: [...cur.childIds, child["id"]],
277
+ stage: "children",
278
+ });
279
+ }
280
+ for (const id of cur.childIds) {
281
+ 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"] : "";
284
+ if (st === "ERROR" || st === "EXPIRED")
285
+ return terminalFailure(a, w.id, st);
286
+ if (st !== "FINISHED" && st !== "PUBLISHED")
287
+ return base(a, w.id, "processing", st || "PROCESSING");
288
+ }
289
+ if (!cur.parentId) {
290
+ const singleKind = cur.options["_mediaKind"];
291
+ const singleUrl = cur.options["_mediaUrl"];
292
+ 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
+ : {}),
356
+ });
357
+ // Persist ambiguity before dispatch so a process crash cannot recreate a
358
+ // possibly accepted container under the same continuation handle.
359
+ 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")
366
+ fail("threads.container.create", "Threads did not return a container ID.");
367
+ cur = await store.update(cur.id, { parentId, stage: "parent" });
368
+ }
369
+ const parentId = cur.parentId;
370
+ if (!parentId)
371
+ fail("threads.container.create", "Missing parent container ID.");
372
+ 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"] : "";
375
+ if (pst === "ERROR" || pst === "EXPIRED")
376
+ return terminalFailure(a, cur.id, pst);
377
+ if (pst === "PUBLISHED") {
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.
380
+ return {
381
+ ...base(a, cur.id, "unknown", pst),
382
+ reason: "ambiguous-submission",
383
+ };
384
+ }
385
+ if (pst !== "FINISHED")
386
+ return base(a, cur.id, "processing", pst || "PROCESSING");
387
+ await store.update(cur.id, { stage: "unknown", backendState: "PUBLISHING" });
388
+ 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") {
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.
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
+ };
402
+ }
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.
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
+ };
414
+ }
415
+ catch (e) {
416
+ await store.update(cur.id, { stage: "unknown", backendState: "AMBIGUOUS" });
417
+ 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
+ };
424
+ throw e;
425
+ }
426
+ }
427
+ const capabilities = {
428
+ schemaVersion: 1,
429
+ backend,
430
+ apiRevision: `Meta Threads Graph ${version}`,
431
+ runtime: ["node>=22.12", "bun"],
432
+ capabilities: [
433
+ { operation: "accounts.read", platform: "threads", availability: "available" },
434
+ {
435
+ operation: "posts.publish",
436
+ platform: "threads",
437
+ availability: "available",
438
+ formats: ["text", "image", "video", "carousel"],
439
+ requiredScopes: ["threads_basic", "threads_content_publish"],
440
+ },
441
+ { operation: "posts.read", platform: "threads", availability: "available" },
442
+ { operation: "posts.list", platform: "threads", availability: "available" },
443
+ { operation: "posts.status", 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
+ },
499
+ { operation: "profile.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
+ },
526
+ {
527
+ operation: "messages.read",
528
+ platform: "threads",
529
+ availability: "unsupported-by-platform",
530
+ notes: "Threads does not expose messaging APIs to standard apps.",
531
+ },
532
+ ],
533
+ };
534
+ 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),
538
+ getContainer: (id, c) => status(id, c),
539
+ resumePublication: async (a, id, c) => {
540
+ authorize(a, "threads.posts.resume");
541
+ const w = await store.get(id);
542
+ if (!w || w.backend !== backend || w.accountId !== a.accountId)
543
+ fail("threads.posts.resume", "Workflow handle is not authorized for this account.", "unauthorized");
544
+ if (!(await store.claim(id)))
545
+ return base(a, id, "processing", "CLAIMED");
546
+ try {
547
+ return await resume((await store.get(id)) ?? w, a, c);
548
+ }
549
+ finally {
550
+ await store.release?.(id);
551
+ }
552
+ },
553
+ async quotePost({ account, postId, text, context }) {
554
+ authorize(account, "threads.posts.quote");
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);
560
+ },
561
+ async repost({ account, postId, context }) {
562
+ authorize(account, "threads.posts.repost");
563
+ return request(`${encodeURIComponent(postId)}/repost`, { method: "POST" }, "threads.posts.repost", context);
564
+ },
565
+ async deletePost({ account, postId, context }) {
566
+ authorize(account, "threads.posts.delete");
567
+ await request(encodeURIComponent(postId), { method: "DELETE" }, "threads.posts.delete", context);
568
+ },
569
+ async search({ account, query, cursor, searchType, searchMode, mediaType, since, until, limit, authorUsername, context, }) {
570
+ authorize(account, "threads.search");
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);
597
+ },
598
+ async mentions({ account, cursor, context }) {
599
+ authorize(account, "threads.mentions");
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);
601
+ },
602
+ async getProfile({ account, context }) {
603
+ authorize(account, "threads.profile.read");
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);
628
+ },
629
+ };
630
+ const adapter = defineAdapter({
631
+ id: backend,
632
+ capabilities,
633
+ native,
634
+ accounts: {
635
+ list: async (_input, context) => ({ items: [await readAccount(context)] }),
636
+ get: async (ref, context) => {
637
+ authorize(ref, "threads.accounts.get");
638
+ return readAccount(context);
639
+ },
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
+ },
707
+ posts: {
708
+ list: listPosts,
709
+ prepareTarget(target) {
710
+ const issues = [];
711
+ const add = (code, message) => issues.push({ code, message, severity: "error", targetIndex: target.targetIndex });
712
+ authorize(target.account, "threads.posts.prepare");
713
+ const media = target.content.media ?? [];
714
+ if ((target.content.text?.length ?? 0) > 500)
715
+ add("text.too_long", "Threads text is limited to 500 characters.");
716
+ if (media.length > 10)
717
+ add("media.too_many", "Threads carousels support at most ten items.");
718
+ if (!media.length && !target.content.text)
719
+ add("content.empty", "Threads requires text or media.");
720
+ for (const item of media) {
721
+ if (item.source.kind !== "https-url")
722
+ add("media.url_required", "Threads requires public HTTPS media URLs.");
723
+ else
724
+ try {
725
+ httpsUrl(item.source.url);
726
+ }
727
+ catch {
728
+ add("media.url_invalid", "Use public HTTPS media without credentials or local hosts.");
729
+ }
730
+ if (item.kind === "image" &&
731
+ item.mimeType &&
732
+ item.mimeType !== "image/jpeg" &&
733
+ item.mimeType !== "image/png")
734
+ add("media.format", "Threads images must be JPEG or PNG.");
735
+ if (item.kind === "video" && item.mimeType && item.mimeType !== "video/mp4")
736
+ add("media.format", "Threads videos must be MP4.");
737
+ if (item.altText && item.altText.length > 1000)
738
+ add("media.alt_text", "Alt text is limited to 1,000 characters.");
739
+ }
740
+ if (target.options !== undefined) {
741
+ const o = object(target.options);
742
+ if (Object.keys(o).some((key) => key !== "replyControl"))
743
+ add("options.unmapped", "A supplied option has no Threads mapping.");
744
+ const replyControl = o["replyControl"];
745
+ if (replyControl !== undefined &&
746
+ replyControl !== "everyone" &&
747
+ replyControl !== "accountsYouFollow" &&
748
+ replyControl !== "mentionedOnly")
749
+ add("options.reply_control", "Unsupported Threads reply control.");
750
+ }
751
+ if (target.schedule)
752
+ add("schedule.unsupported", "Threads scheduling requires an application-owned runner.");
753
+ return issues;
754
+ },
755
+ async publishTarget(target, context) {
756
+ authorize(target.account, "threads.posts.publish");
757
+ const media = target.content.media ?? [];
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.
760
+ 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
+ : {}),
785
+ };
786
+ const w = await store.create({
787
+ backend,
788
+ accountId: target.account.accountId,
789
+ childIds: [],
790
+ caption: target.content.text ?? "",
791
+ options: opts,
792
+ stage: "children",
793
+ });
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
+ }
816
+ },
817
+ async get(ref, c) {
818
+ authorize(ref, "threads.posts.get");
819
+ return native.getPost(ref.postId, c);
820
+ },
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) {
830
+ authorize(ref, "threads.posts.status");
831
+ const w = await store.get(ref.deliveryId);
832
+ if (!w || w.backend !== backend || w.accountId !== ref.accountId)
833
+ fail("threads.posts.status", "Workflow handle is not authorized for this account.", "unauthorized");
834
+ 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
+ };
845
+ 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
+ };
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
+ }
860
+ },
861
+ },
862
+ comments: {
863
+ async list(post, input, context) {
864
+ authorize(post, "threads.comments.list");
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.
867
+ const rows = Array.isArray(result["data"]) ? result["data"] : [];
868
+ return {
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.
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
+ : {}),
895
+ };
896
+ },
897
+ async reply(comment, content, context) {
898
+ authorize(comment, "threads.comments.reply");
899
+ if (!content.text.trim())
900
+ fail("threads.comments.reply", "Comment text is required.", "invalid_input");
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 };
907
+ },
908
+ },
909
+ analytics: {
910
+ getAccountMetrics,
911
+ async getPostMetrics(post, c) {
912
+ 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);
916
+ return array(r["data"]).flatMap((x) => {
917
+ 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"
922
+ ? optionalNumber(object(first)["value"])
923
+ : optionalNumber(row["value"]);
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.
927
+ return value === undefined || typeof name !== "string"
928
+ ? []
929
+ : [
930
+ {
931
+ name,
932
+ value,
933
+ unit: "count",
934
+ period: "lifetime",
935
+ fetchedAt: now(),
936
+ freshness: "unknown",
937
+ source: `threads:${version}`,
938
+ },
939
+ ];
940
+ });
941
+ },
942
+ },
943
+ });
944
+ return adapter;
945
+ }
946
+ //# sourceMappingURL=threads.js.map