@atproto/lex-client 0.2.0 → 0.2.1

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 (74) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/agent.d.ts +1 -1
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js.map +1 -1
  5. package/dist/client.d.ts +10 -9
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +3 -2
  8. package/dist/client.js.map +1 -1
  9. package/dist/errors.d.ts +6 -4
  10. package/dist/errors.d.ts.map +1 -1
  11. package/dist/errors.js +3 -3
  12. package/dist/errors.js.map +1 -1
  13. package/dist/response.d.ts +3 -2
  14. package/dist/response.d.ts.map +1 -1
  15. package/dist/response.js +2 -1
  16. package/dist/response.js.map +1 -1
  17. package/dist/types.d.ts +1 -1
  18. package/dist/types.d.ts.map +1 -1
  19. package/dist/types.js.map +1 -1
  20. package/dist/util.d.ts +3 -2
  21. package/dist/util.d.ts.map +1 -1
  22. package/dist/util.js +1 -0
  23. package/dist/util.js.map +1 -1
  24. package/dist/write-operation-builder.d.ts +3 -2
  25. package/dist/write-operation-builder.d.ts.map +1 -1
  26. package/dist/write-operation-builder.js +2 -2
  27. package/dist/write-operation-builder.js.map +1 -1
  28. package/dist/xrpc.d.ts +8 -6
  29. package/dist/xrpc.d.ts.map +1 -1
  30. package/dist/xrpc.js +1 -1
  31. package/dist/xrpc.js.map +1 -1
  32. package/package.json +11 -14
  33. package/src/agent.test.ts +0 -216
  34. package/src/agent.ts +0 -186
  35. package/src/client.ts +0 -1179
  36. package/src/errors.test.ts +0 -626
  37. package/src/errors.ts +0 -570
  38. package/src/index.ts +0 -6
  39. package/src/lexicons/com/atproto/repo/applyWrites.defs.ts +0 -201
  40. package/src/lexicons/com/atproto/repo/applyWrites.ts +0 -6
  41. package/src/lexicons/com/atproto/repo/createRecord.defs.ts +0 -58
  42. package/src/lexicons/com/atproto/repo/createRecord.ts +0 -6
  43. package/src/lexicons/com/atproto/repo/defs.defs.ts +0 -28
  44. package/src/lexicons/com/atproto/repo/defs.ts +0 -5
  45. package/src/lexicons/com/atproto/repo/deleteRecord.defs.ts +0 -52
  46. package/src/lexicons/com/atproto/repo/deleteRecord.ts +0 -6
  47. package/src/lexicons/com/atproto/repo/getRecord.defs.ts +0 -37
  48. package/src/lexicons/com/atproto/repo/getRecord.ts +0 -6
  49. package/src/lexicons/com/atproto/repo/listRecords.defs.ts +0 -65
  50. package/src/lexicons/com/atproto/repo/listRecords.ts +0 -6
  51. package/src/lexicons/com/atproto/repo/putRecord.defs.ts +0 -59
  52. package/src/lexicons/com/atproto/repo/putRecord.ts +0 -6
  53. package/src/lexicons/com/atproto/repo/uploadBlob.defs.ts +0 -35
  54. package/src/lexicons/com/atproto/repo/uploadBlob.ts +0 -6
  55. package/src/lexicons/com/atproto/repo.ts +0 -12
  56. package/src/lexicons/com/atproto/sync/getBlob.defs.ts +0 -37
  57. package/src/lexicons/com/atproto/sync/getBlob.ts +0 -6
  58. package/src/lexicons/com/atproto/sync.ts +0 -5
  59. package/src/lexicons/com/atproto.ts +0 -6
  60. package/src/lexicons/com.ts +0 -5
  61. package/src/lexicons/index.ts +0 -5
  62. package/src/response.bench.ts +0 -113
  63. package/src/response.ts +0 -366
  64. package/src/types.ts +0 -71
  65. package/src/util.test.ts +0 -333
  66. package/src/util.ts +0 -215
  67. package/src/write-operation-builder.ts +0 -110
  68. package/src/www-authenticate.test.ts +0 -227
  69. package/src/www-authenticate.ts +0 -101
  70. package/src/xrpc.test.ts +0 -1450
  71. package/src/xrpc.ts +0 -446
  72. package/tsconfig.build.json +0 -12
  73. package/tsconfig.json +0 -7
  74. package/tsconfig.tests.json +0 -8
