@atproto/lex-client 0.1.5 → 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 +25 -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 +42 -21
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +97 -16
  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 +6 -2
  21. package/dist/util.d.ts.map +1 -1
  22. package/dist/util.js +25 -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 -1086
  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 -182
  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/xrpc.ts DELETED
@@ -1,446 +0,0 @@
1
- import { LexValue, isLexScalar, isPlainObject } from '@atproto/lex-data'
2
- import { lexStringify } from '@atproto/lex-json'
3
- import {
4
- InferInput,
5
- InferPayload,
6
- Main,
7
- NsidString,
8
- Params,
9
- Payload,
10
- Procedure,
11
- Query,
12
- Restricted,
13
- Subscription,
14
- getMain,
15
- } from '@atproto/lex-schema'
16
- import { Agent, AgentOptions, buildAgent } from './agent.js'
17
- import { XrpcFailure, XrpcFetchError, asXrpcFailure } from './errors.js'
18
- import { XrpcResponse, XrpcResponseOptions } from './response.js'
19
- import { BinaryBodyInit } from './types.js'
20
- import {
21
- XrpcRequestHeadersOptions,
22
- asUint8ArrayArrayBuffer,
23
- buildXrpcRequestHeaders,
24
- isAsyncIterable,
25
- isBlobLike,
26
- toReadableStream,
27
- } from './util.js'
28
-
29
- /**
30
- * The query/path parameters type for an XRPC method, inferred from its schema.
31
- *
32
- * @typeParam M - The XRPC method type (Procedure, Query, or Subscription)
33
- */
34
- export type XrpcRequestParams<M extends Procedure | Query | Subscription> =
35
- InferInput<M['parameters']>
36
-
37
- // If all params are optional, allow omitting the params object
38
- type XrpcRequestParamsOptions<P extends Params> =
39
- NonNullable<unknown> extends P ? { params?: P } : { params: P }
40
-
41
- type XrpcRequestPayload<M extends Procedure | Query> = M extends Procedure
42
- ? InferPayload<M['input'], BinaryBodyInit>
43
- : undefined
44
-
45
- type XrpcRequestPayloadOptions<TPayload> = TPayload extends {
46
- body: infer B
47
- encoding: infer E
48
- }
49
- ? {
50
- body: B
51
-
52
- /**
53
- * mime type hint for binary bodies
54
- *
55
- * Only needed for endpoints that accept binary input (e.g. file uploads)
56
- * when the body is a Blob-like object without a type (e.g. fetch-blob's
57
- * Blob). If the body is a Blob-like object with a type, that type will be
58
- * used as the content-type header instead of this option.
59
- *
60
- * @default "application/octet-stream"
61
- */
62
- encoding?: E
63
- }
64
- : { body?: undefined; encoding?: undefined }
65
-
66
- /**
67
- * Options for making an XRPC request, based on the method schema.
68
- *
69
- * Combines {@link XrpcRequestOptions} and {@link XrpcResponseOptions} with
70
- * method-specific params and body requirements. The type system ensures
71
- * required params/body are provided based on the method schema.
72
- *
73
- * @typeParam M - The XRPC method type (Procedure or Query)
74
- *
75
- * @example Query with params
76
- * ```typescript
77
- * const options: XrpcOptions<typeof app.bsky.feed.getTimeline.main> = {
78
- * params: { limit: 50 }
79
- * }
80
- * ```
81
- *
82
- * @example Procedure with body
83
- * ```typescript
84
- * const options: XrpcOptions<typeof com.atproto.repo.createRecord.main> = {
85
- * body: { repo: did, collection: 'app.bsky.feed.post', record: { ... } }
86
- * }
87
- * ```
88
- */
89
- export type XrpcOptions<M extends Procedure | Query = Procedure | Query> =
90
- XrpcRequestOptions<M> & XrpcResponseOptions
91
-
92
- export type XrpcRequestOptions<
93
- M extends Procedure | Query = Procedure | Query,
94
- > = XrpcRequestProcessingOptions &
95
- XrpcRequestHeadersOptions &
96
- XrpcRequestPayloadOptions<XrpcRequestPayload<M>> &
97
- XrpcRequestParamsOptions<XrpcRequestParams<M>>
98
-
99
- export type XrpcRequestProcessingOptions = {
100
- /**
101
- * AbortSignal to cancel the request.
102
- */
103
- signal?: AbortSignal
104
-
105
- /**
106
- * Whether to validate the request against the method's input schema. Enabling
107
- * this can help catch errors early but may have a performance cost. This
108
- * would typically only be set to `true` in development or debugging
109
- * scenarios.
110
- *
111
- * @default false
112
- */
113
- validateRequest?: boolean
114
- }
115
-
116
- /**
117
- * Makes an XRPC request and throws on failure.
118
- *
119
- * This is the low-level function for making XRPC calls.
120
- *
121
- * @param agent - The {@link Agent} to use for making the request
122
- * @param ns - The lexicon method definition
123
- * @param options - Request {@link XrpcOptions options} (params, body, headers, etc.)
124
- * @returns The successful {@link XrpcResponse}
125
- * @throws {XrpcFailure} When the request fails
126
- *
127
- * @example
128
- * ```typescript
129
- * const response = await xrpc('https://bsky.network', com.atproto.identity.resolveHandle, {
130
- * params: { handle: "atproto.com" }
131
- * })
132
- * ```
133
- *
134
- * @example
135
- * ```typescript
136
- * const response = await xrpc(agent, app.bsky.feed.getTimeline.main, {
137
- * params: { limit: 50 }
138
- * })
139
- * ```
140
- */
141
- export async function xrpc<const M extends Query | Procedure>(
142
- agentOpts: Agent | AgentOptions,
143
- ns: NonNullable<unknown> extends XrpcOptions<M>
144
- ? Main<M>
145
- : Restricted<'This XRPC method requires an "options" argument'>,
146
- ): Promise<XrpcResponse<M>>
147
- export async function xrpc<const M extends Query | Procedure>(
148
- agentOpts: Agent | AgentOptions,
149
- ns: Main<M>,
150
- options: XrpcOptions<M>,
151
- ): Promise<XrpcResponse<M>>
152
- export async function xrpc<const M extends Query | Procedure>(
153
- agentOpts: Agent | AgentOptions,
154
- ns: Main<M>,
155
- options: XrpcOptions<M> = {} as XrpcOptions<M>,
156
- ): Promise<XrpcResponse<M>> {
157
- const response = await xrpcSafe<M>(agentOpts, ns, options)
158
- if (response.success) return response
159
- else throw response
160
- }
161
-
162
- /**
163
- * Union type representing either a successful response or a failure.
164
- *
165
- * Both {@link XrpcResponse} and {@link XrpcFailure} have a `success` property
166
- * that can be used to discriminate between them.
167
- *
168
- * @typeParam M - The XRPC method type
169
- */
170
- export type XrpcResult<M extends Procedure | Query> =
171
- | XrpcResponse<M>
172
- | XrpcFailure<M>
173
-
174
- /**
175
- * Makes an XRPC request without throwing on failure.
176
- *
177
- * Returns a discriminated union that can be checked via the `success` property.
178
- * This is useful for handling errors without try/catch blocks. This also allow
179
- * failure results to be typed with the method schema, which can provide better
180
- * type safety when handling errors (e.g. checking for specific error codes).
181
- *
182
- * @param agent - The {@link Agent} to use for making the request
183
- * @param ns - The lexicon method definition
184
- * @param options - Request {@link XrpcOptions options} (params, body, headers, etc.)
185
- * @returns Either a successful {@link XrpcResponse} or an {@link XrpcFailure}
186
- *
187
- * @example
188
- * ```typescript
189
- * const result = await xrpcSafe('https://example.com', app.bsky.actor.getProfile, {
190
- * params: { actor: 'alice.bsky.social' }
191
- * })
192
- *
193
- * if (result.success) {
194
- * console.log(result.body.displayName)
195
- * } else {
196
- * console.error('Request failed:', result.error)
197
- * }
198
- * ```
199
- */
200
- export async function xrpcSafe<const M extends Query | Procedure>(
201
- agentOpts: Agent | AgentOptions,
202
- ns: NonNullable<unknown> extends XrpcOptions<M>
203
- ? Main<M>
204
- : Restricted<'This XRPC method requires an "options" argument'>,
205
- ): Promise<XrpcResult<M>>
206
- export async function xrpcSafe<const M extends Query | Procedure>(
207
- agentOpts: Agent | AgentOptions,
208
- ns: Main<M>,
209
- options: XrpcOptions<M>,
210
- ): Promise<XrpcResult<M>>
211
- export async function xrpcSafe<const M extends Query | Procedure>(
212
- agentOpts: Agent | AgentOptions,
213
- ns: Main<M>,
214
- options: XrpcOptions<M> = {} as XrpcOptions<M>,
215
- ): Promise<XrpcResult<M>> {
216
- options.signal?.throwIfAborted()
217
- const method: M = getMain(ns)
218
- try {
219
- const agent = buildAgent(agentOpts)
220
- const url = xrpcRequestUrl(method, options)
221
- const request = xrpcRequestInit(method, options)
222
- const response = await agent.fetchHandler(url, request).catch((err) => {
223
- const cause = extractFetchErrorCause(err)
224
- throw new XrpcFetchError(method, cause)
225
- })
226
- return await XrpcResponse.fromFetchResponse<M>(method, response, options)
227
- } catch (cause) {
228
- return asXrpcFailure(method, cause)
229
- }
230
- }
231
-
232
- function xrpcRequestUrl<M extends Procedure | Query | Subscription>(
233
- method: M,
234
- options: { params?: Params },
235
- ): `/xrpc/${NsidString}${'' | `?${string}`}` {
236
- const path = `/xrpc/${method.nsid}` as const
237
-
238
- // @NOTE param.toURLSearchParams() will always validate the params in order to
239
- // apply default values, so we can't disable it with options.validateRequest
240
-
241
- const queryString = method.parameters
242
- ?.toURLSearchParams(options.params ?? {})
243
- .toString()
244
-
245
- return queryString ? (`${path}?${queryString}` as const) : path
246
- }
247
-
248
- function xrpcRequestInit<T extends Procedure | Query>(
249
- schema: T,
250
- options: XrpcRequestProcessingOptions &
251
- XrpcRequestHeadersOptions &
252
- XrpcProcedureInputOptions & {
253
- encoding?: string
254
- },
255
- ): RequestInit & { duplex?: 'half' } {
256
- const headers = buildXrpcRequestHeaders(options)
257
-
258
- // Tell the server what type of response we're expecting
259
- if (schema.output.encoding) {
260
- headers.set('accept', schema.output.encoding)
261
- }
262
-
263
- // Caller should not set content-type header
264
- if (headers.has('content-type')) {
265
- const contentType = headers.get('content-type')
266
- throw new TypeError(`Unexpected content-type header (${contentType})`)
267
- }
268
-
269
- // Requests with body
270
- if ('input' in schema) {
271
- const encodingHint = options.encoding
272
- const input = xrpcProcedureInput(schema, options, encodingHint)
273
-
274
- if (input) {
275
- headers.set('content-type', input.encoding)
276
- } else if (encodingHint != null) {
277
- throw new TypeError(`Unexpected encoding hint (${encodingHint})`)
278
- }
279
-
280
- return {
281
- duplex: 'half',
282
- redirect: 'follow',
283
- referrerPolicy: 'strict-origin-when-cross-origin', // (default)
284
- mode: 'cors', // (default)
285
- signal: options.signal,
286
- method: 'POST',
287
- headers,
288
- body: input?.body,
289
- }
290
- }
291
-
292
- // Requests without body
293
- return {
294
- duplex: 'half',
295
- redirect: 'follow',
296
- referrerPolicy: 'strict-origin-when-cross-origin', // (default)
297
- mode: 'cors', // (default)
298
- signal: options.signal,
299
- method: 'GET',
300
- headers,
301
- }
302
- }
303
-
304
- type XrpcProcedureInputOptions = {
305
- body?: LexValue | BinaryBodyInit
306
- validateRequest?: boolean
307
- }
308
-
309
- function xrpcProcedureInput(
310
- method: Procedure,
311
- options: XrpcProcedureInputOptions,
312
- encodingHint?: string,
313
- ): null | { body: BodyInit; encoding: string } {
314
- const { input } = method
315
- const { body } = options
316
-
317
- if (options.validateRequest) {
318
- input.schema?.check(body)
319
- }
320
-
321
- // Special handling for endpoints expecting application/json input
322
- if (input.encoding === 'application/json') {
323
- // @NOTE **NOT** using isLexValue here to avoid deep checks in order to
324
- // distinguish between LexValue and BinaryBodyInit.
325
- if (!isLexScalar(body) && !isPlainObject(body) && !Array.isArray(body)) {
326
- throw new TypeError(`Expected LexValue body, got ${typeof body}`)
327
- }
328
-
329
- return buildPayload(input, lexStringify(body), encodingHint)
330
- }
331
-
332
- // Other encodings will be sent unaltered (ie. as binary data)
333
- switch (typeof body) {
334
- case 'undefined':
335
- case 'string':
336
- return buildPayload(input, body, encodingHint)
337
- case 'object': {
338
- if (body === null) break
339
- if (ArrayBuffer.isView(body)) {
340
- return buildPayload(input, asUint8ArrayArrayBuffer(body), encodingHint)
341
- } else if (
342
- body instanceof ArrayBuffer ||
343
- body instanceof ReadableStream
344
- ) {
345
- return buildPayload(input, body, encodingHint)
346
- } else if (isAsyncIterable(body)) {
347
- // @NOTE While fetch() does not allow SharedArrayBuffer-backed
348
- // Uint8Arrays as "body", it **does** allow using ReadableStreams made
349
- // of Uint8Arrays<SharedArrayBuffer> (tested on NodeJS 22) as "body".
350
- return buildPayload(input, toReadableStream(body), encodingHint)
351
- } else if (isBlobLike(body)) {
352
- return buildPayload(input, body, encodingHint || body.type)
353
- }
354
- }
355
- }
356
-
357
- throw new TypeError(
358
- `Invalid ${typeof body} body for ${input.encoding} encoding`,
359
- )
360
- }
361
-
362
- function buildPayload(
363
- schema: Payload,
364
- body: undefined | BodyInit,
365
- encodingHint?: string,
366
- ): null | { body: BodyInit; encoding: string } {
367
- if (schema.encoding === undefined) {
368
- if (body !== undefined) {
369
- throw new TypeError(`Endpoint expects no payload`)
370
- }
371
-
372
- return null
373
- }
374
-
375
- if (body === undefined) {
376
- // This error would be returned by the server, but we can catch it earlier
377
- // to avoid un-necessary requests. Note that a content-length of 0 does not
378
- // necessary mean that the body is "empty" (e.g. an empty txt file).
379
- throw new TypeError(`A request body is expected but none was provided`)
380
- }
381
-
382
- const encoding = buildEncoding(schema, encodingHint)
383
- return { encoding, body }
384
- }
385
-
386
- function buildEncoding(schema: Payload, encodingHint?: string): string {
387
- // Should never happen (required for type safety)
388
- if (!schema.encoding) {
389
- throw new TypeError('Unexpected payload')
390
- }
391
-
392
- if (encodingHint?.length) {
393
- if (!schema.matchesEncoding(encodingHint)) {
394
- throw new TypeError(
395
- `Cannot send a body with content-type "${encodingHint}" for "${schema.encoding}" encoding`,
396
- )
397
- }
398
- return encodingHint
399
- }
400
-
401
- // Fallback
402
-
403
- if (schema.encoding === '*/*') {
404
- return 'application/octet-stream'
405
- }
406
-
407
- if (schema.encoding.startsWith('text/')) {
408
- return schema.encoding.includes('*')
409
- ? 'text/plain; charset=utf-8'
410
- : `${schema.encoding}; charset=utf-8`
411
- }
412
-
413
- if (!schema.encoding.includes('*')) {
414
- return schema.encoding
415
- }
416
-
417
- throw new TypeError(
418
- `Unable to determine payload encoding. Please provide a 'content-type' header matching ${schema.encoding}.`,
419
- )
420
- }
421
-
422
- /**
423
- * Extracts the root cause from an error, unwrapping common fetch-related errors
424
- * such as those from undici (Node's internal fetch implementation).
425
- *
426
- * @param err - The error to extract the root cause from
427
- * @returns The root cause error, or the original error if no specific pattern is matched
428
- * @remarks This is useful for getting more specific error information from fetch-related failures, especially in Node environments using undici.
429
- */
430
- export function extractFetchErrorCause(err: unknown): unknown {
431
- // Unwrap the Network error from undici (i.e. Node's internal fetch() implementation)
432
- // https://github.com/nodejs/undici/blob/04cb77327f7ada95c2e5b67424cddcb22d7bf882/lib/web/fetch/index.js#L234-L239
433
- if (
434
- err instanceof TypeError &&
435
- err.message === 'fetch failed' &&
436
- err.cause !== undefined
437
- ) {
438
- return err.cause
439
- }
440
-
441
- // @TODO Add other unwrap patterns here as needed (e.g. for other fetch
442
- // implementations or common network libraries, like "node:http", or in other
443
- // environments like React Native, Deno, Bun, Browser, etc.)
444
-
445
- return err
446
- }
@@ -1,12 +0,0 @@
1
- {
2
- "extends": ["../../../tsconfig/isomorphic.json"],
3
- "include": ["./src"],
4
- "exclude": ["**/*.bench.ts", "**/*.test.ts"],
5
- "compilerOptions": {
6
- "noImplicitAny": true,
7
- "importHelpers": true,
8
- "target": "ES2023",
9
- "rootDir": "./src",
10
- "outDir": "./dist",
11
- },
12
- }
package/tsconfig.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "include": [],
3
- "references": [
4
- { "path": "./tsconfig.build.json" },
5
- { "path": "./tsconfig.tests.json" },
6
- ],
7
- }
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../../tsconfig/vitest.json",
3
- "include": ["./tests", "./src/**/*.test.ts"],
4
- "compilerOptions": {
5
- "noImplicitAny": true,
6
- "rootDir": "./",
7
- },
8
- }