@atproto/lex-client 0.0.10 → 0.0.12

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 (48) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/agent.d.ts +72 -0
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +46 -1
  5. package/dist/agent.js.map +1 -1
  6. package/dist/client.d.ts +442 -46
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +145 -1
  9. package/dist/client.js.map +1 -1
  10. package/dist/errors.d.ts +202 -48
  11. package/dist/errors.d.ts.map +1 -1
  12. package/dist/errors.js +208 -65
  13. package/dist/errors.js.map +1 -1
  14. package/dist/lexicons/com/atproto/repo/createRecord.defs.d.ts +20 -20
  15. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.d.ts +12 -12
  16. package/dist/lexicons/com/atproto/repo/getRecord.defs.d.ts +6 -6
  17. package/dist/lexicons/com/atproto/repo/listRecords.defs.d.ts +6 -6
  18. package/dist/lexicons/com/atproto/repo/putRecord.defs.d.ts +22 -22
  19. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.d.ts +2 -2
  20. package/dist/response.d.ts +17 -6
  21. package/dist/response.d.ts.map +1 -1
  22. package/dist/response.js +45 -32
  23. package/dist/response.js.map +1 -1
  24. package/dist/types.d.ts +51 -0
  25. package/dist/types.d.ts.map +1 -1
  26. package/dist/types.js.map +1 -1
  27. package/dist/util.d.ts +40 -5
  28. package/dist/util.d.ts.map +1 -1
  29. package/dist/util.js +22 -0
  30. package/dist/util.js.map +1 -1
  31. package/dist/www-authenticate.d.ts +35 -0
  32. package/dist/www-authenticate.d.ts.map +1 -0
  33. package/dist/www-authenticate.js +57 -0
  34. package/dist/www-authenticate.js.map +1 -0
  35. package/dist/xrpc.d.ts +82 -10
  36. package/dist/xrpc.d.ts.map +1 -1
  37. package/dist/xrpc.js +15 -28
  38. package/dist/xrpc.js.map +1 -1
  39. package/package.json +7 -7
  40. package/src/agent.ts +101 -1
  41. package/src/client.ts +428 -15
  42. package/src/errors.ts +308 -120
  43. package/src/response.ts +68 -63
  44. package/src/types.ts +52 -0
  45. package/src/util.ts +50 -5
  46. package/src/www-authenticate.test.ts +227 -0
  47. package/src/www-authenticate.ts +101 -0
  48. package/src/xrpc.ts +100 -53
