@atproto/lex-client 0.0.16 → 0.0.18
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 +32 -0
- package/dist/client.d.ts +24 -21
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -7
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +22 -22
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +62 -37
- package/dist/errors.js.map +1 -1
- package/dist/response.d.ts +66 -7
- package/dist/response.d.ts.map +1 -1
- package/dist/response.js +90 -69
- package/dist/response.js.map +1 -1
- package/dist/types.d.ts +8 -37
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +14 -27
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +15 -6
- package/dist/util.js.map +1 -1
- package/dist/xrpc.d.ts +40 -15
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +4 -2
- package/dist/xrpc.js.map +1 -1
- package/package.json +6 -6
- package/src/client.ts +83 -31
- package/src/errors.test.ts +243 -32
- package/src/errors.ts +91 -52
- package/src/response.ts +229 -102
- package/src/types.ts +17 -40
- package/src/util.test.ts +11 -11
- package/src/util.ts +33 -36
- package/src/xrpc.test.ts +691 -142
- package/src/xrpc.ts +73 -29
package/src/response.ts
CHANGED
|
@@ -1,23 +1,120 @@
|
|
|
1
|
-
import { lexParse } from '@atproto/lex-json'
|
|
1
|
+
import { LexParseOptions, lexParse } from '@atproto/lex-json'
|
|
2
2
|
import {
|
|
3
3
|
InferMethodOutputEncoding,
|
|
4
|
+
InferOutput,
|
|
5
|
+
LexValue,
|
|
6
|
+
Payload,
|
|
4
7
|
Procedure,
|
|
5
8
|
Query,
|
|
6
9
|
ResultSuccess,
|
|
10
|
+
Validator,
|
|
7
11
|
} from '@atproto/lex-schema'
|
|
8
12
|
import {
|
|
9
13
|
XrpcAuthenticationError,
|
|
10
14
|
XrpcInvalidResponseError,
|
|
11
15
|
XrpcResponseError,
|
|
12
|
-
|
|
13
|
-
isXrpcErrorPayload,
|
|
16
|
+
XrpcResponseValidationError,
|
|
14
17
|
} from './errors.js'
|
|
15
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
EncodingString,
|
|
20
|
+
XrpcUnknownResponsePayload,
|
|
21
|
+
isEncodingString,
|
|
22
|
+
} from './types.js'
|
|
16
23
|
|
|
17
24
|
const CONTENT_TYPE_BINARY = 'application/octet-stream'
|
|
18
25
|
const CONTENT_TYPE_JSON = 'application/json'
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
// @NOTE the output schema is used in "parse" mode (safeParse), which means that
|
|
28
|
+
// defaults will be applied and coercions will be performed, so we need to use
|
|
29
|
+
// InferOutput here to get the final parsed type, not Infer/InferInput. For this
|
|
30
|
+
// reason, we cannot use InferMethodOutputBody and InferMethodOutput from
|
|
31
|
+
// lex-schema here.
|
|
32
|
+
|
|
33
|
+
type InferEncodingType<TEncoding extends string> = TEncoding extends '*/*'
|
|
34
|
+
? EncodingString
|
|
35
|
+
: TEncoding extends `${infer T extends string}/*`
|
|
36
|
+
? `${T}/${string}`
|
|
37
|
+
: TEncoding
|
|
38
|
+
|
|
39
|
+
type InferBodyType<
|
|
40
|
+
TEncoding extends string,
|
|
41
|
+
TSchema,
|
|
42
|
+
> = TSchema extends Validator
|
|
43
|
+
? InferOutput<TSchema>
|
|
44
|
+
: TEncoding extends `application/json`
|
|
45
|
+
? LexValue
|
|
46
|
+
: Uint8Array
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The body type of an XRPC response, inferred from the method's output schema.
|
|
50
|
+
*
|
|
51
|
+
* For JSON responses, this is the parsed LexValue. For binary responses,
|
|
52
|
+
* this is a Uint8Array.
|
|
53
|
+
*
|
|
54
|
+
* @typeParam M - The XRPC method type (Procedure or Query)
|
|
55
|
+
*/
|
|
56
|
+
export type XrpcResponseBody<M extends Procedure | Query> =
|
|
57
|
+
M['output'] extends Payload<infer TEncoding, infer TSchema>
|
|
58
|
+
? TEncoding extends string
|
|
59
|
+
? InferBodyType<TEncoding, TSchema>
|
|
60
|
+
: undefined | LexValue | Uint8Array
|
|
61
|
+
: never
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The full payload type of an XRPC response, including body and encoding.
|
|
65
|
+
*
|
|
66
|
+
* Returns `null` for methods that have no output.
|
|
67
|
+
*
|
|
68
|
+
* @typeParam M - The XRPC method type (Procedure or Query)
|
|
69
|
+
*/
|
|
70
|
+
export type XrpcResponsePayload<M extends Procedure | Query> =
|
|
71
|
+
M['output'] extends Payload<infer TEncoding, infer TSchema>
|
|
72
|
+
? TEncoding extends string
|
|
73
|
+
? {
|
|
74
|
+
encoding: InferEncodingType<TEncoding>
|
|
75
|
+
body: InferBodyType<TEncoding, TSchema>
|
|
76
|
+
}
|
|
77
|
+
: // If the schema does not specify an output encoding, anything could be
|
|
78
|
+
// returned, including no payload at all (undefined).
|
|
79
|
+
undefined | { body: LexValue | Uint8Array; encoding: string }
|
|
80
|
+
: never
|
|
81
|
+
|
|
82
|
+
export type XrpcResponseOptions = {
|
|
83
|
+
/**
|
|
84
|
+
* Whether to validate the response against the method's output schema.
|
|
85
|
+
* Disabling this can improve performance but may lead to runtime errors if
|
|
86
|
+
* the response does not conform to the expected schema. Only set this to
|
|
87
|
+
* `false` if you are certain that the upstream service will always return
|
|
88
|
+
* valid responses.
|
|
89
|
+
*
|
|
90
|
+
* @default true
|
|
91
|
+
*/
|
|
92
|
+
validateResponse?: boolean
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Whether to strictly process response payloads according to Lex encoding
|
|
96
|
+
* rules. By default, the client will reject responses with invalid Lex data
|
|
97
|
+
* (floats and invalid $bytes / $link objects).
|
|
98
|
+
*
|
|
99
|
+
* Setting this option to `false` will allow the client to accept such
|
|
100
|
+
* responses in a non-strict mode, where invalid Lex data will be returned
|
|
101
|
+
* as-is (e.g., floats will not be rejected, and invalid $bytes / $link
|
|
102
|
+
* objects will not be converted to Uint8Array / Cid). When in non-strict
|
|
103
|
+
* mode, the validation will also be relaxed when validating the response
|
|
104
|
+
* against the method's output schema, allowing values that do not strictly
|
|
105
|
+
* conform to the schema (e.g. datetime strings that are not valid RFC3339
|
|
106
|
+
* format, blobs that are not of the right size/mime-type, etc.) to be
|
|
107
|
+
* accepted as long as their basic structure is correct.
|
|
108
|
+
*
|
|
109
|
+
* When validation is enabled (the default), the values defined through the
|
|
110
|
+
* method schema will be enforced, ensuring that the client can still process
|
|
111
|
+
* the response even if the server returns invalid Lex data.
|
|
112
|
+
*
|
|
113
|
+
* @default true
|
|
114
|
+
* @see {@link LexParseOptions.strict}
|
|
115
|
+
*/
|
|
116
|
+
strictResponseProcessing?: boolean
|
|
117
|
+
}
|
|
21
118
|
|
|
22
119
|
/**
|
|
23
120
|
* Small container for XRPC response data.
|
|
@@ -74,102 +171,101 @@ export class XrpcResponse<M extends Procedure | Query>
|
|
|
74
171
|
* {@link XrpcResponseError.matchesSchemaErrors} to narrow the error type based on
|
|
75
172
|
* the method's declared error schema. This can be narrowed further as a
|
|
76
173
|
* {@link XrpcAuthenticationError} if the error is an authentication error.
|
|
77
|
-
* @throws {
|
|
174
|
+
* @throws {XrpcInvalidResponseError} when the response is not a valid XRPC
|
|
78
175
|
* response, or if the response does not conform to the method's schema.
|
|
79
176
|
*/
|
|
80
177
|
static async fromFetchResponse<const M extends Procedure | Query>(
|
|
81
178
|
method: M,
|
|
82
179
|
response: Response,
|
|
83
|
-
options?:
|
|
180
|
+
options?: XrpcResponseOptions,
|
|
84
181
|
): Promise<XrpcResponse<M>> {
|
|
85
182
|
// @NOTE The body MUST either be read or canceled to avoid resource leaks.
|
|
86
183
|
// Since nothing should cause an exception before "readPayload" is
|
|
87
184
|
// called, we can safely not use a try/finally here.
|
|
88
185
|
|
|
89
|
-
//
|
|
90
|
-
if (response.status
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
method,
|
|
96
|
-
response,
|
|
97
|
-
null,
|
|
98
|
-
'Unable to parse response payload',
|
|
99
|
-
{ cause },
|
|
100
|
-
)
|
|
101
|
-
},
|
|
102
|
-
)
|
|
186
|
+
// Always turn 4xx/5xx responses into XrpcResponseError
|
|
187
|
+
if (response.status >= 400) {
|
|
188
|
+
const payload = await readPayload(method, response, {
|
|
189
|
+
// Always parse errors in non-strict mode
|
|
190
|
+
parse: { strict: false },
|
|
191
|
+
})
|
|
103
192
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
throw response.status === 401
|
|
107
|
-
? new XrpcAuthenticationError<M>(method, response, payload)
|
|
108
|
-
: new XrpcResponseError<M>(method, response, payload)
|
|
193
|
+
if (response.status === 401) {
|
|
194
|
+
throw new XrpcAuthenticationError<M>(method, response, payload)
|
|
109
195
|
}
|
|
110
196
|
|
|
111
|
-
|
|
112
|
-
|
|
197
|
+
throw new XrpcResponseError<M>(method, response, payload)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// @NOTE redirect is set to 'follow', so we shouldn't get 3xx responses here
|
|
201
|
+
if (response.status < 200 || response.status >= 300) {
|
|
202
|
+
await response.body?.cancel()
|
|
203
|
+
|
|
204
|
+
throw new XrpcInvalidResponseError(
|
|
113
205
|
method,
|
|
114
206
|
response,
|
|
115
|
-
|
|
116
|
-
response.status
|
|
117
|
-
? 'Upstream server encountered an error'
|
|
118
|
-
: response.status >= 400
|
|
119
|
-
? 'Invalid response payload'
|
|
120
|
-
: 'Invalid response status code',
|
|
207
|
+
undefined,
|
|
208
|
+
`Unexpected status code ${response.status}`,
|
|
121
209
|
)
|
|
122
210
|
}
|
|
123
211
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
212
|
+
const payload = await readPayload(method, response, {
|
|
213
|
+
// Parse response if there is a schema, or if the encoding is
|
|
214
|
+
// "application/json"
|
|
215
|
+
parse:
|
|
216
|
+
method.output.schema || method.output.encoding === CONTENT_TYPE_JSON
|
|
217
|
+
? { strict: options?.strictResponseProcessing ?? true }
|
|
218
|
+
: // If there is no declared output encoding, we'll parse the output (in loose mode)
|
|
219
|
+
method.output.encoding == null
|
|
220
|
+
? { strict: false }
|
|
221
|
+
: false,
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
if (!method.output.matchesEncoding(payload?.encoding)) {
|
|
225
|
+
throw new XrpcInvalidResponseError(
|
|
129
226
|
method,
|
|
130
227
|
response,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
{ cause },
|
|
228
|
+
payload,
|
|
229
|
+
`Expected ${stringifyEncoding(method.output.encoding)} response (got ${stringifyEncoding(payload?.encoding)})`,
|
|
134
230
|
)
|
|
135
|
-
}
|
|
231
|
+
}
|
|
136
232
|
|
|
137
233
|
// Response is successful (2xx). Validate payload (data and encoding) against schema.
|
|
138
|
-
if (method.output.encoding
|
|
139
|
-
//
|
|
140
|
-
if
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
)
|
|
147
|
-
}
|
|
148
|
-
} else {
|
|
149
|
-
// Schema expects a payload
|
|
150
|
-
if (!payload || !method.output.matchesEncoding(payload.encoding)) {
|
|
151
|
-
throw new XrpcUpstreamError(
|
|
152
|
-
method,
|
|
153
|
-
response,
|
|
154
|
-
payload,
|
|
155
|
-
payload
|
|
156
|
-
? `Expected ${method.output.encoding} response, got ${payload.encoding}`
|
|
157
|
-
: `Expected non-empty response with content-type ${method.output.encoding}`,
|
|
158
|
-
)
|
|
159
|
-
}
|
|
234
|
+
if (method.output.encoding != null) {
|
|
235
|
+
// If the schema specifies an output, verify that the response properly
|
|
236
|
+
// matches the expected format (encoding and schema, if present). If no
|
|
237
|
+
// output is specified, any payload could be returned.
|
|
238
|
+
|
|
239
|
+
// Needed for type safety. Should never happen since matchesEncoding()
|
|
240
|
+
// should return not succeed if there is a schema encoding but no payload.
|
|
241
|
+
if (!payload) throw new Error('Expected payload')
|
|
160
242
|
|
|
161
243
|
// Assert valid response body.
|
|
162
244
|
if (method.output.schema && options?.validateResponse !== false) {
|
|
163
|
-
const result = method.output.schema.safeParse(payload.body
|
|
245
|
+
const result = method.output.schema.safeParse(payload.body, {
|
|
246
|
+
strict: options?.strictResponseProcessing ?? true,
|
|
247
|
+
})
|
|
164
248
|
|
|
165
249
|
if (!result.success) {
|
|
166
|
-
throw new
|
|
250
|
+
throw new XrpcResponseValidationError(
|
|
167
251
|
method,
|
|
168
252
|
response,
|
|
169
253
|
payload,
|
|
170
254
|
result.reason,
|
|
171
255
|
)
|
|
172
256
|
}
|
|
257
|
+
|
|
258
|
+
const parsedPayload = {
|
|
259
|
+
body: result.value,
|
|
260
|
+
encoding: payload.encoding,
|
|
261
|
+
} as XrpcResponsePayload<M>
|
|
262
|
+
|
|
263
|
+
return new XrpcResponse<M>(
|
|
264
|
+
method,
|
|
265
|
+
response.status,
|
|
266
|
+
response.headers,
|
|
267
|
+
parsedPayload,
|
|
268
|
+
)
|
|
173
269
|
}
|
|
174
270
|
}
|
|
175
271
|
|
|
@@ -182,50 +278,81 @@ export class XrpcResponse<M extends Procedure | Query>
|
|
|
182
278
|
}
|
|
183
279
|
}
|
|
184
280
|
|
|
281
|
+
type ReadPayloadOptions = {
|
|
282
|
+
/**
|
|
283
|
+
* Whether to parse the response body as JSON and convert it to LexValue.
|
|
284
|
+
*
|
|
285
|
+
* @default false
|
|
286
|
+
*/
|
|
287
|
+
parse?: false | LexParseOptions
|
|
288
|
+
}
|
|
289
|
+
|
|
185
290
|
/**
|
|
186
291
|
* @note this function always consumes the response body
|
|
187
292
|
*/
|
|
188
293
|
async function readPayload(
|
|
294
|
+
method: Query | Procedure,
|
|
189
295
|
response: Response,
|
|
190
|
-
options?:
|
|
191
|
-
): Promise<
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
296
|
+
options?: ReadPayloadOptions,
|
|
297
|
+
): Promise<undefined | XrpcUnknownResponsePayload> {
|
|
298
|
+
try {
|
|
299
|
+
// @TODO Should we limit the maximum response size here (this could also be
|
|
300
|
+
// done by the FetchHandler)?
|
|
301
|
+
|
|
302
|
+
const encoding = response.headers
|
|
303
|
+
.get('content-type')
|
|
304
|
+
?.split(';')[0]
|
|
305
|
+
.trim()
|
|
306
|
+
.toLowerCase()
|
|
307
|
+
|
|
308
|
+
// Response content-type is undefined
|
|
309
|
+
if (!encoding) {
|
|
310
|
+
// If the body is empty, return undefined (= no payload)
|
|
311
|
+
const arrayBuffer = await response.arrayBuffer()
|
|
312
|
+
if (arrayBuffer.byteLength === 0) return undefined
|
|
313
|
+
|
|
314
|
+
// If we got data despite no content-type, treat it as binary
|
|
315
|
+
return {
|
|
316
|
+
encoding: CONTENT_TYPE_BINARY,
|
|
317
|
+
body: new Uint8Array(arrayBuffer),
|
|
318
|
+
}
|
|
211
319
|
}
|
|
212
|
-
}
|
|
213
320
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
321
|
+
if (!isEncodingString(encoding)) {
|
|
322
|
+
throw new TypeError(`Invalid content-type "${encoding}" in response`)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (options?.parse && encoding === CONTENT_TYPE_JSON) {
|
|
326
|
+
// @NOTE It might be worth returning the raw bytes here (Uint8Array) and
|
|
327
|
+
// perform the lex parsing using cborg/json, allowing to do
|
|
328
|
+
// bytes->LexValue in one step instead of bytes->text->JSON->LexValue.
|
|
329
|
+
// This would require adding encode/decode utilities to lex-json (similar
|
|
330
|
+
// to @ipld/dag-json)
|
|
331
|
+
const text = await response.text()
|
|
332
|
+
|
|
333
|
+
// @NOTE Using `lexParse(text)` (instead of `jsonToLex(json)`) here as
|
|
334
|
+
// using a reviver function during JSON.parse should be faster than
|
|
335
|
+
// parsing to JSON then converting to Lex (?)
|
|
221
336
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
337
|
+
// @TODO verify statement above
|
|
338
|
+
return { encoding, body: lexParse(text, options.parse) }
|
|
339
|
+
}
|
|
225
340
|
|
|
226
|
-
|
|
227
|
-
return { encoding, body:
|
|
341
|
+
const arrayBuffer = await response.arrayBuffer()
|
|
342
|
+
return { encoding, body: new Uint8Array(arrayBuffer) }
|
|
343
|
+
} catch (cause) {
|
|
344
|
+
const message = 'Unable to parse response payload'
|
|
345
|
+
const messageDetail = cause instanceof TypeError ? cause.message : undefined
|
|
346
|
+
throw new XrpcInvalidResponseError(
|
|
347
|
+
method,
|
|
348
|
+
response,
|
|
349
|
+
undefined,
|
|
350
|
+
messageDetail ? `${message}: ${messageDetail}` : message,
|
|
351
|
+
{ cause },
|
|
352
|
+
)
|
|
228
353
|
}
|
|
354
|
+
}
|
|
229
355
|
|
|
230
|
-
|
|
356
|
+
function stringifyEncoding(encoding: string | undefined) {
|
|
357
|
+
return encoding ? `"${encoding}"` : 'no payload'
|
|
231
358
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { DidString, UnknownString } from '@atproto/lex-schema'
|
|
1
|
+
import { DidString, LexValue, UnknownString } from '@atproto/lex-schema'
|
|
2
2
|
|
|
3
|
-
export type { DidString, UnknownString }
|
|
3
|
+
export type { DidString, LexValue, UnknownString }
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Service identifier fragment for DID service endpoints.
|
|
@@ -22,44 +22,6 @@ export type DidServiceIdentifier = 'atproto_labeler' | UnknownString
|
|
|
22
22
|
*/
|
|
23
23
|
export type Service = `${DidString}#${DidServiceIdentifier}`
|
|
24
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
|
-
*/
|
|
31
|
-
export type CallOptions = {
|
|
32
|
-
/** Labeler DIDs to request labels from for content moderation. */
|
|
33
|
-
labelers?: Iterable<DidString>
|
|
34
|
-
/** AbortSignal to cancel the request. */
|
|
35
|
-
signal?: AbortSignal
|
|
36
|
-
/** Additional HTTP headers to include in the request. */
|
|
37
|
-
headers?: HeadersInit
|
|
38
|
-
/** Service proxy identifier for routing requests through a specific service. */
|
|
39
|
-
service?: Service
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Whether to validate the request against the method's input schema. Enabling
|
|
43
|
-
* this can help catch errors early but may have a performance cost. This
|
|
44
|
-
* would typically only be set to `true` in development or debugging
|
|
45
|
-
* scenarios.
|
|
46
|
-
*
|
|
47
|
-
* @default false
|
|
48
|
-
*/
|
|
49
|
-
validateRequest?: boolean
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Whether to validate the response against the method's output schema.
|
|
53
|
-
* Disabling this can improve performance but may lead to runtime errors if
|
|
54
|
-
* the response does not conform to the expected schema. Only set this to
|
|
55
|
-
* `false` if you are certain that the upstream service will always return
|
|
56
|
-
* valid responses.
|
|
57
|
-
*
|
|
58
|
-
* @default true
|
|
59
|
-
*/
|
|
60
|
-
validateResponse?: boolean
|
|
61
|
-
}
|
|
62
|
-
|
|
63
25
|
/**
|
|
64
26
|
* Valid input types for binary request bodies.
|
|
65
27
|
*
|
|
@@ -92,3 +54,18 @@ export type BinaryBodyInit =
|
|
|
92
54
|
| ReadableStream<Uint8Array>
|
|
93
55
|
| AsyncIterable<Uint8Array>
|
|
94
56
|
| string
|
|
57
|
+
|
|
58
|
+
export type EncodingString = `${string}/${string}`
|
|
59
|
+
|
|
60
|
+
export function isEncodingString(
|
|
61
|
+
contentType: string,
|
|
62
|
+
): contentType is EncodingString {
|
|
63
|
+
return contentType.includes('/')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type XrpcUnknownResponsePayload<
|
|
67
|
+
TBinary extends BinaryBodyInit = Uint8Array,
|
|
68
|
+
> = {
|
|
69
|
+
encoding: EncodingString
|
|
70
|
+
body: LexValue | TBinary
|
|
71
|
+
}
|
package/src/util.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
buildXrpcRequestHeaders,
|
|
4
4
|
isAsyncIterable,
|
|
5
5
|
isBlobLike,
|
|
6
6
|
toReadableStream,
|
|
@@ -111,24 +111,24 @@ describe(isAsyncIterable, () => {
|
|
|
111
111
|
})
|
|
112
112
|
|
|
113
113
|
// ============================================================================
|
|
114
|
-
//
|
|
114
|
+
// buildXrpcRequestHeaders
|
|
115
115
|
// ============================================================================
|
|
116
116
|
|
|
117
|
-
describe(
|
|
117
|
+
describe(buildXrpcRequestHeaders, () => {
|
|
118
118
|
it('returns empty headers when no options are set', () => {
|
|
119
|
-
const headers =
|
|
119
|
+
const headers = buildXrpcRequestHeaders({})
|
|
120
120
|
expect([...headers.entries()]).toEqual([])
|
|
121
121
|
})
|
|
122
122
|
|
|
123
123
|
it('sets atproto-proxy header from service option', () => {
|
|
124
|
-
const headers =
|
|
124
|
+
const headers = buildXrpcRequestHeaders({
|
|
125
125
|
service: 'did:plc:1234#atproto_labeler',
|
|
126
126
|
})
|
|
127
127
|
expect(headers.get('atproto-proxy')).toBe('did:plc:1234#atproto_labeler')
|
|
128
128
|
})
|
|
129
129
|
|
|
130
130
|
it('does not override existing atproto-proxy header', () => {
|
|
131
|
-
const headers =
|
|
131
|
+
const headers = buildXrpcRequestHeaders({
|
|
132
132
|
headers: { 'atproto-proxy': 'did:plc:existing#service' },
|
|
133
133
|
service: 'did:plc:new#service',
|
|
134
134
|
})
|
|
@@ -136,7 +136,7 @@ describe(buildAtprotoHeaders, () => {
|
|
|
136
136
|
})
|
|
137
137
|
|
|
138
138
|
it('sets atproto-accept-labelers from labelers option', () => {
|
|
139
|
-
const headers =
|
|
139
|
+
const headers = buildXrpcRequestHeaders({
|
|
140
140
|
labelers: ['did:plc:labeler1', 'did:plc:labeler2'] as const,
|
|
141
141
|
})
|
|
142
142
|
expect(headers.get('atproto-accept-labelers')).toBe(
|
|
@@ -145,7 +145,7 @@ describe(buildAtprotoHeaders, () => {
|
|
|
145
145
|
})
|
|
146
146
|
|
|
147
147
|
it('appends to existing atproto-accept-labelers header', () => {
|
|
148
|
-
const headers =
|
|
148
|
+
const headers = buildXrpcRequestHeaders({
|
|
149
149
|
headers: { 'atproto-accept-labelers': 'did:plc:existing' },
|
|
150
150
|
labelers: ['did:plc:new'] as const,
|
|
151
151
|
})
|
|
@@ -155,7 +155,7 @@ describe(buildAtprotoHeaders, () => {
|
|
|
155
155
|
})
|
|
156
156
|
|
|
157
157
|
it('passes through base headers', () => {
|
|
158
|
-
const headers =
|
|
158
|
+
const headers = buildXrpcRequestHeaders({
|
|
159
159
|
headers: { Authorization: 'Bearer token123' },
|
|
160
160
|
})
|
|
161
161
|
expect(headers.get('Authorization')).toBe('Bearer token123')
|
|
@@ -163,12 +163,12 @@ describe(buildAtprotoHeaders, () => {
|
|
|
163
163
|
|
|
164
164
|
it('accepts Headers instance as base headers', () => {
|
|
165
165
|
const base = new Headers({ 'X-Custom': 'value' })
|
|
166
|
-
const headers =
|
|
166
|
+
const headers = buildXrpcRequestHeaders({ headers: base })
|
|
167
167
|
expect(headers.get('X-Custom')).toBe('value')
|
|
168
168
|
})
|
|
169
169
|
|
|
170
170
|
it('sets empty header for empty labelers iterable', () => {
|
|
171
|
-
const headers =
|
|
171
|
+
const headers = buildXrpcRequestHeaders({ labelers: [] })
|
|
172
172
|
// An empty array still sets the header (to empty string), distinguishing
|
|
173
173
|
// "no labelers requested" from "labelers option not provided"
|
|
174
174
|
expect(headers.has('atproto-accept-labelers')).toBe(true)
|
package/src/util.ts
CHANGED
|
@@ -1,32 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DidString,
|
|
3
|
-
InferMethodOutput,
|
|
4
|
-
InferMethodOutputBody,
|
|
5
|
-
Procedure,
|
|
6
|
-
Query,
|
|
7
|
-
} from '@atproto/lex-schema'
|
|
1
|
+
import type { DidString, Service } from './types.js'
|
|
8
2
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
*/
|
|
17
|
-
export type XrpcResponseBody<M extends Procedure | Query = Procedure | Query> =
|
|
18
|
-
InferMethodOutputBody<M, Uint8Array>
|
|
3
|
+
export function applyDefaults<
|
|
4
|
+
TDefaults extends Record<string, unknown>,
|
|
5
|
+
TOptions extends {
|
|
6
|
+
[K in keyof TDefaults]?: TDefaults[K]
|
|
7
|
+
},
|
|
8
|
+
>(options: TOptions, defaults: TDefaults): TOptions & TDefaults {
|
|
9
|
+
const combined: Partial<TDefaults> = { ...options }
|
|
19
10
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
11
|
+
// @NOTE We make sure that options with an explicit `undefined` value get the
|
|
12
|
+
// default, since spreading doesn't override with `undefined`.
|
|
13
|
+
for (const key of Object.keys(defaults) as (keyof typeof defaults)[]) {
|
|
14
|
+
if (options[key] === undefined) {
|
|
15
|
+
combined[key] = defaults[key]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return combined as TOptions & TDefaults
|
|
20
|
+
}
|
|
30
21
|
|
|
31
22
|
/**
|
|
32
23
|
* Type guard to check if a value is {@link Blob}-like.
|
|
@@ -64,6 +55,17 @@ export function isAsyncIterable<T>(
|
|
|
64
55
|
)
|
|
65
56
|
}
|
|
66
57
|
|
|
58
|
+
export type XrpcRequestHeadersOptions = {
|
|
59
|
+
/** Additional HTTP headers to include in the request. */
|
|
60
|
+
headers?: HeadersInit
|
|
61
|
+
|
|
62
|
+
/** Labeler DIDs to request labels from for content moderation. */
|
|
63
|
+
labelers?: Iterable<DidString>
|
|
64
|
+
|
|
65
|
+
/** Service proxy identifier for routing requests through a specific service. */
|
|
66
|
+
service?: Service
|
|
67
|
+
}
|
|
68
|
+
|
|
67
69
|
/**
|
|
68
70
|
* Builds HTTP headers for AT Protocol requests.
|
|
69
71
|
*
|
|
@@ -71,17 +73,12 @@ export function isAsyncIterable<T>(
|
|
|
71
73
|
* - `atproto-proxy`: Service routing header (if service is specified)
|
|
72
74
|
* - `atproto-accept-labelers`: Comma-separated list of labeler DIDs
|
|
73
75
|
*
|
|
74
|
-
* @
|
|
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
|
|
76
|
+
* @see {@link XrpcRequestHeadersOptions}
|
|
78
77
|
* @returns A new Headers object with AT Protocol headers added
|
|
79
78
|
*/
|
|
80
|
-
export function
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
labelers?: Iterable<DidString>
|
|
84
|
-
}): Headers {
|
|
79
|
+
export function buildXrpcRequestHeaders(
|
|
80
|
+
options: XrpcRequestHeadersOptions,
|
|
81
|
+
): Headers {
|
|
85
82
|
const headers = new Headers(options?.headers)
|
|
86
83
|
|
|
87
84
|
if (options.service && !headers.has('atproto-proxy')) {
|