@atproto/lex-client 0.0.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 (111) hide show
  1. package/dist/agent.d.ts +33 -0
  2. package/dist/agent.d.ts.map +1 -0
  3. package/dist/agent.js +21 -0
  4. package/dist/agent.js.map +1 -0
  5. package/dist/client.d.ts +456 -0
  6. package/dist/client.d.ts.map +1 -0
  7. package/dist/client.js +236 -0
  8. package/dist/client.js.map +1 -0
  9. package/dist/error.d.ts +70 -0
  10. package/dist/error.d.ts.map +1 -0
  11. package/dist/error.js +98 -0
  12. package/dist/error.js.map +1 -0
  13. package/dist/index.d.ts +7 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +10 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/lexicons/com/atproto/repo/createRecord.d.ts +3 -0
  18. package/dist/lexicons/com/atproto/repo/createRecord.d.ts.map +1 -0
  19. package/dist/lexicons/com/atproto/repo/createRecord.defs.d.ts +100 -0
  20. package/dist/lexicons/com/atproto/repo/createRecord.defs.d.ts.map +1 -0
  21. package/dist/lexicons/com/atproto/repo/createRecord.defs.js +42 -0
  22. package/dist/lexicons/com/atproto/repo/createRecord.defs.js.map +1 -0
  23. package/dist/lexicons/com/atproto/repo/createRecord.js +10 -0
  24. package/dist/lexicons/com/atproto/repo/createRecord.js.map +1 -0
  25. package/dist/lexicons/com/atproto/repo/defs.d.ts +3 -0
  26. package/dist/lexicons/com/atproto/repo/defs.d.ts.map +1 -0
  27. package/dist/lexicons/com/atproto/repo/defs.defs.d.ts +12 -0
  28. package/dist/lexicons/com/atproto/repo/defs.defs.d.ts.map +1 -0
  29. package/dist/lexicons/com/atproto/repo/defs.defs.js +16 -0
  30. package/dist/lexicons/com/atproto/repo/defs.defs.js.map +1 -0
  31. package/dist/lexicons/com/atproto/repo/defs.js +10 -0
  32. package/dist/lexicons/com/atproto/repo/defs.js.map +1 -0
  33. package/dist/lexicons/com/atproto/repo/deleteRecord.d.ts +3 -0
  34. package/dist/lexicons/com/atproto/repo/deleteRecord.d.ts.map +1 -0
  35. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.d.ts +70 -0
  36. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.d.ts.map +1 -0
  37. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.js +33 -0
  38. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.js.map +1 -0
  39. package/dist/lexicons/com/atproto/repo/deleteRecord.js +10 -0
  40. package/dist/lexicons/com/atproto/repo/deleteRecord.js.map +1 -0
  41. package/dist/lexicons/com/atproto/repo/getRecord.d.ts +3 -0
  42. package/dist/lexicons/com/atproto/repo/getRecord.d.ts.map +1 -0
  43. package/dist/lexicons/com/atproto/repo/getRecord.defs.d.ts +82 -0
  44. package/dist/lexicons/com/atproto/repo/getRecord.defs.d.ts.map +1 -0
  45. package/dist/lexicons/com/atproto/repo/getRecord.defs.js +30 -0
  46. package/dist/lexicons/com/atproto/repo/getRecord.defs.js.map +1 -0
  47. package/dist/lexicons/com/atproto/repo/getRecord.js +10 -0
  48. package/dist/lexicons/com/atproto/repo/getRecord.js.map +1 -0
  49. package/dist/lexicons/com/atproto/repo/listRecords.d.ts +3 -0
  50. package/dist/lexicons/com/atproto/repo/listRecords.d.ts.map +1 -0
  51. package/dist/lexicons/com/atproto/repo/listRecords.defs.d.ts +75 -0
  52. package/dist/lexicons/com/atproto/repo/listRecords.defs.d.ts.map +1 -0
  53. package/dist/lexicons/com/atproto/repo/listRecords.defs.js +42 -0
  54. package/dist/lexicons/com/atproto/repo/listRecords.defs.js.map +1 -0
  55. package/dist/lexicons/com/atproto/repo/listRecords.js +10 -0
  56. package/dist/lexicons/com/atproto/repo/listRecords.js.map +1 -0
  57. package/dist/lexicons/com/atproto/repo/putRecord.d.ts +3 -0
  58. package/dist/lexicons/com/atproto/repo/putRecord.d.ts.map +1 -0
  59. package/dist/lexicons/com/atproto/repo/putRecord.defs.d.ts +110 -0
  60. package/dist/lexicons/com/atproto/repo/putRecord.defs.d.ts.map +1 -0
  61. package/dist/lexicons/com/atproto/repo/putRecord.defs.js +46 -0
  62. package/dist/lexicons/com/atproto/repo/putRecord.defs.js.map +1 -0
  63. package/dist/lexicons/com/atproto/repo/putRecord.js +10 -0
  64. package/dist/lexicons/com/atproto/repo/putRecord.js.map +1 -0
  65. package/dist/lexicons/com/atproto/repo/uploadBlob.d.ts +3 -0
  66. package/dist/lexicons/com/atproto/repo/uploadBlob.d.ts.map +1 -0
  67. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.d.ts +25 -0
  68. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.d.ts.map +1 -0
  69. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.js +22 -0
  70. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.js.map +1 -0
  71. package/dist/lexicons/com/atproto/repo/uploadBlob.js +10 -0
  72. package/dist/lexicons/com/atproto/repo/uploadBlob.js.map +1 -0
  73. package/dist/lexicons/com/atproto/repo.d.ts +8 -0
  74. package/dist/lexicons/com/atproto/repo.d.ts.map +1 -0
  75. package/dist/lexicons/com/atproto/repo.js +15 -0
  76. package/dist/lexicons/com/atproto/repo.js.map +1 -0
  77. package/dist/lexicons/com/atproto.d.ts +2 -0
  78. package/dist/lexicons/com/atproto.d.ts.map +1 -0
  79. package/dist/lexicons/com/atproto.js +9 -0
  80. package/dist/lexicons/com/atproto.js.map +1 -0
  81. package/dist/lexicons/com.d.ts +2 -0
  82. package/dist/lexicons/com.d.ts.map +1 -0
  83. package/dist/lexicons/com.js +9 -0
  84. package/dist/lexicons/com.js.map +1 -0
  85. package/dist/response.d.ts +21 -0
  86. package/dist/response.d.ts.map +1 -0
  87. package/dist/response.js +31 -0
  88. package/dist/response.js.map +1 -0
  89. package/dist/types.d.ts +17 -0
  90. package/dist/types.d.ts.map +1 -0
  91. package/dist/types.js +7 -0
  92. package/dist/types.js.map +1 -0
  93. package/dist/xrpc.d.ts +37 -0
  94. package/dist/xrpc.d.ts.map +1 -0
  95. package/dist/xrpc.js +185 -0
  96. package/dist/xrpc.js.map +1 -0
  97. package/jest.config.js +5 -0
  98. package/package.json +46 -0
  99. package/scripts/lex-build.mjs +40 -0
  100. package/src/agent.ts +63 -0
  101. package/src/client.ts +513 -0
  102. package/src/error.ts +154 -0
  103. package/src/index.ts +6 -0
  104. package/src/response.ts +42 -0
  105. package/src/types.ts +21 -0
  106. package/src/xrpc.ts +335 -0
  107. package/tests/client.test.ts +370 -0
  108. package/tsconfig.build.json +12 -0
  109. package/tsconfig.build.tsbuildinfo +1 -0
  110. package/tsconfig.json +7 -0
  111. package/tsconfig.tests.json +12 -0