@@ -0,0 +1,101 @@
1
+ type WWWAuthenticateParams = { [authParam in string]: string }
2
+
3
+ /**
4
+ * Parsed representation of a WWW-Authenticate HTTP header.
5
+ *
6
+ * Maps authentication scheme names to either:
7
+ * - A token68 string (compact authentication data)
8
+ * - A params object with key-value pairs
9
+ *
10
+ * @example Bearer with realm
11
+ * ```typescript
12
+ * // WWW-Authenticate: Bearer realm="example"
13
+ * const parsed: WWWAuthenticate = {
14
+ * Bearer: { realm: 'example' }
15
+ * }
16
+ * ```
17
+ *
18
+ * @example DPoP with error
19
+ * ```typescript
20
+ * // WWW-Authenticate: DPoP error="use_dpop_nonce", error_description="..."
21
+ * const parsed: WWWAuthenticate = {
22
+ * DPoP: { error: 'use_dpop_nonce', error_description: '...' }
23
+ * }
24
+ * ```
25
+ */
26
+ export type WWWAuthenticate = {
27
+ [authScheme in string]:
28
+ | string // token68
29
+ | WWWAuthenticateParams
30
+ }
31
+
32
+ /**
33
+ * Returns `undefined` if the header is malformed.
34
+ */
35
+ export function parseWWWAuthenticateHeader(
36
+ header?: unknown,
37
+ ): undefined | WWWAuthenticate {
38
+ if (typeof header !== 'string') return undefined
39
+
40
+ const wwwAuthenticate: WWWAuthenticate = {}
41
+
42
+ // Split over commas, not within quoted strings
43
+ const trimmedHeader = header.trim()
44
+ if (!trimmedHeader) return wwwAuthenticate
45
+
46
+ const parts = trimmedHeader.split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/)
47
+
48
+ let currentParams: WWWAuthenticateParams | null = null
49
+
50
+ for (let part of parts) {
51
+ // Check if the part starts with an auth scheme
52
+ const schemeMatch = part.trim().match(/^([^"=\s]+)(\s+.*)?$/)
53
+ if (schemeMatch) {
54
+ const scheme = schemeMatch[1]
55
+
56
+ // Duplicate scheme
57
+ if (Object.hasOwn(wwwAuthenticate, scheme)) return undefined
58
+
59
+ const rest = schemeMatch[2]?.trim()
60
+ if (!rest) {
61
+ // Scheme only (no params or token68)
62
+ currentParams = null
63
+ wwwAuthenticate[scheme] = Object.create(null)
64
+ continue
65
+ }
66
+
67
+ if (!rest.includes('=')) {
68
+ // Scheme with token68
69
+ currentParams = null
70
+ wwwAuthenticate[scheme] = rest
71
+ continue
72
+ }
73
+
74
+ // Scheme with params
75
+
76
+ currentParams = Object.create(null) as WWWAuthenticateParams
77
+ wwwAuthenticate[scheme] = currentParams
78
+
79
+ // Fall through to parse params
80
+ part = rest
81
+ }
82
+
83
+ // Invalid header
84
+ if (!currentParams) return undefined
85
+
86
+ const param = part.match(
87
+ /^\s*([^"\s=]+)=(?:("[^"\\]*(?:\\.[^"\\]*)*")|([^\s,"]*))\s*$/,
88
+ )
89
+
90
+ // invalid param
91
+ if (!param) return undefined
92
+
93
+ const paramName = param[1]
94
+ const paramValue =
95
+ param[3] ?? param[2]!.slice(1, -1).replaceAll(/\\(.)/g, '$1')
96
+
97
+ currentParams[paramName] = paramValue
98
+ }
99
+
100
+ return wwwAuthenticate
101
+ }
package/src/xrpc.ts CHANGED
@@ -13,15 +13,10 @@ import {
13
13
  getMain,
14
14
  } from '@atproto/lex-schema'
15
15
  import { Agent } from './agent.js'
16
- import {
17
- XrpcResponseError,
18
- XrpcUnexpectedError,
19
- XrpcUpstreamError,
20
- } from './errors.js'
16
+ import { XrpcFailure, asXrpcFailure } from './errors.js'
21
17
  import { XrpcResponse } from './response.js'
22
18
  import { BinaryBodyInit, CallOptions } from './types.js'
