@atproto/lex-client 0.0.13 → 0.0.15
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 +21 -0
- package/dist/agent.d.ts +24 -19
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +11 -33
- package/dist/agent.js.map +1 -1
- package/dist/client.d.ts +11 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -7
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +35 -7
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +94 -19
- package/dist/errors.js.map +1 -1
- package/dist/response.d.ts +1 -1
- package/dist/response.d.ts.map +1 -1
- package/dist/response.js +2 -2
- package/dist/response.js.map +1 -1
- package/dist/xrpc.d.ts +14 -8
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +5 -3
- package/dist/xrpc.js.map +1 -1
- package/package.json +6 -6
- package/src/agent.ts +38 -21
- package/src/client.ts +16 -8
- package/src/errors.test.ts +346 -0
- package/src/errors.ts +120 -19
- package/src/response.ts +4 -4
- package/src/xrpc.ts +22 -14
package/src/errors.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LexError, LexErrorCode, LexErrorData } from '@atproto/lex-data'
|
|
2
2
|
import {
|
|
3
3
|
InferMethodError,
|
|
4
|
+
LexValidationError,
|
|
4
5
|
Procedure,
|
|
5
6
|
Query,
|
|
6
7
|
ResultFailure,
|
|
@@ -12,6 +13,13 @@ import {
|
|
|
12
13
|
parseWWWAuthenticateHeader,
|
|
13
14
|
} from './www-authenticate.js'
|
|
14
15
|
|
|
16
|
+
export type DownstreamError<N extends LexErrorCode = LexErrorCode> = {
|
|
17
|
+
status: number
|
|
18
|
+
headers?: Headers
|
|
19
|
+
encoding?: 'application/json'
|
|
20
|
+
body: LexErrorData<N>
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
/**
|
|
16
24
|
* HTTP status codes that indicate a transient error that may succeed on retry.
|
|
17
25
|
*
|
|
@@ -115,7 +123,9 @@ export abstract class XrpcError<
|
|
|
115
123
|
*/
|
|
116
124
|
abstract shouldRetry(): boolean
|
|
117
125
|
|
|
118
|
-
|
|
126
|
+
abstract toDownstreamError(): DownstreamError
|
|
127
|
+
|
|
128
|
+
matchesSchemaErrors(): this is XrpcError<M, InferMethodError<M>> {
|
|
119
129
|
return this.method.errors?.includes(this.error) ?? false
|
|
120
130
|
}
|
|
121
131
|
}
|
|
@@ -127,7 +137,7 @@ export abstract class XrpcError<
|
|
|
127
137
|
* a non-2xx status with a valid JSON error payload containing `error` and
|
|
128
138
|
* optional `message` fields.
|
|
129
139
|
*
|
|
130
|
-
* Use {@link
|
|
140
|
+
* Use {@link matchesSchemaErrors} to check if the error matches the method's declared
|
|
131
141
|
* error types for type-safe error handling.
|
|
132
142
|
*
|
|
133
143
|
* @typeParam M - The XRPC method type
|
|
@@ -168,25 +178,32 @@ export class XrpcResponseError<
|
|
|
168
178
|
return RETRYABLE_HTTP_STATUS_CODES.has(this.response.status)
|
|
169
179
|
}
|
|
170
180
|
|
|
171
|
-
override toJSON() {
|
|
181
|
+
override toJSON(): LexErrorData<N> {
|
|
172
182
|
return this.payload.body
|
|
173
183
|
}
|
|
174
184
|
|
|
175
|
-
override
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
185
|
+
override toDownstreamError(): DownstreamError {
|
|
186
|
+
// If the upstream server returned a 5xx error, we want to return a 502 Bad
|
|
187
|
+
// Gateway to downstream clients, as the issue is with the upstream server,
|
|
188
|
+
// not us. We still return the original error code and message in the body
|
|
189
|
+
// for transparency, but we do not want to expose internal server errors
|
|
190
|
+
// from the upstream server as-is to downstream clients.
|
|
191
|
+
return {
|
|
192
|
+
status: this.response.status === 500 ? 502 : this.status,
|
|
193
|
+
headers: stripHopByHopHeaders(this.headers),
|
|
194
|
+
body: this.toJSON(),
|
|
180
195
|
}
|
|
196
|
+
}
|
|
181
197
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
Response.json({ error: 'UpstreamFailure' }, { status: 502 })
|
|
185
|
-
: // If the error is on our side, return a generic internal server error
|
|
186
|
-
Response.json({ error: 'InternalServerError' }, { status: 500 })
|
|
198
|
+
get status(): number {
|
|
199
|
+
return this.response.status
|
|
187
200
|
}
|
|
188
201
|
|
|
189
|
-
get
|
|
202
|
+
get headers(): Headers {
|
|
203
|
+
return this.response.headers
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
get body(): LexErrorData<N> {
|
|
190
207
|
return this.payload.body
|
|
191
208
|
}
|
|
192
209
|
}
|
|
@@ -240,6 +257,14 @@ export class XrpcAuthenticationError<
|
|
|
240
257
|
this.response.headers.get('www-authenticate'),
|
|
241
258
|
) ?? {})
|
|
242
259
|
}
|
|
260
|
+
|
|
261
|
+
override toDownstreamError(): DownstreamError {
|
|
262
|
+
return {
|
|
263
|
+
status: 401,
|
|
264
|
+
headers: stripHopByHopHeaders(this.headers),
|
|
265
|
+
body: this.toJSON(),
|
|
266
|
+
}
|
|
267
|
+
}
|
|
243
268
|
}
|
|
244
269
|
|
|
245
270
|
/**
|
|
@@ -280,8 +305,44 @@ export class XrpcUpstreamError<
|
|
|
280
305
|
return RETRYABLE_HTTP_STATUS_CODES.has(this.response.status)
|
|
281
306
|
}
|
|
282
307
|
|
|
283
|
-
override
|
|
284
|
-
return
|
|
308
|
+
override toDownstreamError(): DownstreamError {
|
|
309
|
+
return { status: 502, body: this.toJSON() }
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Error class for invalid XRPC responses that fail schema validation.
|
|
315
|
+
*
|
|
316
|
+
* This is a specific type of {@link XrpcUpstreamError} that indicates the
|
|
317
|
+
* upstream server returned a response that was structurally valid but did not
|
|
318
|
+
* conform to the expected schema for the method. This likely indicates a
|
|
319
|
+
* mismatch between client and server versions or an issue with the server's
|
|
320
|
+
* XRPC implementation.
|
|
321
|
+
*
|
|
322
|
+
* @typeParam M - The XRPC method type
|
|
323
|
+
*/
|
|
324
|
+
export class XrpcInvalidResponseError<
|
|
325
|
+
M extends Procedure | Query = Procedure | Query,
|
|
326
|
+
> extends XrpcUpstreamError<M> {
|
|
327
|
+
name = 'XrpcInvalidResponseError'
|
|
328
|
+
|
|
329
|
+
constructor(
|
|
330
|
+
method: M,
|
|
331
|
+
response: Response,
|
|
332
|
+
payload: XrpcResponsePayload,
|
|
333
|
+
readonly cause: LexValidationError,
|
|
334
|
+
) {
|
|
335
|
+
super(method, response, payload, `Invalid response: ${cause.message}`, {
|
|
336
|
+
cause,
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
override toDownstreamError(): DownstreamError {
|
|
341
|
+
// @NOTE This could be reflected as both a 500 ("we" are at fault) and 502
|
|
342
|
+
// ("they" are at fault). We are using 502 here to allow downstream clients
|
|
343
|
+
// to determine that the issue lies at the interface between us and the
|
|
344
|
+
// upstream server, rather than an issue with our internal processing.
|
|
345
|
+
return { status: 502, body: this.toJSON() }
|
|
285
346
|
}
|
|
286
347
|
}
|
|
287
348
|
|
|
@@ -326,9 +387,13 @@ export class XrpcInternalError<
|
|
|
326
387
|
return true
|
|
327
388
|
}
|
|
328
389
|
|
|
329
|
-
override
|
|
330
|
-
// Do not expose internal error details to downstream clients
|
|
331
|
-
return
|
|
390
|
+
override toJSON(): LexErrorData<'InternalServerError'> {
|
|
391
|
+
// @NOTE Do not expose internal error details to downstream clients
|
|
392
|
+
return { error: this.error, message: 'Internal Server Error' }
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
override toDownstreamError(): DownstreamError {
|
|
396
|
+
return { status: 500, body: this.toJSON() }
|
|
332
397
|
}
|
|
333
398
|
}
|
|
334
399
|
|
|
@@ -393,3 +458,39 @@ export function asXrpcFailure<M extends Procedure | Query>(
|
|
|
393
458
|
|
|
394
459
|
return new XrpcInternalError(method, undefined, { cause })
|
|
395
460
|
}
|
|
461
|
+
|
|
462
|
+
const HOP_BY_HOP_HEADERS = new Set([
|
|
463
|
+
'connection',
|
|
464
|
+
'keep-alive',
|
|
465
|
+
'proxy-authenticate',
|
|
466
|
+
'proxy-authorization',
|
|
467
|
+
'te',
|
|
468
|
+
'trailer',
|
|
469
|
+
'transfer-encoding',
|
|
470
|
+
'upgrade',
|
|
471
|
+
])
|
|
472
|
+
|
|
473
|
+
function stripHopByHopHeaders(headers: Headers): Headers {
|
|
474
|
+
const result = new Headers(headers)
|
|
475
|
+
|
|
476
|
+
// Remove statically known hop-by-hop headers
|
|
477
|
+
for (const name of HOP_BY_HOP_HEADERS) {
|
|
478
|
+
result.delete(name)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Remove headers listed in the "Connection" header
|
|
482
|
+
const connection = headers.get('connection')
|
|
483
|
+
if (connection) {
|
|
484
|
+
for (const name of connection.split(',')) {
|
|
485
|
+
result.delete(name.trim())
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// These are not actually hop-by-hop headers, but we remove them because the
|
|
490
|
+
// upstream payload gets parsed and re-serialized, so content length and
|
|
491
|
+
// encoding may no longer be accurate.
|
|
492
|
+
result.delete('content-length')
|
|
493
|
+
result.delete('content-encoding')
|
|
494
|
+
|
|
495
|
+
return result
|
|
496
|
+
}
|
package/src/response.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from '@atproto/lex-schema'
|
|
8
8
|
import {
|
|
9
9
|
XrpcAuthenticationError,
|
|
10
|
+
XrpcInvalidResponseError,
|
|
10
11
|
XrpcResponseError,
|
|
11
12
|
XrpcUpstreamError,
|
|
12
13
|
isXrpcErrorPayload,
|
|
@@ -70,7 +71,7 @@ export class XrpcResponse<M extends Procedure | Query>
|
|
|
70
71
|
|
|
71
72
|
/**
|
|
72
73
|
* @throws {XrpcResponseError} in case of (valid) XRPC error responses. Use
|
|
73
|
-
* {@link XrpcResponseError.
|
|
74
|
+
* {@link XrpcResponseError.matchesSchemaErrors} to narrow the error type based on
|
|
74
75
|
* the method's declared error schema. This can be narrowed further as a
|
|
75
76
|
* {@link XrpcAuthenticationError} if the error is an authentication error.
|
|
76
77
|
* @throws {XrpcUpstreamError} when the response is not a valid XRPC
|
|
@@ -162,12 +163,11 @@ export class XrpcResponse<M extends Procedure | Query>
|
|
|
162
163
|
const result = method.output.schema.safeParse(payload.body)
|
|
163
164
|
|
|
164
165
|
if (!result.success) {
|
|
165
|
-
throw new
|
|
166
|
+
throw new XrpcInvalidResponseError(
|
|
166
167
|
method,
|
|
167
168
|
response,
|
|
168
169
|
payload,
|
|
169
|
-
|
|
170
|
-
{ cause: result.reason },
|
|
170
|
+
result.reason,
|
|
171
171
|
)
|
|
172
172
|
}
|
|
173
173
|
}
|
package/src/xrpc.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
InferInput,
|
|
5
5
|
InferPayload,
|
|
6
6
|
Main,
|
|
7
|
+
NsidString,
|
|
7
8
|
Params,
|
|
8
9
|
Payload,
|
|
9
10
|
Procedure,
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
Subscription,
|
|
13
14
|
getMain,
|
|
14
15
|
} from '@atproto/lex-schema'
|
|
15
|
-
import { Agent } from './agent.js'
|
|
16
|
+
import { Agent, AgentOptions, buildAgent } from './agent.js'
|
|
16
17
|
import { XrpcFailure, asXrpcFailure } from './errors.js'
|
|
17
18
|
import { XrpcResponse } from './response.js'
|
|
18
19
|
import { BinaryBodyInit, CallOptions } from './types.js'
|
|
@@ -77,8 +78,7 @@ export type XrpcOptions<M extends Procedure | Query = Procedure | Query> =
|
|
|
77
78
|
/**
|
|
78
79
|
* Makes an XRPC request and throws on failure.
|
|
79
80
|
*
|
|
80
|
-
* This is the low-level function for making XRPC calls.
|
|
81
|
-
* prefer using {@link Client.xrpc} which provides a more ergonomic API.
|
|
81
|
+
* This is the low-level function for making XRPC calls.
|
|
82
82
|
*
|
|
83
83
|
* @param agent - The {@link Agent} to use for making the request
|
|
84
84
|
* @param ns - The lexicon method definition
|
|
@@ -88,28 +88,35 @@ export type XrpcOptions<M extends Procedure | Query = Procedure | Query> =
|
|
|
88
88
|
*
|
|
89
89
|
* @example
|
|
90
90
|
* ```typescript
|
|
91
|
+
* const response = await xrpc('https://bsky.network', com.atproto.identity.resolveHandle, {
|
|
92
|
+
* params: { handle: "atproto.com" }
|
|
93
|
+
* })
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
91
98
|
* const response = await xrpc(agent, app.bsky.feed.getTimeline.main, {
|
|
92
99
|
* params: { limit: 50 }
|
|
93
100
|
* })
|
|
94
101
|
* ```
|
|
95
102
|
*/
|
|
96
103
|
export async function xrpc<const M extends Query | Procedure>(
|
|
97
|
-
|
|
104
|
+
agentOpts: Agent | AgentOptions,
|
|
98
105
|
ns: NonNullable<unknown> extends XrpcOptions<M>
|
|
99
106
|
? Main<M>
|
|
100
107
|
: Restricted<'This XRPC method requires an "options" argument'>,
|
|
101
108
|
): Promise<XrpcResponse<M>>
|
|
102
109
|
export async function xrpc<const M extends Query | Procedure>(
|
|
103
|
-
|
|
110
|
+
agentOpts: Agent | AgentOptions,
|
|
104
111
|
ns: Main<M>,
|
|
105
112
|
options: XrpcOptions<M>,
|
|
106
113
|
): Promise<XrpcResponse<M>>
|
|
107
114
|
export async function xrpc<const M extends Query | Procedure>(
|
|
108
|
-
|
|
115
|
+
agentOpts: Agent | AgentOptions,
|
|
109
116
|
ns: Main<M>,
|
|
110
117
|
options: XrpcOptions<M> = {} as XrpcOptions<M>,
|
|
111
118
|
): Promise<XrpcResponse<M>> {
|
|
112
|
-
const response = await xrpcSafe<M>(
|
|
119
|
+
const response = await xrpcSafe<M>(agentOpts, ns, options)
|
|
113
120
|
if (response.success) return response
|
|
114
121
|
else throw response
|
|
115
122
|
}
|
|
@@ -141,7 +148,7 @@ export type XrpcResult<M extends Procedure | Query> =
|
|
|
141
148
|
*
|
|
142
149
|
* @example
|
|
143
150
|
* ```typescript
|
|
144
|
-
* const result = await xrpcSafe(
|
|
151
|
+
* const result = await xrpcSafe('https://example.com', app.bsky.actor.getProfile, {
|
|
145
152
|
* params: { actor: 'alice.bsky.social' }
|
|
146
153
|
* })
|
|
147
154
|
*
|
|
@@ -153,24 +160,25 @@ export type XrpcResult<M extends Procedure | Query> =
|
|
|
153
160
|
* ```
|
|
154
161
|
*/
|
|
155
162
|
export async function xrpcSafe<const M extends Query | Procedure>(
|
|
156
|
-
|
|
163
|
+
agentOpts: Agent | AgentOptions,
|
|
157
164
|
ns: NonNullable<unknown> extends XrpcOptions<M>
|
|
158
165
|
? Main<M>
|
|
159
166
|
: Restricted<'This XRPC method requires an "options" argument'>,
|
|
160
167
|
): Promise<XrpcResult<M>>
|
|
161
168
|
export async function xrpcSafe<const M extends Query | Procedure>(
|
|
162
|
-
|
|
169
|
+
agentOpts: Agent | AgentOptions,
|
|
163
170
|
ns: Main<M>,
|
|
164
171
|
options: XrpcOptions<M>,
|
|
165
172
|
): Promise<XrpcResult<M>>
|
|
166
173
|
export async function xrpcSafe<const M extends Query | Procedure>(
|
|
167
|
-
|
|
174
|
+
agentOpts: Agent | AgentOptions,
|
|
168
175
|
ns: Main<M>,
|
|
169
176
|
options: XrpcOptions<M> = {} as XrpcOptions<M>,
|
|
170
177
|
): Promise<XrpcResult<M>> {
|
|
171
178
|
options.signal?.throwIfAborted()
|
|
172
179
|
const method: M = getMain(ns)
|
|
173
180
|
try {
|
|
181
|
+
const agent = buildAgent(agentOpts)
|
|
174
182
|
const url = xrpcRequestUrl(method, options)
|
|
175
183
|
const request = xrpcRequestInit(method, options)
|
|
176
184
|
const response = await agent.fetchHandler(url, request)
|
|
@@ -183,14 +191,14 @@ export async function xrpcSafe<const M extends Query | Procedure>(
|
|
|
183
191
|
function xrpcRequestUrl<M extends Procedure | Query | Subscription>(
|
|
184
192
|
method: M,
|
|
185
193
|
options: CallOptions & { params?: Params },
|
|
186
|
-
) {
|
|
187
|
-
const path = `/xrpc/${method.nsid}`
|
|
194
|
+
): `/xrpc/${NsidString}${'' | `?${string}`}` {
|
|
195
|
+
const path = `/xrpc/${method.nsid}` as const
|
|
188
196
|
|
|
189
197
|
const queryString = method.parameters
|
|
190
198
|
?.toURLSearchParams(options.params ?? {})
|
|
191
199
|
.toString()
|
|
192
200
|
|
|
193
|
-
return queryString ? `${path}?${queryString}` : path
|
|
201
|
+
return queryString ? (`${path}?${queryString}` as const) : path
|
|
194
202
|
}
|
|
195
203
|
|
|
196
204
|
function xrpcRequestInit<T extends Procedure | Query>(
|