@opencoredev/social-sdk 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +10 -2
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +1 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/cloud/common.d.ts.map +1 -1
  6. package/dist/cloud/common.js +48 -20
  7. package/dist/cloud/common.js.map +1 -1
  8. package/dist/cloud/lifecycle.d.ts.map +1 -1
  9. package/dist/cloud/lifecycle.js +5 -0
  10. package/dist/cloud/lifecycle.js.map +1 -1
  11. package/dist/cloud/media.d.ts.map +1 -1
  12. package/dist/cloud/media.js +12 -8
  13. package/dist/cloud/media.js.map +1 -1
  14. package/dist/cloud/outcomes.d.ts.map +1 -1
  15. package/dist/cloud/outcomes.js +17 -9
  16. package/dist/cloud/outcomes.js.map +1 -1
  17. package/dist/cloud/post-for-me.d.ts.map +1 -1
  18. package/dist/cloud/post-for-me.js +99 -28
  19. package/dist/cloud/post-for-me.js.map +1 -1
  20. package/dist/cloud/zernio.d.ts.map +1 -1
  21. package/dist/cloud/zernio.js +100 -9
  22. package/dist/cloud/zernio.js.map +1 -1
  23. package/dist/core/adapter.d.ts +42 -1
  24. package/dist/core/adapter.d.ts.map +1 -1
  25. package/dist/core/adapter.js.map +1 -1
  26. package/dist/core/client.d.ts +35 -1
  27. package/dist/core/client.d.ts.map +1 -1
  28. package/dist/core/client.js +503 -110
  29. package/dist/core/client.js.map +1 -1
  30. package/dist/core/concurrency.d.ts.map +1 -1
  31. package/dist/core/concurrency.js +5 -3
  32. package/dist/core/concurrency.js.map +1 -1
  33. package/dist/core/errors.d.ts.map +1 -1
  34. package/dist/core/errors.js +14 -7
  35. package/dist/core/errors.js.map +1 -1
  36. package/dist/core/idempotency.d.ts.map +1 -1
  37. package/dist/core/idempotency.js.map +1 -1
  38. package/dist/core/index.d.ts.map +1 -1
  39. package/dist/core/index.js.map +1 -1
  40. package/dist/core/pagination.d.ts.map +1 -1
  41. package/dist/core/pagination.js +1 -0
  42. package/dist/core/pagination.js.map +1 -1
  43. package/dist/core/types.d.ts +70 -2
  44. package/dist/core/types.d.ts.map +1 -1
  45. package/dist/core/types.js +10 -0
  46. package/dist/core/types.js.map +1 -1
  47. package/dist/platforms/bluesky.d.ts +131 -1
  48. package/dist/platforms/bluesky.d.ts.map +1 -1
  49. package/dist/platforms/bluesky.js +772 -44
  50. package/dist/platforms/bluesky.js.map +1 -1
  51. package/dist/platforms/instagram.d.ts +70 -3
  52. package/dist/platforms/instagram.d.ts.map +1 -1
  53. package/dist/platforms/instagram.js +403 -47
  54. package/dist/platforms/instagram.js.map +1 -1
  55. package/dist/platforms/linkedin.d.ts +55 -0
  56. package/dist/platforms/linkedin.d.ts.map +1 -1
  57. package/dist/platforms/linkedin.js +350 -39
  58. package/dist/platforms/linkedin.js.map +1 -1
  59. package/dist/platforms/threads.d.ts +37 -4
  60. package/dist/platforms/threads.d.ts.map +1 -1
  61. package/dist/platforms/threads.js +419 -44
  62. package/dist/platforms/threads.js.map +1 -1
  63. package/dist/platforms/tiktok.d.ts.map +1 -1
  64. package/dist/platforms/tiktok.js +109 -26
  65. package/dist/platforms/tiktok.js.map +1 -1
  66. package/dist/platforms/x-engagement.d.ts.map +1 -1
  67. package/dist/platforms/x-engagement.js +4 -0
  68. package/dist/platforms/x-engagement.js.map +1 -1
  69. package/dist/platforms/x.d.ts +227 -9
  70. package/dist/platforms/x.d.ts.map +1 -1
  71. package/dist/platforms/x.js +826 -49
  72. package/dist/platforms/x.js.map +1 -1
  73. package/dist/platforms/youtube-upload.d.ts.map +1 -1
  74. package/dist/platforms/youtube-upload.js +25 -9
  75. package/dist/platforms/youtube-upload.js.map +1 -1
  76. package/dist/platforms/youtube.d.ts +56 -2
  77. package/dist/platforms/youtube.d.ts.map +1 -1
  78. package/dist/platforms/youtube.js +571 -31
  79. package/dist/platforms/youtube.js.map +1 -1
  80. package/dist/server/connections.d.ts.map +1 -1
  81. package/dist/server/connections.js +4 -2
  82. package/dist/server/connections.js.map +1 -1
  83. package/dist/server/credentials.d.ts.map +1 -1
  84. package/dist/server/credentials.js +10 -16
  85. package/dist/server/credentials.js.map +1 -1
  86. package/dist/server/index.d.ts.map +1 -1
  87. package/dist/server/index.js.map +1 -1
  88. package/dist/server/oauth.d.ts +5 -0
  89. package/dist/server/oauth.d.ts.map +1 -1
  90. package/dist/server/oauth.js +118 -47
  91. package/dist/server/oauth.js.map +1 -1
  92. package/dist/server/webhooks.d.ts.map +1 -1
  93. package/dist/server/webhooks.js +11 -4
  94. package/dist/server/webhooks.js.map +1 -1
  95. package/dist/testing/index.d.ts.map +1 -1
  96. package/dist/testing/index.js +153 -1
  97. package/dist/testing/index.js.map +1 -1
  98. package/dist/transport/binary.d.ts.map +1 -1
  99. package/dist/transport/binary.js.map +1 -1
  100. package/dist/transport/budget.d.ts.map +1 -1
  101. package/dist/transport/budget.js.map +1 -1
  102. package/dist/transport/http.d.ts +2 -1
  103. package/dist/transport/http.d.ts.map +1 -1
  104. package/dist/transport/http.js +42 -12
  105. package/dist/transport/http.js.map +1 -1
  106. package/dist/transport/json.d.ts +7 -1
  107. package/dist/transport/json.d.ts.map +1 -1
  108. package/dist/transport/json.js +7 -1
  109. package/dist/transport/json.js.map +1 -1
  110. package/dist/transport/upload.d.ts +1 -0
  111. package/dist/transport/upload.d.ts.map +1 -1
  112. package/dist/transport/upload.js +38 -17
  113. package/dist/transport/upload.js.map +1 -1
  114. package/dist/transport/validation.d.ts.map +1 -1
  115. package/dist/transport/validation.js +1 -0
  116. package/dist/transport/validation.js.map +1 -1
  117. package/package.json +2 -1
@@ -1,6 +1,7 @@
1
+ /* oxlint-disable anti-slop/no-chained-type-assertions, anti-slop/no-known-value-widening, anti-slop/require-safety-comment-for-type-assertion, anti-slop/require-readable-spacing, anti-slop/no-conditional-empty-object-spread, anti-slop/no-runtime-typeof -- validated external boundary or fixture contract. */
1
2
  import { readBinary } from "../transport/binary.js";
2
3
  import { remainingBudget } from "../transport/budget.js";
