@atproto/lex-client 0.0.11 → 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.
- package/CHANGELOG.md +17 -0
- package/dist/agent.d.ts +67 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +31 -0
- package/dist/agent.js.map +1 -1
- package/dist/client.d.ts +438 -44
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +145 -1
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +162 -9
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +132 -8
- package/dist/errors.js.map +1 -1
- package/dist/lexicons/com/atproto/repo/createRecord.defs.d.ts +20 -20
- package/dist/lexicons/com/atproto/repo/deleteRecord.defs.d.ts +12 -12
- package/dist/lexicons/com/atproto/repo/getRecord.defs.d.ts +6 -6
- package/dist/lexicons/com/atproto/repo/listRecords.defs.d.ts +6 -6
- package/dist/lexicons/com/atproto/repo/putRecord.defs.d.ts +22 -22
- package/dist/lexicons/com/atproto/repo/uploadBlob.defs.d.ts +2 -2
- package/dist/response.d.ts +14 -4
- package/dist/response.d.ts.map +1 -1
- package/dist/response.js +19 -9
- package/dist/response.js.map +1 -1
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +40 -5
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +22 -0
- package/dist/util.js.map +1 -1
- package/dist/www-authenticate.d.ts +23 -0
- package/dist/www-authenticate.d.ts.map +1 -1
- package/dist/www-authenticate.js.map +1 -1
- package/dist/xrpc.d.ts +81 -1
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js.map +1 -1
- package/package.json +7 -7
- package/src/agent.ts +67 -0
- package/src/client.ts +420 -7
- package/src/errors.ts +165 -12
- package/src/response.ts +22 -19
- package/src/types.ts +52 -0
- package/src/util.ts +50 -5
- package/src/www-authenticate.ts +24 -0
- package/src/xrpc.ts +83 -4
package/src/errors.ts
CHANGED
|
@@ -6,12 +6,26 @@ import {
|
|
|
6
6
|
ResultFailure,
|
|
7
7
|
lexErrorDataSchema,
|
|
8
8
|
} from '@atproto/lex-schema'
|
|
9
|
-
import {
|
|
9
|
+
import { XrpcResponsePayload } from './util.js'
|
|
10
10
|
import {
|
|
11
11
|
WWWAuthenticate,
|
|
12
12
|
parseWWWAuthenticateHeader,
|
|
13
13
|
} from './www-authenticate.js'
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* HTTP status codes that indicate a transient error that may succeed on retry.
|
|
17
|
+
*
|
|
18
|
+
* Includes:
|
|
19
|
+
* - 408 Request Timeout
|
|
20
|
+
* - 425 Too Early
|
|
21
|
+
* - 429 Too Many Requests (rate limited)
|
|
22
|
+
* - 500 Internal Server Error
|
|
23
|
+
* - 502 Bad Gateway
|
|
24
|
+
* - 503 Service Unavailable
|
|
25
|
+
* - 504 Gateway Timeout
|
|
26
|
+
* - 522 Connection Timed Out (Cloudflare)
|
|
27
|
+
* - 524 A Timeout Occurred (Cloudflare)
|
|
28
|
+
*/
|
|
15
29
|
export const RETRYABLE_HTTP_STATUS_CODES: ReadonlySet<number> = new Set([
|
|
16
30
|
408, 425, 429, 500, 502, 503, 504, 522, 524,
|
|
17
31
|
])
|
|
@@ -19,8 +33,17 @@ export const RETRYABLE_HTTP_STATUS_CODES: ReadonlySet<number> = new Set([
|
|
|
19
33
|
export { LexError }
|
|
20
34
|
export type { LexErrorCode, LexErrorData }
|
|
21
35
|
|
|
22
|
-
|
|
23
|
-
|
|
36
|
+
/**
|
|
37
|
+
* The payload structure for XRPC error responses.
|
|
38
|
+
*
|
|
39
|
+
* All XRPC errors return JSON with an `error` code and optional `message`.
|
|
40
|
+
*
|
|
41
|
+
* @typeParam N - The specific error code type
|
|
42
|
+
*/
|
|
43
|
+
export type XrpcErrorPayload<N extends LexErrorCode = LexErrorCode> = {
|
|
44
|
+
body: LexErrorData<N>
|
|
45
|
+
encoding: 'application/json'
|
|
46
|
+
}
|
|
24
47
|
|
|
25
48
|
/**
|
|
26
49
|
* All unsuccessful responses should follow a standard error response
|
|
@@ -35,15 +58,29 @@ export type XrpcErrorPayload<N extends LexErrorCode = LexErrorCode> =
|
|
|
35
58
|
* This function checks whether a given payload matches this schema.
|
|
36
59
|
*/
|
|
37
60
|
export function isXrpcErrorPayload(
|
|
38
|
-
payload:
|
|
61
|
+
payload: XrpcResponsePayload | null | undefined,
|
|
39
62
|
): payload is XrpcErrorPayload {
|
|
40
63
|
return (
|
|
41
|
-
payload
|
|
64
|
+
payload != null &&
|
|
42
65
|
payload.encoding === 'application/json' &&
|
|
43
66
|
lexErrorDataSchema.matches(payload.body)
|
|
44
67
|
)
|
|
45
68
|
}
|
|
46
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Abstract base class for all XRPC errors.
|
|
72
|
+
*
|
|
73
|
+
* Extends {@link LexError} and implements {@link ResultFailure} for use with
|
|
74
|
+
* safe/result-based error handling patterns.
|
|
75
|
+
*
|
|
76
|
+
* @typeParam M - The XRPC method type (Procedure or Query)
|
|
77
|
+
* @typeParam N - The error code type
|
|
78
|
+
* @typeParam TReason - The reason type for ResultFailure
|
|
79
|
+
*
|
|
80
|
+
* @see {@link XrpcResponseError} - For valid XRPC error responses
|
|
81
|
+
* @see {@link XrpcUpstreamError} - For invalid/unexpected responses
|
|
82
|
+
* @see {@link XrpcInternalError} - For network/internal errors
|
|
83
|
+
*/
|
|
47
84
|
export abstract class XrpcError<
|
|
48
85
|
M extends Procedure | Query = Procedure | Query,
|
|
49
86
|
N extends LexErrorCode = LexErrorCode,
|
|
@@ -84,8 +121,28 @@ export abstract class XrpcError<
|
|
|
84
121
|
}
|
|
85
122
|
|
|
86
123
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
124
|
+
* Error class for valid XRPC error responses from the server.
|
|
125
|
+
*
|
|
126
|
+
* This represents a properly formatted XRPC error where the server returned
|
|
127
|
+
* a non-2xx status with a valid JSON error payload containing `error` and
|
|
128
|
+
* optional `message` fields.
|
|
129
|
+
*
|
|
130
|
+
* Use {@link matchesSchema} to check if the error matches the method's declared
|
|
131
|
+
* error types for type-safe error handling.
|
|
132
|
+
*
|
|
133
|
+
* @typeParam M - The XRPC method type
|
|
134
|
+
* @typeParam N - The error code type (inferred from method or generic)
|
|
135
|
+
*
|
|
136
|
+
* @example Handling specific errors
|
|
137
|
+
* ```typescript
|
|
138
|
+
* try {
|
|
139
|
+
* await client.xrpc(someMethod, options)
|
|
140
|
+
* } catch (err) {
|
|
141
|
+
* if (err instanceof XrpcResponseError && err.error === 'RecordNotFound') {
|
|
142
|
+
* // Handle not found case
|
|
143
|
+
* }
|
|
144
|
+
* }
|
|
145
|
+
* ```
|
|
89
146
|
*/
|
|
90
147
|
export class XrpcResponseError<
|
|
91
148
|
M extends Procedure | Query = Procedure | Query,
|
|
@@ -135,6 +192,33 @@ export class XrpcResponseError<
|
|
|
135
192
|
}
|
|
136
193
|
|
|
137
194
|
export type { WWWAuthenticate }
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Error class for 401 Unauthorized XRPC responses.
|
|
198
|
+
*
|
|
199
|
+
* Extends {@link XrpcResponseError} with access to parsed WWW-Authenticate header
|
|
200
|
+
* information, useful for implementing authentication flows.
|
|
201
|
+
*
|
|
202
|
+
* Authentication errors are never retryable as they require user intervention
|
|
203
|
+
* (e.g., re-authentication, token refresh).
|
|
204
|
+
*
|
|
205
|
+
* @typeParam M - The XRPC method type
|
|
206
|
+
* @typeParam N - The error code type
|
|
207
|
+
*
|
|
208
|
+
* @example Handling authentication errors
|
|
209
|
+
* ```typescript
|
|
210
|
+
* try {
|
|
211
|
+
* await client.xrpc(someMethod, options)
|
|
212
|
+
* } catch (err) {
|
|
213
|
+
* if (err instanceof XrpcAuthenticationError) {
|
|
214
|
+
* const { DPoP } = err.wwwAuthenticate
|
|
215
|
+
* if (DPoP?.error === 'use_dpop_nonce') {
|
|
216
|
+
* // Handle DPoP nonce requirement
|
|
217
|
+
* }
|
|
218
|
+
* }
|
|
219
|
+
* }
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
138
222
|
export class XrpcAuthenticationError<
|
|
139
223
|
M extends Procedure | Query = Procedure | Query,
|
|
140
224
|
N extends LexErrorCode = LexErrorCode,
|
|
@@ -145,9 +229,13 @@ export class XrpcAuthenticationError<
|
|
|
145
229
|
return false
|
|
146
230
|
}
|
|
147
231
|
|
|
148
|
-
#
|
|
232
|
+
#wwwAuthenticateCached?: WWWAuthenticate
|
|
233
|
+
/**
|
|
234
|
+
* Parsed WWW-Authenticate header from the response.
|
|
235
|
+
* Contains authentication scheme parameters (e.g., Bearer realm, DPoP nonce).
|
|
236
|
+
*/
|
|
149
237
|
get wwwAuthenticate(): WWWAuthenticate {
|
|
150
|
-
return (this.#
|
|
238
|
+
return (this.#wwwAuthenticateCached ??=
|
|
151
239
|
parseWWWAuthenticateHeader(
|
|
152
240
|
this.response.headers.get('www-authenticate'),
|
|
153
241
|
) ?? {})
|
|
@@ -155,8 +243,19 @@ export class XrpcAuthenticationError<
|
|
|
155
243
|
}
|
|
156
244
|
|
|
157
245
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
246
|
+
* Error class for invalid or unprocessable XRPC responses from upstream servers.
|
|
247
|
+
*
|
|
248
|
+
* This occurs when the server returns a response that doesn't conform to the
|
|
249
|
+
* XRPC protocol, such as:
|
|
250
|
+
* - Missing or invalid Content-Type header
|
|
251
|
+
* - Response body that doesn't match the method's output schema
|
|
252
|
+
* - Non-JSON error responses
|
|
253
|
+
* - Responses from non-XRPC endpoints
|
|
254
|
+
*
|
|
255
|
+
* The error code is always 'UpstreamFailure' and maps to HTTP 502 Bad Gateway
|
|
256
|
+
* when converted to a response.
|
|
257
|
+
*
|
|
258
|
+
* @typeParam M - The XRPC method type
|
|
160
259
|
*/
|
|
161
260
|
export class XrpcUpstreamError<
|
|
162
261
|
M extends Procedure | Query = Procedure | Query,
|
|
@@ -166,7 +265,7 @@ export class XrpcUpstreamError<
|
|
|
166
265
|
constructor(
|
|
167
266
|
method: M,
|
|
168
267
|
readonly response: Response,
|
|
169
|
-
readonly payload:
|
|
268
|
+
readonly payload: XrpcResponsePayload | null = null,
|
|
170
269
|
message: string = `Unexpected upstream XRPC response`,
|
|
171
270
|
options?: ErrorOptions,
|
|
172
271
|
) {
|
|
@@ -186,6 +285,21 @@ export class XrpcUpstreamError<
|
|
|
186
285
|
}
|
|
187
286
|
}
|
|
188
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Error class for internal/client-side errors during XRPC requests.
|
|
290
|
+
*
|
|
291
|
+
* This represents errors that occur before or during the request that are not
|
|
292
|
+
* server responses, such as:
|
|
293
|
+
* - Network errors (connection refused, DNS failure)
|
|
294
|
+
* - Request timeouts
|
|
295
|
+
* - Request aborted via AbortSignal
|
|
296
|
+
* - Invalid request construction
|
|
297
|
+
*
|
|
298
|
+
* The error code is always 'InternalServerError' and these errors are
|
|
299
|
+
* optimistically considered retryable.
|
|
300
|
+
*
|
|
301
|
+
* @typeParam M - The XRPC method type
|
|
302
|
+
*/
|
|
189
303
|
export class XrpcInternalError<
|
|
190
304
|
M extends Procedure | Query = Procedure | Query,
|
|
191
305
|
> extends XrpcError<M, 'InternalServerError', XrpcInternalError<M>> {
|
|
@@ -218,6 +332,25 @@ export class XrpcInternalError<
|
|
|
218
332
|
}
|
|
219
333
|
}
|
|
220
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Union type of all possible XRPC failure types.
|
|
337
|
+
*
|
|
338
|
+
* Used as the return type for safe/non-throwing XRPC methods. Check the
|
|
339
|
+
* `success` property to distinguish between success and failure:
|
|
340
|
+
*
|
|
341
|
+
* @typeParam M - The XRPC method type
|
|
342
|
+
*
|
|
343
|
+
* @example
|
|
344
|
+
* ```typescript
|
|
345
|
+
* const result = await client.xrpcSafe(someMethod, options)
|
|
346
|
+
* if (result.success) {
|
|
347
|
+
* console.log(result.body) // XrpcResponse
|
|
348
|
+
* } else {
|
|
349
|
+
* // result is XrpcFailure (XrpcResponseError | XrpcUpstreamError | XrpcInternalError)
|
|
350
|
+
* console.error(result.error, result.message)
|
|
351
|
+
* }
|
|
352
|
+
* ```
|
|
353
|
+
*/
|
|
221
354
|
export type XrpcFailure<M extends Procedure | Query = Procedure | Query> =
|
|
222
355
|
// The server returned a valid XRPC error response
|
|
223
356
|
| XrpcResponseError<M>
|
|
@@ -226,6 +359,26 @@ export type XrpcFailure<M extends Procedure | Query = Procedure | Query> =
|
|
|
226
359
|
// Something went wrong (network error, etc.)
|
|
227
360
|
| XrpcInternalError<M>
|
|
228
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Converts an unknown error into an appropriate {@link XrpcFailure} type.
|
|
364
|
+
*
|
|
365
|
+
* If the error is already an XrpcFailure for the given method, returns it as-is.
|
|
366
|
+
* Otherwise, wraps it in an {@link XrpcInternalError}.
|
|
367
|
+
*
|
|
368
|
+
* @param method - The XRPC method that was called
|
|
369
|
+
* @param cause - The error to convert
|
|
370
|
+
* @returns An XrpcFailure instance
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```typescript
|
|
374
|
+
* try {
|
|
375
|
+
* const response = await fetch(...)
|
|
376
|
+
* // ... process response
|
|
377
|
+
* } catch (err) {
|
|
378
|
+
* return asXrpcFailure(method, err)
|
|
379
|
+
* }
|
|
380
|
+
* ```
|
|
381
|
+
*/
|
|
229
382
|
export function asXrpcFailure<M extends Procedure | Query>(
|
|
230
383
|
method: M,
|
|
231
384
|
cause: unknown,
|
package/src/response.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { lexParse } from '@atproto/lex-json'
|
|
2
2
|
import {
|
|
3
|
-
InferMethodOutputBody,
|
|
4
3
|
InferMethodOutputEncoding,
|
|
5
4
|
Procedure,
|
|
6
5
|
Query,
|
|
@@ -12,15 +11,12 @@ import {
|
|
|
12
11
|
XrpcUpstreamError,
|
|
13
12
|
isXrpcErrorPayload,
|
|
14
13
|
} from './errors.js'
|
|
15
|
-
import {
|
|
14
|
+
import { XrpcResponseBody, XrpcResponsePayload } from './util.js'
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const CONTENT_TYPE_BINARY = 'application/octet-stream'
|
|
17
|
+
const CONTENT_TYPE_JSON = 'application/json'
|
|
19
18
|
|
|
20
|
-
export type
|
|
21
|
-
InferMethodOutputEncoding<M> extends infer E extends string
|
|
22
|
-
? XrpcPayload<XrpcResponseBody<M>, E>
|
|
23
|
-
: null
|
|
19
|
+
export type { XrpcResponseBody, XrpcResponsePayload }
|
|
24
20
|
|
|
25
21
|
/**
|
|
26
22
|
* Small container for XRPC response data.
|
|
@@ -50,13 +46,24 @@ export class XrpcResponse<M extends Procedure | Query>
|
|
|
50
46
|
* in binary form {@link Uint8Array} (`false`).
|
|
51
47
|
*/
|
|
52
48
|
get isParsed() {
|
|
53
|
-
return this.encoding ===
|
|
49
|
+
return this.method.output.encoding === CONTENT_TYPE_JSON
|
|
54
50
|
}
|
|
55
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The Content-Type encoding of the response (e.g., 'application/json').
|
|
54
|
+
* Returns `undefined` if the response has no body.
|
|
55
|
+
*/
|
|
56
56
|
get encoding() {
|
|
57
57
|
return this.payload?.encoding as InferMethodOutputEncoding<M>
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The parsed response body.
|
|
62
|
+
*
|
|
63
|
+
* For 'application/json' responses, this is the parsed and validated LexValue.
|
|
64
|
+
* For binary responses, this is a Uint8Array.
|
|
65
|
+
* Returns `undefined` if the response has no body.
|
|
66
|
+
*/
|
|
60
67
|
get body() {
|
|
61
68
|
return this.payload?.body as XrpcResponseBody<M>
|
|
62
69
|
}
|
|
@@ -115,7 +122,7 @@ export class XrpcResponse<M extends Procedure | Query>
|
|
|
115
122
|
|
|
116
123
|
// Only parse json if the schema expects it
|
|
117
124
|
const payload = await readPayload(response, {
|
|
118
|
-
parse:
|
|
125
|
+
parse: method.output.encoding === CONTENT_TYPE_JSON,
|
|
119
126
|
}).catch((cause) => {
|
|
120
127
|
throw new XrpcUpstreamError(
|
|
121
128
|
method,
|
|
@@ -175,17 +182,13 @@ export class XrpcResponse<M extends Procedure | Query>
|
|
|
175
182
|
}
|
|
176
183
|
}
|
|
177
184
|
|
|
178
|
-
function shouldParse(method: Procedure | Query) {
|
|
179
|
-
return method.output.encoding === 'application/json'
|
|
180
|
-
}
|
|
181
|
-
|
|
182
185
|
/**
|
|
183
186
|
* @note this function always consumes the response body
|
|
184
187
|
*/
|
|
185
188
|
async function readPayload(
|
|
186
189
|
response: Response,
|
|
187
190
|
options?: { parse?: boolean },
|
|
188
|
-
): Promise<
|
|
191
|
+
): Promise<XrpcResponsePayload> {
|
|
189
192
|
// @TODO Should we limit the maximum response size here (this could also be
|
|
190
193
|
// done by the FetchHandler)?
|
|
191
194
|
|
|
@@ -197,18 +200,18 @@ async function readPayload(
|
|
|
197
200
|
|
|
198
201
|
// Response content-type is undefined
|
|
199
202
|
if (!encoding) {
|
|
200
|
-
// If the body is empty, return
|
|
203
|
+
// If the body is empty, return undefined (= no payload)
|
|
201
204
|
const body = await response.arrayBuffer()
|
|
202
|
-
if (body.byteLength === 0) return
|
|
205
|
+
if (body.byteLength === 0) return undefined
|
|
203
206
|
|
|
204
207
|
// If we got data despite no content-type, treat it as binary
|
|
205
208
|
return {
|
|
206
|
-
encoding:
|
|
209
|
+
encoding: CONTENT_TYPE_BINARY,
|
|
207
210
|
body: new Uint8Array(body),
|
|
208
211
|
}
|
|
209
212
|
}
|
|
210
213
|
|
|
211
|
-
if (options?.parse && encoding ===
|
|
214
|
+
if (options?.parse && encoding === CONTENT_TYPE_JSON) {
|
|
212
215
|
// @NOTE It might be worth returning the raw bytes here (Uint8Array) and
|
|
213
216
|
// perform the lex parsing using cborg/json, allowing to do
|
|
214
217
|
// bytes->LexValue in one step instead of bytes->text->JSON->LexValue.
|
package/src/types.ts
CHANGED
|
@@ -2,13 +2,40 @@ import { DidString, UnknownString } from '@atproto/lex-schema'
|
|
|
2
2
|
|
|
3
3
|
export type { DidString, UnknownString }
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Service identifier fragment for DID service endpoints.
|
|
7
|
+
*
|
|
8
|
+
* Common values include 'atproto_labeler' for labeling services,
|
|
9
|
+
* or custom service identifiers.
|
|
10
|
+
*/
|
|
5
11
|
export type DidServiceIdentifier = 'atproto_labeler' | UnknownString
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A full service proxy identifier combining a DID with a service fragment.
|
|
15
|
+
*
|
|
16
|
+
* Used to route requests through a specific service endpoint.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const service: Service = 'did:web:api.bsky.app#bsky_appview'
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
6
23
|
export type Service = `${DidString}#${DidServiceIdentifier}`
|
|
7
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Common options available for all XRPC calls.
|
|
27
|
+
*
|
|
28
|
+
* These options can be passed to any method that makes XRPC requests,
|
|
29
|
+
* including `xrpc()`, `call()`, and record operations.
|
|
30
|
+
*/
|
|
8
31
|
export type CallOptions = {
|
|
32
|
+
/** Labeler DIDs to request labels from for content moderation. */
|
|
9
33
|
labelers?: Iterable<DidString>
|
|
34
|
+
/** AbortSignal to cancel the request. */
|
|
10
35
|
signal?: AbortSignal
|
|
36
|
+
/** Additional HTTP headers to include in the request. */
|
|
11
37
|
headers?: HeadersInit
|
|
38
|
+
/** Service proxy identifier for routing requests through a specific service. */
|
|
12
39
|
service?: Service
|
|
13
40
|
|
|
14
41
|
/**
|
|
@@ -33,6 +60,31 @@ export type CallOptions = {
|
|
|
33
60
|
validateResponse?: boolean
|
|
34
61
|
}
|
|
35
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Valid input types for binary request bodies.
|
|
65
|
+
*
|
|
66
|
+
* These types can be used as the body for procedures that expect
|
|
67
|
+
* non-JSON content (e.g., blob uploads, binary data).
|
|
68
|
+
*
|
|
69
|
+
* @example Uploading a blob
|
|
70
|
+
* ```typescript
|
|
71
|
+
* const imageData: BinaryBodyInit = new Uint8Array(buffer)
|
|
72
|
+
* await client.uploadBlob(imageData, { encoding: 'image/png' })
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @example Streaming upload
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const stream: BinaryBodyInit = someReadableStream
|
|
78
|
+
* await client.xrpc(uploadMethod, { body: stream })
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* @example File upload in browser
|
|
82
|
+
* ```typescript
|
|
83
|
+
* const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement
|
|
84
|
+
* const file: BinaryBodyInit = fileInput.files[0]
|
|
85
|
+
* await client.xrpc(uploadMethod, { body: file })
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
36
88
|
export type BinaryBodyInit =
|
|
37
89
|
| Uint8Array
|
|
38
90
|
| ArrayBuffer
|
package/src/util.ts
CHANGED
|
@@ -1,10 +1,42 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
DidString,
|
|
3
|
+
InferMethodOutput,
|
|
4
|
+
InferMethodOutputBody,
|
|
5
|
+
Procedure,
|
|
6
|
+
Query,
|
|
7
|
+
} from '@atproto/lex-schema'
|
|
2
8
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The body type of an XRPC response, inferred from the method's output schema.
|
|
11
|
+
*
|
|
12
|
+
* For JSON responses, this is the parsed LexValue. For binary responses,
|
|
13
|
+
* this is a Uint8Array.
|
|
14
|
+
*
|
|
15
|
+
* @typeParam M - The XRPC method type (Procedure or Query)
|
|
16
|
+
*/
|
|
17
|
+
export type XrpcResponseBody<M extends Procedure | Query = Procedure | Query> =
|
|
18
|
+
InferMethodOutputBody<M, Uint8Array>
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The full payload type of an XRPC response, including body and encoding.
|
|
22
|
+
*
|
|
23
|
+
* Returns `null` for methods that have no output.
|
|
24
|
+
*
|
|
25
|
+
* @typeParam M - The XRPC method type (Procedure or Query)
|
|
26
|
+
*/
|
|
27
|
+
export type XrpcResponsePayload<
|
|
28
|
+
M extends Procedure | Query = Procedure | Query,
|
|
29
|
+
> = InferMethodOutput<M, Uint8Array>
|
|
7
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Type guard to check if a value is {@link Blob}-like.
|
|
33
|
+
*
|
|
34
|
+
* Handles both native Blobs and polyfilled Blob implementations
|
|
35
|
+
* (e.g., fetch-blob from node-fetch).
|
|
36
|
+
*
|
|
37
|
+
* @param value - The value to check
|
|
38
|
+
* @returns `true` if the value is a Blob or Blob-like object
|
|
39
|
+
*/
|
|
8
40
|
export function isBlobLike(value: unknown): value is Blob {
|
|
9
41
|
if (value == null) return false
|
|
10
42
|
if (typeof value !== 'object') return false
|
|
@@ -32,6 +64,19 @@ export function isAsyncIterable<T>(
|
|
|
32
64
|
)
|
|
33
65
|
}
|
|
34
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Builds HTTP headers for AT Protocol requests.
|
|
69
|
+
*
|
|
70
|
+
* Adds the following headers when applicable:
|
|
71
|
+
* - `atproto-proxy`: Service routing header (if service is specified)
|
|
72
|
+
* - `atproto-accept-labelers`: Comma-separated list of labeler DIDs
|
|
73
|
+
*
|
|
74
|
+
* @param options - Header building options
|
|
75
|
+
* @param options.headers - Base headers to include
|
|
76
|
+
* @param options.service - Service proxy identifier
|
|
77
|
+
* @param options.labelers - Labeler DIDs to request labels from
|
|
78
|
+
* @returns A new Headers object with AT Protocol headers added
|
|
79
|
+
*/
|
|
35
80
|
export function buildAtprotoHeaders(options: {
|
|
36
81
|
headers?: HeadersInit
|
|
37
82
|
service?: `${DidString}#${string}`
|
package/src/www-authenticate.ts
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
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
|
+
*/
|
|
2
26
|
export type WWWAuthenticate = {
|
|
3
27
|
[authScheme in string]:
|
|
4
28
|
| string // token68
|
package/src/xrpc.ts
CHANGED
|
@@ -17,7 +17,6 @@ import { XrpcFailure, asXrpcFailure } from './errors.js'
|
|
|
17
17
|
import { XrpcResponse } from './response.js'
|
|
18
18
|
import { BinaryBodyInit, CallOptions } from './types.js'
|
|
19
19
|
import {
|
|
20
|
-
XrpcPayload,
|
|
21
20
|
buildAtprotoHeaders,
|
|
22
21
|
isAsyncIterable,
|
|
23
22
|
isBlobLike,
|
|
@@ -28,6 +27,11 @@ import {
|
|
|
28
27
|
type XrpcParamsOptions<P extends Params> =
|
|
29
28
|
NonNullable<unknown> extends P ? { params?: P } : { params: P }
|
|
30
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
|
+
*/
|
|
31
35
|
export type XrpcRequestParams<M extends Procedure | Query | Subscription> =
|
|
32
36
|
InferInput<M['parameters']>
|
|
33
37
|
|
|
@@ -40,13 +44,54 @@ type XrpcInputOptions<In> = In extends { body: infer B; encoding: infer E }
|
|
|
40
44
|
{ body: B; encoding?: E }
|
|
41
45
|
: { body?: undefined; encoding?: undefined }
|
|
42
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
|
+
*/
|
|
43
72
|
export type XrpcOptions<M extends Procedure | Query = Procedure | Query> =
|
|
44
73
|
CallOptions &
|
|
45
74
|
XrpcInputOptions<XrpcRequestPayload<M>> &
|
|
46
75
|
XrpcParamsOptions<XrpcRequestParams<M>>
|
|
47
76
|
|
|
48
77
|
/**
|
|
49
|
-
*
|
|
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
|
+
* ```
|
|
50
95
|
*/
|
|
51
96
|
export async function xrpc<const M extends Query | Procedure>(
|
|
52
97
|
agent: Agent,
|
|
@@ -69,10 +114,44 @@ export async function xrpc<const M extends Query | Procedure>(
|
|
|
69
114
|
else throw response
|
|
70
115
|
}
|
|
71
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
|
+
*/
|
|
72
125
|
export type XrpcResult<M extends Procedure | Query> =
|
|
73
126
|
| XrpcResponse<M>
|
|
74
127
|
| XrpcFailure<M>
|
|
75
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
|
+
*/
|
|
76
155
|
export async function xrpcSafe<const M extends Query | Procedure>(
|
|
77
156
|
agent: Agent,
|
|
78
157
|
ns: NonNullable<unknown> extends XrpcOptions<M>
|
|
@@ -173,7 +252,7 @@ function xrpcProcedureInput(
|
|
|
173
252
|
method: Procedure,
|
|
174
253
|
options: CallOptions & { body?: LexValue | BinaryBodyInit },
|
|
175
254
|
encodingHint?: string,
|
|
176
|
-
): null |
|
|
255
|
+
): null | { body: BodyInit; encoding: string } {
|
|
177
256
|
const { input } = method
|
|
178
257
|
const { body } = options
|
|
179
258
|
|
|
@@ -222,7 +301,7 @@ function buildPayload(
|
|
|
222
301
|
schema: Payload,
|
|
223
302
|
body: undefined | BodyInit,
|
|
224
303
|
encodingHint?: string,
|
|
225
|
-
): null |
|
|
304
|
+
): null | { body: BodyInit; encoding: string } {
|
|
226
305
|
if (schema.encoding === undefined) {
|
|
227
306
|
if (body !== undefined) {
|
|
228
307
|
throw new TypeError(
|