@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/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
- XrpcUpstreamError,
13
- isXrpcErrorPayload,
16
+ XrpcResponseValidationError,
14
17
  } from './errors.js'
15
- import { XrpcResponseBody, XrpcResponsePayload } from './util.js'
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
- export type { XrpcResponseBody, XrpcResponsePayload }
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 {XrpcUpstreamError} when the response is not a valid XRPC
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?: { validateResponse?: boolean },
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
- // @NOTE redirect is set to 'follow', so we shouldn't get 3xx responses here
90
- if (response.status < 200 || response.status >= 300) {
91
- // Always parse json for error responses
92
- const payload = await readPayload(response, { parse: true }).catch(
93
- (cause) => {
94
- throw new XrpcUpstreamError(
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
- // Properly formatted XRPC error response ?
105
- if (response.status >= 400 && isXrpcErrorPayload(payload)) {
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
- // Invalid XRPC response (we probably did not hit an XRPC implementation)
112
- throw new XrpcUpstreamError(
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
- payload,
116
- response.status >= 500
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
- // Only parse json if the schema expects it
125
- const payload = await readPayload(response, {
126
- parse: method.output.encoding === CONTENT_TYPE_JSON,
127
- }).catch((cause) => {
128
- throw new XrpcUpstreamError(
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
- null,
132
- 'Unable to parse response payload',
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 == null) {
139
- // Schema expects no payload
140
- if (payload) {
141
- throw new XrpcUpstreamError(
142
- method,
143
- response,
144
- payload,
145
- `Expected response with no body, got ${payload.encoding}`,
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 XrpcInvalidResponseError(
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?: { parse?: boolean },
191
- ): Promise<XrpcResponsePayload> {
192
- // @TODO Should we limit the maximum response size here (this could also be
193
- // done by the FetchHandler)?
194
-
195
- const encoding = response.headers
196
- .get('content-type')
197
- ?.split(';')[0]
198
- .trim()
199
- .toLowerCase()
200
-
201
- // Response content-type is undefined
202
- if (!encoding) {
203
- // If the body is empty, return undefined (= no payload)
204
- const body = await response.arrayBuffer()
205
- if (body.byteLength === 0) return undefined
206
-
207
- // If we got data despite no content-type, treat it as binary
208
- return {
209
- encoding: CONTENT_TYPE_BINARY,
210
- body: new Uint8Array(body),
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
- if (options?.parse && encoding === CONTENT_TYPE_JSON) {
215
- // @NOTE It might be worth returning the raw bytes here (Uint8Array) and
216
- // perform the lex parsing using cborg/json, allowing to do
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
+ }
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
- // @NOTE Using `lexParse(text)` (instead of `jsonToLex(json)`) here as
223
- // using a reviver function during JSON.parse should be faster than
224
- // parsing to JSON then converting to Lex (?)
337
+ // @TODO verify statement above
338
+ return { encoding, body: lexParse(text, options.parse) }
339
+ }
225
340
 
226
- // @TODO verify statement above
227
- return { encoding, body: lexParse(text) }
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
- return { encoding, body: new Uint8Array(await response.arrayBuffer()) }
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
- buildAtprotoHeaders,
3
+ buildXrpcRequestHeaders,
4
4
  isAsyncIterable,
5
5
  isBlobLike,
6
6
  toReadableStream,
@@ -111,24 +111,24 @@ describe(isAsyncIterable, () => {
111
111
  })
112
112
 
113
113
  // ============================================================================
114
- // buildAtprotoHeaders
114
+ // buildXrpcRequestHeaders
115
115
  // ============================================================================
116
116
 
117
- describe(buildAtprotoHeaders, () => {
117
+ describe(buildXrpcRequestHeaders, () => {
118
118
  it('returns empty headers when no options are set', () => {
119
- const headers = buildAtprotoHeaders({})
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 = buildAtprotoHeaders({
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 = buildAtprotoHeaders({
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 = buildAtprotoHeaders({
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 = buildAtprotoHeaders({
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 = buildAtprotoHeaders({
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 = buildAtprotoHeaders({ headers: base })
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 = buildAtprotoHeaders({ labelers: [] })
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
- * 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>
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
- * 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>
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
- * @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
76
+ * @see {@link XrpcRequestHeadersOptions}
78
77
  * @returns A new Headers object with AT Protocol headers added
79
78
  */
80
- export function buildAtprotoHeaders(options: {
81
- headers?: HeadersInit
82
- service?: `${DidString}#${string}`
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')) {