3
- import { connectedAccountRef, defineAdapter, platformPostRef, } from "../core/index.js";
4
+ import { connectedAccountRef, defineAdapter, platformPostRef, profileRef, } from "../core/index.js";
4
5
  import { SocialError } from "../core/errors.js";
5
6
  import { abortable, createHttp, HttpError } from "../transport/http.js";
6
7
  import { httpsUrl } from "../transport/upload.js";
@@ -35,6 +36,7 @@ function accountMatches(ref, auth, backend) {
35
36
  function authHeaders(auth) {
36
37
  return auth.accessJwt === undefined ? {} : { Authorization: `Bearer ${auth.accessJwt}` };
37
38
  }
39
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- validated boundary or fixture contract.
38
40
  function operationError(operation, error, mutation = true) {
39
41
  if (error instanceof SocialError)
40
42
  return error;
@@ -42,17 +44,35 @@ function operationError(operation, error, mutation = true) {
42
44
  const ambiguous = mutation &&
43
45
  error.dispatched &&
44
46
  (error.kind !== "http" || (error.status !== undefined && error.status >= 500));
47
+ const code = ambiguous
48
+ ? "ambiguous_outcome"
49
+ : error.kind === "timeout"
50
+ ? "timeout"
51
+ : error.kind === "cancelled"
52
+ ? "cancelled"
53
+ : error.status === 401
54
+ ? "reconnect_required"
55
+ : error.status === 403
56
+ ? "missing_permission"
57
+ : error.status === 404
58
+ ? "not_found"
59
+ : error.status === 429
60
+ ? "rate_limited"
61
+ : "upstream_failure";
45
62
  return new SocialError({
46
- code: ambiguous
47
- ? "ambiguous_outcome"
48
- : error.kind === "timeout"
49
- ? "timeout"
50
- : error.kind === "cancelled"
51
- ? "cancelled"
52
- : "upstream_failure",
63
+ code,
53
64
  operation,
54
65
  message: error.message,
55
- retryDisposition: ambiguous ? { kind: "reconcile-first" } : { kind: "never" },
66
+ retryDisposition: ambiguous
67
+ ? { kind: "reconcile-first" }
68
+ : error.status === 401
69
+ ? { kind: "after-reconnect" }
70
+ : error.status === 429 && error.retryAfterMs !== undefined
71
+ ? { kind: "after-delay", delayMs: error.retryAfterMs }
72
+ : !mutation && error.status !== undefined && error.status >= 500
73
+ ? { kind: "after-delay", delayMs: 1000 }
74
+ : { kind: "never" },
75
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
56
76
  ...(error.status === undefined ? {} : { upstreamStatus: error.status }),
57
77
  });
58
78
  }
@@ -64,6 +84,7 @@ function operationError(operation, error, mutation = true) {
64
84
  cause: error,
65
85
  });
66
86
  }
