@atproto/lex-client 0.0.16 → 0.0.17
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 +19 -0
- package/dist/client.d.ts +24 -21
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -7
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +6 -5
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/response.d.ts +61 -6
- package/dist/response.d.ts.map +1 -1
- package/dist/response.js +59 -40
- 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 +3 -1
- package/dist/xrpc.js.map +1 -1
- package/package.json +6 -6
- package/src/client.ts +81 -31
- package/src/errors.ts +6 -4
- package/src/response.ts +186 -63
- package/src/types.ts +17 -40
- package/src/util.test.ts +11 -11
- package/src/util.ts +33 -36
- package/src/xrpc.test.ts +641 -92
- package/src/xrpc.ts +72 -26
package/src/response.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
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,
|
|
@@ -12,12 +16,104 @@ import {
|
|
|
12
16
|
XrpcUpstreamError,
|
|
13
17
|
isXrpcErrorPayload,
|
|
14
18
|
} from './errors.js'
|
|
15
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
EncodingString,
|
|
21
|
+
XrpcUnknownResponsePayload,
|
|
22
|
+
isEncodingString,
|
|
23
|
+
} from './types.js'
|
|
16
24
|
|
|
17
25
|
const CONTENT_TYPE_BINARY = 'application/octet-stream'
|
|
18
26
|
const CONTENT_TYPE_JSON = 'application/json'
|
|
19
27
|
|
|
20
|
-
|
|
28
|
+
// @NOTE the output schema is used in "parse" mode (safeParse), which means that
|
|
29
|
+
// defaults will be applied and coercions will be performed, so we need to use
|
|
30
|
+
// InferOutput here to get the final parsed type, not Infer/InferInput. For this
|
|
31
|
+
// reason, we cannot use InferMethodOutputBody and InferMethodOutput from
|
|
32
|
+
// lex-schema here.
|
|
33
|
+
|
|
34
|
+
type InferEncodingType<TEncoding extends string> = TEncoding extends '*/*'
|
|
35
|
+
? EncodingString
|
|
36
|
+
: TEncoding extends `${infer T extends string}/*`
|
|
37
|
+
? `${T}/${string}`
|
|
38
|
+
: TEncoding
|
|
39
|
+
|
|
40
|
+
type InferBodyType<
|
|
41
|
+
TEncoding extends string,
|
|
42
|
+
TSchema,
|
|
43
|
+
> = TSchema extends Validator
|
|
44
|
+
? InferOutput<TSchema>
|
|
45
|
+
: TEncoding extends `application/json`
|
|
46
|
+
? LexValue
|
|
47
|
+
: Uint8Array
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The body type of an XRPC response, inferred from the method's output schema.
|
|
51
|
+
*
|
|
52
|
+
* For JSON responses, this is the parsed LexValue. For binary responses,
|
|
53
|
+
* this is a Uint8Array.
|
|
54
|
+
*
|
|
55
|
+
* @typeParam M - The XRPC method type (Procedure or Query)
|
|
56
|
+
*/
|
|
57
|
+
export type XrpcResponseBody<M extends Procedure | Query> =
|
|
58
|
+
M['output'] extends Payload<infer TEncoding, infer TSchema>
|
|
59
|
+
? TEncoding extends string
|
|
60
|
+
? InferBodyType<TEncoding, TSchema>
|
|
61
|
+
: undefined
|
|
62
|
+
: never
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The full payload type of an XRPC response, including body and encoding.
|
|
66
|
+
*
|
|
67
|
+
* Returns `null` for methods that have no output.
|
|
68
|
+
*
|
|
69
|
+
* @typeParam M - The XRPC method type (Procedure or Query)
|
|
70
|
+
*/
|
|
71
|
+
export type XrpcResponsePayload<M extends Procedure | Query> =
|
|
72
|
+
M['output'] extends Payload<infer TEncoding, infer TSchema>
|
|
73
|
+
? TEncoding extends string
|
|
74
|
+
? {
|
|
75
|
+
encoding: InferEncodingType<TEncoding>
|
|
76
|
+
body: InferBodyType<TEncoding, TSchema>
|
|
77
|
+
}
|
|
78
|
+
: undefined
|
|
79
|
+
: never
|
|
80
|
+
|
|
81
|
+
export type XrpcResponseOptions = {
|
|
82
|
+
/**
|
|
83
|
+
* Whether to validate the response against the method's output schema.
|
|
84
|
+
* Disabling this can improve performance but may lead to runtime errors if
|
|
85
|
+
* the response does not conform to the expected schema. Only set this to
|
|
86
|
+
* `false` if you are certain that the upstream service will always return
|
|
87
|
+
* valid responses.
|
|
88
|
+
*
|
|
89
|
+
* @default true
|
|
90
|
+
*/
|
|
91
|
+
validateResponse?: boolean
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Whether to strictly process response payloads according to Lex encoding
|
|
95
|
+
* rules. By default, the client will reject responses with invalid Lex data
|
|
96
|
+
* (floats and invalid $bytes / $link objects).
|
|
97
|
+
*
|
|
98
|
+
* Setting this option to `false` will allow the client to accept such
|
|
99
|
+
* responses in a non-strict mode, where invalid Lex data will be returned
|
|
100
|
+
* as-is (e.g., floats will not be rejected, and invalid $bytes / $link
|
|
101
|
+
* objects will not be converted to Uint8Array / Cid). When in non-strict
|
|
102
|
+
* mode, the validation will also be relaxed when validating the response
|
|
103
|
+
* against the method's output schema, allowing values that do not strictly
|
|
104
|
+
* conform to the schema (e.g. datetime strings that are not valid RFC3339
|
|
105
|
+
* format, blobs that are not of the right size/mime-type, etc.) to be
|
|
106
|
+
* accepted as long as their basic structure is correct.
|
|
107
|
+
*
|
|
108
|
+
* When validation is enabled (the default), the values defined through the
|
|
109
|
+
* method schema will be enforced, ensuring that the client can still process
|
|
110
|
+
* the response even if the server returns invalid Lex data.
|
|
111
|
+
*
|
|
112
|
+
* @default true
|
|
113
|
+
* @see {@link LexParseOptions.strict}
|
|
114
|
+
*/
|
|
115
|
+
strictResponseProcessing?: boolean
|
|
116
|
+
}
|
|
21
117
|
|
|
22
118
|
/**
|
|
23
119
|
* Small container for XRPC response data.
|
|
@@ -80,7 +176,7 @@ export class XrpcResponse<M extends Procedure | Query>
|
|
|
80
176
|
static async fromFetchResponse<const M extends Procedure | Query>(
|
|
81
177
|
method: M,
|
|
82
178
|
response: Response,
|
|
83
|
-
options?:
|
|
179
|
+
options?: XrpcResponseOptions,
|
|
84
180
|
): Promise<XrpcResponse<M>> {
|
|
85
181
|
// @NOTE The body MUST either be read or canceled to avoid resource leaks.
|
|
86
182
|
// Since nothing should cause an exception before "readPayload" is
|
|
@@ -89,17 +185,9 @@ export class XrpcResponse<M extends Procedure | Query>
|
|
|
89
185
|
// @NOTE redirect is set to 'follow', so we shouldn't get 3xx responses here
|
|
90
186
|
if (response.status < 200 || response.status >= 300) {
|
|
91
187
|
// Always parse json for error responses
|
|
92
|
-
const payload = await readPayload(response, {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
method,
|
|
96
|
-
response,
|
|
97
|
-
null,
|
|
98
|
-
'Unable to parse response payload',
|
|
99
|
-
{ cause },
|
|
100
|
-
)
|
|
101
|
-
},
|
|
102
|
-
)
|
|
188
|
+
const payload = await readPayload(method, response, {
|
|
189
|
+
parse: { strict: options?.strictResponseProcessing ?? true },
|
|
190
|
+
})
|
|
103
191
|
|
|
104
192
|
// Properly formatted XRPC error response ?
|
|
105
193
|
if (response.status >= 400 && isXrpcErrorPayload(payload)) {
|
|
@@ -121,17 +209,11 @@ export class XrpcResponse<M extends Procedure | Query>
|
|
|
121
209
|
)
|
|
122
210
|
}
|
|
123
211
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
parse: method.output.encoding === CONTENT_TYPE_JSON
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
method,
|
|
130
|
-
response,
|
|
131
|
-
null,
|
|
132
|
-
'Unable to parse response payload',
|
|
133
|
-
{ cause },
|
|
134
|
-
)
|
|
212
|
+
const payload = await readPayload(method, response, {
|
|
213
|
+
// Only parse json if the schema expects it
|
|
214
|
+
parse: method.output.encoding === CONTENT_TYPE_JSON && {
|
|
215
|
+
strict: options?.strictResponseProcessing ?? true,
|
|
216
|
+
},
|
|
135
217
|
})
|
|
136
218
|
|
|
137
219
|
// Response is successful (2xx). Validate payload (data and encoding) against schema.
|
|
@@ -160,7 +242,9 @@ export class XrpcResponse<M extends Procedure | Query>
|
|
|
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
250
|
throw new XrpcInvalidResponseError(
|
|
@@ -170,6 +254,18 @@ export class XrpcResponse<M extends Procedure | Query>
|
|
|
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,77 @@ 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
|
-
// bytes->LexValue in one step instead of bytes->text->JSON->LexValue.
|
|
218
|
-
// This would require adding encode/decode utilities to lex-json (similar
|
|
219
|
-
// to @ipld/dag-json)
|
|
220
|
-
const text = await response.text()
|
|
321
|
+
if (!isEncodingString(encoding)) {
|
|
322
|
+
throw new TypeError(`Invalid content-type "${encoding}" in response`)
|
|
323
|
+
}
|
|
221
324
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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()
|
|
225
332
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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 (?)
|
|
336
|
+
|
|
337
|
+
// @TODO verify statement above
|
|
338
|
+
return { encoding, body: lexParse(text, options.parse) }
|
|
339
|
+
}
|
|
229
340
|
|
|
230
|
-
|
|
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 XrpcUpstreamError(
|
|
347
|
+
method,
|
|
348
|
+
response,
|
|
349
|
+
null,
|
|
350
|
+
messageDetail ? `${message}: ${messageDetail}` : message,
|
|
351
|
+
{ cause },
|
|
352
|
+
)
|
|
353
|
+
}
|
|
231
354
|
}
|
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')) {
|