@opencoredev/social-sdk 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +4 -2
  2. package/package.json +1 -1
  3. package/dist/cli.d.ts +0 -14
  4. package/dist/cli.d.ts.map +0 -1
  5. package/dist/cli.js +0 -223
  6. package/dist/cli.js.map +0 -1
  7. package/dist/cloud/common.d.ts +0 -26
  8. package/dist/cloud/common.d.ts.map +0 -1
  9. package/dist/cloud/common.js +0 -307
  10. package/dist/cloud/common.js.map +0 -1
  11. package/dist/cloud/lifecycle.d.ts +0 -11
  12. package/dist/cloud/lifecycle.d.ts.map +0 -1
  13. package/dist/cloud/lifecycle.js +0 -108
  14. package/dist/cloud/lifecycle.js.map +0 -1
  15. package/dist/cloud/media.d.ts +0 -24
  16. package/dist/cloud/media.d.ts.map +0 -1
  17. package/dist/cloud/media.js +0 -97
  18. package/dist/cloud/media.js.map +0 -1
  19. package/dist/cloud/outcomes.d.ts +0 -10
  20. package/dist/cloud/outcomes.d.ts.map +0 -1
  21. package/dist/cloud/outcomes.js +0 -188
  22. package/dist/cloud/outcomes.js.map +0 -1
  23. package/dist/cloud/post-for-me.d.ts +0 -70
  24. package/dist/cloud/post-for-me.d.ts.map +0 -1
  25. package/dist/cloud/post-for-me.js +0 -326
  26. package/dist/cloud/post-for-me.js.map +0 -1
  27. package/dist/cloud/zernio.d.ts +0 -112
  28. package/dist/cloud/zernio.d.ts.map +0 -1
  29. package/dist/cloud/zernio.js +0 -542
  30. package/dist/cloud/zernio.js.map +0 -1
  31. package/dist/core/adapter.d.ts +0 -118
  32. package/dist/core/adapter.d.ts.map +0 -1
  33. package/dist/core/adapter.js +0 -4
  34. package/dist/core/adapter.js.map +0 -1
  35. package/dist/core/client.d.ts +0 -108
  36. package/dist/core/client.d.ts.map +0 -1
  37. package/dist/core/client.js +0 -893
  38. package/dist/core/client.js.map +0 -1
  39. package/dist/core/concurrency.d.ts +0 -10
  40. package/dist/core/concurrency.d.ts.map +0 -1
  41. package/dist/core/concurrency.js +0 -78
  42. package/dist/core/concurrency.js.map +0 -1
  43. package/dist/core/errors.d.ts +0 -45
  44. package/dist/core/errors.d.ts.map +0 -1
  45. package/dist/core/errors.js +0 -44
  46. package/dist/core/errors.js.map +0 -1
  47. package/dist/core/idempotency.d.ts +0 -34
  48. package/dist/core/idempotency.d.ts.map +0 -1
  49. package/dist/core/idempotency.js +0 -59
  50. package/dist/core/idempotency.js.map +0 -1
  51. package/dist/core/index.d.ts +0 -7
  52. package/dist/core/index.d.ts.map +0 -1
  53. package/dist/core/index.js +0 -7
  54. package/dist/core/index.js.map +0 -1
  55. package/dist/core/pagination.d.ts +0 -12
  56. package/dist/core/pagination.d.ts.map +0 -1
  57. package/dist/core/pagination.js +0 -81
  58. package/dist/core/pagination.js.map +0 -1
  59. package/dist/core/types.d.ts +0 -329
  60. package/dist/core/types.d.ts.map +0 -1
  61. package/dist/core/types.js +0 -7
  62. package/dist/core/types.js.map +0 -1
  63. package/dist/index.d.ts +0 -2
  64. package/dist/index.d.ts.map +0 -1
  65. package/dist/index.js +0 -2
  66. package/dist/index.js.map +0 -1
  67. package/dist/platforms/bluesky.d.ts +0 -132
  68. package/dist/platforms/bluesky.d.ts.map +0 -1
  69. package/dist/platforms/bluesky.js +0 -1049
  70. package/dist/platforms/bluesky.js.map +0 -1
  71. package/dist/platforms/instagram.d.ts +0 -63
  72. package/dist/platforms/instagram.d.ts.map +0 -1
  73. package/dist/platforms/instagram.js +0 -676
  74. package/dist/platforms/instagram.js.map +0 -1
  75. package/dist/platforms/linkedin.d.ts +0 -54
  76. package/dist/platforms/linkedin.d.ts.map +0 -1
  77. package/dist/platforms/linkedin.js +0 -580
  78. package/dist/platforms/linkedin.js.map +0 -1
  79. package/dist/platforms/threads.d.ts +0 -102
  80. package/dist/platforms/threads.d.ts.map +0 -1
  81. package/dist/platforms/threads.js +0 -571
  82. package/dist/platforms/threads.js.map +0 -1
  83. package/dist/platforms/tiktok.d.ts +0 -32
  84. package/dist/platforms/tiktok.d.ts.map +0 -1
  85. package/dist/platforms/tiktok.js +0 -511
  86. package/dist/platforms/tiktok.js.map +0 -1
  87. package/dist/platforms/x-engagement.d.ts +0 -17
  88. package/dist/platforms/x-engagement.d.ts.map +0 -1
  89. package/dist/platforms/x-engagement.js +0 -47
  90. package/dist/platforms/x-engagement.js.map +0 -1
  91. package/dist/platforms/x.d.ts +0 -82
  92. package/dist/platforms/x.d.ts.map +0 -1
  93. package/dist/platforms/x.js +0 -511
  94. package/dist/platforms/x.js.map +0 -1
  95. package/dist/platforms/youtube-upload.d.ts +0 -33
  96. package/dist/platforms/youtube-upload.d.ts.map +0 -1
  97. package/dist/platforms/youtube-upload.js +0 -281
  98. package/dist/platforms/youtube-upload.js.map +0 -1
  99. package/dist/platforms/youtube.d.ts +0 -55
  100. package/dist/platforms/youtube.d.ts.map +0 -1
  101. package/dist/platforms/youtube.js +0 -561
  102. package/dist/platforms/youtube.js.map +0 -1
  103. package/dist/server/connections.d.ts +0 -124
  104. package/dist/server/connections.d.ts.map +0 -1
  105. package/dist/server/connections.js +0 -334
  106. package/dist/server/connections.js.map +0 -1
  107. package/dist/server/credentials.d.ts +0 -52
  108. package/dist/server/credentials.d.ts.map +0 -1
  109. package/dist/server/credentials.js +0 -115
  110. package/dist/server/credentials.js.map +0 -1
  111. package/dist/server/index.d.ts +0 -5
  112. package/dist/server/index.d.ts.map +0 -1
  113. package/dist/server/index.js +0 -5
  114. package/dist/server/index.js.map +0 -1
  115. package/dist/server/oauth.d.ts +0 -48
  116. package/dist/server/oauth.d.ts.map +0 -1
  117. package/dist/server/oauth.js +0 -483
  118. package/dist/server/oauth.js.map +0 -1
  119. package/dist/server/webhooks.d.ts +0 -58
  120. package/dist/server/webhooks.d.ts.map +0 -1
  121. package/dist/server/webhooks.js +0 -198
  122. package/dist/server/webhooks.js.map +0 -1
  123. package/dist/testing/index.d.ts +0 -47
  124. package/dist/testing/index.d.ts.map +0 -1
  125. package/dist/testing/index.js +0 -413
  126. package/dist/testing/index.js.map +0 -1
  127. package/dist/testing.d.ts +0 -2
  128. package/dist/testing.d.ts.map +0 -1
  129. package/dist/testing.js +0 -2
  130. package/dist/testing.js.map +0 -1
  131. package/dist/transport/binary.d.ts +0 -3
  132. package/dist/transport/binary.d.ts.map +0 -1
  133. package/dist/transport/binary.js +0 -30
  134. package/dist/transport/binary.js.map +0 -1
  135. package/dist/transport/budget.d.ts +0 -4
  136. package/dist/transport/budget.d.ts.map +0 -1
  137. package/dist/transport/budget.js +0 -27
  138. package/dist/transport/budget.js.map +0 -1
  139. package/dist/transport/http.d.ts +0 -44
  140. package/dist/transport/http.d.ts.map +0 -1
  141. package/dist/transport/http.js +0 -230
  142. package/dist/transport/http.js.map +0 -1
  143. package/dist/transport/json.d.ts +0 -3
  144. package/dist/transport/json.d.ts.map +0 -1
  145. package/dist/transport/json.js +0 -11
  146. package/dist/transport/json.js.map +0 -1
  147. package/dist/transport/upload.d.ts +0 -25
  148. package/dist/transport/upload.d.ts.map +0 -1
  149. package/dist/transport/upload.js +0 -133
  150. package/dist/transport/upload.js.map +0 -1
  151. package/dist/transport/validation.d.ts +0 -6
  152. package/dist/transport/validation.d.ts.map +0 -1
  153. package/dist/transport/validation.js +0 -24
  154. package/dist/transport/validation.js.map +0 -1