23
19
  import {
24
- XrpcPayload,
25
20
  buildAtprotoHeaders,
26
21
  isAsyncIterable,
27
22
  isBlobLike,
@@ -32,6 +27,11 @@ import {
32
27
  type XrpcParamsOptions<P extends Params> =
33
28
  NonNullable<unknown> extends P ? { params?: P } : { params: P }
34
29
 
30
+ /**
31
+ * The query/path parameters type for an XRPC method, inferred from its schema.
32
+ *
33
+ * @typeParam M - The XRPC method type (Procedure, Query, or Subscription)
34
+ */
35
35
  export type XrpcRequestParams<M extends Procedure | Query | Subscription> =
36
36
  InferInput<M['parameters']>
37
37
 
@@ -44,39 +44,54 @@ type XrpcInputOptions<In> = In extends { body: infer B; encoding: infer E }
44
44
  { body: B; encoding?: E }
45
45
  : { body?: undefined; encoding?: undefined }
46
46
 
47
+ /**
48
+ * Options for making an XRPC request.
49
+ *
50
+ * Combines {@link CallOptions} with method-specific params and body requirements.
51
+ * The type system ensures required params/body are provided based on the method schema.
52
+ *
53
+ * @typeParam M - The XRPC method type (Procedure or Query)
54
+ * @see {@link CallOptions} for general request options like signal and validateRequest
55
+ * @see {@link XrpcParamsOptions} for method-specific query parameters
56
+ * @see {@link XrpcInputOptions} for method-specific body and encoding requirements
57
+ *
58
+ * @example Query with params
59
+ * ```typescript
60
+ * const options: XrpcOptions<typeof app.bsky.feed.getTimeline.main> = {
61
+ * params: { limit: 50 }
62
+ * }
63
+ * ```
64
+ *
65
+ * @example Procedure with body
66
+ * ```typescript
67
+ * const options: XrpcOptions<typeof com.atproto.repo.createRecord.main> = {
68
+ * body: { repo: did, collection: 'app.bsky.feed.post', record: { ... } }
69
+ * }
70
+ * ```
71
+ */
47
72
  export type XrpcOptions<M extends Procedure | Query = Procedure | Query> =
48
73
  CallOptions &
49
74
  XrpcInputOptions<XrpcRequestPayload<M>> &
50
75
  XrpcParamsOptions<XrpcRequestParams<M>>
51
76
 
52
- export type XrpcFailure<M extends Procedure | Query> =
53
- // The server returned a valid XRPC error response
54
- | XrpcResponseError<M>
55
- // The response was not a valid XRPC response, or it does not match the schema
56
- | XrpcUpstreamError
57
- // Something went wrong (network error, etc.)
58
- | XrpcUnexpectedError
59
-
60
- export type XrpcResult<M extends Procedure | Query> =
61
- | XrpcResponse<M>
62
- | XrpcFailure<M>
63
-
64
- /**
65
- * Utility method to type cast the error thrown by {@link xrpc} to an
66
- * {@link XrpcFailure} matching the provided method. Only use this function
67
- * inside a catch block right after calling {@link xrpc}, and use the same
68
- * method type parameter as used in the {@link xrpc} call.
69
- */
70
- export function asXrpcFailure<M extends Procedure | Query = Procedure | Query>(
71
- err: unknown,
72
- ): XrpcFailure<M> {
73
- if (err instanceof XrpcResponseError) return err
74
- if (err instanceof XrpcUpstreamError) return err
75
- return XrpcUnexpectedError.from(err)
76
- }
77
-
78
77
  /**
79
- * @throws XrpcFailure<M>
78
+ * Makes an XRPC request and throws on failure.
79
+ *
80
+ * This is the low-level function for making XRPC calls. For most use cases,
81
+ * prefer using {@link Client.xrpc} which provides a more ergonomic API.
82
+ *
83
+ * @param agent - The {@link Agent} to use for making the request
84
+ * @param ns - The lexicon method definition
85
+ * @param options - Request {@link XrpcOptions options} (params, body, headers, etc.)
86
+ * @returns The successful {@link XrpcResponse}
87
+ * @throws {XrpcFailure} When the request fails
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * const response = await xrpc(agent, app.bsky.feed.getTimeline.main, {
92
+ * params: { limit: 50 }
93
+ * })
94
+ * ```
80
95
  */
81
96
  export async function xrpc<const M extends Query | Procedure>(
82
97
  agent: Agent,
@@ -94,13 +109,49 @@ export async function xrpc<const M extends Query | Procedure>(
94
109
  ns: Main<M>,
95
110
  options: XrpcOptions<M> = {} as XrpcOptions<M>,
96
111
  ): Promise<XrpcResponse<M>> {
97
- try {
98
- return await lexRpcRequest<M>(agent, ns, options)
99
- } catch (err) {
100
- throw asXrpcFailure<M>(err)
101
- }
112
+ const response = await xrpcSafe<M>(agent, ns, options)
113
+ if (response.success) return response
114
+ else throw response
102
115
  }
103
116
 
117
+ /**
118
+ * Union type representing either a successful response or a failure.
119
+ *
120
+ * Both {@link XrpcResponse} and {@link XrpcFailure} have a `success` property
121
+ * that can be used to discriminate between them.
122
+ *
123
+ * @typeParam M - The XRPC method type
124
+ */
125
+ export type XrpcResult<M extends Procedure | Query> =
126
+ | XrpcResponse<M>
127
+ | XrpcFailure<M>
128
+
129
+ /**
130
+ * Makes an XRPC request without throwing on failure.
131
+ *
132
+ * Returns a discriminated union that can be checked via the `success` property.
133
+ * This is useful for handling errors without try/catch blocks. This also allow
134
+ * failure results to be typed with the method schema, which can provide better
135
+ * type safety when handling errors (e.g. checking for specific error codes).
136
+ *
137
+ * @param agent - The {@link Agent} to use for making the request
138
+ * @param ns - The lexicon method definition
139
+ * @param options - Request {@link XrpcOptions options} (params, body, headers, etc.)
140
+ * @returns Either a successful {@link XrpcResponse} or an {@link XrpcFailure}
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * const result = await xrpcSafe(agent, app.bsky.actor.getProfile.main, {
145
+ * params: { actor: 'alice.bsky.social' }
146
+ * })
147
+ *
148
+ * if (result.success) {
149
+ * console.log(result.body.displayName)
150
+ * } else {
151
+ * console.error('Request failed:', result.error)
152
+ * }
153
+ * ```
154
+ */
104
155
  export async function xrpcSafe<const M extends Query | Procedure>(
105
156
  agent: Agent,
106
157
  ns: NonNullable<unknown> extends XrpcOptions<M>
@@ -117,20 +168,16 @@ export async function xrpcSafe<const M extends Query | Procedure>(
117
168
  ns: Main<M>,
118
169
  options: XrpcOptions<M> = {} as XrpcOptions<M>,
119
170
  ): Promise<XrpcResult<M>> {
120
- return lexRpcRequest<M>(agent, ns, options).catch(asXrpcFailure<M>)
121
- }
122
-
123
- async function lexRpcRequest<const M extends Query | Procedure>(
124
- agent: Agent,
125
- ns: Main<M>,
126
- options: XrpcOptions<M> = {} as XrpcOptions<M>,
127
- ): Promise<XrpcResponse<M>> {
128
- const method = getMain(ns)
129
171
  options.signal?.throwIfAborted()
130
- const url = xrpcRequestUrl(method, options)
131
- const request = xrpcRequestInit(method, options)
132
- const response = await agent.fetchHandler(url, request)
133
- return XrpcResponse.fromFetchResponse<M>(method, response, options)
172
+ const method: M = getMain(ns)
173
+ try {
174
+ const url = xrpcRequestUrl(method, options)
175
+ const request = xrpcRequestInit(method, options)
176
+ const response = await agent.fetchHandler(url, request)
177
+ return await XrpcResponse.fromFetchResponse<M>(method, response, options)
178
+ } catch (cause) {
179
+ return asXrpcFailure(method, cause)
180
+ }
134
181
  }
135
182
 
136
183
  function xrpcRequestUrl<M extends Procedure | Query | Subscription>(
@@ -205,7 +252,7 @@ function xrpcProcedureInput(
205
252
  method: Procedure,
206
253
  options: CallOptions & { body?: LexValue | BinaryBodyInit },
207
254
  encodingHint?: string,
208
- ): null | XrpcPayload<BodyInit> {
255
+ ): null | { body: BodyInit; encoding: string } {
209
256
  const { input } = method
210
257
  const { body } = options
211
258
 
@@ -254,7 +301,7 @@ function buildPayload(
254
301
  schema: Payload,
255
302
  body: undefined | BodyInit,
256
303
  encodingHint?: string,
257
- ): null | XrpcPayload<BodyInit> {
304
+ ): null | { body: BodyInit; encoding: string } {
258
305
  if (schema.encoding === undefined) {
259
306
  if (body !== undefined) {
260
307
  throw new TypeError(