package/src/client.ts DELETED
@@ -1,1179 +0,0 @@
1
- import { LexMap, LexValue, TypedLexMap } from '@atproto/lex-data'
2
- import {
3
- AtIdentifierString,
4
- AtUriString,
5
- CidString,
6
- DidString,
7
- Infer,
8
- InferMethodInputBody,
9
- InferMethodOutputBody,
10
- InferRecordKey,
11
- LexiconRecordKey,
12
- Main,
13
- NsidString,
14
- Params,
15
- Procedure,
16
- Query,
17
- RecordSchema,
18
- Restricted,
19
- getMain,
20
- } from '@atproto/lex-schema'
21
- import { Agent, AgentOptions, buildAgent } from './agent.js'
22
- import { XrpcError, XrpcFailure, XrpcResponseError } from './errors.js'
23
- // @NOTE We could use import { com } from "./lexicons/index.js" here, but some
24
- // consumers might not know how to properly tree-shake that, so we import only
25
- // the needed lexicon schemas directly.
26
- import applyWrites from './lexicons/com/atproto/repo/applyWrites.js'
27
- import createRecord from './lexicons/com/atproto/repo/createRecord.js'
28
- import deleteRecord from './lexicons/com/atproto/repo/deleteRecord.js'
29
- import getRecord from './lexicons/com/atproto/repo/getRecord.js'
30
- import listRecords, {
31
- type Record as ListRecordsRecord,
32
- } from './lexicons/com/atproto/repo/listRecords.js'
33
- import putRecord from './lexicons/com/atproto/repo/putRecord.js'
34
- import uploadBlob from './lexicons/com/atproto/repo/uploadBlob.js'
35
- import getBlob from './lexicons/com/atproto/sync/getBlob.js'
36
- import {
37
- XrpcResponse,
38
- XrpcResponseBody,
39
- XrpcResponseOptions,
40
- } from './response.js'
41
- import { BinaryBodyInit, Service } from './types.js'
42
- import {
43
- RecordKeyOptions,
44
- XrpcRequestHeadersOptions,
45
- applyDefaults,
46
- buildXrpcRequestHeaders,
47
- getDefaultRecordKey,
48
- getLiteralRecordKey,
49
- wait,
50
- } from './util.js'
51
- import {
52
- WriteOperation,
53
- WriteOperationCreateOptions,
54
- WriteOperationDeleteOptions,
55
- WriteOperationHelper,
56
- WriteOperationUpdateOptions,
57
- WriteOperationsFactory,
58
- } from './write-operation-builder.js'
59
- import {
60
- XrpcOptions,
61
- XrpcRequestParams,
62
- XrpcRequestProcessingOptions,
63
- xrpc,
64
- xrpcSafe,
65
- } from './xrpc.js'
66
-
67
- export {
68
- type AtIdentifierString,
69
- type CidString,
70
- type DidString,
71
- type Infer,
72
- type InferMethodInputBody,
73
- type InferMethodOutputBody,
74
- type InferRecordKey,
75
- type LexMap,
76
- type LexValue,
77
- type LexiconRecordKey,
78
- type Main,
79
- type NsidString,
80
- type Params,
81
- Procedure,
82
- Query,
83
- RecordSchema,
84
- type Restricted,
85
- type TypedLexMap,
86
- type WriteOperation,
87
- type WriteOperationCreateOptions,
88
- type WriteOperationDeleteOptions,
89
- WriteOperationHelper,
90
- type WriteOperationUpdateOptions,
91
- type WriteOperationsFactory,
92
- }
93
-
94
- /**
95
- * Configuration options for creating a {@link Client}.
96
- *
97
- * @property {@link ClientOptions.labelers} - An iterable of labeler DIDs to include in requests. These will be combined with any global app labelers configured via {@link Client.configure}.
98
- * @property {@link ClientOptions.service} - An optional service identifier (DID or URL) for routing requests with service proxying.
99
- * @property {@link ClientOptions.headers} - Custom headers to include in all requests made by this client instance.
100
- * @property {@link ClientOptions.validateRequest} - If true, validates request bodies against their lexicon schemas before sending. Defaults to false for performance.
101
- * @property {@link ClientOptions.validateResponse} - If false, skips validation of response bodies against their lexicon schemas. Defaults to true to catch errors, but can be disabled for performance if you trust the server responses. Note that defaults will not be applied if validation is disabled, which can cause typing inconsistencies, so use with caution.
102
- * @property {@link ClientOptions.strictResponseProcessing} - If false, relaxes certain validation rules during response processing (e.g., allowing floats, deeper nesting, etc.). Defaults to true for strict compliance with {@link https://atproto.com/specs/data-model lexicon data model}, but can be disabled to handle non-compliant responses.
103
- *
104
- * @see {@link XrpcRequestHeadersOptions}
105
- * @see {@link XrpcRequestProcessingOptions}
106
- * @see {@link XrpcResponseOptions}
107
- *
108
- * @example
109
- * ```typescript
110
- * const options: ClientOptions = {
111
- * labelers: ['did:plc:labeler1'],
112
- * service: 'did:web:api.bsky.app#bsky_appview',
113
- * headers: { 'X-Custom-Header': 'value' },
114
- * validateRequest: false,
115
- * validateResponse: true,
116
- * strictResponseProcessing: false,
117
- * }
118
- * ```
119
- */
120
- export type ClientOptions = XrpcRequestHeadersOptions &
121
- Pick<XrpcRequestProcessingOptions, 'validateRequest'> &
122
- XrpcResponseOptions
123
-
124
- export type ActionOptions = {
125
- /** AbortSignal to cancel the request. */
126
- signal?: AbortSignal
127
- }
128
-
129
- /**
130
- * A composable action that can be invoked via {@link Client.call}.
131
- *
132
- * Actions provide a way to define custom operations that integrate with the
133
- * Client's call interface, enabling type-safe, reusable business logic.
134
- *
135
- * @typeParam I - The input type for the action
136
- * @typeParam O - The output type for the action
137
- *
138
- * @example
139
- * ```typescript
140
- * const myAction: Action<{ userId: string }, { profile: Profile }> = async (client, input, options) => {
141
- * const response = await client.xrpc(someMethod, { params: { actor: input.userId }, ...options })
142
- * return { profile: response.body }
143
- * }
144
- * ```
145
- */
146
- export type Action<I = any, O = any> = (
147
- client: Client,
148
- input: I,
149
- options: ActionOptions,
150
- ) => O | Promise<O>
151
-
152
- /**
153
- * Extracts the input type from an {@link Action}.
154
- * @typeParam A - The Action type to extract from
155
- */
156
- export type InferActionInput<A extends Action> =
157
- A extends Action<infer I, any> ? I : never
158
-
159
- /**
160
- * Extracts the output type from an {@link Action}.
161
- * @typeParam A - The Action type to extract from
162
- */
163
- export type InferActionOutput<A extends Action> =
164
- A extends Action<any, infer O> ? O : never
165
-
166
- /**
167
- * Options for creating a record in an AT Protocol repository.
168
- *
169
- * @see {@link Client.createRecord}
170
- */
171
- export type CreateRecordOptions = Omit<
172
- XrpcOptions<typeof createRecord>,
173
- 'body'
174
- > & {
175
- /** Repository identifier (DID or handle). Defaults to authenticated user's DID. */
176
- repo?: AtIdentifierString
177
- /** Compare-and-swap on the repo commit. If specified, must match current commit. */
178
- swapCommit?: string
179
- /**
180
- * Whether the PDS should validate the record against its lexicon schema.
181
- * When `true`, the PDS is asked to explicitly validate the record. When
182
- * `false`, the PDS is asked to explicitly skip validation. When `undefined`
183
- * (default), the PDS decides -- typically validating only collections whose
184
- * schemas it knows. This is server-side validation; for client-side
185
- * validation before sending, use {@link XrpcRequestProcessingOptions.validateRequest}.
186
- */
187
- validate?: boolean
188
- }
189
-
190
- /**
191
- * Options for deleting a record from an AT Protocol repository.
192
- *
193
- * @see {@link Client.deleteRecord}
194
- */
195
- export type DeleteRecordOptions = Omit<
196
- XrpcOptions<typeof deleteRecord>,
197
- 'body'
198
- > & {
199
- /** Repository identifier (DID or handle). Defaults to authenticated user's DID. */
200
- repo?: AtIdentifierString
201
- /** Compare-and-swap on the repo commit. If specified, must match current commit. */
202
- swapCommit?: string
203
- /** Compare-and-swap on the record CID. If specified, must match current record. */
204
- swapRecord?: string
205
- }
206
-
207
- /**
208
- * Options for retrieving a record from an AT Protocol repository.
209
- *
210
- * @see {@link Client.getRecord}
211
- */
212
- export type GetRecordOptions = Omit<XrpcOptions<typeof getRecord>, 'params'> & {
213
- /** Repository identifier (DID or handle). Defaults to authenticated user's DID. */
214
- repo?: AtIdentifierString
215
- }
216
-
217
- /**
218
- * Options for creating or updating a record in an AT Protocol repository.
219
- *
220
- * @see {@link Client.putRecord}
221
- */
222
- export type PutRecordOptions = Omit<XrpcOptions<typeof putRecord>, 'body'> & {
223
- /** Repository identifier (DID or handle). Defaults to authenticated user's DID. */
224
- repo?: AtIdentifierString
225
- /** Compare-and-swap on the repo commit. If specified, must match current commit. */
226
- swapCommit?: string
227
- /** Compare-and-swap on the record CID. If specified, must match current record. */
228
- swapRecord?: string
229
- /**
230
- * Whether the PDS should validate the record against its lexicon schema.
231
- * When `true`, the PDS is asked to explicitly validate the record. When
232
- * `false`, the PDS is asked to explicitly skip validation. When `undefined`
233
- * (default), the PDS decides — typically validating only collections whose
234
- * schemas it knows. This is server-side validation; for client-side
235
- * validation before sending, use {@link XrpcRequestProcessingOptions.validateRequest}.
236
- */
237
- validate?: boolean
238
- }
239
-
240
- /**
241
- * Options for listing records in an AT Protocol repository collection.
242
- *
243
- * @see {@link Client.listRecords}
244
- */
245
- export type ListRecordsOptions = Omit<
246
- XrpcOptions<typeof listRecords>,
247
- 'params'
248
- > & {
249
- /** Repository identifier (DID or handle). Defaults to authenticated user's DID. */
250
- repo?: AtIdentifierString
251
- /** Maximum number of records to return. */
252
- limit?: number
253
- /** Pagination cursor from a previous response. */
254
- cursor?: string
255
- /** If true, returns records in reverse chronological order. */
256
- reverse?: boolean
257
- }
258
-
259
- /**
260
- * Options for applying a batch of writes (create/update/delete) to an AT Protocol repository.
261
- *
262
- * @see {@link Client.applyWrites}
263
- */
264
- export type ApplyWritesOptions = Omit<
265
- XrpcOptions<typeof applyWrites>,
266
- 'body'
267
- > & {
268
- /** Repository identifier (DID or handle). Defaults to authenticated user's DID. */
269
- repo?: AtIdentifierString
270
- /**
271
- * Whether the PDS should validate the records against their lexicon schemas.
272
- * When `true`, the PDS is asked to explicitly validate every record. When
273
- * `false`, the PDS is asked to explicitly skip validation. When `undefined`
274
- * (default), the PDS decides — typically validating only collections whose
275
- * schemas it knows.
276
- */
277
- validate?: boolean
278
- /** Compare-and-swap on the repo commit. If specified, must match current commit. */
279
- swapCommit?: CidString
280
- }
281
-
282
- export type UploadBlobOptions = Omit<XrpcOptions<typeof uploadBlob>, 'body'>
283
-
284
- export type GetBlobOptions = Omit<XrpcOptions<typeof getBlob>, 'params'>
285
-
286
- /**
287
- * Type-safe options for {@link Client.create}, combining record options with key requirements.
288
- * @typeParam T - The record schema type
289
- * @see {@link CreateRecordOptions}
290
- */
291
- export type CreateOptions<T extends RecordSchema> = CreateRecordOptions &
292
- RecordKeyOptions<T, 'tid' | 'any'>
293
-
294
- /**
295
- * Output type for record creation operations.
296
- * Contains the URI and CID of the newly created record.
297
- */
298
- export type CreateOutput = InferMethodOutputBody<
299
- typeof createRecord,
300
- Uint8Array
301
- >
302
-
303
- /**
304
- * Type-safe options for {@link Client.delete}, combining delete options with key requirements.
305
- * @typeParam T - The record schema type
306
- */
307
- export type DeleteOptions<T extends RecordSchema> = DeleteRecordOptions &
308
- RecordKeyOptions<T>
309
-
310
- /**
311
- * Output type for record deletion operations.
312
- */
313
- export type DeleteOutput = InferMethodOutputBody<
314
- typeof deleteRecord,
315
- Uint8Array
316
- >
317
-
318
- /**
319
- * Type-safe options for {@link Client.get}, combining get options with key requirements.
320
- * @typeParam T - The record schema type
321
- */
322
- export type GetOptions<T extends RecordSchema> = GetRecordOptions &
323
- RecordKeyOptions<T>
324
-
325
- /**
326
- * Output type for record retrieval operations.
327
- * Contains the record value validated against the schema type.
328
- * @typeParam T - The record schema type
329
- */
330
- export type GetOutput<T extends RecordSchema> = Omit<
331
- InferMethodOutputBody<typeof getRecord, Uint8Array>,
332
- 'value'
333
- > & { value: Infer<T> }
334
-
335
- /**
336
- * Type-safe options for {@link Client.put}, combining put options with key requirements.
337
- * @typeParam T - The record schema type
338
- */
339
- export type PutOptions<T extends RecordSchema> = PutRecordOptions &
340
- RecordKeyOptions<T>
341
-
342
- /**
343
- * Output type for record put (create/update) operations.
344
- * Contains the URI and CID of the record.
345
- */
346
- export type PutOutput = InferMethodOutputBody<typeof putRecord, Uint8Array>
347
-
348
- /**
349
- * Options for {@link Client.list} operations.
350
- */
351
- export type ListOptions = ListRecordsOptions
352
-
353
- /**
354
- * Output type for record listing operations.
355
- * Contains validated records and any invalid records that failed schema validation.
356
- * @typeParam T - The record schema type
357
- */
358
- export type ListOutput<T extends RecordSchema> = Omit<
359
- InferMethodOutputBody<typeof listRecords>,
360
- 'records'
361
- > & {
362
- /** Records that successfully validated against the schema. */
363
- records: ListRecordItem<Infer<T>>[]
364
- }
365
-
366
- /**
367
- * A discriminated union type representing the result of a record listing
368
- * operation.
369
- */
370
- export type ListRecordItem<Value extends LexMap> =
371
- | { uri: AtUriString; cid: CidString; valid: true; value: Value }
372
- | { uri: AtUriString; cid: CidString; valid: false; value: LexMap }
373
-
374
- /**
375
- * The Client class is the primary interface for interacting with AT Protocol
376
- * services. It provides type-safe methods for XRPC calls, record operations,
377
- * and blob handling.
378
- *
379
- * @example
380
- * ```typescript
381
- * import { Client } from '@atproto/lex'
382
- * import { app } from '#/lexicons
383
- *
384
- * const client = new Client(oauthSession)
385
- *
386
- * const response = await client.xrpc(app.bsky.feed.getTimeline.main, {
387
- * params: { limit: 50 }
388
- * })
389
- * ```
390
- */
391
- export class Client implements Agent {
392
- static appLabelers: readonly DidString[] = []
393
-
394
- /**
395
- * Configures the Client (or its sub classes) globally.
396
- */
397
- static configure(opts: { appLabelers?: Iterable<DidString> }) {
398
- if (opts.appLabelers) this.appLabelers = [...opts.appLabelers]
399
- }
400
-
401
- /** The underlying agent used for making requests. */
402
- public readonly agent: Agent
403
-
404
- /** Custom headers included in all requests. */
405
- public readonly headers: Headers
406
-
407
- /** Optional service identifier for routing requests. */
408
- public readonly service?: Service
409
-
410
- /** Set of labeler DIDs specific to this client instance. */
411
- public readonly labelers: Set<DidString>
412
-
413
- public readonly xrpcDefaults: {
414
- readonly validateRequest: boolean
415
- readonly validateResponse: boolean
416
- readonly strictResponseProcessing: boolean
417
- }
418
-
419
- constructor(agent: Agent | AgentOptions, options: ClientOptions = {}) {
420
- this.agent = buildAgent(agent)
421
- this.service = options.service
422
- this.labelers = new Set(options.labelers)
423
- this.headers = new Headers(options.headers)
424
- this.xrpcDefaults = Object.freeze({
425
- validateRequest: options.validateRequest ?? false,
426
- validateResponse: options.validateResponse ?? true,
427
- strictResponseProcessing: options.strictResponseProcessing ?? true,
428
- })
429
- }
430
-
431
- /**
432
- * The DID of the authenticated user, or `undefined` if not authenticated.
433
- */
434
- get did(): DidString | undefined {
435
- return this.agent.did
436
- }
437
-
438
- /**
439
- * The DID of the authenticated user.
440
- * @throws {Error} if not authenticated
441
- */
442
- get assertDid(): DidString {
443
- this.assertAuthenticated()
444
- return this.did
445
- }
446
-
447
- /**
448
- * Asserts that the client is authenticated.
449
- * Use as a type guard when you need to ensure authentication.
450
- *
451
- * @throws {Error} if not authenticated
452
- *
453
- * @example
454
- * ```typescript
455
- * client.assertAuthenticated()
456
- * // TypeScript now knows client.did is defined
457
- * console.log(client.did)
458
- * ```
459
- */
460
- public assertAuthenticated(): asserts this is { did: DidString } {
461
- if (!this.did) throw new Error('Client is not authenticated')
462
- }
463
-
464
- /**
465
- * Replaces all labelers with the given set.
466
- * @param labelers - Iterable of labeler DIDs
467
- */
468
- public setLabelers(labelers: Iterable<DidString> = []) {
469
- this.clearLabelers()
470
- this.addLabelers(labelers)
471
- }
472
-
473
- /**
474
- * Adds labelers to the current set.
475
- * @param labelers - Iterable of labeler DIDs to add
476
- */
477
- public addLabelers(labelers: Iterable<DidString>) {
478
- for (const labeler of labelers) this.labelers.add(labeler)
479
- }
480
-
481
- /**
482
- * Removes all labelers from this client instance.
483
- */
484
- public clearLabelers() {
485
- this.labelers.clear()
486
- }
487
-
488
- /**
489
- * {@link Agent}'s {@link Agent.fetchHandler} implementation, which adds
490
- * labelers and service proxying headers. This method allow a {@link Client}
491
- * instance to be used directly as an {@link Agent} for another
492
- * {@link Client}, enabling composition of headers (labelers, proxying, etc.).
493
- *
494
- * @param path - The request path
495
- * @param init - Request initialization options
496
- */
497
- public fetchHandler(
498
- path: `/${string}`,
499
- init: RequestInit,
500
- ): Promise<Response> {
501
- const headers = buildXrpcRequestHeaders({
502
- headers: init.headers,
503
- service: this.service,
504
- labelers: [
505
- ...(this.constructor as typeof Client).appLabelers.map(
506
- (l) => `${l};redact` as const,
507
- ),
508
- ...this.labelers,
509
- ],
510
- })
511
-
512
- // Incoming headers take precedence
513
- for (const [key, value] of this.headers) {
514
- if (!headers.has(key)) headers.set(key, value)
515
- }
516
-
517
- // @NOTE The agent here could be another Client instance.
518
- return this.agent.fetchHandler(path, { ...init, headers })
519
- }
520
-
521
- /**
522
- * Makes an XRPC request. Throws on failure.
523
- *
524
- * @param ns - The lexicon method definition (e.g., `app.bsky.feed.getTimeline`)
525
- * @param options - Request options including params and body
526
- * @returns The successful XRPC response
527
- * @throws {XrpcFailure} when the request fails or returns an error
528
- *
529
- * @example Query with parameters
530
- * ```typescript
531
- * const response = await client.xrpc(app.bsky.feed.getTimeline, {
532
- * params: { limit: 50, cursor: 'abc123' }
533
- * })
534
- * console.log(response.body.feed)
535
- * ```
536
- *
537
- * @example Procedure with body
538
- * ```typescript
539
- * const response = await client.xrpc(com.atproto.repo.createRecord, {
540
- * body: {
541
- * repo: client.assertDid,
542
- * collection: 'app.bsky.feed.post',
543
- * record: { text: 'Hello!', createdAt: new Date().toISOString() }
544
- * }
545
- * })
546
- * ```
547
- *
548
- * @see {@link xrpcSafe} for a non-throwing variant
549
- */
550
- async xrpc<const M extends Query | Procedure>(
551
- ns: NonNullable<unknown> extends XrpcOptions<M>
552
- ? Main<M>
553
- : Restricted<'This XRPC method requires an "options" argument'>,
554
- ): Promise<XrpcResponse<M>>
555
- async xrpc<const M extends Query | Procedure>(
556
- ns: Main<M>,
557
- options: XrpcOptions<M>,
558
- ): Promise<XrpcResponse<M>>
559
- async xrpc<const M extends Query | Procedure>(
560
- ns: Main<M>,
561
- options: XrpcOptions<M> = {} as XrpcOptions<M>,
562
- ): Promise<XrpcResponse<M>> {
563
- return xrpc(this, ns, applyDefaults(options, this.xrpcDefaults))
564
- }
565
-
566
- /**
567
- * Makes an XRPC request without throwing on failure.
568
- * Returns either a successful response or a failure object.
569
- *
570
- * @param ns - The lexicon method definition
571
- * @param options - Request options
572
- * @returns Either an XrpcResponse on success or XrpcFailure on failure
573
- *
574
- * @example
575
- * ```typescript
576
- * const result = await client.xrpcSafe(app.bsky.actor.getProfile.main, {
577
- * params: { actor: 'alice.bsky.social' }
578
- * })
579
- *
580
- * if (result.success) {
581
- * console.log(result.body.displayName)
582
- * } else {
583
- * console.error('Failed:', result.error)
584
- * }
585
- * ```
586
- *
587
- * @see {@link xrpc} for a throwing variant
588
- */
589
- async xrpcSafe<const M extends Query | Procedure>(
590
- ns: NonNullable<unknown> extends XrpcOptions<M>
591
- ? Main<M>
592
- : Restricted<'This XRPC method requires an "options" argument'>,
593
- ): Promise<XrpcResponse<M> | XrpcFailure<M>>
594
- async xrpcSafe<const M extends Query | Procedure>(
595
- ns: Main<M>,
596
- options: XrpcOptions<M>,
597
- ): Promise<XrpcResponse<M> | XrpcFailure<M>>
598
- async xrpcSafe<const M extends Query | Procedure>(
599
- ns: Main<M>,
600
- options: XrpcOptions<M> = {} as XrpcOptions<M>,
601
- ): Promise<XrpcResponse<M> | XrpcFailure<M>> {
602
- return xrpcSafe(this, ns, applyDefaults(options, this.xrpcDefaults))
603
- }
604
-
605
- /**
606
- * Creates a new record in an AT Protocol repository.
607
- *
608
- * @param record - The record to create, must include an {@link NsidString} `$type`
609
- * @param rkey - Optional record key; if omitted, server generates a TID
610
- * @param options - Create options including repo, swapCommit, validate
611
- * @returns The XRPC response containing the created record's URI and CID
612
- *
613
- * @example
614
- * ```typescript
615
- * const response = await client.createRecord(
616
- * { $type: 'app.bsky.feed.post', text: 'Hello!', createdAt: new Date().toISOString() },
617
- * undefined, // Let server generate rkey
618
- * { validate: true }
619
- * )
620
- * console.log(response.body.uri)
621
- * ```
622
- *
623
- * @see {@link create} for a higher-level typed alternative
624
- */
625
- public async createRecord(
626
- record: TypedLexMap<NsidString>,
627
- rkey?: string,
628
- options?: CreateRecordOptions,
629
- ) {
630
- return this.xrpc(createRecord, {
631
- ...options,
632
- body: {
633
- repo: options?.repo ?? this.assertDid,
634
- collection: record.$type,
635
- record,
636
- rkey,
637
- validate: options?.validate,
638
- swapCommit: options?.swapCommit,
639
- },
640
- })
641
- }
642
-
643
- /**
644
- * Deletes a record from an AT Protocol repository.
645
- *
646
- * @param collection - The collection NSID
647
- * @param rkey - The record key
648
- * @param options - Delete options including repo, swapCommit, swapRecord
649
- *
650
- * @see {@link delete} for a higher-level typed alternative
651
- */
652
- async deleteRecord(
653
- collection: NsidString,
654
- rkey: string,
655
- options?: DeleteRecordOptions,
656
- ) {
657
- return this.xrpc(deleteRecord, {
658
- ...options,
659
- body: {
660
- repo: options?.repo ?? this.assertDid,
661
- collection,
662
- rkey,
663
- swapCommit: options?.swapCommit,
664
- swapRecord: options?.swapRecord,
665
- },
666
- })
667
- }
668
-
669
- /**
670
- * Retrieves a record from an AT Protocol repository.
671
- *
672
- * @param collection - The collection NSID
673
- * @param rkey - The record key
674
- * @param options - Get options including repo
675
- *
676
- * @see {@link get} for a higher-level typed alternative
677
- */
678
- public async getRecord(
679
- collection: NsidString,
680
- rkey: string,
681
- options?: GetRecordOptions,
682
- ) {
683
- return this.xrpc(getRecord, {
684
- ...options,
685
- params: {
686
- repo: options?.repo ?? this.assertDid,
687
- collection,
688
- rkey,
689
- },
690
- })
691
- }
692
-
693
- /**
694
- * Creates or updates a record in a repository.
695
- *
696
- * @param record - The record to put, must include an {@link NsidString} `$type`
697
- * @param rkey - The record key
698
- * @param options - Put options including repo, swapCommit, swapRecord, validate
699
- *
700
- * @see {@link put} for a higher-level typed alternative
701
- */
702
- async putRecord(
703
- record: TypedLexMap<NsidString>,
704
- rkey: string,
705
- options?: PutRecordOptions,
706
- ) {
707
- return this.xrpc(putRecord, {
708
- ...options,
709
- body: {
710
- repo: options?.repo ?? this.assertDid,
711
- collection: record.$type,
712
- rkey,
713
- record,
714
- validate: options?.validate,
715
- swapCommit: options?.swapCommit,
716
- swapRecord: options?.swapRecord,
717
- },
718
- })
719
- }
720
-
721
- /**
722
- * Lists records in a collection.
723
- *
724
- * @param nsid - The collection NSID
725
- * @param options - List options including repo, limit, cursor, reverse
726
- *
727
- * @see {@link list} for a higher-level typed alternative
728
- */
729
- async listRecords(nsid: NsidString, options?: ListRecordsOptions) {
730
- return this.xrpc(listRecords, {
731
- ...options,
732
- params: {
733
- repo: options?.repo ?? this.assertDid,
734
- collection: nsid,
735
- cursor: options?.cursor,
736
- limit: options?.limit,
737
- reverse: options?.reverse,
738
- },
739
- })
740
- }
741
-
742
- /**
743
- * Performs an atomic batch of create, update, and delete operations on records in a repository.
744
- *
745
- * @param builder - A function that receives an {@link ApplyWritesOperations} instance to build the operations
746
- * @param options - ApplyWrites options including repo, validate, swapCommit
747
- * @returns The XRPC response from the applyWrites call
748
- *
749
- * @example
750
- * ```typescript
751
- * const response = await client.applyWrites((op) => [
752
- * op.create(app.bsky.feed.post, { text: 'Hello!' }),
753
- * op.update(app.bsky.feed.post, { text: 'Updated text' }, { rkey: 'post123' }),
754
- * op.delete(app.bsky.feed.post, 'post456'),
755
- * op.update(app.bsky.actor.profile, { displayName: 'Alice' }),
756
- * ], {
757
- * validate: true,
758
- * })
759
- *
760
- * for (const result of response.body.results) {
761
- * console.log(result.uri)
762
- * }
763
- * ```
764
- */
765
- async applyWrites(
766
- factory: WriteOperationsFactory,
767
- options?: ApplyWritesOptions,
768
- ) {
769
- return this.xrpc(applyWrites, {
770
- ...options,
771
- body: {
772
- repo: options?.repo ?? this.assertDid,
773
- writes: WriteOperationHelper.build(factory),
774
- validate: options?.validate,
775
- swapCommit: options?.swapCommit,
776
- },
777
- })
778
- }
779
-
780
- /**
781
- * Uploads a blob to an AT Protocol repository.
782
- *
783
- * @param body - The blob data (Uint8Array, ReadableStream, Blob, etc.)
784
- * @param options - Upload options including encoding hint
785
- * @returns Response containing the blob reference
786
- *
787
- * @example
788
- * ```typescript
789
- * const imageData = await fetch('image.png').then(r => r.arrayBuffer())
790
- * const response = await client.uploadBlob(new Uint8Array(imageData), {
791
- * encoding: 'image/png'
792
- * })
793
- * console.log(response.body.blob) // Use this ref in records
794
- * ```
795
- */
796
- async uploadBlob(body: BinaryBodyInit, options?: UploadBlobOptions) {
797
- return this.xrpc(uploadBlob, { ...options, body })
798
- }
799
-
800
- /**
801
- * Retrieves a blob by DID and CID.
802
- *
803
- * @param did - The DID of the repository containing the blob
804
- * @param cid - The CID of the blob
805
- * @param options - Call options
806
- */
807
- async getBlob(did: DidString, cid: CidString, options?: GetBlobOptions) {
808
- return this.xrpc(getBlob, {
809
- ...options,
810
- params: { did, cid },
811
- })
812
- }
813
-
814
- /**
815
- * Universal call method for queries, procedures, and custom actions.
816
- * Automatically determines the call type based on the lexicon definition.
817
- *
818
- * @param ns - The lexicon method or action definition
819
- * @param arg - The input argument (params for queries, body for procedures, input for actions)
820
- * @param options - Call options
821
- * @returns The method response body or action output
822
- * @see {@link xrpc} if you need access to the full response object
823
- *
824
- * @example Query
825
- * ```typescript
826
- * const profile = await client.call(app.bsky.actor.getProfile.main, {
827
- * actor: 'alice.bsky.social'
828
- * })
829
- * ```
830
- *
831
- * @example Procedure
832
- * ```typescript
833
- * const result = await client.call(com.atproto.repo.createRecord.main, {
834
- * repo: did,
835
- * collection: 'app.bsky.feed.post',
836
- * record: { text: 'Hello!' }
837
- * })
838
- * ```
839
- */
840
- public async call<const T extends Query>(
841
- ns: NonNullable<unknown> extends XrpcRequestParams<T>
842
- ? Main<T>
843
- : Restricted<'This query type requires a "params" argument'>,
844
- ): Promise<XrpcResponseBody<T>>
845
- public async call<const T extends Procedure>(
846
- ns: undefined extends InferMethodInputBody<T, Uint8Array>
847
- ? Main<T>
848
- : Restricted<'This procedure type requires an "input" argument'>,
849
- ): Promise<XrpcResponseBody<T>>
850
- public async call<const T extends Action>(
851
- ns: void extends InferActionInput<T>
852
- ? Main<T>
853
- : Restricted<'This action type requires an "input" argument'>,
854
- ): Promise<InferActionOutput<T>>
855
- public async call<const T extends Action | Procedure | Query>(
856
- ns: Main<T>,
857
- arg: T extends Action
858
- ? InferActionInput<T>
859
- : T extends Procedure
860
- ? InferMethodInputBody<T, Uint8Array>
861
- : T extends Query
862
- ? XrpcRequestParams<T>
863
- : never,
864
- options?: T extends Action
865
- ? ActionOptions
866
- : T extends Procedure
867
- ? Omit<XrpcOptions<T>, 'body'>
868
- : T extends Query
869
- ? Omit<XrpcOptions<T>, 'params'>
870
- : never,
871
- ): Promise<
872
- T extends Action
873
- ? InferActionOutput<T>
874
- : T extends Procedure
875
- ? XrpcResponseBody<T>
876
- : T extends Query
877
- ? XrpcResponseBody<T>
878
- : never
879
- >
880
- public async call(
881
- ns: Main<Action> | Main<Procedure> | Main<Query>,
882
- arg?: LexValue | Params,
883
- options: ActionOptions = {},
884
- ): Promise<unknown> {
885
- const method = getMain(ns)
886
-
887
- if (typeof method === 'function') {
888
- return method(this, arg, options)
889
- }
890
-
891
- if (method instanceof Procedure) {
892
- const result = await this.xrpc(method, { ...options, body: arg as any })
893
- return result.body
894
- } else if (method instanceof Query) {
895
- const result = await this.xrpc(method, { ...options, params: arg as any })
896
- return result.body
897
- } else {
898
- throw new TypeError('Invalid lexicon')
899
- }
900
- }
901
-
902
- /**
903
- * Creates a new record with full type safety based on the schema.
904
- *
905
- * @param ns - The record schema definition
906
- * @param input - The record data (without `$type`, which is added automatically)
907
- * @param options - Create options including rkey (required for some record types)
908
- * @returns The create output including URI and CID
909
- *
910
- * @example Creating a post
911
- * ```typescript
912
- * const result = await client.create(app.bsky.feed.post.main, {
913
- * text: 'Hello, world!',
914
- * createdAt: new Date().toISOString()
915
- * })
916
- * console.log(result.uri)
917
- * ```
918
- *
919
- * @example Creating a record with explicit rkey
920
- * ```typescript
921
- * const result = await client.create(app.bsky.actor.profile.main, {
922
- * displayName: 'Alice'
923
- * }, { rkey: 'self' })
924
- * ```
925
- */
926
- public async create<const T extends RecordSchema>(
927
- ns: NonNullable<unknown> extends CreateOptions<T>
928
- ? Main<T>
929
- : Restricted<'This record type requires an "options" argument'>,
930
- input: Omit<Infer<T>, '$type'>,
931
- ): Promise<CreateOutput>
932
- public async create<const T extends RecordSchema>(
933
- ns: Main<T>,
934
- input: Omit<Infer<T>, '$type'>,
935
- options: CreateOptions<T>,
936
- ): Promise<CreateOutput>
937
- public async create<const T extends RecordSchema>(
938
- ns: Main<T>,
939
- input: Omit<Infer<T>, '$type'>,
940
- options: CreateOptions<T> = {} as CreateOptions<T>,
941
- ): Promise<CreateOutput> {
942
- const schema: T = getMain(ns)
943
- const record = schema.build(input) as TypedLexMap<NsidString>
944
- if (options?.validateRequest) schema.validate(record)
945
- const rkey = options.rkey ?? getDefaultRecordKey(schema)
946
- if (rkey !== undefined) schema.keySchema.assert(rkey)
947
- const response = await this.createRecord(record, rkey, options)
948
- return response.body
949
- }
950
-
951
- /**
952
- * Deletes a record with type-safe options.
953
- *
954
- * @param ns - The record schema definition
955
- * @param options - Delete options (rkey required for non-literal keys)
956
- * @returns The delete output
957
- */
958
- public async delete<const T extends RecordSchema>(
959
- ns: NonNullable<unknown> extends DeleteOptions<T>
960
- ? Main<T>
961
- : Restricted<'This record type requires an "options" argument'>,
962
- ): Promise<DeleteOutput>
963
- public async delete<const T extends RecordSchema>(
964
- ns: Main<T>,
965
- options?: DeleteOptions<T>,
966
- ): Promise<DeleteOutput>
967
- public async delete<const T extends RecordSchema>(
968
- ns: Main<T>,
969
- options: DeleteOptions<T> = {} as DeleteOptions<T>,
970
- ): Promise<DeleteOutput> {
971
- const schema = getMain(ns)
972
- const rkey = schema.keySchema.parse(
973
- options.rkey ?? getLiteralRecordKey(schema),
974
- )
975
- const response = await this.deleteRecord(schema.$type, rkey, options)
976
- return response.body
977
- }
978
-
979
- /**
980
- * Retrieves a record with type-safe validation.
981
- *
982
- * @param ns - The record schema definition
983
- * @param options - Get options (rkey required for non-literal keys)
984
- * @returns The record data validated against the schema
985
- *
986
- * @example
987
- * ```typescript
988
- * const profile = await client.get(app.bsky.actor.profile.main)
989
- * // profile.value is typed as app.bsky.actor.profile.Record
990
- * console.log(profile.value.displayName)
991
- * ```
992
- */
993
- public async get<const T extends RecordSchema>(
994
- ns: T['key'] extends `literal:${string}`
995
- ? Main<T>
996
- : Restricted<'This record type requires an "options" argument'>,
997
- ): Promise<GetOutput<T>>
998
- public async get<const T extends RecordSchema>(
999
- ns: Main<T>,
1000
- options: GetOptions<T>,
1001
- ): Promise<GetOutput<T>>
1002
- public async get<const T extends RecordSchema>(
1003
- ns: Main<T>,
1004
- options: GetOptions<T> = {} as GetOptions<T>,
1005
- ): Promise<GetOutput<T>> {
1006
- const schema = getMain(ns)
1007
- const rkey = schema.keySchema.parse(
1008
- options.rkey ?? getLiteralRecordKey(schema),
1009
- )
1010
- const response = await this.getRecord(schema.$type, rkey, options)
1011
- const value = schema.validate(response.body.value)
1012
- return { ...response.body, value }
1013
- }
1014
-
1015
- /**
1016
- * Creates or updates a record with full type safety.
1017
- *
1018
- * @param ns - The record schema definition
1019
- * @param input - The record data
1020
- * @param options - Put options (rkey required for non-literal keys)
1021
- * @returns The put output including URI and CID
1022
- */
1023
- public async put<const T extends RecordSchema>(
1024
- ns: NonNullable<unknown> extends PutOptions<T>
1025
- ? Main<T>
1026
- : Restricted<'This record type requires an "options" argument'>,
1027
- input: Omit<Infer<T>, '$type'>,
1028
- ): Promise<PutOutput>
1029
- public async put<const T extends RecordSchema>(
1030
- ns: Main<T>,
1031
- input: Omit<Infer<T>, '$type'>,
1032
- options: PutOptions<T>,
1033
- ): Promise<PutOutput>
1034
- public async put<const T extends RecordSchema>(
1035
- ns: Main<T>,
1036
- input: Omit<Infer<T>, '$type'>,
1037
- options: PutOptions<T> = {} as PutOptions<T>,
1038
- ): Promise<PutOutput> {
1039
- const schema: T = getMain(ns)
1040
- const record = schema.build(input) as TypedLexMap<NsidString>
1041
- if (options?.validateRequest) schema.validate(record)
1042
- const rkey = options.rkey ?? getLiteralRecordKey(schema)
1043
- const response = await this.putRecord(record, rkey, options)
1044
- return response.body
1045
- }
1046
-
1047
- /**
1048
- * Lists records with type-safe validation and separation of valid/invalid records.
1049
- *
1050
- * @param ns - The record schema definition
1051
- * @param options - List options
1052
- * @returns Records validated against the schema, with invalid records included as LexMap
1053
- *
1054
- * @example
1055
- * ```typescript
1056
- * const result = await client.list(app.bsky.feed.post.main, { limit: 100 })
1057
- * for (const record of result.records) {
1058
- * if (record.valid) {
1059
- * record.value // Fully typed
1060
- * } else {
1061
- * record.value // Invalid record, typed as LexMap
1062
- * }
1063
- * }
1064
- * ```
1065
- */
1066
- async list<const T extends RecordSchema>(
1067
- ns: Main<T>,
1068
- options?: ListOptions,
1069
- ): Promise<ListOutput<T>> {
1070
- const schema = getMain(ns)
1071
- const { body } = await this.listRecords(schema.$type, options)
1072
- const records = body.records.map(processListRecord, schema)
1073
- return { ...body, records }
1074
- }
1075
-
1076
- /**
1077
- * Asynchronously iterates over all records in a collection, handling
1078
- * pagination automatically.
1079
- *
1080
- * @param ns - The record schema definition
1081
- * @param options - List options including limit and cursor
1082
- * @returns An async generator yielding each record validated against the schema
1083
- */
1084
- async *listAll<const T extends RecordSchema>(
1085
- ns: Main<T>,
1086
- { maxRetries = 3, ...options }: ListOptions & { maxRetries?: number } = {},
1087
- ): AsyncGenerator<ListRecordItem<Infer<T>>, void, unknown> {
1088
- const schema = getMain(ns)
1089
- let currentErrorCount = 0
1090
-
1091
- do {
1092
- options.signal?.throwIfAborted()
1093
-
1094
- try {
1095
- const { body } = await this.listRecords(schema.$type, options)
1096
-
1097
- // We got a successful response, reset error count
1098
- currentErrorCount = 0
1099
-
1100
- // We don't use this.list() here so that we can lazily process records as
1101
- // they come in, rather than mapping and yielding the entire page at once.
1102
- for (const record of body.records) {
1103
- yield processListRecord.call(schema, record)
1104
- }
1105
-
1106
- // If the server returns the same cursor, we may be in a loop. Stop
1107
- // iteration.
1108
- if (body.cursor && body.cursor === options.cursor) {
1109
- return
1110
- }
1111
-
1112
- options.cursor = body.cursor
1113
- } catch (err) {
1114
- currentErrorCount++
1115
- if (currentErrorCount > maxRetries) {
1116
- throw err
1117
- }
1118
-
1119
- if (err instanceof XrpcResponseError) {
1120
- // Page not found, return empty iterator
1121
- if (err.status === 404 || err.error === 'NotFound') {
1122
- return
1123
- }
1124
-
1125
- // Rate limit error
1126
- if (err.status === 429 || err.error === 'RateLimitExceeded') {
1127
- const resetsAt = err.headers.get('RateLimit-Reset') // epoch
1128
- if (resetsAt != null && /^\s*\d+\s*$/.test(resetsAt)) {
1129
- const resetsIn = Number(resetsAt) * 1000 - Date.now()
1130
- await wait(Math.max(resetsIn, 1e3), options)
1131
- continue
1132
- }
1133
-
1134
- // Unable to determine when to retry; fall through
1135
- }
1136
-
1137
- // Server asks to retry after a certain time
1138
- const retryAfter = err.headers.get('Retry-After')
1139
- if (retryAfter != null) {
1140
- const waitTime = /^\s*\d+\s*$/.test(retryAfter)
1141
- ? // Retry-After is in seconds
1142
- Number(retryAfter) * 1000
1143
- : // Retry-After is an http-date
1144
- new Date(retryAfter).getTime() - Date.now()
1145
-
1146
- if (!Number.isNaN(waitTime)) {
1147
- await wait(Math.max(waitTime, 1e3), options)
1148
- continue
1149
- }
1150
-
1151
- // Invalid date; fall through
1152
- }
1153
- }
1154
-
1155
- // Exponential backoff for transient errors
1156
- if (err instanceof XrpcError && err.shouldRetry()) {
1157
- const waitTime = Math.min(2 ** currentErrorCount * 1000, 30_000)
1158
- await wait(waitTime, options)
1159
- continue
1160
- }
1161
-
1162
- // Propagate unexpected and non-retryable errors
1163
- throw err
1164
- }
1165
- } while (options.cursor)
1166
- }
1167
- }
1168
-
1169
- function processListRecord<T extends RecordSchema>(
1170
- this: T,
1171
- record: ListRecordsRecord,
1172
- ): ListRecordItem<Infer<T>> {
1173
- const result = this.safeValidate(record.value)
1174
- if (result.success) {
1175
- return { ...record, valid: true, value: result.value }
1176
- } else {
1177
- return { ...record, valid: false }
1178
- }
1179
- }