@@ -1,1049 +0,0 @@
1
- import { readBinary } from "../transport/binary.js";
2
- import { remainingBudget } from "../transport/budget.js";
3
- import { connectedAccountRef, defineAdapter, platformPostRef, } from "../core/index.js";
4
- import { SocialError } from "../core/errors.js";
5
- import { abortable, createHttp, HttpError } from "../transport/http.js";
6
- import { httpsUrl } from "../transport/upload.js";
7
- import { array, object, string } from "../transport/validation.js";
8
- import { publicFields } from "../cloud/common.js";
9
- const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
10
- const MAX_IMAGES = 4;
11
- function endpoint(service, method) {
12
- let base;
13
- try {
14
- base = new URL(service);
15
- }
16
- catch {
17
- throw new SocialError({
18
- code: "invalid_config",
19
- operation: "bluesky.configure",
20
- message: "Bluesky service must be an absolute URL.",
21
- });
22
- }
23
- if (base.protocol !== "https:" || base.username || base.password) {
24
- throw new SocialError({
25
- code: "invalid_config",
26
- operation: "bluesky.configure",
27
- message: "Bluesky service must use HTTPS without embedded credentials.",
28
- });
29
- }
30
- return new URL(`/xrpc/${method}`, base);
31
- }
32
- function accountMatches(ref, auth, backend) {
33
- return ref.backend === backend && ref.platform === "bluesky" && ref.accountId === auth.did;
34
- }
35
- function authHeaders(auth) {
36
- return auth.accessJwt === undefined ? {} : { Authorization: `Bearer ${auth.accessJwt}` };
37
- }
38
- function operationError(operation, error, mutation = true) {
39
- if (error instanceof SocialError)
40
- return error;
41
- if (error instanceof HttpError) {
42
- const ambiguous = mutation &&
43
- error.dispatched &&
44
- (error.kind !== "http" || (error.status !== undefined && error.status >= 500));
45
- return new SocialError({
46
- code: ambiguous
47
- ? "ambiguous_outcome"
48
- : error.kind === "timeout"
49
- ? "timeout"
50
- : error.kind === "cancelled"
51
- ? "cancelled"
52
- : "upstream_failure",
53
- operation,
54
- message: error.message,
55
- retryDisposition: ambiguous ? { kind: "reconcile-first" } : { kind: "never" },
56
- ...(error.status === undefined ? {} : { upstreamStatus: error.status }),
57
- });
58
- }
59
- return new SocialError({
60
- code: "upstream_failure",
61
- operation,
62
- message: "Bluesky request failed.",
63
- retryDisposition: { kind: "reconcile-first" },
64
- cause: error,
65
- });
66
- }
67
- function postRef(value) {
68
- const record = object(value);
69
- return { uri: string(record["uri"]), cid: string(record["cid"]) };
70
- }
71
- function linkFacets(text) {
72
- const facets = [];
73
- const urlPattern = /https?:\/\/[^\s<>]+/g;
74
- for (const match of text.matchAll(urlPattern)) {
75
- const value = match[0];
76
- const start = match.index;
77
- if (start === undefined || value === undefined)
78
- continue;
79
- const end = start + value.length;
80
- const byteStart = new TextEncoder().encode(text.slice(0, start)).byteLength;
81
- const byteEnd = new TextEncoder().encode(text.slice(0, end)).byteLength;
82
- facets.push({
83
- index: { byteStart, byteEnd },
84
- features: [{ $type: "app.bsky.richtext.facet#link", uri: value }],
85
- });
86
- }
87
- return facets;
88
- }
89
- function richTextOptions(text, options) {
90
- const invalid = (message) => {
91
- throw new SocialError({ code: "invalid_input", operation: "bluesky.prepare", message });
92
- };
93
- if (options !== undefined &&
94
- (options === null || typeof options !== "object" || Array.isArray(options)))
95
- invalid("Bluesky options must be an object.");
96
- const config = (options ?? {});
97
- if (Object.keys(config).some((key) => key !== "languages" && key !== "mentions"))
98
- invalid("A supplied Bluesky option is not supported.");
99
- let langs;
100
- if (config["languages"] !== undefined) {
101
- const languages = config["languages"];
102
- if (!Array.isArray(languages) ||
103
- languages.length > 3 ||
104
- languages.some((value) => typeof value !== "string"))
105
- invalid("Bluesky accepts at most three language tags.");
106
- try {
107
- langs = Intl.getCanonicalLocales(languages);
108
- }
109
- catch {
110
- invalid("Bluesky languages must be valid BCP 47 tags.");
111
- }
112
- }
113
- const facets = [...linkFacets(text)];
114
- const bytes = new TextEncoder().encode(text);
115
- const mentions = config["mentions"] ?? [];
116
- if (!Array.isArray(mentions) || mentions.length > 100)
117
- invalid("Bluesky mentions must be an array of at most 100 DID references.");
118
- const spans = [];
119
- for (const value of mentions) {
120
- if (!value || typeof value !== "object" || Array.isArray(value))
121
- invalid("Each mention requires UTF-8 offsets and a DID.");
122
- const start = value["byteStart"], end = value["byteEnd"], did = value["did"];
123
- if (typeof start !== "number" ||
124
- typeof end !== "number" ||
125
- !Number.isSafeInteger(start) ||
126
- !Number.isSafeInteger(end) ||
127
- start < 0 ||
128
- end <= start ||
129
- end > bytes.length ||
130
- (bytes[start] & 0xc0) === 0x80 ||
131
- (end < bytes.length && (bytes[end] & 0xc0) === 0x80) ||
132
- typeof did !== "string" ||
133
- !/^did:[a-z]+:[A-Za-z0-9._:%-]+$/.test(did))
134
- invalid("Mention offsets must span complete UTF-8 characters and identify a DID.");
135
- const byteStart = start, byteEnd = end;
136
- if (!new TextDecoder().decode(bytes.slice(byteStart, byteEnd)).startsWith("@"))
137
- invalid("A mention span must start with @.");
138
- if (spans.some((span) => byteStart < span.end && byteEnd > span.start) ||
139
- facets.some((facet) => {
140
- const index = facet["index"];
141
- return byteStart < Number(index["byteEnd"]) && byteEnd > Number(index["byteStart"]);
142
- }))
143
- invalid("Mention spans must not overlap links or other mentions.");
144
- spans.push({ start: byteStart, end: byteEnd });
145
- facets.push({
146
- index: { byteStart, byteEnd },
147
- features: [{ $type: "app.bsky.richtext.facet#mention", did: did }],
148
- });
149
- }
150
- facets.sort((left, right) => Number(left["index"]["byteStart"]) -
151
- Number(right["index"]["byteStart"]));
152
- return { ...(langs === undefined ? {} : { langs }), ...(facets.length ? { facets } : {}) };
153
- }
154
- function graphemeCount(text) {
155
- const Segmenter = Intl.Segmenter;
156
- return Segmenter === undefined
157
- ? Array.from(text).length
158
- : [...new Segmenter(undefined, { granularity: "grapheme" }).segment(text)].length;
159
- }
160
- async function readMedia(source, fetcher, context, allowMediaHost) {
161
- const duration = remainingBudget(context);
162
- const controller = new AbortController();
163
- const abort = () => controller.abort(context.signal?.reason);
164
- if (context.signal?.aborted)
165
- abort();
166
- context.signal?.addEventListener("abort", abort, { once: true });
167
- const timer = setTimeout(() => controller.abort(new Error("Media deadline exceeded")), duration);
168
- try {
169
- controller.signal.throwIfAborted();
170
- if (source.kind === "blob") {
171
- if (source.blob.size > MAX_IMAGE_BYTES)
172
- throw new SocialError({
173
- code: "media_error",
174
- operation: "bluesky.uploadBlob",
175
- message: "Image exceeds the 10 MiB limit.",
176
- });
177
- return {
178
- bytes: await readBinary(source.blob.stream(), MAX_IMAGE_BYTES, controller.signal),
179
- mimeType: source.blob.type || "application/octet-stream",
180
- };
181
- }
182
- if (source.kind === "stream")
183
- return {
184
- bytes: await readBinary(source.open(), MAX_IMAGE_BYTES, controller.signal),
185
- mimeType: "application/octet-stream",
186
- };
187
- if (source.kind === "https-url") {
188
- const url = httpsUrl(source.url);
189
- if (!allowMediaHost(url.hostname))
190
- throw new SocialError({
191
- code: "media_error",
192
- operation: "bluesky.uploadBlob",
193
- message: "Remote image host is outside the configured egress policy.",
194
- });
195
- const pending = fetcher(url, { redirect: "error", signal: controller.signal });
196
- void pending.then((response) => {
197
- if (controller.signal.aborted)
198
- void response.body?.cancel().catch(() => undefined);
199
- }, () => undefined);
200
- const response = await abortable(pending, controller.signal);
201
- if (!response.ok ||
202
- !response.body ||
203
- Number(response.headers.get("content-length")) > MAX_IMAGE_BYTES) {
204
- void response.body?.cancel().catch(() => undefined);
205
- throw new SocialError({
206
- code: "media_error",
207
- operation: "bluesky.uploadBlob",
208
- message: "Remote image was unavailable or exceeded its byte limit.",
209
- });
210
- }
211
- return {
212
- bytes: await readBinary(response.body, MAX_IMAGE_BYTES, controller.signal),
213
- mimeType: response.headers.get("content-type")?.split(";")[0] || "application/octet-stream",
214
- };
215
- }
216
- throw new SocialError({
217
- code: "media_error",
218
- operation: "bluesky.uploadBlob",
219
- message: "This image source cannot be uploaded.",
220
- });
221
- }
222
- catch (error) {
223
- if (controller.signal.aborted)
224
- throw new SocialError({
225
- code: context.signal?.aborted ? "cancelled" : "timeout",
226
- operation: "bluesky.uploadBlob",
227
- message: "Image preparation was interrupted before publication.",
228
- retryDisposition: { kind: "never" },
229
- });
230
- throw error;
231
- }
232
- finally {
233
- clearTimeout(timer);
234
- context.signal?.removeEventListener("abort", abort);
235
- }
236
- }
237
- export function bluesky(options) {
238
- const backend = options.backend ?? "default";
239
- const fetcher = options.fetch ?? globalThis.fetch;
240
- const allowMediaHost = options.allowMediaHost ?? (() => false);
241
- const http = createHttp({
242
- fetch: options.session
243
- ? (input, init) => {
244
- const url = new URL(input instanceof Request ? input.url : String(input));
245
- // OAuthSession resolves a relative XRPC path against its verified token
246
- // audience. Never override that audience with caller-provided service URLs.
247
- return options.session.fetchHandler(url.pathname + url.search, init);
248
- }
249
- : fetcher,
250
- ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
251
- });
252
- const auth = options.auth;
253
- if (options.session) {
254
- if (options.session.did !== auth.did)
255
- throw new SocialError({
256
- code: "invalid_config",
257
- operation: "bluesky.configure",
258
- message: "OAuth session DID must match the configured account DID.",
259
- });
260
- if (auth.accessJwt !== undefined)
261
- throw new SocialError({
262
- code: "invalid_config",
263
- operation: "bluesky.configure",
264
- message: "Use OAuth session transport or bearer credentials, not both.",
265
- });
266
- }
267
- else if (auth.accessJwt === undefined) {
268
- throw new SocialError({
269
- code: "invalid_config",
270
- operation: "bluesky.configure",
271
- message: "Bluesky authorization requires accessJwt or an OAuth session.",
272
- });
273
- }
274
- const service = auth.service;
275
- const account = connectedAccountRef({ backend, platform: "bluesky", accountId: auth.did });
276
- const capabilities = {
277
- schemaVersion: 1,
278
- backend,
279
- apiRevision: "AT Protocol XRPC 2026-09",
280
- runtime: ["node>=22.12", "bun"],
281
- capabilities: [
282
- {
283
- operation: "accounts.read",
284
- platform: "bluesky",
285
- availability: "available",
286
- requiredScopes: ["repo"],
287
- },
288
- {
289
- operation: "posts.publish",
290
- platform: "bluesky",
291
- availability: "available",
292
- formats: ["text", "image"],
293
- requiredScopes: ["repo"],
294
- },
295
- {
296
- operation: "posts.read",
297
- platform: "bluesky",
298
- availability: "available",
299
- formats: ["text", "image"],
300
- },
301
- {
302
- operation: "posts.list",
303
- platform: "bluesky",
304
- availability: "available",
305
- formats: ["text", "image"],
306
- },
307
- {
308
- operation: "comments.read",
309
- platform: "bluesky",
310
- availability: "available",
311
- formats: ["text"],
312
- },
313
- {
314
- operation: "analytics.read",
315
- platform: "bluesky",
316
- availability: "available",
317
- formats: ["text", "image"],
318
- },
319
- { operation: "analytics.account.read", platform: "bluesky", availability: "available" },
320
- {
321
- operation: "comments.write",
322
- platform: "bluesky",
323
- availability: "available",
324
- formats: ["text"],
325
- requiredScopes: ["repo"],
326
- },
327
- {
328
- operation: "posts.publish.video",
329
- platform: "bluesky",
330
- availability: "available",
331
- formats: ["video"],
332
- requiredScopes: ["repo"],
333
- },
334
- ...[
335
- "posts.repost",
336
- "posts.quote",
337
- "posts.delete",
338
- "graph.follow",
339
- "graph.block",
340
- "graph.mute",
341
- "notifications.read",
342
- "notifications.seen",
343
- "profile.read",
344
- "profile.update",
345
- "feeds.read",
346
- "chat.read",
347
- "chat.write",
348
- ].map((operation) => ({
349
- operation,
350
- platform: "bluesky",
351
- availability: operation.startsWith("chat.")
352
- ? "available"
353
- : "available",
354
- })),
355
- ],
356
- };
357
- async function xrpc(method, context, init = {}) {
358
- try {
359
- return await http({
360
- url: endpoint(service, method),
361
- timeoutMs: remainingBudget(context),
362
- method: init.body === undefined ? "GET" : "POST",
363
- ...(options.session
364
- ? init.headers === undefined
365
- ? {}
366
- : { headers: init.headers }
367
- : { headers: { ...authHeaders(auth), ...init.headers } }),
368
- ...(init.body === undefined ? {} : { body: init.body }),
369
- ...(context.signal === undefined ? {} : { signal: context.signal }),
370
- maxAttempts: init.maxAttempts ??
371
- (init.body === undefined ? Math.min(context.retryBudget.maxAttempts, 3) : 1),
372
- });
373
- }
374
- catch (error) {
375
- throw operationError(`bluesky.${method}`, error, init.body !== undefined);
376
- }
377
- }
378
- async function verifiedSession(context) {
379
- const session = object(await xrpc(options.session
380
- ? `app.bsky.actor.getProfile?actor=${encodeURIComponent(auth.did)}`
381
- : "com.atproto.server.getSession", context));
382
- const did = string(session["did"]);
383
- if (did !== auth.did) {
384
- throw new SocialError({
385
- code: "unauthorized",
386
- operation: "bluesky.accounts.get",
387
- message: "The configured credential belongs to a different DID.",
388
- });
389
- }
390
- const handle = typeof session["handle"] === "string" ? session["handle"] : undefined;
391
- return { did, ...(handle === undefined ? {} : { handle }) };
392
- }
393
- const nativeContext = (context, operation) => context ?? {
394
- correlationId: operation,
395
- retryBudget: { maxAttempts: 2, maxElapsedMs: 30_000 },
396
- backendInstance: backend,
397
- };
398
- const assertNativeAccount = (selected, context, operation) => {
399
- if (!accountMatches(selected, auth, backend) || context.backendInstance !== backend)
400
- throw new SocialError({
401
- code: "unauthorized",
402
- operation,
403
- message: "Account reference does not belong to this Bluesky adapter.",
404
- });
405
- };
406
- const native = {
407
- async getPost(input) {
408
- const context = input.context ?? {
409
- correlationId: "bluesky-native",
410
- retryBudget: { maxAttempts: 2, maxElapsedMs: 30_000 },
411
- backendInstance: backend,
412
- };
413
- const query = new URLSearchParams({ uris: input.uri });
414
- const response = object(await xrpc(`app.bsky.feed.getPosts?${query.toString()}`, context));
415
- const posts = array(response["posts"]);
416
- const post = posts.find((value) => object(value)["uri"] === input.uri);
417
- if (post === undefined)
418
- throw new SocialError({
419
- code: "upstream_failure",
420
- operation: "bluesky.getPost",
421
- message: "Bluesky post was not found.",
422
- });
423
- return object(post);
424
- },
425
- async getPostThread(input) {
426
- const context = input.context ?? {
427
- correlationId: "bluesky-native",
428
- retryBudget: { maxAttempts: 2, maxElapsedMs: 30_000 },
429
- backendInstance: backend,
430
- };
431
- const query = new URLSearchParams({ uri: input.uri });
432
- return object(await xrpc(`app.bsky.feed.getPostThread?${query.toString()}`, context));
433
- },
434
- async likePost(input) {
435
- if (!accountMatches(input.account, auth, backend) ||
436
- (input.context && input.context.backendInstance !== backend))
437
- throw new SocialError({
438
- code: "unauthorized",
439
- operation: "bluesky.reactions.like",
440
- message: "Account reference does not belong to this Bluesky adapter.",
441
- });
442
- if (!/^at:\/\/did:[a-z]+:[A-Za-z0-9._:%-]+\/app\.bsky\.feed\.post\/[A-Za-z0-9._~:-]{1,512}$/.test(input.post.uri) ||
443
- !/^[A-Za-z0-9]{1,2048}$/.test(input.post.cid))
444
- throw new SocialError({
445
- code: "invalid_input",
446
- operation: "bluesky.reactions.like",
447
- message: "Provide the native post URI and CID returned by a Bluesky read.",
448
- });
449
- const context = input.context ?? {
450
- correlationId: "bluesky-like",
451
- retryBudget: { maxAttempts: 1, maxElapsedMs: 30_000 },
452
- backendInstance: backend,
453
- };
454
- const response = object(await xrpc("com.atproto.repo.createRecord", context, {
455
- body: JSON.stringify({
456
- repo: auth.did,
457
- collection: "app.bsky.feed.like",
458
- record: {
459
- $type: "app.bsky.feed.like",
460
- subject: { uri: input.post.uri, cid: input.post.cid },
461
- createdAt: new Date().toISOString(),
462
- },
463
- }),
464
- }));
465
- const prefix = `at://${auth.did}/app.bsky.feed.like/`;
466
- const uri = response["uri"], cid = response["cid"];
467
- if (typeof uri !== "string" ||
468
- !uri.startsWith(prefix) ||
469
- !/^[A-Za-z0-9._~:-]{1,512}$/.test(uri.slice(prefix.length)) ||
470
- typeof cid !== "string" ||
471
- !cid)
472
- throw new SocialError({
473
- code: "ambiguous_outcome",
474
- operation: "bluesky.reactions.like",
475
- message: "Bluesky did not return an owned like record. Reconcile before retrying.",
476
- retryDisposition: { kind: "reconcile-first" },
477
- });
478
- return { uri, cid };
479
- },
480
- async unlikePost(input) {
481
- if (!accountMatches(input.account, auth, backend) ||
482
- (input.context && input.context.backendInstance !== backend))
483
- throw new SocialError({
484
- code: "unauthorized",
485
- operation: "bluesky.reactions.unlike",
486
- message: "Account reference does not belong to this Bluesky adapter.",
487
- });
488
- const context = input.context ?? {
489
- correlationId: "bluesky-unlike",
490
- retryBudget: { maxAttempts: 1, maxElapsedMs: 30_000 },
491
- backendInstance: backend,
492
- };
493
- const prefix = `at://${auth.did}/app.bsky.feed.like/`;
494
- const recordKey = input.likeUri.startsWith(prefix) ? input.likeUri.slice(prefix.length) : "";
495
- if (!/^[A-Za-z0-9._~:-]{1,512}$/.test(recordKey) || recordKey === "." || recordKey === "..")
496
- throw new SocialError({
497
- code: "unauthorized",
498
- operation: "bluesky.reactions.unlike",
499
- message: "Like record must belong to this Bluesky account.",
500
- });
501
- await xrpc("com.atproto.repo.deleteRecord", context, {
502
- body: JSON.stringify({ repo: auth.did, collection: "app.bsky.feed.like", rkey: recordKey }),
503
- });
504
- },
505
- async repostPost(input) {
506
- const context = nativeContext(input.context, "bluesky.repost");
507
- assertNativeAccount(input.account, context, "bluesky.repost");
508
- const result = object(await xrpc("com.atproto.repo.createRecord", context, {
509
- body: JSON.stringify({
510
- repo: auth.did,
511
- collection: "app.bsky.feed.repost",
512
- record: {
513
- $type: "app.bsky.feed.repost",
514
- subject: input.post,
515
- createdAt: new Date().toISOString(),
516
- },
517
- }),
518
- }));
519
- return postRef(result);
520
- },
521
- async quotePost(input) {
522
- const context = nativeContext(input.context, "bluesky.quote");
523
- assertNativeAccount(input.account, context, "bluesky.quote");
524
- if (!input.text.trim() || input.text.length > 300)
525
- throw new SocialError({
526
- code: "invalid_input",
527
- operation: "bluesky.quote",
528
- message: "Quote text must be 1-300 characters.",
529
- });
530
- const result = object(await xrpc("com.atproto.repo.createRecord", context, {
531
- body: JSON.stringify({
532
- repo: auth.did,
533
- collection: "app.bsky.feed.post",
534
- record: {
535
- $type: "app.bsky.feed.post",
536
- text: input.text,
537
- createdAt: new Date().toISOString(),
538
- embed: { $type: "app.bsky.embed.record", record: input.post },
539
- },
540
- }),
541
- }));
542
- return postRef(result);
543
- },
544
- async deletePost(input) {
545
- const context = nativeContext(input.context, "bluesky.delete");
546
- assertNativeAccount(input.account, context, "bluesky.delete");
547
- const prefix = `at://${auth.did}/app.bsky.feed.post/`;
548
- const rkey = input.post.uri.startsWith(prefix) ? input.post.uri.slice(prefix.length) : "";
549
- if (!rkey)
550
- throw new SocialError({
551
- code: "unauthorized",
552
- operation: "bluesky.delete",
553
- message: "Post URI does not belong to this account.",
554
- });
555
- await xrpc("com.atproto.repo.deleteRecord", context, {
556
- body: JSON.stringify({ repo: auth.did, collection: "app.bsky.feed.post", rkey }),
557
- });
558
- },
559
- async uploadVideo(input) {
560
- const context = nativeContext(input.context, "bluesky.video.upload");
561
- assertNativeAccount(input.account, context, "bluesky.video.upload");
562
- if (!input.video.type.startsWith("video/") || input.video.size <= 0)
563
- throw new SocialError({
564
- code: "invalid_input",
565
- operation: "bluesky.video.upload",
566
- message: "Provide a non-empty video Blob.",
567
- });
568
- return object(await xrpc("app.bsky.video.uploadVideo", context, {
569
- body: input.video,
570
- headers: { "Content-Type": input.mimeType ?? input.video.type },
571
- }));
572
- },
573
- async follow(input) {
574
- const context = nativeContext(input.context, "bluesky.follow");
575
- assertNativeAccount(input.account, context, "bluesky.follow");
576
- return object(await xrpc("com.atproto.repo.createRecord", context, {
577
- body: JSON.stringify({
578
- repo: auth.did,
579
- collection: "app.bsky.graph.follow",
580
- record: {
581
- $type: "app.bsky.graph.follow",
582
- subject: input.did,
583
- createdAt: new Date().toISOString(),
584
- },
585
- }),
586
- }));
587
- },
588
- async block(input) {
589
- const context = nativeContext(input.context, "bluesky.block");
590
- assertNativeAccount(input.account, context, "bluesky.block");
591
- return object(await xrpc("com.atproto.repo.createRecord", context, {
592
- body: JSON.stringify({
593
- repo: auth.did,
594
- collection: "app.bsky.graph.block",
595
- record: {
596
- $type: "app.bsky.graph.block",
597
- subject: input.did,
598
- createdAt: new Date().toISOString(),
599
- },
600
- }),
601
- }));
602
- },
603
- async mute(input) {
604
- const context = nativeContext(input.context, "bluesky.mute");
605
- assertNativeAccount(input.account, context, "bluesky.mute");
606
- return object(await xrpc("app.bsky.graph.muteActor", context, {
607
- body: JSON.stringify({ actor: input.did }),
608
- }));
609
- },
610
- async listNotifications(input) {
611
- const context = nativeContext(input.context, "bluesky.notifications.list");
612
- assertNativeAccount(input.account, context, "bluesky.notifications.list");
613
- const query = new URLSearchParams({
614
- limit: String(input.limit ?? 50),
615
- ...(input.cursor ? { cursor: input.cursor } : {}),
616
- });
617
- return object(await xrpc(`app.bsky.notification.listNotifications?${query}`, context));
618
- },
619
- async markNotificationsSeen(input) {
620
- const context = nativeContext(input.context, "bluesky.notifications.seen");
621
- assertNativeAccount(input.account, context, "bluesky.notifications.seen");
622
- await xrpc("app.bsky.notification.updateSeen", context, {
623
- body: JSON.stringify({ seenAt: input.seenAt ?? new Date().toISOString() }),
624
- });
625
- },
626
- async getProfile(input) {
627
- const context = nativeContext(input.context, "bluesky.profile.get");
628
- assertNativeAccount(input.account, context, "bluesky.profile.get");
629
- return object(await xrpc(`app.bsky.actor.getProfile?actor=${encodeURIComponent(input.actor ?? auth.did)}`, context));
630
- },
631
- async updateProfile(input) {
632
- const context = nativeContext(input.context, "bluesky.profile.update");
633
- assertNativeAccount(input.account, context, "bluesky.profile.update");
634
- await xrpc("com.atproto.repo.putRecord", context, {
635
- body: JSON.stringify({
636
- repo: auth.did,
637
- collection: "app.bsky.actor.profile",
638
- rkey: "self",
639
- record: input.profile,
640
- }),
641
- });
642
- },
643
- async listFeeds(input) {
644
- const context = nativeContext(input.context, "bluesky.feeds.list");
645
- assertNativeAccount(input.account, context, "bluesky.feeds.list");
646
- const query = new URLSearchParams({
647
- limit: String(input.limit ?? 50),
648
- ...(input.cursor ? { cursor: input.cursor } : {}),
649
- });
650
- return object(await xrpc(`app.bsky.feed.getSuggestedFeeds?${query}`, context));
651
- },
652
- async listConversations(input) {
653
- const context = nativeContext(input.context, "bluesky.chat.list");
654
- assertNativeAccount(input.account, context, "bluesky.chat.list");
655
- const query = new URLSearchParams({
656
- limit: String(input.limit ?? 50),
657
- ...(input.cursor ? { cursor: input.cursor } : {}),
658
- });
659
- return object(await xrpc(`chat.bsky.convo.listConvos?${query}`, context));
660
- },
661
- async listMessages(input) {
662
- const context = nativeContext(input.context, "bluesky.chat.messages");
663
- assertNativeAccount(input.account, context, "bluesky.chat.messages");
664
- const query = new URLSearchParams({
665
- limit: String(input.limit ?? 50),
666
- ...(input.cursor ? { cursor: input.cursor } : {}),
667
- });
668
- return object(await xrpc(`chat.bsky.convo.getMessages?convoId=${encodeURIComponent(input.conversationId)}&${query}`, context));
669
- },
670
- async sendMessage(input) {
671
- const context = nativeContext(input.context, "bluesky.chat.send");
672
- assertNativeAccount(input.account, context, "bluesky.chat.send");
673
- if (!input.text.trim())
674
- throw new SocialError({
675
- code: "invalid_input",
676
- operation: "bluesky.chat.send",
677
- message: "Message text is required.",
678
- });
679
- return object(await xrpc("chat.bsky.convo.sendMessage", context, {
680
- body: JSON.stringify({ convoId: input.conversationId, message: { text: input.text } }),
681
- }));
682
- },
683
- };
684
- const adapter = defineAdapter({
685
- id: backend,
686
- capabilities,
687
- native,
688
- accounts: {
689
- async list(_input, context) {
690
- const session = await verifiedSession(context);
691
- return {
692
- items: [
693
- {
694
- ref: account,
695
- displayName: session.handle ?? session.did,
696
- ...(session.handle === undefined ? {} : { handle: session.handle }),
697
- status: "connected",
698
- },
699
- ],
700
- };
701
- },
702
- async get(ref, context) {
703
- if (!accountMatches(ref, auth, backend))
704
- throw new SocialError({
705
- code: "unauthorized",
706
- operation: "bluesky.accounts.get",
707
- message: "The account reference does not belong to this Bluesky adapter.",
708
- account: ref,
709
- });
710
- const session = await verifiedSession(context);
711
- return {
712
- ref: account,
713
- displayName: session.handle ?? session.did,
714
- ...(session.handle === undefined ? {} : { handle: session.handle }),
715
- status: "connected",
716
- };
717
- },
718
- },
719
- posts: {
720
- async list(account, input, context) {
721
- if (!accountMatches(account, auth, backend))
722
- throw new SocialError({
723
- code: "unauthorized",
724
- operation: "bluesky.posts.list",
725
- message: "The account reference does not belong to this adapter.",
726
- });
727
- const limit = input.limit ?? 50;
728
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
729
- throw new SocialError({
730
- code: "invalid_input",
731
- operation: "posts.read",
732
- message: "Bluesky author-feed page size must be between 1 and 100.",
733
- });
734
- const query = new URLSearchParams({
735
- actor: auth.did,
736
- limit: String(limit),
737
- ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
738
- });
739
- const response = object(await xrpc(`app.bsky.feed.getAuthorFeed?${query.toString()}`, context));
740
- const feed = array(response["feed"]).map((value) => {
741
- const entry = object(value);
742
- const post = object(entry["post"]);
743
- const safePost = {
744
- ...publicFields(post, [
745
- "uri",
746
- "cid",
747
- "likeCount",
748
- "repostCount",
749
- "replyCount",
750
- "quoteCount",
751
- "indexedAt",
752
- "labels",
753
- ]),
754
- ...(post["author"] === undefined
755
- ? {}
756
- : {
757
- author: publicFields(object(post["author"]), [
758
- "did",
759
- "handle",
760
- "displayName",
761
- "avatar",
762
- ]),
763
- }),
764
- ...(post["record"] === undefined
765
- ? {}
766
- : {
767
- record: publicFields(object(post["record"]), [
768
- "text",
769
- "facets",
770
- "createdAt",
771
- "reply",
772
- "embed",
773
- ]),
774
- }),
775
- };
776
- return {
777
- post: safePost,
778
- ...(entry["reason"] === undefined
779
- ? {}
780
- : { reason: publicFields(object(entry["reason"]), ["$type", "by", "indexedAt"]) }),
781
- };
782
- });
783
- const cursor = typeof response["cursor"] === "string" ? response["cursor"] : undefined;
784
- return {
785
- items: feed,
786
- ...(cursor === undefined ? {} : { nextCursor: cursor }),
787
- };
788
- },
789
- prepareTarget(target) {
790
- const issues = [];
791
- const text = target.content.text ?? "";
792
- try {
793
- richTextOptions(text, target.options);
794
- }
795
- catch (error) {
796
- issues.push({
797
- code: "bluesky.richtext",
798
- message: error instanceof Error ? error.message : "Invalid Bluesky rich text options.",
799
- severity: "error",
800
- });
801
- }
802
- if (new TextEncoder().encode(text).byteLength > 3000 || graphemeCount(text) > 300)
803
- issues.push({
804
- code: "text.too_long",
805
- message: "Bluesky posts must be at most 300 graphemes and 3,000 UTF-8 bytes.",
806
- severity: "error",
807
- });
808
- if (target.account.platform !== "bluesky")
809
- issues.push({
810
- code: "platform.unsupported",
811
- message: "Bluesky adapter received a different platform.",
812
- severity: "error",
813
- });
814
- if (target.schedule !== undefined)
815
- issues.push({
816
- code: "schedule.unsupported",
817
- message: "Bluesky scheduling requires an application-owned job runner.",
818
- severity: "error",
819
- });
820
- if (target.content.media?.some((media) => media.kind !== "image"))
821
- issues.push({
822
- code: "media.unsupported",
823
- message: "Bluesky direct publishing supports images, not video.",
824
- severity: "error",
825
- });
826
- if ((target.content.media?.length ?? 0) > MAX_IMAGES)
827
- issues.push({
828
- code: "media.too_many",
829
- message: `Bluesky supports at most ${MAX_IMAGES} images per post.`,
830
- severity: "error",
831
- });
832
- return issues;
833
- },
834
- async publishTarget(target, context) {
835
- if (!accountMatches(target.account, auth, backend))
836
- throw new SocialError({
837
- code: "unauthorized",
838
- operation: "bluesky.publish",
839
- message: "The account reference does not belong to this adapter.",
840
- account: target.account,
841
- });
842
- try {
843
- const content = target.content;
844
- const text = content.text ?? "";
845
- const record = {
846
- $type: "app.bsky.feed.post",
847
- text,
848
- createdAt: new Date().toISOString(),
849
- };
850
- Object.assign(record, richTextOptions(text, target.options));
851
- if (target.replyTo !== undefined) {
852
- if (target.replyTo.kind !== "platform-post")
853
- throw new SocialError({
854
- code: "unsupported_capability",
855
- operation: "bluesky.reply",
856
- message: "Bluesky replies require a platform post reference.",
857
- });
858
- const parent = await native.getPost({
859
- uri: target.replyTo.native?.["uri"]?.toString() ?? target.replyTo.postId,
860
- context,
861
- });
862
- const parentRecord = object(parent["record"]);
863
- const parentRef = target.replyTo.native === undefined
864
- ? { uri: string(parent["uri"]), cid: string(parent["cid"]) }
865
- : {
866
- uri: string(target.replyTo.native["uri"]),
867
- cid: string(target.replyTo.native["cid"]),
868
- };
869
- const rootValue = parentRecord["reply"];
870
- const root = rootValue === undefined ? parentRef : object(object(rootValue)["root"]);
871
- Object.assign(record, { reply: { root, parent: parentRef } });
872
- }
873
- if (content.media !== undefined && content.media.length > 0) {
874
- const blobs = [];
875
- for (const media of content.media) {
876
- const source = await readMedia(media.source, fetcher, context, allowMediaHost);
877
- const blob = object(await xrpc("com.atproto.repo.uploadBlob", context, {
878
- body: new Blob([source.bytes.slice().buffer], {
879
- type: source.mimeType,
880
- }),
881
- headers: { "Content-Type": source.mimeType },
882
- }));
883
- blobs.push(object(blob["blob"]));
884
- }
885
- Object.assign(record, {
886
- embed: {
887
- $type: "app.bsky.embed.images",
888
- images: blobs.map((blob, index) => ({
889
- image: blob,
890
- alt: content.media?.[index]?.altText ?? "",
891
- })),
892
- },
893
- });
894
- }
895
- const response = object(await xrpc("com.atproto.repo.createRecord", context, {
896
- body: JSON.stringify({ repo: auth.did, collection: "app.bsky.feed.post", record }),
897
- headers: { "Content-Type": "application/json" },
898
- }));
899
- const created = postRef(response);
900
- const delivery = {
901
- kind: "delivery",
902
- version: 1,
903
- backend,
904
- deliveryId: created.uri,
905
- platform: "bluesky",
906
- accountId: auth.did,
907
- };
908
- return {
909
- state: "published",
910
- targetIndex: target.targetIndex,
911
- account: target.account,
912
- delivery,
913
- post: {
914
- ...platformPostRef({
915
- backend,
916
- platform: "bluesky",
917
- accountId: auth.did,
918
- postId: created.uri,
919
- }),
920
- native: { uri: created.uri, cid: created.cid },
921
- },
922
- observedAt: new Date().toISOString(),
923
- };
924
- }
925
- catch (error) {
926
- throw operationError("bluesky.publish", error);
927
- }
928
- },
929
- async get(ref, context) {
930
- if (ref.backend !== backend || ref.platform !== "bluesky" || ref.accountId !== auth.did)
931
- throw new SocialError({
932
- code: "unauthorized",
933
- operation: "bluesky.posts.get",
934
- message: "The account reference does not belong to this adapter.",
935
- });
936
- return native.getPost({ uri: ref.postId, context });
937
- },
938
- },
939
- comments: {
940
- async list(post, input, context) {
941
- if (input.cursor !== undefined)
942
- throw new SocialError({
943
- code: "unsupported_capability",
944
- operation: "comments.read",
945
- message: "This adapter does not expose upstream comment pagination.",
946
- });
947
- const thread = await native.getPostThread({ uri: post.postId, context });
948
- const replies = array(object(thread["thread"])["replies"] ?? []);
949
- return { items: replies.map((reply) => object(reply)) };
950
- },
951
- async reply(comment, content, context) {
952
- const outcome = await adapter.posts?.publishTarget({
953
- targetIndex: 0,
954
- targetKey: comment.backend + ":" + comment.platform + ":" + comment.accountId,
955
- account: connectedAccountRef({
956
- backend: comment.backend,
957
- platform: "bluesky",
958
- accountId: comment.accountId,
959
- }),
960
- content: { text: content.text },
961
- replyTo: platformPostRef({
962
- backend: comment.backend,
963
- platform: "bluesky",
964
- accountId: comment.accountId,
965
- postId: comment.commentId,
966
- }),
967
- }, context);
968
- if (outcome?.state !== "published")
969
- throw new SocialError({
970
- code: "upstream_failure",
971
- operation: "bluesky.comments.reply",
972
- message: "Bluesky did not confirm the comment reply.",
973
- });
974
- return {
975
- kind: "comment",
976
- version: 1,
977
- backend: comment.backend,
978
- platform: "bluesky",
979
- accountId: comment.accountId,
980
- postId: comment.postId,
981
- commentId: outcome.post.postId,
982
- };
983
- },
984
- },
985
- analytics: {
986
- async getPostMetrics(post, context) {
987
- const value = await native.getPost({ uri: post.postId, context });
988
- const record = object(value);
989
- const fetchedAt = new Date().toISOString();
990
- const metrics = [];
991
- for (const [name, field] of [
992
- ["likes", "likeCount"],
993
- ["reposts", "repostCount"],
994
- ["replies", "replyCount"],
995
- ["quotes", "quoteCount"],
996
- ]) {
997
- const count = record[field];
998
- if (typeof count === "number" && Number.isFinite(count))
999
- metrics.push({
1000
- name,
1001
- value: count,
1002
- unit: "count",
1003
- period: "lifetime",
1004
- fetchedAt,
1005
- freshness: "unknown",
1006
- source: "app.bsky.feed.getPosts",
1007
- });
1008
- }
1009
- return metrics;
1010
- },
1011
- async getAccountMetrics(accountRef, context) {
1012
- // https://docs.bsky.app/docs/api/app-bsky-actor-get-profile
1013
- if (!accountMatches(accountRef, auth, backend))
1014
- throw new SocialError({
1015
- code: "unauthorized",
1016
- operation: "bluesky.analytics.account",
1017
- message: "Account reference does not belong to this Bluesky adapter.",
1018
- });
1019
- const profile = object(await xrpc(`app.bsky.actor.getProfile?actor=${encodeURIComponent(auth.did)}`, context));
1020
- if (profile["did"] !== auth.did)
1021
- throw new SocialError({
1022
- code: "unauthorized",
1023
- operation: "bluesky.analytics.account",
1024
- message: "Bluesky profile identity does not match the requested DID.",
1025
- });
1026
- const fetchedAt = new Date().toISOString();
1027
- return [
1028
- ["followers", profile["followersCount"]],
1029
- ["following", profile["followsCount"]],
1030
- ["posts", profile["postsCount"]],
1031
- ].flatMap(([name, value]) => typeof value === "number" && Number.isFinite(value)
1032
- ? [
1033
- {
1034
- name,
1035
- value,
1036
- unit: "count",
1037
- period: "lifetime",
1038
- fetchedAt,
1039
- freshness: "unknown",
1040
- source: "app.bsky.actor.getProfile",
1041
- },
1042
- ]
1043
- : []);
1044
- },
1045
- },
1046
- });
1047
- return adapter;
1048
- }
1049
- //# sourceMappingURL=bluesky.js.map