87
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- validated boundary or fixture contract.
67
88
  function postRef(value) {
68
89
  const record = object(value);
69
90
  return { uri: string(record["uri"]), cid: string(record["cid"]) };
@@ -72,7 +93,7 @@ function linkFacets(text) {
72
93
  const facets = [];
73
94
  const urlPattern = /https?:\/\/[^\s<>]+/g;
74
95
  for (const match of text.matchAll(urlPattern)) {
75
- const value = match[0];
96
+ const value = match[0]?.replace(/[.,;:!?)]*$/u, "");
76
97
  const start = match.index;
77
98
  if (start === undefined || value === undefined)
78
99
  continue;
@@ -86,13 +107,16 @@ function linkFacets(text) {
86
107
  }
87
108
  return facets;
88
109
  }
110
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- validated boundary or fixture contract.
89
111
  function richTextOptions(text, options) {
90
112
  const invalid = (message) => {
91
113
  throw new SocialError({ code: "invalid_input", operation: "bluesky.prepare", message });
92
114
  };
93
115
  if (options !== undefined &&
116
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
94
117
  (options === null || typeof options !== "object" || Array.isArray(options)))
95
118
  invalid("Bluesky options must be an object.");
119
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
96
120
  const config = (options ?? {});
97
121
  if (Object.keys(config).some((key) => key !== "languages" && key !== "mentions"))
98
122
  invalid("A supplied Bluesky option is not supported.");
@@ -101,9 +125,11 @@ function richTextOptions(text, options) {
101
125
  const languages = config["languages"];
102
126
  if (!Array.isArray(languages) ||
103
127
  languages.length > 3 ||
128
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
104
129
  languages.some((value) => typeof value !== "string"))
105
130
  invalid("Bluesky accepts at most three language tags.");
106
131
  try {
132
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
107
133
  langs = Intl.getCanonicalLocales(languages);
108
134
  }
109
135
  catch {
@@ -116,11 +142,16 @@ function richTextOptions(text, options) {
116
142
  if (!Array.isArray(mentions) || mentions.length > 100)
117
143
  invalid("Bluesky mentions must be an array of at most 100 DID references.");
118
144
  const spans = [];
145
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
119
146
  for (const value of mentions) {
147
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
120
148
  if (!value || typeof value !== "object" || Array.isArray(value))
121
149
  invalid("Each mention requires UTF-8 offsets and a DID.");
122
150
  const start = value["byteStart"], end = value["byteEnd"], did = value["did"];
123
- if (typeof start !== "number" ||
151
+ if (
152
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
153
+ typeof start !== "number" ||
154
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
124
155
  typeof end !== "number" ||
125
156
  !Number.isSafeInteger(start) ||
126
157
  !Number.isSafeInteger(end) ||
@@ -129,14 +160,19 @@ function richTextOptions(text, options) {
129
160
  end > bytes.length ||
130
161
  (bytes[start] & 0xc0) === 0x80 ||
131
162
  (end < bytes.length && (bytes[end] & 0xc0) === 0x80) ||
163
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
132
164
  typeof did !== "string" ||
133
165
  !/^did:[a-z]+:[A-Za-z0-9._:%-]+$/.test(did))
134
166
  invalid("Mention offsets must span complete UTF-8 characters and identify a DID.");
135
- const byteStart = start, byteEnd = end;
167
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
168
+ const byteStart = start,
169
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
170
+ byteEnd = end;
136
171
  if (!new TextDecoder().decode(bytes.slice(byteStart, byteEnd)).startsWith("@"))
137
172
  invalid("A mention span must start with @.");
138
173
  if (spans.some((span) => byteStart < span.end && byteEnd > span.start) ||
139
174
  facets.some((facet) => {
175
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
140
176
  const index = facet["index"];
141
177
  return byteStart < Number(index["byteEnd"]) && byteEnd > Number(index["byteStart"]);
142
178
  }))
@@ -144,15 +180,35 @@ function richTextOptions(text, options) {
144
180
  spans.push({ start: byteStart, end: byteEnd });
145
181
  facets.push({
146
182
  index: { byteStart, byteEnd },
183
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
147
184
  features: [{ $type: "app.bsky.richtext.facet#mention", did: did }],
148
185
  });
149
186
  }
150
- facets.sort((left, right) => Number(left["index"]["byteStart"]) -
187
+ facets.sort((left, right) =>
188
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
189
+ Number(left["index"]["byteStart"]) -
190
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
151
191
  Number(right["index"]["byteStart"]));
192
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
152
193
  return { ...(langs === undefined ? {} : { langs }), ...(facets.length ? { facets } : {}) };
153
194
  }
154
195
  function graphemeCount(text) {
155
- const Segmenter = Intl.Segmenter;
196
+ const Segmenter =
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 -- provider payload is validated at this adapter boundary.
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
+ // oxlint-disable-next-line anti-slop/no-chained-type-assertions -- validated external boundary or fixture contract.
206
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract.
207
+ // oxlint-disable-next-line anti-slop/no-chained-type-assertions -- validated external boundary or fixture contract.
208
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract.
209
+ // oxlint-disable-next-line anti-slop/no-chained-type-assertions -- validated external boundary or fixture contract.
210
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract.
211
+ Intl.Segmenter;
156
212
  return Segmenter === undefined
157
213
  ? Array.from(text).length
158
214
  : [...new Segmenter(undefined, { granularity: "grapheme" }).segment(text)].length;
@@ -247,6 +303,7 @@ export function bluesky(options) {
247
303
  return options.session.fetchHandler(url.pathname + url.search, init);
248
304
  }
249
305
  : fetcher,
306
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
250
307
  ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
251
308
  });
252
309
  const auth = options.auth;
@@ -298,6 +355,26 @@ export function bluesky(options) {
298
355
  availability: "available",
299
356
  formats: ["text", "image"],
300
357
  },
358
+ ...[
359
+ "graph.read",
360
+ "graph.follow",
361
+ "graph.unfollow",
362
+ "graph.block",
363
+ "graph.unblock",
364
+ "graph.mute",
365
+ "graph.unmute",
366
+ "likes.read",
367
+ "likes.write",
368
+ "lists.read",
369
+ "lists.write",
370
+ "profiles.search",
371
+ "moderation.report",
372
+ ].map((operation) => ({
373
+ operation,
374
+ platform: "bluesky",
375
+ availability: "available",
376
+ requiredScopes: ["repo"],
377
+ })),
301
378
  {
302
379
  operation: "posts.list",
303
380
  platform: "bluesky",
@@ -327,7 +404,7 @@ export function bluesky(options) {
327
404
  {
328
405
  operation: "posts.publish.video",
329
406
  platform: "bluesky",
330
- availability: "available",
407
+ availability: "not-implemented-by-adapter",
331
408
  formats: ["video"],
332
409
  requiredScopes: ["repo"],
333
410
  },
@@ -335,14 +412,14 @@ export function bluesky(options) {
335
412
  "posts.repost",
336
413
  "posts.quote",
337
414
  "posts.delete",
338
- "graph.follow",
339
- "graph.block",
340
- "graph.mute",
415
+ "posts.removeFromPlatform",
341
416
  "notifications.read",
342
417
  "notifications.seen",
343
418
  "profile.read",
419
+ "profiles.read",
344
420
  "profile.update",
345
421
  "feeds.read",
422
+ "search.posts",
346
423
  "chat.read",
347
424
  "chat.write",
348
425
  ].map((operation) => ({
@@ -356,16 +433,21 @@ export function bluesky(options) {
356
433
  };
357
434
  async function xrpc(method, context, init = {}) {
358
435
  try {
436
+ const headers = new Headers(init.headers);
437
+ if (typeof init.body === "string" && !headers.has("Content-Type"))
438
+ headers.set("Content-Type", "application/json");
439
+ if (method.startsWith("chat.bsky.") && !headers.has("atproto-proxy"))
440
+ headers.set("atproto-proxy", "did:web:api.bsky.chat#bsky_chat");
359
441
  return await http({
360
442
  url: endpoint(service, method),
361
443
  timeoutMs: remainingBudget(context),
362
444
  method: init.body === undefined ? "GET" : "POST",
363
445
  ...(options.session
364
- ? init.headers === undefined
365
- ? {}
366
- : { headers: init.headers }
367
- : { headers: { ...authHeaders(auth), ...init.headers } }),
446
+ ? { headers }
447
+ : { headers: { ...authHeaders(auth), ...Object.fromEntries(headers.entries()) } }),
448
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
368
449
  ...(init.body === undefined ? {} : { body: init.body }),
450
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
369
451
  ...(context.signal === undefined ? {} : { signal: context.signal }),
370
452
  maxAttempts: init.maxAttempts ??
371
453
  (init.body === undefined ? Math.min(context.retryBudget.maxAttempts, 3) : 1),
@@ -387,7 +469,9 @@ export function bluesky(options) {
387
469
  message: "The configured credential belongs to a different DID.",
388
470
  });
389
471
  }
472
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
390
473
  const handle = typeof session["handle"] === "string" ? session["handle"] : undefined;
474
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
391
475
  return { did, ...(handle === undefined ? {} : { handle }) };
392
476
  }
393
477
  const nativeContext = (context, operation) => context ?? {
@@ -403,6 +487,34 @@ export function bluesky(options) {
403
487
  message: "Account reference does not belong to this Bluesky adapter.",
404
488
  });
405
489
  };
490
+ const pageQuery = (input) => {
491
+ const limit = input.limit ?? 50;
492
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
493
+ throw new SocialError({
494
+ code: "invalid_input",
495
+ operation: "bluesky.graph.read",
496
+ message: "Page size must be between 1 and 100.",
497
+ });
498
+ return new URLSearchParams({
499
+ limit: String(limit),
500
+ ...(input.cursor ? { cursor: input.cursor } : {}),
501
+ });
502
+ };
503
+ const recordKey = (uri, collection) => {
504
+ const prefix = `at://${auth.did}/${collection}/`;
505
+ const key = uri.startsWith(prefix) ? uri.slice(prefix.length) : "";
506
+ if (!/^[A-Za-z0-9._~:-]{1,512}$/.test(key) || key === "." || key === "..")
507
+ throw new SocialError({
508
+ code: "invalid_input",
509
+ operation: "bluesky.record.delete",
510
+ message: "Record URI must belong to this account.",
511
+ });
512
+ return key;
513
+ };
514
+ const actorPage = (response, key) => ({
515
+ actors: array(response[key] ?? []).map((value) => object(value)),
516
+ ...(typeof response["cursor"] === "string" ? { cursor: response["cursor"] } : {}),
517
+ });
406
518
  const native = {
407
519
  async getPost(input) {
408
520
  const context = input.context ?? {
@@ -420,6 +532,7 @@ export function bluesky(options) {
420
532
  operation: "bluesky.getPost",
421
533
  message: "Bluesky post was not found.",
422
534
  });
535
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
423
536
  return object(post);
424
537
  },
425
538
  async getPostThread(input) {
@@ -429,8 +542,87 @@ export function bluesky(options) {
429
542
  backendInstance: backend,
430
543
  };
431
544
  const query = new URLSearchParams({ uri: input.uri });
545
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
432
546
  return object(await xrpc(`app.bsky.feed.getPostThread?${query.toString()}`, context));
433
547
  },
548
+ async searchPosts(input) {
549
+ const context = nativeContext(input.context, "bluesky.search.posts");
550
+ assertNativeAccount(input.account, context, "bluesky.search.posts");
551
+ const queryText = input.query.trim();
552
+ if (!queryText)
553
+ throw new SocialError({
554
+ code: "invalid_input",
555
+ operation: "bluesky.search.posts",
556
+ message: "Bluesky search query is required.",
557
+ retryDisposition: { kind: "never" },
558
+ });
559
+ const limit = input.limit ?? 50;
560
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
561
+ throw new SocialError({
562
+ code: "invalid_input",
563
+ operation: "bluesky.search.posts",
564
+ message: "Bluesky search page size must be between 1 and 100.",
565
+ retryDisposition: { kind: "never" },
566
+ });
567
+ if (input.sort !== undefined && input.sort !== "latest" && input.sort !== "top")
568
+ throw new SocialError({
569
+ code: "invalid_input",
570
+ operation: "bluesky.search.posts",
571
+ message: "Bluesky search sort must be latest or top.",
572
+ retryDisposition: { kind: "never" },
573
+ });
574
+ if (input.scope === "all")
575
+ throw new SocialError({
576
+ code: "invalid_input",
577
+ operation: "bluesky.search.posts",
578
+ message: "Bluesky search does not support scope 'all'.",
579
+ });
580
+ if (input.context !== undefined && input.context.backendInstance !== backend)
581
+ throw new SocialError({
582
+ code: "unauthorized",
583
+ operation: "bluesky.search.posts",
584
+ message: "Account reference does not belong to this Bluesky adapter.",
585
+ });
586
+ const query = new URLSearchParams({ q: queryText, limit: String(limit) });
587
+ const optional = {
588
+ cursor: input.cursor,
589
+ sort: input.sort,
590
+ since: input.since,
591
+ until: input.until,
592
+ mentions: input.mentions,
593
+ author: input.author,
594
+ lang: input.lang,
595
+ domain: input.domain,
596
+ url: input.url,
597
+ };
598
+ for (const [key, value] of Object.entries(optional))
599
+ if (value !== undefined && value.trim() !== "")
600
+ query.set(key, value);
601
+ for (const tag of input.tags ?? []) {
602
+ const normalizedTag = tag.trim();
603
+ if (normalizedTag)
604
+ query.append("tag", normalizedTag);
605
+ }
606
+ const response = object(await xrpc(`app.bsky.feed.searchPosts?${query}`, context));
607
+ const posts = array(response["posts"]).map((post) => {
608
+ const value = object(post);
609
+ // The XRPC transport has already decoded a JSON payload; `object` validates
610
+ // the record boundary while the recursive JSON shape is preserved for callers.
611
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion
612
+ return value;
613
+ });
614
+ const cursorValue = response["cursor"];
615
+ const hitsTotalValue = response["hitsTotal"];
616
+ return {
617
+ posts,
618
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
619
+ ...(typeof cursorValue === "string" ? { cursor: cursorValue } : {}),
620
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
621
+ ...(typeof hitsTotalValue === "number" && Number.isSafeInteger(hitsTotalValue)
622
+ ? { hitsTotal: hitsTotalValue }
623
+ : {}),
624
+ };
625
+ },
434
626
  async likePost(input) {
435
627
  if (!accountMatches(input.account, auth, backend) ||
436
628
  (input.context && input.context.backendInstance !== backend))
@@ -464,9 +656,12 @@ export function bluesky(options) {
464
656
  }));
465
657
  const prefix = `at://${auth.did}/app.bsky.feed.like/`;
466
658
  const uri = response["uri"], cid = response["cid"];
467
- if (typeof uri !== "string" ||
659
+ if (
660
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
661
+ typeof uri !== "string" ||
468
662
  !uri.startsWith(prefix) ||
469
663
  !/^[A-Za-z0-9._~:-]{1,512}$/.test(uri.slice(prefix.length)) ||
664
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
470
665
  typeof cid !== "string" ||
471
666
  !cid)
472
667
  throw new SocialError({
@@ -521,7 +716,7 @@ export function bluesky(options) {
521
716
  async quotePost(input) {
522
717
  const context = nativeContext(input.context, "bluesky.quote");
523
718
  assertNativeAccount(input.account, context, "bluesky.quote");
524
- if (!input.text.trim() || input.text.length > 300)
719
+ if (!input.text.trim() || graphemeCount(input.text) > 300)
525
720
  throw new SocialError({
526
721
  code: "invalid_input",
527
722
  operation: "bluesky.quote",
@@ -556,23 +751,17 @@ export function bluesky(options) {
556
751
  body: JSON.stringify({ repo: auth.did, collection: "app.bsky.feed.post", rkey }),
557
752
  });
558
753
  },
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
- }));
754
+ async uploadVideo(_input) {
755
+ throw new SocialError({
756
+ code: "unsupported_capability",
757
+ operation: "bluesky.video.upload",
758
+ message: "Bluesky video upload requires the video service authentication and job polling flow; this adapter does not implement it.",
759
+ });
572
760
  },
573
761
  async follow(input) {
574
762
  const context = nativeContext(input.context, "bluesky.follow");
575
763
  assertNativeAccount(input.account, context, "bluesky.follow");
764
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
576
765
  return object(await xrpc("com.atproto.repo.createRecord", context, {
577
766
  body: JSON.stringify({
578
767
  repo: auth.did,
@@ -585,9 +774,21 @@ export function bluesky(options) {
585
774
  }),
586
775
  }));
587
776
  },
777
+ async unfollow(input) {
778
+ const context = nativeContext(input.context, "bluesky.unfollow");
779
+ assertNativeAccount(input.account, context, "bluesky.unfollow");
780
+ await xrpc("com.atproto.repo.deleteRecord", context, {
781
+ body: JSON.stringify({
782
+ repo: auth.did,
783
+ collection: "app.bsky.graph.follow",
784
+ rkey: recordKey(input.followUri, "app.bsky.graph.follow"),
785
+ }),
786
+ });
787
+ },
588
788
  async block(input) {
589
789
  const context = nativeContext(input.context, "bluesky.block");
590
790
  assertNativeAccount(input.account, context, "bluesky.block");
791
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
591
792
  return object(await xrpc("com.atproto.repo.createRecord", context, {
592
793
  body: JSON.stringify({
593
794
  repo: auth.did,
@@ -600,20 +801,284 @@ export function bluesky(options) {
600
801
  }),
601
802
  }));
602
803
  },
804
+ async unblock(input) {
805
+ const context = nativeContext(input.context, "bluesky.unblock");
806
+ assertNativeAccount(input.account, context, "bluesky.unblock");
807
+ await xrpc("com.atproto.repo.deleteRecord", context, {
808
+ body: JSON.stringify({
809
+ repo: auth.did,
810
+ collection: "app.bsky.graph.block",
811
+ rkey: recordKey(input.blockUri, "app.bsky.graph.block"),
812
+ }),
813
+ });
814
+ },
603
815
  async mute(input) {
604
816
  const context = nativeContext(input.context, "bluesky.mute");
605
817
  assertNativeAccount(input.account, context, "bluesky.mute");
606
- return object(await xrpc("app.bsky.graph.muteActor", context, {
818
+ await xrpc("app.bsky.graph.muteActor", context, {
607
819
  body: JSON.stringify({ actor: input.did }),
608
- }));
820
+ });
821
+ },
822
+ async unmute(input) {
823
+ const context = nativeContext(input.context, "bluesky.unmute");
824
+ assertNativeAccount(input.account, context, "bluesky.unmute");
825
+ await xrpc("app.bsky.graph.unmuteActor", context, {
826
+ body: JSON.stringify({ actor: input.did }),
827
+ });
828
+ },
829
+ async getFollowers(input) {
830
+ const context = nativeContext(input.context, "bluesky.graph.followers");
831
+ assertNativeAccount(input.account, context, "bluesky.graph.followers");
832
+ const q = pageQuery(input);
833
+ q.set("actor", input.actor ?? auth.did);
834
+ return actorPage(object(await xrpc(`app.bsky.graph.getFollowers?${q}`, context)), "followers");
835
+ },
836
+ async getFollows(input) {
837
+ const context = nativeContext(input.context, "bluesky.graph.follows");
838
+ assertNativeAccount(input.account, context, "bluesky.graph.follows");
839
+ const q = pageQuery(input);
840
+ q.set("actor", input.actor ?? auth.did);
841
+ return actorPage(object(await xrpc(`app.bsky.graph.getFollows?${q}`, context)), "follows");
842
+ },
843
+ async getMutes(input) {
844
+ const context = nativeContext(input.context, "bluesky.graph.mutes");
845
+ assertNativeAccount(input.account, context, "bluesky.graph.mutes");
846
+ return actorPage(object(await xrpc(`app.bsky.graph.getMutes?${pageQuery(input)}`, context)), "mutes");
847
+ },
848
+ async getBlocks(input) {
849
+ const context = nativeContext(input.context, "bluesky.graph.blocks");
850
+ assertNativeAccount(input.account, context, "bluesky.graph.blocks");
851
+ return actorPage(object(await xrpc(`app.bsky.graph.getBlocks?${pageQuery(input)}`, context)), "blocks");
852
+ },
853
+ async getLikes(input) {
854
+ const context = nativeContext(input.context, "bluesky.likes.read");
855
+ assertNativeAccount(input.account, context, "bluesky.likes.read");
856
+ const q = pageQuery(input);
857
+ q.set("uri", input.uri);
858
+ if (input.cid)
859
+ q.set("cid", input.cid);
860
+ const value = object(await xrpc(`app.bsky.feed.getLikes?${q}`, context));
861
+ return {
862
+ likes: array(value["likes"] ?? []).map((entry) => object(entry)),
863
+ ...(typeof value["cursor"] === "string" ? { cursor: value["cursor"] } : {}),
864
+ };
865
+ },
866
+ async getActorLikes(input) {
867
+ const context = nativeContext(input.context, "bluesky.likes.actor");
868
+ assertNativeAccount(input.account, context, "bluesky.likes.actor");
869
+ const q = pageQuery(input);
870
+ q.set("actor", input.actor ?? auth.did);
871
+ const value = object(await xrpc(`app.bsky.feed.getActorLikes?${q}`, context));
872
+ return {
873
+ feed: array(value["feed"] ?? []).map((entry) => object(entry)),
874
+ ...(typeof value["cursor"] === "string" ? { cursor: value["cursor"] } : {}),
875
+ };
876
+ },
877
+ async searchActors(input) {
878
+ const context = nativeContext(input.context, "bluesky.profiles.search");
879
+ assertNativeAccount(input.account, context, "bluesky.profiles.search");
880
+ if (!input.query.trim())
881
+ throw new SocialError({
882
+ code: "invalid_input",
883
+ operation: "bluesky.profiles.search",
884
+ message: "Actor search query is required.",
885
+ });
886
+ const q = pageQuery(input);
887
+ q.set("q", input.query.trim());
888
+ return actorPage(object(await xrpc(`app.bsky.actor.searchActors?${q}`, context)), "actors");
889
+ },
890
+ async searchActorsTypeahead(input) {
891
+ const context = nativeContext(input.context, "bluesky.profiles.search");
892
+ assertNativeAccount(input.account, context, "bluesky.profiles.search");
893
+ if (!input.query.trim())
894
+ throw new SocialError({
895
+ code: "invalid_input",
896
+ operation: "bluesky.profiles.search",
897
+ message: "Actor search query is required.",
898
+ });
899
+ const limit = input.limit ?? 8;
900
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
901
+ throw new SocialError({
902
+ code: "invalid_input",
903
+ operation: "bluesky.profiles.search",
904
+ message: "Page size must be between 1 and 100.",
905
+ });
906
+ const q = new URLSearchParams({ q: input.query.trim(), limit: String(limit) });
907
+ return actorPage(object(await xrpc(`app.bsky.actor.searchActorsTypeahead?${q}`, context)), "actors");
908
+ },
909
+ async createList(input) {
910
+ const context = nativeContext(input.context, "bluesky.lists.create");
911
+ assertNativeAccount(input.account, context, "bluesky.lists.create");
912
+ const record = {
913
+ $type: "app.bsky.graph.list",
914
+ name: input.name,
915
+ purpose: input.purpose,
916
+ createdAt: new Date().toISOString(),
917
+ ...(input.description === undefined ? {} : { description: input.description }),
918
+ };
919
+ return postRef(object(await xrpc("com.atproto.repo.createRecord", context, {
920
+ body: JSON.stringify({ repo: auth.did, collection: "app.bsky.graph.list", record }),
921
+ })));
922
+ },
923
+ async updateList(input) {
924
+ const context = nativeContext(input.context, "bluesky.lists.update");
925
+ assertNativeAccount(input.account, context, "bluesky.lists.update");
926
+ const existing = object(await xrpc(`com.atproto.repo.getRecord?repo=${encodeURIComponent(auth.did)}&collection=app.bsky.graph.list&rkey=${encodeURIComponent(recordKey(input.listUri, "app.bsky.graph.list"))}`, context));
927
+ const record = object(existing["value"]);
928
+ const next = {
929
+ ...record,
930
+ name: input.name,
931
+ purpose: input.purpose,
932
+ ...(input.description === undefined ? {} : { description: input.description }),
933
+ };
934
+ await xrpc("com.atproto.repo.putRecord", context, {
935
+ body: JSON.stringify({
936
+ repo: auth.did,
937
+ collection: "app.bsky.graph.list",
938
+ rkey: recordKey(input.listUri, "app.bsky.graph.list"),
939
+ record: next,
940
+ }),
941
+ });
942
+ },
943
+ async deleteList(input) {
944
+ const context = nativeContext(input.context, "bluesky.lists.delete");
945
+ assertNativeAccount(input.account, context, "bluesky.lists.delete");
946
+ await xrpc("com.atproto.repo.deleteRecord", context, {
947
+ body: JSON.stringify({
948
+ repo: auth.did,
949
+ collection: "app.bsky.graph.list",
950
+ rkey: recordKey(input.uri, "app.bsky.graph.list"),
951
+ }),
952
+ });
953
+ },
954
+ async addListItem(input) {
955
+ const context = nativeContext(input.context, "bluesky.lists.items.add");
956
+ assertNativeAccount(input.account, context, "bluesky.lists.items.add");
957
+ return postRef(object(await xrpc("com.atproto.repo.createRecord", context, {
958
+ body: JSON.stringify({
959
+ repo: auth.did,
960
+ collection: "app.bsky.graph.listitem",
961
+ record: {
962
+ $type: "app.bsky.graph.listitem",
963
+ list: input.listUri,
964
+ subject: input.subject,
965
+ createdAt: new Date().toISOString(),
966
+ },
967
+ }),
968
+ })));
969
+ },
970
+ async removeListItem(input) {
971
+ const context = nativeContext(input.context, "bluesky.lists.items.remove");
972
+ assertNativeAccount(input.account, context, "bluesky.lists.items.remove");
973
+ await xrpc("com.atproto.repo.deleteRecord", context, {
974
+ body: JSON.stringify({
975
+ repo: auth.did,
976
+ collection: "app.bsky.graph.listitem",
977
+ rkey: recordKey(input.uri, "app.bsky.graph.listitem"),
978
+ }),
979
+ });
980
+ },
981
+ async getList(input) {
982
+ const context = nativeContext(input.context, "bluesky.lists.get");
983
+ assertNativeAccount(input.account, context, "bluesky.lists.get");
984
+ const q = pageQuery(input);
985
+ q.set("list", input.listUri);
986
+ return object(await xrpc(`app.bsky.graph.getList?${q}`, context));
987
+ },
988
+ async getLists(input) {
989
+ const context = nativeContext(input.context, "bluesky.lists.list");
990
+ assertNativeAccount(input.account, context, "bluesky.lists.list");
991
+ const q = pageQuery(input);
992
+ q.set("actor", input.actor ?? auth.did);
993
+ return object(await xrpc(`app.bsky.graph.getLists?${q}`, context));
994
+ },
995
+ async muteList(input) {
996
+ const context = nativeContext(input.context, "bluesky.lists.mute");
997
+ assertNativeAccount(input.account, context, "bluesky.lists.mute");
998
+ await xrpc("app.bsky.graph.muteActorList", context, {
999
+ body: JSON.stringify({ list: input.listUri }),
1000
+ });
1001
+ },
1002
+ async unmuteList(input) {
1003
+ const context = nativeContext(input.context, "bluesky.lists.unmute");
1004
+ assertNativeAccount(input.account, context, "bluesky.lists.unmute");
1005
+ await xrpc("app.bsky.graph.unmuteActorList", context, {
1006
+ body: JSON.stringify({ list: input.listUri }),
1007
+ });
1008
+ },
1009
+ async blockList(input) {
1010
+ const context = nativeContext(input.context, "bluesky.lists.block");
1011
+ assertNativeAccount(input.account, context, "bluesky.lists.block");
1012
+ await xrpc("com.atproto.repo.createRecord", context, {
1013
+ body: JSON.stringify({
1014
+ repo: auth.did,
1015
+ collection: "app.bsky.graph.listblock",
1016
+ record: {
1017
+ $type: "app.bsky.graph.listblock",
1018
+ subject: input.listUri,
1019
+ createdAt: new Date().toISOString(),
1020
+ },
1021
+ }),
1022
+ });
1023
+ },
1024
+ async unblockList(input) {
1025
+ const context = nativeContext(input.context, "bluesky.lists.unblock");
1026
+ assertNativeAccount(input.account, context, "bluesky.lists.unblock");
1027
+ let cursor;
1028
+ let record;
1029
+ do {
1030
+ const query = new URLSearchParams({
1031
+ repo: auth.did,
1032
+ collection: "app.bsky.graph.listblock",
1033
+ limit: "100",
1034
+ ...(cursor === undefined ? {} : { cursor }),
1035
+ });
1036
+ const records = object(await xrpc(`com.atproto.repo.listRecords?${query.toString()}`, context));
1037
+ record = array(records["records"])
1038
+ .map((value) => object(value))
1039
+ .find((value) => object(value["value"])["subject"] === input.listUri);
1040
+ cursor = typeof records["cursor"] === "string" ? records["cursor"] : undefined;
1041
+ } while (record === undefined && cursor !== undefined);
1042
+ if (record === undefined)
1043
+ throw new SocialError({
1044
+ code: "invalid_input",
1045
+ operation: "bluesky.lists.unblock",
1046
+ message: "No list block record was found for this list.",
1047
+ });
1048
+ await xrpc("com.atproto.repo.deleteRecord", context, {
1049
+ body: JSON.stringify({
1050
+ repo: auth.did,
1051
+ collection: "app.bsky.graph.listblock",
1052
+ rkey: recordKey(string(record["uri"]), "app.bsky.graph.listblock"),
1053
+ }),
1054
+ });
1055
+ },
1056
+ async createModerationReport(input) {
1057
+ const context = nativeContext(input.context, "bluesky.moderation.report");
1058
+ assertNativeAccount(input.account, context, "bluesky.moderation.report");
1059
+ const body = {
1060
+ reasonType: input.reasonType,
1061
+ subject: input.subject,
1062
+ ...(input.reason === undefined ? {} : { reason: input.reason }),
1063
+ };
1064
+ return object(await xrpc("com.atproto.moderation.createReport", context, { body: JSON.stringify(body) }));
609
1065
  },
610
1066
  async listNotifications(input) {
611
1067
  const context = nativeContext(input.context, "bluesky.notifications.list");
612
1068
  assertNativeAccount(input.account, context, "bluesky.notifications.list");
1069
+ const limit = input.limit ?? 50;
1070
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
1071
+ throw new SocialError({
1072
+ code: "invalid_input",
1073
+ operation: "bluesky.notifications.list",
1074
+ message: "Page size must be between 1 and 100.",
1075
+ });
613
1076
  const query = new URLSearchParams({
614
- limit: String(input.limit ?? 50),
1077
+ limit: String(limit),
1078
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
615
1079
  ...(input.cursor ? { cursor: input.cursor } : {}),
616
1080
  });
1081
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
617
1082
  return object(await xrpc(`app.bsky.notification.listNotifications?${query}`, context));
618
1083
  },
619
1084
  async markNotificationsSeen(input) {
@@ -626,6 +1091,7 @@ export function bluesky(options) {
626
1091
  async getProfile(input) {
627
1092
  const context = nativeContext(input.context, "bluesky.profile.get");
628
1093
  assertNativeAccount(input.account, context, "bluesky.profile.get");
1094
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
629
1095
  return object(await xrpc(`app.bsky.actor.getProfile?actor=${encodeURIComponent(input.actor ?? auth.did)}`, context));
630
1096
  },
631
1097
  async updateProfile(input) {
@@ -643,28 +1109,55 @@ export function bluesky(options) {
643
1109
  async listFeeds(input) {
644
1110
  const context = nativeContext(input.context, "bluesky.feeds.list");
645
1111
  assertNativeAccount(input.account, context, "bluesky.feeds.list");
1112
+ const limit = input.limit ?? 50;
1113
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
1114
+ throw new SocialError({
1115
+ code: "invalid_input",
1116
+ operation: "bluesky.feeds.list",
1117
+ message: "Page size must be between 1 and 100.",
1118
+ });
646
1119
  const query = new URLSearchParams({
647
- limit: String(input.limit ?? 50),
1120
+ limit: String(limit),
1121
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
648
1122
  ...(input.cursor ? { cursor: input.cursor } : {}),
649
1123
  });
1124
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
650
1125
  return object(await xrpc(`app.bsky.feed.getSuggestedFeeds?${query}`, context));
651
1126
  },
652
1127
  async listConversations(input) {
653
1128
  const context = nativeContext(input.context, "bluesky.chat.list");
654
1129
  assertNativeAccount(input.account, context, "bluesky.chat.list");
1130
+ const limit = input.limit ?? 50;
1131
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
1132
+ throw new SocialError({
1133
+ code: "invalid_input",
1134
+ operation: "bluesky.chat.list",
1135
+ message: "Page size must be between 1 and 100.",
1136
+ });
655
1137
  const query = new URLSearchParams({
656
- limit: String(input.limit ?? 50),
1138
+ limit: String(limit),
1139
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
657
1140
  ...(input.cursor ? { cursor: input.cursor } : {}),
658
1141
  });
1142
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
659
1143
  return object(await xrpc(`chat.bsky.convo.listConvos?${query}`, context));
660
1144
  },
661
1145
  async listMessages(input) {
662
1146
  const context = nativeContext(input.context, "bluesky.chat.messages");
663
1147
  assertNativeAccount(input.account, context, "bluesky.chat.messages");
1148
+ const limit = input.limit ?? 50;
1149
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
1150
+ throw new SocialError({
1151
+ code: "invalid_input",
1152
+ operation: "bluesky.chat.messages",
1153
+ message: "Page size must be between 1 and 100.",
1154
+ });
664
1155
  const query = new URLSearchParams({
665
- limit: String(input.limit ?? 50),
1156
+ limit: String(limit),
1157
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
666
1158
  ...(input.cursor ? { cursor: input.cursor } : {}),
667
1159
  });
1160
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
668
1161
  return object(await xrpc(`chat.bsky.convo.getMessages?convoId=${encodeURIComponent(input.conversationId)}&${query}`, context));
669
1162
  },
670
1163
  async sendMessage(input) {
@@ -676,6 +1169,7 @@ export function bluesky(options) {
676
1169
  operation: "bluesky.chat.send",
677
1170
  message: "Message text is required.",
678
1171
  });
1172
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
679
1173
  return object(await xrpc("chat.bsky.convo.sendMessage", context, {
680
1174
  body: JSON.stringify({ convoId: input.conversationId, message: { text: input.text } }),
681
1175
  }));
@@ -685,6 +1179,123 @@ export function bluesky(options) {
685
1179
  id: backend,
686
1180
  capabilities,
687
1181
  native,
1182
+ graph: {
1183
+ async getProfile(account, input, context) {
1184
+ const value = await native.getProfile({
1185
+ account,
1186
+ ...(input.profileId === undefined ? {} : { actor: input.profileId }),
1187
+ ...(input.profileId === undefined && input.handle !== undefined
1188
+ ? { actor: input.handle }
1189
+ : {}),
1190
+ context,
1191
+ });
1192
+ const actor = object(value);
1193
+ const id = string(actor["did"]);
1194
+ return {
1195
+ ref: profileRef({
1196
+ backend,
1197
+ platform: "bluesky",
1198
+ accountId: account.accountId,
1199
+ profileId: id,
1200
+ }),
1201
+ ...(typeof actor["displayName"] === "string"
1202
+ ? { displayName: actor["displayName"] }
1203
+ : {}),
1204
+ ...(typeof actor["handle"] === "string" ? { handle: actor["handle"] } : {}),
1205
+ ...(typeof actor["avatar"] === "string" ? { avatarUrl: actor["avatar"] } : {}),
1206
+ ...(typeof actor["description"] === "string" ? { bio: actor["description"] } : {}),
1207
+ native: actor,
1208
+ };
1209
+ },
1210
+ async listRelationships(account, input, context) {
1211
+ const result = input.kind === "following"
1212
+ ? await native.getFollows({
1213
+ account,
1214
+ context,
1215
+ ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
1216
+ ...(input.limit === undefined ? {} : { limit: input.limit }),
1217
+ })
1218
+ : input.kind === "followers"
1219
+ ? await native.getFollowers({
1220
+ account,
1221
+ context,
1222
+ ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
1223
+ ...(input.limit === undefined ? {} : { limit: input.limit }),
1224
+ })
1225
+ : input.kind === "blocked"
1226
+ ? await native.getBlocks({
1227
+ account,
1228
+ context,
1229
+ ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
1230
+ ...(input.limit === undefined ? {} : { limit: input.limit }),
1231
+ })
1232
+ : await native.getMutes({
1233
+ account,
1234
+ context,
1235
+ ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
1236
+ ...(input.limit === undefined ? {} : { limit: input.limit }),
1237
+ });
1238
+ return {
1239
+ items: result.actors.map((actor) => {
1240
+ const value = object(actor);
1241
+ const id = string(value["did"]);
1242
+ return {
1243
+ profile: profileRef({
1244
+ backend,
1245
+ platform: "bluesky",
1246
+ accountId: account.accountId,
1247
+ profileId: id,
1248
+ }),
1249
+ relationship: input.kind === "following"
1250
+ ? "following"
1251
+ : input.kind === "followers"
1252
+ ? "follower"
1253
+ : input.kind,
1254
+ };
1255
+ }),
1256
+ ...(result.cursor === undefined ? {} : { nextCursor: result.cursor }),
1257
+ };
1258
+ },
1259
+ async follow(target, context) {
1260
+ await native.follow({ account, did: target.profileId, context });
1261
+ return { profile: target, relationship: "following" };
1262
+ },
1263
+ async unfollow(target, context) {
1264
+ const profile = object(await native.getProfile({ account, actor: target.profileId, context }));
1265
+ const viewer = object(profile["viewer"] ?? {});
1266
+ const uri = viewer["following"];
1267
+ if (typeof uri !== "string" || !uri)
1268
+ throw new SocialError({
1269
+ code: "invalid_input",
1270
+ operation: "graph.unfollow",
1271
+ message: "A follow record URI is required to unfollow.",
1272
+ });
1273
+ await native.unfollow({ account, followUri: uri, context });
1274
+ },
1275
+ async block(target, context) {
1276
+ await native.block({ account, did: target.profileId, context });
1277
+ return { profile: target, relationship: "blocked" };
1278
+ },
1279
+ async unblock(target, context) {
1280
+ const profile = object(await native.getProfile({ account, actor: target.profileId, context }));
1281
+ const viewer = object(profile["viewer"] ?? {});
1282
+ const uri = viewer["blocking"];
1283
+ if (typeof uri !== "string" || !uri)
1284
+ throw new SocialError({
1285
+ code: "invalid_input",
1286
+ operation: "graph.unblock",
1287
+ message: "A block record URI is required to unblock.",
1288
+ });
1289
+ await native.unblock({ account, blockUri: uri, context });
1290
+ },
1291
+ async mute(target, context) {
1292
+ await native.mute({ account, did: target.profileId, context });
1293
+ return { profile: target, relationship: "muted" };
1294
+ },
1295
+ async unmute(target, context) {
1296
+ await native.unmute({ account, did: target.profileId, context });
1297
+ },
1298
+ },
688
1299
  accounts: {
689
1300
  async list(_input, context) {
690
1301
  const session = await verifiedSession(context);
@@ -693,6 +1304,7 @@ export function bluesky(options) {
693
1304
  {
694
1305
  ref: account,
695
1306
  displayName: session.handle ?? session.did,
1307
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
696
1308
  ...(session.handle === undefined ? {} : { handle: session.handle }),
697
1309
  status: "connected",
698
1310
  },
@@ -711,11 +1323,85 @@ export function bluesky(options) {
711
1323
  return {
712
1324
  ref: account,
713
1325
  displayName: session.handle ?? session.did,
1326
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
714
1327
  ...(session.handle === undefined ? {} : { handle: session.handle }),
715
1328
  status: "connected",
716
1329
  };
717
1330
  },
718
1331
  },
1332
+ search: {
1333
+ async posts(account, input, context) {
1334
+ if (!accountMatches(account, auth, backend))
1335
+ throw new SocialError({
1336
+ code: "unauthorized",
1337
+ operation: "search.posts",
1338
+ message: "The account reference does not belong to this adapter.",
1339
+ account,
1340
+ });
1341
+ const result = await native.searchPosts({
1342
+ account,
1343
+ query: input.query,
1344
+ ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
1345
+ ...(input.limit === undefined ? {} : { limit: input.limit }),
1346
+ ...(input.startTime === undefined ? {} : { since: input.startTime }),
1347
+ ...(input.endTime === undefined ? {} : { until: input.endTime }),
1348
+ ...(input.scope === undefined ? {} : { scope: input.scope }),
1349
+ context,
1350
+ });
1351
+ return {
1352
+ items: result.posts,
1353
+ ...(result.cursor === undefined ? {} : { nextCursor: result.cursor }),
1354
+ ...(result.hitsTotal === undefined ? {} : { metadata: { hitsTotal: result.hitsTotal } }),
1355
+ };
1356
+ },
1357
+ },
1358
+ notifications: {
1359
+ async list(account, input, context) {
1360
+ if (!accountMatches(account, auth, backend))
1361
+ throw new SocialError({
1362
+ code: "unauthorized",
1363
+ operation: "bluesky.notifications.list",
1364
+ message: "The account reference does not belong to this adapter.",
1365
+ account,
1366
+ });
1367
+ const response = await native.listNotifications({
1368
+ account,
1369
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
1370
+ ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
1371
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
1372
+ ...(input.limit === undefined ? {} : { limit: input.limit }),
1373
+ context,
1374
+ });
1375
+ const values = response["notifications"];
1376
+ const items = values === undefined
1377
+ ? []
1378
+ : array(values).map((value) => {
1379
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated provider notification object.
1380
+ return object(value);
1381
+ });
1382
+ const cursor = response["cursor"];
1383
+ return {
1384
+ items,
1385
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
1386
+ ...(typeof cursor === "string" ? { nextCursor: cursor } : {}),
1387
+ };
1388
+ },
1389
+ async markSeen(account, input, context) {
1390
+ if (!accountMatches(account, auth, backend))
1391
+ throw new SocialError({
1392
+ code: "unauthorized",
1393
+ operation: "bluesky.notifications.seen",
1394
+ message: "The account reference does not belong to this adapter.",
1395
+ account,
1396
+ });
1397
+ await native.markNotificationsSeen({
1398
+ account,
1399
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
1400
+ ...(input.seenAt === undefined ? {} : { seenAt: input.seenAt }),
1401
+ context,
1402
+ });
1403
+ },
1404
+ },
719
1405
  posts: {
720
1406
  async list(account, input, context) {
721
1407
  if (!accountMatches(account, auth, backend))
@@ -734,6 +1420,7 @@ export function bluesky(options) {
734
1420
  const query = new URLSearchParams({
735
1421
  actor: auth.did,
736
1422
  limit: String(limit),
1423
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
737
1424
  ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
738
1425
  });
739
1426
  const response = object(await xrpc(`app.bsky.feed.getAuthorFeed?${query.toString()}`, context));
@@ -751,6 +1438,7 @@ export function bluesky(options) {
751
1438
  "indexedAt",
752
1439
  "labels",
753
1440
  ]),
1441
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
754
1442
  ...(post["author"] === undefined
755
1443
  ? {}
756
1444
  : {
@@ -761,6 +1449,7 @@ export function bluesky(options) {
761
1449
  "avatar",
762
1450
  ]),
763
1451
  }),
1452
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
764
1453
  ...(post["record"] === undefined
765
1454
  ? {}
766
1455
  : {
@@ -773,16 +1462,21 @@ export function bluesky(options) {
773
1462
  ]),
774
1463
  }),
775
1464
  };
1465
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
776
1466
  return {
777
1467
  post: safePost,
1468
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
778
1469
  ...(entry["reason"] === undefined
779
1470
  ? {}
780
1471
  : { reason: publicFields(object(entry["reason"]), ["$type", "by", "indexedAt"]) }),
781
1472
  };
782
1473
  });
1474
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
783
1475
  const cursor = typeof response["cursor"] === "string" ? response["cursor"] : undefined;
784
1476
  return {
1477
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
785
1478
  items: feed,
1479
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
786
1480
  ...(cursor === undefined ? {} : { nextCursor: cursor }),
787
1481
  };
788
1482
  },
@@ -842,6 +1536,14 @@ export function bluesky(options) {
842
1536
  try {
843
1537
  const content = target.content;
844
1538
  const text = content.text ?? "";
1539
+ // oxlint-disable-next-line anti-slop/no-known-value-widening -- validated boundary or fixture contract.
1540
+ // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- validated boundary or fixture contract.
1541
+ // oxlint-disable-next-line anti-slop/no-known-value-widening -- provider payload is validated at this adapter boundary.
1542
+ // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- validated external boundary or fixture contract.
1543
+ // oxlint-disable-next-line anti-slop/no-known-value-widening -- validated external boundary or fixture contract.
1544
+ // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- validated external boundary or fixture contract.
1545
+ // oxlint-disable-next-line anti-slop/no-known-value-widening -- validated external boundary or fixture contract.
1546
+ // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- validated external boundary or fixture contract.
845
1547
  const record = {
846
1548
  $type: "app.bsky.feed.post",
847
1549
  text,
@@ -871,10 +1573,12 @@ export function bluesky(options) {
871
1573
  Object.assign(record, { reply: { root, parent: parentRef } });
872
1574
  }
873
1575
  if (content.media !== undefined && content.media.length > 0) {
1576
+ // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- validated boundary or fixture contract.
874
1577
  const blobs = [];
875
1578
  for (const media of content.media) {
876
1579
  const source = await readMedia(media.source, fetcher, context, allowMediaHost);
877
1580
  const blob = object(await xrpc("com.atproto.repo.uploadBlob", context, {
1581
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
878
1582
  body: new Blob([source.bytes.slice().buffer], {
879
1583
  type: source.mimeType,
880
1584
  }),
@@ -935,6 +1639,26 @@ export function bluesky(options) {
935
1639
  });
936
1640
  return native.getPost({ uri: ref.postId, context });
937
1641
  },
1642
+ async removeFromPlatform(ref, context) {
1643
+ if (!accountMatches(connectedAccountRef({
1644
+ backend: ref.backend,
1645
+ platform: "bluesky",
1646
+ accountId: ref.accountId,
1647
+ }), auth, backend))
1648
+ throw new SocialError({
1649
+ code: "unauthorized",
1650
+ operation: "posts.removeFromPlatform",
1651
+ message: "The post reference does not belong to this Bluesky adapter.",
1652
+ });
1653
+ await native.deletePost({
1654
+ account,
1655
+ post: {
1656
+ uri: ref.native?.["uri"]?.toString() ?? ref.postId,
1657
+ cid: ref.native?.["cid"]?.toString() ?? "",
1658
+ },
1659
+ context,
1660
+ });
1661
+ },
938
1662
  },
939
1663
  comments: {
940
1664
  async list(post, input, context) {
@@ -946,6 +1670,7 @@ export function bluesky(options) {
946
1670
  });
947
1671
  const thread = await native.getPostThread({ uri: post.postId, context });
948
1672
  const replies = array(object(thread["thread"])["replies"] ?? []);
1673
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
949
1674
  return { items: replies.map((reply) => object(reply)) };
950
1675
  },
951
1676
  async reply(comment, content, context) {
@@ -995,6 +1720,7 @@ export function bluesky(options) {
995
1720
  ["quotes", "quoteCount"],
996
1721
  ]) {
997
1722
  const count = record[field];
1723
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
998
1724
  if (typeof count === "number" && Number.isFinite(count))
999
1725
  metrics.push({
1000
1726
  name,
@@ -1028,7 +1754,9 @@ export function bluesky(options) {
1028
1754
  ["followers", profile["followersCount"]],
1029
1755
  ["following", profile["followsCount"]],
1030
1756
  ["posts", profile["postsCount"]],
1031
- ].flatMap(([name, value]) => typeof value === "number" && Number.isFinite(value)
1757
+ ].flatMap(([name, value]) =>
1758
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
1759
+ typeof value === "number" && Number.isFinite(value)
1032
1760
  ? [
1033
1761
  {
1034
1762
  name,