package/src/client.ts ADDED
@@ -0,0 +1,513 @@
1
+ import { LexMap, LexValue } from '@atproto/lex-data'
2
+ import {
3
+ AtIdentifier,
4
+ Did,
5
+ Infer,
6
+ InferProcedureInputBody,
7
+ InferProcedureOutputBody,
8
+ InferQueryOutputBody,
9
+ InferQueryParameters,
10
+ InferRecordKey,
11
+ Nsid,
12
+ Params,
13
+ Procedure,
14
+ Query,
15
+ RecordKey,
16
+ RecordSchema,
17
+ Restricted,
18
+ ValidationResult,
19
+ } from '@atproto/lex-schema'
20
+ import { Agent, AgentOptions, buildAgent } from './agent.js'
21
+ import {
22
+ KnownError,
23
+ XrpcError,
24
+ XrpcRequestFailure,
25
+ asXrpcRequestFailureFor,
26
+ } from './error.js'
27
+ import * as com from './lexicons/com.js'
28
+ import { XrpcResponse, XrpcResponseBody } from './response.js'
29
+ import { CallOptions, Namespace, Service, getMain } from './types.js'
30
+ import { XrpcOptions, xrpc, xrpcRequestHeaders } from './xrpc.js'
31
+
32
+ export type ClientOptions = {
33
+ labelers?: Iterable<Did>
34
+ headers?: HeadersInit
35
+ service?: Service
36
+ }
37
+
38
+ export type Action<I = any, O = any> = (
39
+ client: Client,
40
+ input: I,
41
+ options: CallOptions,
42
+ ) => O | Promise<O>
43
+ export type InferActionInput<A extends Action> =
44
+ A extends Action<infer I, any> ? I : never
45
+ export type InferActionOutput<A extends Action> =
46
+ A extends Action<any, infer O> ? O : never
47
+
48
+ export type CreateRecordOptions = CallOptions & {
49
+ repo?: AtIdentifier
50
+ swapCommit?: string
51
+ validate?: boolean
52
+ }
53
+
54
+ export type DeleteRecordOptions = CallOptions & {
55
+ repo?: AtIdentifier
56
+ swapCommit?: string
57
+ swapRecord?: string
58
+ }
59
+
60
+ export type GetRecordOptions = CallOptions & {
61
+ repo?: AtIdentifier
62
+ }
63
+
64
+ export type PutRecordOptions = CallOptions & {
65
+ repo?: AtIdentifier
66
+ swapCommit?: string
67
+ swapRecord?: string
68
+ validate?: boolean
69
+ }
70
+
71
+ export type ListRecordsOptions = CallOptions & {
72
+ repo?: AtIdentifier
73
+ limit?: number
74
+ cursor?: string
75
+ reverse?: boolean
76
+ }
77
+
78
+ export type RecordKeyOptions<
79
+ T extends RecordSchema,
80
+ AlsoOptionalWhenRecordKeyIs extends RecordKey = never,
81
+ > = T['key'] extends `literal:${string}` | AlsoOptionalWhenRecordKeyIs
82
+ ? { rkey?: InferRecordKey<T> }
83
+ : { rkey: InferRecordKey<T> }
84
+
85
+ export type CreateOptions<T extends RecordSchema> = CreateRecordOptions &
86
+ RecordKeyOptions<T, 'tid'>
87
+ export type CreateOutput = XrpcResponseBody<
88
+ typeof com.atproto.repo.createRecord.main
89
+ >
90
+
91
+ export type DeleteOptions<T extends RecordSchema> = DeleteRecordOptions &
92
+ RecordKeyOptions<T>
93
+ export type DeleteOutput = XrpcResponseBody<
94
+ typeof com.atproto.repo.deleteRecord.main
95
+ >
96
+ export type GetOptions<T extends RecordSchema> = GetRecordOptions &
97
+ RecordKeyOptions<T>
98
+ export type GetOutput<T extends RecordSchema> = Omit<
99
+ XrpcResponseBody<typeof com.atproto.repo.getRecord.main>,
100
+ 'value'
101
+ > & { value: Infer<T> }
102
+
103
+ export type PutOptions<T extends RecordSchema> = PutRecordOptions &
104
+ RecordKeyOptions<T>
105
+ export type PutOutput = XrpcResponseBody<typeof com.atproto.repo.putRecord.main>
106
+
107
+ export type ListOptions = ListRecordsOptions
108
+ export type ListOutput<T extends RecordSchema> = XrpcResponseBody<
109
+ typeof com.atproto.repo.listRecords.main
110
+ > & {
111
+ records: ListRecord<T>[]
112
+ // @NOTE Because the schema uses "type": "unknown" instead of an open union,
113
+ // we have to use LexMap instead of TypedObject here.
114
+ invalid: LexMap[]
115
+ }
116
+ export type ListRecord<T extends RecordSchema> =
117
+ com.atproto.repo.listRecords.DefRecord & {
118
+ value: Infer<T>
119
+ }
120
+
121
+ export class Client implements Agent {
122
+ static appLabelers: readonly Did[] = []
123
+
124
+ /**
125
+ * Configures the Client (or its sub classes) globally.
126
+ */
127
+ static configure(opts: { appLabelers?: Iterable<Did> }) {
128
+ if (opts.appLabelers) this.appLabelers = [...opts.appLabelers]
129
+ }
130
+
131
+ public readonly agent: Agent
132
+ public readonly headers: Headers
133
+ public readonly service?: Service
134
+ public readonly labelers: Set<Did>
135
+
136
+ constructor(agent: Agent | AgentOptions, options: ClientOptions = {}) {
137
+ this.agent =
138
+ typeof agent === 'object' && 'fetchHandler' in agent
139
+ ? agent
140
+ : buildAgent(agent)
141
+ this.service = options.service
142
+ this.labelers = new Set(options.labelers)
143
+ this.headers = new Headers(options.headers)
144
+ }
145
+
146
+ get did(): Did | undefined {
147
+ return this.agent.did
148
+ }
149
+
150
+ get assertDid(): Did {
151
+ this.assertAuthenticated()
152
+ return this.did
153
+ }
154
+
155
+ public assertAuthenticated(): asserts this is { did: Did } {
156
+ if (!this.did) throw new XrpcError(KnownError.AuthenticationRequired)
157
+ }
158
+
159
+ public setLabelers(labelers: Iterable<Did> = []) {
160
+ this.clearLabelers()
161
+ this.addLabelers(labelers)
162
+ }
163
+
164
+ public addLabelers(labelers: Iterable<Did>) {
165
+ for (const labeler of labelers) this.labelers.add(labeler)
166
+ }
167
+
168
+ public clearLabelers() {
169
+ this.labelers.clear()
170
+ }
171
+
172
+ public fetchHandler(path: string, init: RequestInit): Promise<Response> {
173
+ const headers = xrpcRequestHeaders({
174
+ headers: init.headers,
175
+ service: this.service,
176
+ labelers: [
177
+ ...(this.constructor as typeof Client).appLabelers.map(
178
+ (l) => `${l};redact` as const,
179
+ ),
180
+ ...this.labelers,
181
+ ],
182
+ })
183
+
184
+ // Incoming headers take precedence
185
+ for (const [key, value] of this.headers) {
186
+ if (!headers.has(key)) headers.set(key, value)
187
+ }
188
+
189
+ return this.agent.fetchHandler(path, { ...init, headers })
190
+ }
191
+
192
+ async xrpc<const M extends Query | Procedure>(
193
+ ns: NonNullable<unknown> extends XrpcOptions<M>
194
+ ? Namespace<M>
195
+ : Restricted<'This XRPC method requires an "options" argument'>,
196
+ ): Promise<XrpcResponse<M>>
197
+ async xrpc<const M extends Query | Procedure>(
198
+ ns: Namespace<M>,
199
+ options: XrpcOptions<M>,
200
+ ): Promise<XrpcResponse<M>>
201
+ async xrpc<const M extends Query | Procedure>(
202
+ ns: Namespace<M>,
203
+ options: XrpcOptions<M> = {} as XrpcOptions<M>,
204
+ ): Promise<XrpcResponse<M>> {
205
+ return xrpc(this, ns, options)
206
+ }
207
+
208
+ async xrpcSafe<const M extends Query | Procedure>(
209
+ ns: NonNullable<unknown> extends XrpcOptions<M>
210
+ ? Namespace<M>
211
+ : Restricted<'This XRPC method requires an "options" argument'>,
212
+ ): Promise<XrpcResponse<M> | XrpcRequestFailure<M>>
213
+ async xrpcSafe<const M extends Query | Procedure>(
214
+ ns: Namespace<M>,
215
+ options: XrpcOptions<M>,
216
+ ): Promise<XrpcResponse<M> | XrpcRequestFailure<M>>
217
+ async xrpcSafe<const M extends Query | Procedure>(
218
+ ns: Namespace<M>,
219
+ options: XrpcOptions<M> = {} as XrpcOptions<M>,
220
+ ): Promise<unknown> {
221
+ const schema = getMain(ns)
222
+ return this.xrpc(schema, options).catch(asXrpcRequestFailureFor(schema))
223
+ }
224
+
225
+ /**
226
+ * @param rkey Leave `undefined` to have the server generate a TID.
227
+ */
228
+ public async createRecord(
229
+ record: { $type: Nsid } & LexMap,
230
+ rkey?: string,
231
+ options?: CreateRecordOptions,
232
+ ) {
233
+ return this.xrpc(com.atproto.repo.createRecord.main, {
234
+ ...options,
235
+ body: {
236
+ repo: options?.repo ?? this.assertDid,
237
+ collection: record.$type,
238
+ record,
239
+ rkey,
240
+ validate: options?.validate,
241
+ swapCommit: options?.swapCommit,
242
+ },
243
+ })
244
+ }
245
+
246
+ async createRecordsSafe(...args: Parameters<Client['createRecord']>) {
247
+ return this.createRecord(...args).catch(
248
+ asXrpcRequestFailureFor(com.atproto.repo.createRecord.main),
249
+ )
250
+ }
251
+
252
+ async deleteRecord(
253
+ collection: Nsid,
254
+ rkey: string,
255
+ options?: DeleteRecordOptions,
256
+ ) {
257
+ return this.xrpc(com.atproto.repo.deleteRecord.main, {
258
+ ...options,
259
+ body: {
260
+ repo: options?.repo ?? this.assertDid,
261
+ collection,
262
+ rkey,
263
+ swapCommit: options?.swapCommit,
264
+ swapRecord: options?.swapRecord,
265
+ },
266
+ })
267
+ }
268
+
269
+ async deleteRecordsSafe(...args: Parameters<Client['deleteRecord']>) {
270
+ return this.deleteRecord(...args).catch(
271
+ asXrpcRequestFailureFor(com.atproto.repo.deleteRecord.main),
272
+ )
273
+ }
274
+
275
+ public async getRecord(
276
+ collection: Nsid,
277
+ rkey: string,
278
+ options?: GetRecordOptions,
279
+ ) {
280
+ return this.xrpc(com.atproto.repo.getRecord.main, {
281
+ ...options,
282
+ params: {
283
+ repo: options?.repo ?? this.assertDid,
284
+ collection,
285
+ rkey,
286
+ },
287
+ })
288
+ }
289
+
290
+ async getRecordsSafe(...args: Parameters<Client['getRecord']>) {
291
+ return this.getRecord(...args).catch(
292
+ asXrpcRequestFailureFor(com.atproto.repo.getRecord.main),
293
+ )
294
+ }
295
+
296
+ async putRecord(
297
+ record: { $type: Nsid } & LexMap,
298
+ rkey: string,
299
+ options?: PutRecordOptions,
300
+ ) {
301
+ return this.xrpc(com.atproto.repo.putRecord.main, {
302
+ ...options,
303
+ body: {
304
+ repo: options?.repo ?? this.assertDid,
305
+ collection: record.$type,
306
+ rkey,
307
+ record,
308
+ validate: options?.validate,
309
+ swapCommit: options?.swapCommit,
310
+ swapRecord: options?.swapRecord,
311
+ },
312
+ })
313
+ }
314
+
315
+ async putRecordsSafe(...args: Parameters<Client['putRecord']>) {
316
+ return this.putRecord(...args).catch(
317
+ asXrpcRequestFailureFor(com.atproto.repo.putRecord.main),
318
+ )
319
+ }
320
+
321
+ async listRecords(nsid: Nsid, options?: ListRecordsOptions) {
322
+ return this.xrpc(com.atproto.repo.listRecords.main, {
323
+ ...options,
324
+ params: {
325
+ repo: options?.repo ?? this.assertDid,
326
+ collection: nsid,
327
+ cursor: options?.cursor,
328
+ limit: options?.limit,
329
+ reverse: options?.reverse,
330
+ },
331
+ })
332
+ }
333
+
334
+ public async call<const T extends Action>(
335
+ ns: Namespace<T>,
336
+ input: InferActionInput<T>,
337
+ options?: CallOptions,
338
+ ): Promise<InferActionOutput<T>>
339
+ public async call<const T extends Procedure>(
340
+ ns: Namespace<T>,
341
+ body: InferProcedureInputBody<T>,
342
+ options?: CallOptions,
343
+ ): Promise<InferProcedureOutputBody<T>>
344
+ public async call<const T extends Query>(
345
+ ns: NonNullable<unknown> extends InferQueryParameters<T>
346
+ ? Namespace<T>
347
+ : Restricted<'This query type requires a "params" argument'>,
348
+ ): Promise<InferQueryOutputBody<T>>
349
+ public async call<const T extends Query>(
350
+ ns: Namespace<T>,
351
+ params: InferQueryParameters<T>,
352
+ options?: CallOptions,
353
+ ): Promise<InferQueryOutputBody<T>>
354
+ public async call(
355
+ ns: Namespace<Action> | Namespace<Procedure> | Namespace<Query>,
356
+ arg?: LexValue | Params,
357
+ options: CallOptions = {},
358
+ ): Promise<unknown> {
359
+ const method = getMain(ns)
360
+
361
+ if (typeof method === 'function') {
362
+ return method(this, arg, options)
363
+ }
364
+
365
+ if (method instanceof Procedure) {
366
+ const body = arg as LexValue | undefined
367
+ const result = await this.xrpc(method, { ...options, body })
368
+ return result.body
369
+ } else if (method instanceof Query) {
370
+ const params = arg as Params | undefined
371
+ const result = await this.xrpc(method, { ...options, params })
372
+ return result.body
373
+ } else {
374
+ throw new TypeError('Invalid lexicon')
375
+ }
376
+ }
377
+
378
+ public async create<const T extends RecordSchema>(
379
+ ns: NonNullable<unknown> extends CreateOptions<T>
380
+ ? Namespace<T>
381
+ : Restricted<'This record type requires an "options" argument'>,
382
+ input: Omit<Infer<T>, '$type'>,
383
+ ): Promise<CreateOutput>
384
+ public async create<const T extends RecordSchema>(
385
+ ns: Namespace<T>,
386
+ input: Omit<Infer<T>, '$type'>,
387
+ options: CreateOptions<T>,
388
+ ): Promise<CreateOutput>
389
+ public async create<const T extends RecordSchema>(
390
+ ns: Namespace<T>,
391
+ input: Omit<Infer<T>, '$type'>,
392
+ options: CreateOptions<T> = {} as CreateOptions<T>,
393
+ ): Promise<CreateOutput> {
394
+ const schema: T = getMain(ns)
395
+ const record = options.validate
396
+ ? schema.parse(schema.build(input))
397
+ : schema.build(input)
398
+ const rkey = options.rkey ?? getDefaultRecordKey(schema)
399
+ if (rkey !== undefined) schema.keySchema.assert(rkey)
400
+ const response = await this.createRecord(record, rkey, options)
401
+ return response.body
402
+ }
403
+
404
+ public async delete<const T extends RecordSchema>(
405
+ ns: NonNullable<unknown> extends DeleteOptions<T>
406
+ ? Namespace<T>
407
+ : Restricted<'This record type requires an "options" argument'>,
408
+ ): Promise<DeleteOutput>
409
+ public async delete<const T extends RecordSchema>(
410
+ ns: Namespace<T>,
411
+ options?: DeleteOptions<T>,
412
+ ): Promise<DeleteOutput>
413
+ public async delete<const T extends RecordSchema>(
414
+ ns: Namespace<T>,
415
+ options: DeleteOptions<T> = {} as DeleteOptions<T>,
416
+ ): Promise<DeleteOutput> {
417
+ const schema = getMain(ns)
418
+ const rkey = schema.keySchema.parse(
419
+ options.rkey ?? getLiteralRecordKey(schema),
420
+ )
421
+ const response = await this.deleteRecord(schema.$type, rkey, options)
422
+ return response.body
423
+ }
424
+
425
+ public async get<const T extends RecordSchema>(
426
+ ns: T['key'] extends `literal:${string}`
427
+ ? Namespace<T>
428
+ : Restricted<'This record type requires an "options" argument'>,
429
+ ): Promise<GetOutput<T>>
430
+ public async get<const T extends RecordSchema>(
431
+ ns: Namespace<T>,
432
+ options?: GetOptions<T>,
433
+ ): Promise<GetOutput<T>>
434
+ public async get<const T extends RecordSchema>(
435
+ ns: Namespace<T>,
436
+ options: GetOptions<T> = {} as GetOptions<T>,
437
+ ): Promise<GetOutput<T>> {
438
+ const schema = getMain(ns)
439
+ const rkey = schema.keySchema.parse(
440
+ options.rkey ?? getLiteralRecordKey(schema),
441
+ )
442
+ const response = await this.getRecord(schema.$type, rkey, options)
443
+ const value = schema.parse(response.body.value) as Infer<T>
444
+ return { ...response.body, value }
445
+ }
446
+
447
+ public async put<const T extends RecordSchema>(
448
+ ns: NonNullable<unknown> extends PutOptions<T>
449
+ ? Namespace<T>
450
+ : Restricted<'This record type requires an "options" argument'>,
451
+ input: Omit<Infer<T>, '$type'>,
452
+ ): Promise<PutOutput>
453
+ public async put<const T extends RecordSchema>(
454
+ ns: Namespace<T>,
455
+ input: Omit<Infer<T>, '$type'>,
456
+ options: PutOptions<T>,
457
+ ): Promise<PutOutput>
458
+ public async put<const T extends RecordSchema>(
459
+ ns: Namespace<T>,
460
+ input: Omit<Infer<T>, '$type'>,
461
+ options: PutOptions<T> = {} as PutOptions<T>,
462
+ ): Promise<PutOutput> {
463
+ const schema = getMain(ns)
464
+ const record = schema.build(input)
465
+ const rkey = options.rkey ?? getLiteralRecordKey(schema)
466
+ const response = await this.putRecord(record, rkey, options)
467
+ return response.body
468
+ }
469
+
470
+ async list<const T extends RecordSchema>(
471
+ ns: Namespace<T>,
472
+ options?: ListOptions,
473
+ ): Promise<ListOutput<T>> {
474
+ const schema = getMain(ns)
475
+ const { body } = await this.listRecords(schema.$type, options)
476
+
477
+ const records: ListRecord<T>[] = []
478
+ const invalid: LexMap[] = []
479
+
480
+ for (const record of body.records) {
481
+ const parsed = schema.validate(record.value) as ValidationResult<Infer<T>>
482
+ if (parsed.success) {
483
+ records.push({ ...record, value: parsed.value })
484
+ } else {
485
+ invalid.push(record.value)
486
+ }
487
+ }
488
+
489
+ return { ...body, records, invalid }
490
+ }
491
+ }
492
+
493
+ function getDefaultRecordKey<const T extends RecordSchema>(
494
+ schema: T,
495
+ ): undefined | InferRecordKey<T> {
496
+ // Let the server generate the TID
497
+ if (schema.key === 'tid') return undefined
498
+ if (schema.key === 'any') return undefined
499
+
500
+ return getLiteralRecordKey(schema)
501
+ }
502
+
503
+ function getLiteralRecordKey<const T extends RecordSchema>(
504
+ schema: T,
505
+ ): InferRecordKey<T> {
506
+ if (schema.key.startsWith('literal:')) {
507
+ return schema.key.slice(8)
508
+ }
509
+
510
+ throw new TypeError(
511
+ `An "rkey" must be provided for record key type "${schema.key}" (${schema.$type})`,
512
+ )
513
+ }
package/src/error.ts ADDED
@@ -0,0 +1,154 @@
1
+ import { LexValue } from '@atproto/lex-data'
2
+ import {
3
+ Infer,
4
+ ObjectSchema,
5
+ Procedure,
6
+ Query,
7
+ ResultFailure,
8
+ StringSchema,
9
+ Validator,
10
+ } from '@atproto/lex-schema'
11
+
12
+ export enum KnownError {
13
+ Unknown = 'Unknown',
14
+ AuthenticationRequired = 'AuthenticationRequired',
15
+ Forbidden = 'Forbidden',
16
+ InternalServerError = 'InternalServerError',
17
+ InvalidRequest = 'InvalidRequest',
18
+ InvalidResponse = 'InvalidResponse',
19
+ MethodNotImplemented = 'MethodNotImplemented',
20
+ NotAcceptable = 'NotAcceptable',
21
+ NotEnoughResources = 'NotEnoughResources',
22
+ PayloadTooLarge = 'PayloadTooLarge',
23
+ RateLimitExceeded = 'RateLimitExceeded',
24
+ UnsupportedMediaType = 'UnsupportedMediaType',
25
+ UpstreamFailure = 'UpstreamFailure',
26
+ UpstreamTimeout = 'UpstreamTimeout',
27
+ XRPCNotSupported = 'XRPCNotSupported',
28
+ }
29
+
30
+ export type XrpcFailure<N extends string, E> = ResultFailure<E> & {
31
+ name: N
32
+ }
33
+
34
+ export type XrpcErrorName = Infer<typeof xrpcErrorNameSchema>
35
+ export const xrpcErrorNameSchema = new StringSchema({
36
+ minLength: 1,
37
+ knownValues: Object.keys(KnownError) as KnownError[],
38
+ })
39
+
40
+ export type XrpcErrorBody<N extends XrpcErrorName = XrpcErrorName> = {
41
+ error: N
42
+ message?: string
43
+ }
44
+ export const xrpcErrorBodySchema = new ObjectSchema(
45
+ { error: xrpcErrorNameSchema, message: new StringSchema({}) },
46
+ { required: ['error'] },
47
+ ) satisfies Validator<XrpcErrorBody>
48
+
49
+ /**
50
+ * @implements {XrpcFailure<N, XrpcError<N>>} for convenience in result handling contexts.
51
+ */
52
+ export class XrpcError<N extends XrpcErrorName = XrpcErrorName>
53
+ extends Error
54
+ implements XrpcFailure<N, XrpcError<N>>
55
+ {
56
+ constructor(
57
+ public readonly name: N,
58
+ message: string = name === KnownError.InvalidResponse
59
+ ? `XRPC service returned an invalid response`
60
+ : name === KnownError.InternalServerError
61
+ ? `XRPC service encountered an internal error`
62
+ : name === KnownError.UpstreamFailure ||
63
+ name === KnownError.UpstreamTimeout
64
+ ? `XRPC service upstream error`
65
+ : `XRPC ${name} error`,
66
+ options?: ErrorOptions,
67
+ ) {
68
+ super(message, options)
69
+ }
70
+
71
+ /** @see {@link ResultFailure.success} */
72
+ readonly success = false as const
73
+
74
+ /** @see {@link ResultFailure.error} */
75
+ get error(): this {
76
+ return this
77
+ }
78
+
79
+ static from(cause: unknown, message?: string): XrpcError {
80
+ if (cause instanceof XrpcError) {
81
+ return cause
82
+ }
83
+ return new XrpcError(
84
+ 'Unknown',
85
+ message ?? (cause instanceof Error ? cause.message : undefined),
86
+ { cause },
87
+ )
88
+ }
89
+ }
90
+
91
+ export class XrpcServiceError<
92
+ N extends XrpcErrorName = XrpcErrorName,
93
+ > extends XrpcError<N> {
94
+ constructor(
95
+ name: N,
96
+ public readonly status: number,
97
+ public readonly headers: Headers,
98
+ public readonly body: undefined | LexValue,
99
+ message?: string,
100
+ options?: ErrorOptions,
101
+ ) {
102
+ super(name, message, options)
103
+ }
104
+ }
105
+
106
+ export class XrpcResponseError<
107
+ N extends XrpcErrorName = XrpcErrorName,
108
+ B extends XrpcErrorBody<N> = XrpcErrorBody<N>,
109
+ > extends XrpcError<N> {
110
+ constructor(
111
+ public readonly status: number,
112
+ public readonly headers: Headers,
113
+ public readonly encoding: undefined | string,
114
+ public readonly body: B,
115
+ options?: ErrorOptions,
116
+ ) {
117
+ super(body.error, body.message, options)
118
+ }
119
+ }
120
+
121
+ export type XrpcRequestFailure<M extends Procedure | Query> =
122
+ // The server responded with a declared error.
123
+ | (M extends { errors: readonly (infer N extends string)[] }
124
+ ? XrpcResponseError<N> // implements XrpcRequestFailure<N, XrpcResponseError<N>>
125
+ : never)
126
+ // The server responded with an error that is not declared in the method's
127
+ // `errors` list.
128
+ | XrpcFailure<'Unknown', XrpcResponseError<string>>
129
+ // An unexpected error occurred (e.g., network error, invalid response, etc.)
130
+ | XrpcFailure<'UnexpectedError', unknown>
131
+
132
+ export function asXrpcRequestFailureFor<M extends Procedure | Query>(
133
+ schema: M,
134
+ ) {
135
+ // Performance: Using .bind instead of arrow function to avoid creating a closure
136
+ return asXrpcRequestFailure.bind(schema) as (
137
+ error: unknown,
138
+ ) => XrpcRequestFailure<M>
139
+ }
140
+
141
+ function asXrpcRequestFailure<M extends Procedure | Query>(
142
+ this: M,
143
+ error: unknown,
144
+ ): XrpcRequestFailure<M> {
145
+ if (!(error instanceof XrpcResponseError)) {
146
+ return { success: false, error, name: 'UnexpectedError' }
147
+ }
148
+
149
+ if (!this.errors.includes(error.name)) {
150
+ return { success: false, error, name: 'Unknown' }
151
+ }
152
+
153
+ return error
154
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './agent.js'
2
+ export * from './client.js'
3
+ export * from './error.js'
4
+ export * from './response.js'
5
+ export * from './types.js'
6
+ export * from './xrpc.js'
@@ -0,0 +1,42 @@
1
+ import {
2
+ InferPayloadBody,
3
+ InferPayloadEncoding,
4
+ Procedure,
5
+ Query,
6
+ ResultSuccess,
7
+ } from '@atproto/lex-schema'
8
+
9
+ export type XrpcResponseEncoding<M extends Procedure | Query> =
10
+ InferPayloadEncoding<M['output']>
11
+
12
+ export type XrpcResponseBody<M extends Procedure | Query> = InferPayloadBody<
13
+ M['output']
14
+ >
15
+
16
+ /**
17
+ * Small container for XRPC response data.
18
+ *
19
+ * @implements {ResultSuccess<XrpcResponse<M>>} for convenience in result handling contexts.
20
+ */
21
+ export class XrpcResponse<M extends Procedure | Query>
22
+ implements ResultSuccess<XrpcResponse<M>>
23
+ {
24
+ /** @see {@link ResultSuccess.success} */
25
+ readonly success = true as const
26
+
27
+ /** @see {@link ResultSuccess.value} */
28
+ get value(): this {
29
+ return this
30
+ }
31
+
32
+ get encoding(): XrpcResponseEncoding<M> {
33
+ return this.method.output?.encoding
34
+ }
35
+
36
+ constructor(
37
+ readonly method: M,
38
+ readonly status: number,
39
+ readonly headers: Headers,
40
+ readonly body: XrpcResponseBody<M>,
41
+ ) {}
42
+ }