@atproto/xrpc 0.8.2 → 0.8.3

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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # @atproto/xrpc
2
2
 
3
+ ## 0.8.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update TypeScript build to rely on references to composite internal projects
8
+
9
+ - [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Bundle only necessary files in the NPM tarball, including the `CHANGELOG.md` and `README.md` files (if present).
10
+
11
+ - Updated dependencies [[`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07)]:
12
+ - @atproto/lexicon@0.7.4
13
+
3
14
  ## 0.8.2
4
15
 
5
16
  ### Patch Changes
package/package.json CHANGED
@@ -1,9 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/xrpc",
3
- "version": "0.8.2",
4
- "engines": {
5
- "node": ">=22"
6
- },
3
+ "version": "0.8.3",
7
4
  "license": "MIT",
8
5
  "description": "atproto HTTP API (XRPC) client library",
9
6
  "keywords": [
@@ -16,11 +13,11 @@
16
13
  "url": "https://github.com/bluesky-social/atproto",
17
14
  "directory": "packages/xrpc"
18
15
  },
19
- "dependencies": {
20
- "zod": "^3.23.8",
21
- "@atproto/lexicon": "^0.7.3"
22
- },
23
- "devDependencies": {},
16
+ "files": [
17
+ "./dist",
18
+ "./README.md",
19
+ "./CHANGELOG.md"
20
+ ],
24
21
  "type": "module",
25
22
  "exports": {
26
23
  ".": {
@@ -28,6 +25,13 @@
28
25
  "default": "./dist/index.js"
29
26
  }
30
27
  },
28
+ "engines": {
29
+ "node": ">=22"
30
+ },
31
+ "dependencies": {
32
+ "zod": "^3.23.8",
33
+ "@atproto/lexicon": "^0.7.4"
34
+ },
31
35
  "scripts": {
32
36
  "build": "tsgo --build tsconfig.build.json"
33
37
  }
package/src/client.ts DELETED
@@ -1,73 +0,0 @@
1
- import { LexiconDoc, Lexicons } from '@atproto/lexicon'
2
- import { CallOptions, QueryParams } from './types.js'
3
- import { combineHeaders } from './util.js'
4
- import { XrpcClient } from './xrpc-client.js'
5
-
6
- /** @deprecated Use {@link XrpcClient} instead */
7
- export class Client {
8
- /** @deprecated */
9
- get fetch(): never {
10
- throw new Error(
11
- 'Client.fetch is no longer supported. Use an XrpcClient instead.',
12
- )
13
- }
14
-
15
- /** @deprecated */
16
- set fetch(_: never) {
17
- throw new Error(
18
- 'Client.fetch is no longer supported. Use an XrpcClient instead.',
19
- )
20
- }
21
-
22
- lex = new Lexicons()
23
-
24
- // method calls
25
- //
26
-
27
- async call(
28
- serviceUri: string | URL,
29
- methodNsid: string,
30
- params?: QueryParams,
31
- data?: BodyInit | null,
32
- opts?: CallOptions,
33
- ) {
34
- return this.service(serviceUri).call(methodNsid, params, data, opts)
35
- }
36
-
37
- service(serviceUri: string | URL) {
38
- return new ServiceClient(this, serviceUri)
39
- }
40
-
41
- // schemas
42
- // =
43
-
44
- addLexicon(doc: LexiconDoc) {
45
- this.lex.add(doc)
46
- }
47
-
48
- addLexicons(docs: LexiconDoc[]) {
49
- for (const doc of docs) {
50
- this.addLexicon(doc)
51
- }
52
- }
53
-
54
- removeLexicon(uri: string) {
55
- this.lex.remove(uri)
56
- }
57
- }
58
-
59
- /** @deprecated Use {@link XrpcClient} instead */
60
- export class ServiceClient extends XrpcClient {
61
- uri: URL
62
-
63
- constructor(
64
- public baseClient: Client,
65
- serviceUri: string | URL,
66
- ) {
67
- super(async (input, init) => {
68
- const headers = combineHeaders(init.headers, Object.entries(this.headers))
69
- return fetch(new URL(input, this.uri), { ...init, headers })
70
- }, baseClient.lex)
71
- this.uri = typeof serviceUri === 'string' ? new URL(serviceUri) : serviceUri
72
- }
73
- }
@@ -1,90 +0,0 @@
1
- import { Gettable } from './types.js'
2
- import { combineHeaders } from './util.js'
3
-
4
- export type FetchHandler = (
5
- this: void,
6
- /**
7
- * The URL (pathname + query parameters) to make the request to, without the
8
- * origin. The origin (protocol, hostname, and port) must be added by this
9
- * {@link FetchHandler}, typically based on authentication or other factors.
10
- */
11
- url: string,
12
- init: RequestInit,
13
- ) => Promise<Response>
14
-
15
- export type FetchHandlerOptions = BuildFetchHandlerOptions | string | URL
16
-
17
- export type BuildFetchHandlerOptions = {
18
- /**
19
- * The service URL to make requests to. This can be a string, URL, or a
20
- * function that returns a string or URL. This is useful for dynamic URLs,
21
- * such as a service URL that changes based on authentication.
22
- */
23
- service: Gettable<string | URL>
24
-
25
- /**
26
- * Headers to be added to every request. If a function is provided, it will be
27
- * called on each request to get the headers. This is useful for dynamic
28
- * headers, such as authentication tokens that may expire.
29
- */
30
- headers?: {
31
- [_ in string]?: Gettable<null | string>
32
- }
33
-
34
- /**
35
- * Bring your own fetch implementation. Typically useful for testing, logging,
36
- * mocking, or adding retries, session management, signatures, proof of
37
- * possession (DPoP), SSRF protection, etc. Defaults to the global `fetch`
38
- * function.
39
- */
40
- fetch?: typeof globalThis.fetch
41
- }
42
-
43
- export interface FetchHandlerObject {
44
- fetchHandler: (
45
- this: FetchHandlerObject,
46
- /**
47
- * The URL (pathname + query parameters) to make the request to, without the
48
- * origin. The origin (protocol, hostname, and port) must be added by this
49
- * {@link FetchHandler}, typically based on authentication or other factors.
50
- */
51
- url: string,
52
- init: RequestInit,
53
- ) => Promise<Response>
54
- }
55
-
56
- export function buildFetchHandler(
57
- options: FetchHandler | FetchHandlerObject | FetchHandlerOptions,
58
- ): FetchHandler {
59
- // Already a fetch handler (allowed for convenience)
60
- if (typeof options === 'function') return options
61
- if (typeof options === 'object' && 'fetchHandler' in options) {
62
- return options.fetchHandler.bind(options)
63
- }
64
-
65
- const {
66
- service,
67
- headers: defaultHeaders = undefined,
68
- fetch = globalThis.fetch,
69
- } = typeof options === 'string' || options instanceof URL
70
- ? { service: options }
71
- : options
72
-
73
- if (typeof fetch !== 'function') {
74
- throw new TypeError(
75
- 'XrpcDispatcher requires fetch() to be available in your environment.',
76
- )
77
- }
78
-
79
- const defaultHeadersEntries =
80
- defaultHeaders != null ? Object.entries(defaultHeaders) : undefined
81
-
82
- return async function (url, init) {
83
- const base = typeof service === 'function' ? service() : service
84
- const fullUrl = new URL(url, base)
85
-
86
- const headers = combineHeaders(init.headers, defaultHeadersEntries)
87
-
88
- return fetch(fullUrl, { ...init, headers })
89
- }
90
- }
package/src/index.ts DELETED
@@ -1,10 +0,0 @@
1
- export * from './client.js'
2
- export * from './fetch-handler.js'
3
- export * from './types.js'
4
- export * from './util.js'
5
- export * from './xrpc-client.js'
6
-
7
- import { Client } from './client.js'
8
- /** @deprecated create a local {@link XrpcClient} instance instead */
9
- const defaultInst = new Client()
10
- export default defaultInst
package/src/types.ts DELETED
@@ -1,181 +0,0 @@
1
- import { z } from 'zod'
2
- import { ValidationError } from '@atproto/lexicon'
3
-
4
- export type QueryParams = Record<string, any>
5
- export type HeadersMap = Record<string, string | undefined>
6
-
7
- export type {
8
- /** @deprecated not to be confused with the WHATWG Headers constructor */
9
- HeadersMap as Headers,
10
- }
11
-
12
- export type Gettable<T> = T | (() => T)
13
-
14
- export interface CallOptions {
15
- encoding?: string
16
- signal?: AbortSignal
17
- headers?: HeadersMap
18
- }
19
-
20
- export const errorResponseBody = z.object({
21
- error: z.string().optional(),
22
- message: z.string().optional(),
23
- })
24
- export type ErrorResponseBody = z.infer<typeof errorResponseBody>
25
-
26
- export enum ResponseType {
27
- /**
28
- * Network issue, unable to get response from the server.
29
- */
30
- Unknown = 1,
31
- /**
32
- * Response failed lexicon validation.
33
- */
34
- InvalidResponse = 2,
35
- Success = 200,
36
- InvalidRequest = 400,
37
- AuthenticationRequired = 401,
38
- Forbidden = 403,
39
- XRPCNotSupported = 404,
40
- NotAcceptable = 406,
41
- PayloadTooLarge = 413,
42
- UnsupportedMediaType = 415,
43
- RateLimitExceeded = 429,
44
- InternalServerError = 500,
45
- MethodNotImplemented = 501,
46
- UpstreamFailure = 502,
47
- NotEnoughResources = 503,
48
- UpstreamTimeout = 504,
49
- }
50
-
51
- export function httpResponseCodeToEnum(status: number): ResponseType {
52
- if (status in ResponseType) {
53
- return status
54
- } else if (status >= 100 && status < 200) {
55
- return ResponseType.XRPCNotSupported
56
- } else if (status >= 200 && status < 300) {
57
- return ResponseType.Success
58
- } else if (status >= 300 && status < 400) {
59
- return ResponseType.XRPCNotSupported
60
- } else if (status >= 400 && status < 500) {
61
- return ResponseType.InvalidRequest
62
- } else {
63
- return ResponseType.InternalServerError
64
- }
65
- }
66
-
67
- export function httpResponseCodeToName(status: number): string {
68
- return ResponseType[httpResponseCodeToEnum(status)]
69
- }
70
-
71
- export const ResponseTypeStrings = {
72
- [ResponseType.Unknown]: 'Unknown',
73
- [ResponseType.InvalidResponse]: 'Invalid Response',
74
- [ResponseType.Success]: 'Success',
75
- [ResponseType.InvalidRequest]: 'Invalid Request',
76
- [ResponseType.AuthenticationRequired]: 'Authentication Required',
77
- [ResponseType.Forbidden]: 'Forbidden',
78
- [ResponseType.XRPCNotSupported]: 'XRPC Not Supported',
79
- [ResponseType.NotAcceptable]: 'Not Acceptable',
80
- [ResponseType.PayloadTooLarge]: 'Payload Too Large',
81
- [ResponseType.UnsupportedMediaType]: 'Unsupported Media Type',
82
- [ResponseType.RateLimitExceeded]: 'Rate Limit Exceeded',
83
- [ResponseType.InternalServerError]: 'Internal Server Error',
84
- [ResponseType.MethodNotImplemented]: 'Method Not Implemented',
85
- [ResponseType.UpstreamFailure]: 'Upstream Failure',
86
- [ResponseType.NotEnoughResources]: 'Not Enough Resources',
87
- [ResponseType.UpstreamTimeout]: 'Upstream Timeout',
88
- } as const satisfies Record<ResponseType, string>
89
-
90
- export function httpResponseCodeToString(status: number): string {
91
- return ResponseTypeStrings[httpResponseCodeToEnum(status)]
92
- }
93
-
94
- export class XRPCResponse {
95
- success = true
96
-
97
- constructor(
98
- public data: any,
99
- public headers: HeadersMap,
100
- ) {}
101
- }
102
-
103
- export class XRPCError extends Error {
104
- success = false
105
-
106
- public status: ResponseType
107
-
108
- constructor(
109
- statusCode: number,
110
- public error: string = httpResponseCodeToName(statusCode),
111
- message?: string,
112
- public headers?: HeadersMap,
113
- options?: ErrorOptions,
114
- ) {
115
- super(message || error || httpResponseCodeToString(statusCode), options)
116
-
117
- this.status = httpResponseCodeToEnum(statusCode)
118
-
119
- // Pre 2022 runtimes won't handle the "options" constructor argument
120
- const cause = options?.cause
121
- if (this.cause === undefined && cause !== undefined) {
122
- this.cause = cause
123
- }
124
- }
125
-
126
- static from(cause: unknown, fallbackStatus?: ResponseType): XRPCError {
127
- if (cause instanceof XRPCError) {
128
- return cause
129
- }
130
-
131
- // Type cast the cause to an Error if it is one
132
- const causeErr = cause instanceof Error ? cause : undefined
133
-
134
- // Try and find a Response object in the cause
135
- const causeResponse: Response | undefined =
136
- cause instanceof Response
137
- ? cause
138
- : cause?.['response'] instanceof Response
139
- ? cause['response']
140
- : undefined
141
-
142
- const statusCode: unknown =
143
- // Extract status code from "http-errors" like errors
144
- causeErr?.['statusCode'] ??
145
- causeErr?.['status'] ??
146
- // Use the status code from the response object as fallback
147
- causeResponse?.status
148
-
149
- // Convert the status code to a ResponseType
150
- const status: ResponseType =
151
- typeof statusCode === 'number'
152
- ? httpResponseCodeToEnum(statusCode)
153
- : fallbackStatus ?? ResponseType.Unknown
154
-
155
- const message = causeErr?.message ?? String(cause)
156
-
157
- const headers = causeResponse
158
- ? Object.fromEntries(causeResponse.headers.entries())
159
- : undefined
160
-
161
- return new XRPCError(status, undefined, message, headers, { cause })
162
- }
163
- }
164
-
165
- export class XRPCInvalidResponseError extends XRPCError {
166
- constructor(
167
- public lexiconNsid: string,
168
- public validationError: ValidationError,
169
- public responseBody: unknown,
170
- ) {
171
- super(
172
- ResponseType.InvalidResponse,
173
- // @NOTE: This is probably wrong and should use ResponseTypeNames instead.
174
- // But it would mean a breaking change.
175
- ResponseTypeStrings[ResponseType.InvalidResponse],
176
- `The server gave an invalid response and may be out of date.`,
177
- undefined,
178
- { cause: validationError },
179
- )
180
- }
181
- }
package/src/util.ts DELETED
@@ -1,384 +0,0 @@
1
- import {
2
- LexXrpcProcedure,
3
- LexXrpcQuery,
4
- jsonStringToLex,
5
- stringifyLex,
6
- } from '@atproto/lexicon'
7
- import {
8
- CallOptions,
9
- ErrorResponseBody,
10
- Gettable,
11
- QueryParams,
12
- ResponseType,
13
- XRPCError,
14
- errorResponseBody,
15
- } from './types.js'
16
-
17
- const ReadableStream =
18
- globalThis.ReadableStream ||
19
- (class {
20
- constructor() {
21
- // This anonymous class will never pass any "instanceof" check and cannot
22
- // be instantiated.
23
- throw new Error('ReadableStream is not supported in this environment')
24
- }
25
- } as typeof globalThis.ReadableStream)
26
-
27
- export function isErrorResponseBody(v: unknown): v is ErrorResponseBody {
28
- return errorResponseBody.safeParse(v).success
29
- }
30
-
31
- export function getMethodSchemaHTTPMethod(
32
- schema: LexXrpcProcedure | LexXrpcQuery,
33
- ) {
34
- if (schema.type === 'procedure') {
35
- return 'post'
36
- }
37
- return 'get'
38
- }
39
-
40
- export function constructMethodCallUri(
41
- nsid: string,
42
- schema: LexXrpcProcedure | LexXrpcQuery,
43
- serviceUri: URL,
44
- params?: QueryParams,
45
- ): string {
46
- const uri = new URL(constructMethodCallUrl(nsid, schema, params), serviceUri)
47
- return uri.toString()
48
- }
49
-
50
- export function constructMethodCallUrl(
51
- nsid: string,
52
- schema: LexXrpcProcedure | LexXrpcQuery,
53
- params?: QueryParams,
54
- ): string {
55
- const pathname = `/xrpc/${encodeURIComponent(nsid)}`
56
- if (!params) return pathname
57
-
58
- const searchParams: [string, string][] = []
59
-
60
- for (const [key, value] of Object.entries(params)) {
61
- const paramSchema = schema.parameters?.properties?.[key]
62
- if (!paramSchema) {
63
- throw new Error(`Invalid query parameter: ${key}`)
64
- }
65
- if (value !== undefined) {
66
- if (paramSchema.type === 'array') {
67
- const values = Array.isArray(value) ? value : [value]
68
- for (const val of values) {
69
- searchParams.push([
70
- key,
71
- encodeQueryParam(paramSchema.items.type, val),
72
- ])
73
- }
74
- } else {
75
- searchParams.push([key, encodeQueryParam(paramSchema.type, value)])
76
- }
77
- }
78
- }
79
-
80
- if (!searchParams.length) return pathname
81
-
82
- return `${pathname}?${new URLSearchParams(searchParams).toString()}`
83
- }
84
-
85
- export function encodeQueryParam(
86
- type:
87
- | 'string'
88
- | 'float'
89
- | 'integer'
90
- | 'boolean'
91
- | 'datetime'
92
- | 'array'
93
- | 'unknown',
94
- value: any,
95
- ): string {
96
- if (type === 'string' || type === 'unknown') {
97
- return String(value)
98
- }
99
- if (type === 'float') {
100
- return String(Number(value))
101
- } else if (type === 'integer') {
102
- return String(Number(value) | 0)
103
- } else if (type === 'boolean') {
104
- return value ? 'true' : 'false'
105
- } else if (type === 'datetime') {
106
- if (value instanceof Date) {
107
- return value.toISOString()
108
- }
109
- return String(value)
110
- }
111
- throw new Error(`Unsupported query param type: ${type}`)
112
- }
113
-
114
- export function constructMethodCallHeaders(
115
- schema: LexXrpcProcedure | LexXrpcQuery,
116
- data?: unknown,
117
- opts?: CallOptions,
118
- ): Headers {
119
- // Not using `new Headers(opts?.headers)` to avoid duplicating headers values
120
- // due to inconsistent casing in headers name. In case of multiple headers
121
- // with the same name (but using a different case), the last one will be used.
122
-
123
- // new Headers({ 'content-type': 'foo', 'Content-Type': 'bar' }).get('content-type')
124
- // => 'foo, bar'
125
- const headers = new Headers()
126
-
127
- if (opts?.headers) {
128
- for (const name in opts.headers) {
129
- if (headers.has(name)) {
130
- throw new TypeError(`Duplicate header: ${name}`)
131
- }
132
-
133
- const value = opts.headers[name]
134
- if (value != null) {
135
- headers.set(name, value)
136
- }
137
- }
138
- }
139
-
140
- if (schema.type === 'procedure') {
141
- if (opts?.encoding) {
142
- headers.set('content-type', opts.encoding)
143
- } else if (!headers.has('content-type') && typeof data !== 'undefined') {
144
- // Special handling of BodyInit types before falling back to JSON encoding
145
- if (
146
- data instanceof ArrayBuffer ||
147
- data instanceof ReadableStream ||
148
- ArrayBuffer.isView(data)
149
- ) {
150
- headers.set('content-type', 'application/octet-stream')
151
- } else if (data instanceof FormData) {
152
- // Note: The multipart form data boundary is missing from the header
153
- // we set here, making that header invalid. This special case will be
154
- // handled in encodeMethodCallBody()
155
- headers.set('content-type', 'multipart/form-data')
156
- } else if (data instanceof URLSearchParams) {
157
- headers.set(
158
- 'content-type',
159
- 'application/x-www-form-urlencoded;charset=UTF-8',
160
- )
161
- } else if (isBlobLike(data)) {
162
- headers.set('content-type', data.type || 'application/octet-stream')
163
- } else if (typeof data === 'string') {
164
- headers.set('content-type', 'text/plain;charset=UTF-8')
165
- }
166
- // At this point, data is not a valid BodyInit type.
167
- else if (isIterable(data)) {
168
- headers.set('content-type', 'application/octet-stream')
169
- } else if (
170
- typeof data === 'boolean' ||
171
- typeof data === 'number' ||
172
- typeof data === 'string' ||
173
- typeof data === 'object' // covers "null"
174
- ) {
175
- headers.set('content-type', 'application/json')
176
- } else {
177
- // symbol, function, bigint
178
- throw new XRPCError(
179
- ResponseType.InvalidRequest,
180
- `Unsupported data type: ${typeof data}`,
181
- )
182
- }
183
- }
184
- }
185
- return headers
186
- }
187
-
188
- export function combineHeaders(
189
- headersInit: undefined | HeadersInit,
190
- defaultHeaders?: Iterable<[string, undefined | Gettable<null | string>]>,
191
- ): undefined | HeadersInit {
192
- if (!defaultHeaders) return headersInit
193
-
194
- let headers: Headers | undefined = undefined
195
-
196
- for (const [name, definition] of defaultHeaders) {
197
- // Ignore undefined values (allowed for convenience when using
198
- // Object.entries).
199
- if (definition === undefined) continue
200
-
201
- // Lazy initialization of the headers object
202
- headers ??= new Headers(headersInit)
203
-
204
- if (headers.has(name)) continue
205
-
206
- const value = typeof definition === 'function' ? definition() : definition
207
-
208
- if (typeof value === 'string') headers.set(name, value)
209
- else if (value === null) headers.delete(name)
210
- else throw new TypeError(`Invalid "${name}" header value: ${typeof value}`)
211
- }
212
-
213
- return headers ?? headersInit
214
- }
215
-
216
- function isBlobLike(value: unknown): value is Blob {
217
- if (value == null) return false
218
- if (typeof value !== 'object') return false
219
- if (typeof Blob === 'function' && value instanceof Blob) return true
220
-
221
- // Support for Blobs provided by libraries that don't use the native Blob
222
- // (e.g. fetch-blob from node-fetch).
223
- // https://github.com/node-fetch/fetch-blob/blob/a1a182e5978811407bef4ea1632b517567dda01f/index.js#L233-L244
224
-
225
- const tag = value[Symbol.toStringTag]
226
- if (tag === 'Blob' || tag === 'File') {
227
- return 'stream' in value && typeof value.stream === 'function'
228
- }
229
-
230
- return false
231
- }
232
-
233
- export function isBodyInit(value: unknown): value is BodyInit {
234
- switch (typeof value) {
235
- case 'string':
236
- return true
237
- case 'object':
238
- return (
239
- value instanceof ArrayBuffer ||
240
- value instanceof FormData ||
241
- value instanceof URLSearchParams ||
242
- value instanceof ReadableStream ||
243
- ArrayBuffer.isView(value) ||
244
- isBlobLike(value)
245
- )
246
- default:
247
- return false
248
- }
249
- }
250
-
251
- export function isIterable(
252
- value: unknown,
253
- ): value is Iterable<unknown> | AsyncIterable<unknown> {
254
- return (
255
- value != null &&
256
- typeof value === 'object' &&
257
- (Symbol.iterator in value || Symbol.asyncIterator in value)
258
- )
259
- }
260
-
261
- export function encodeMethodCallBody(
262
- headers: Headers,
263
- data?: unknown,
264
- ): BodyInit | undefined {
265
- // Silently ignore the body if there is no content-type header.
266
- const contentType = headers.get('content-type')
267
- if (!contentType) {
268
- return undefined
269
- }
270
-
271
- if (typeof data === 'undefined') {
272
- // This error would be returned by the server, but we can catch it earlier
273
- // to avoid un-necessary requests. Note that a content-length of 0 does not
274
- // necessary mean that the body is "empty" (e.g. an empty txt file).
275
- throw new XRPCError(
276
- ResponseType.InvalidRequest,
277
- `A request body is expected but none was provided`,
278
- )
279
- }
280
-
281
- if (isBodyInit(data)) {
282
- if (data instanceof FormData && contentType === 'multipart/form-data') {
283
- // fetch() will encode FormData payload itself, but it won't override the
284
- // content-type header if already present. This would cause the boundary
285
- // to be missing from the content-type header, resulting in a 400 error.
286
- // Deleting the content-type header here to let fetch() re-create it.
287
- headers.delete('content-type')
288
- }
289
-
290
- // Will be encoded by the fetch API.
291
- return data
292
- }
293
-
294
- if (isIterable(data)) {
295
- // Note that some environments support using Iterable & AsyncIterable as the
296
- // body (e.g. Node's fetch), but not all of them do (browsers).
297
- return iterableToReadableStream(data)
298
- }
299
-
300
- if (contentType.startsWith('text/')) {
301
- return new TextEncoder().encode(String(data))
302
- }
303
- if (contentType.startsWith('application/json')) {
304
- const json = stringifyLex(data)
305
- // Server would return a 400 error if the JSON is invalid (e.g. trying to
306
- // JSONify a function, or an object that implements toJSON() poorly).
307
- if (json === undefined) {
308
- throw new XRPCError(
309
- ResponseType.InvalidRequest,
310
- `Failed to encode request body as JSON`,
311
- )
312
- }
313
- return new TextEncoder().encode(json)
314
- }
315
-
316
- // At this point, "data" is not a valid BodyInit value, and we don't know how
317
- // to encode it into one. Passing it to fetch would result in an error. Let's
318
- // throw our own error instead.
319
-
320
- const type =
321
- !data || typeof data !== 'object'
322
- ? typeof data
323
- : data.constructor !== Object &&
324
- typeof data.constructor === 'function' &&
325
- typeof data.constructor?.name === 'string'
326
- ? data.constructor.name
327
- : 'object'
328
-
329
- throw new XRPCError(
330
- ResponseType.InvalidRequest,
331
- `Unable to encode ${type} as ${contentType} data`,
332
- )
333
- }
334
-
335
- /**
336
- * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/from_static}
337
- */
338
- function iterableToReadableStream(
339
- iterable: Iterable<unknown> | AsyncIterable<unknown>,
340
- ): ReadableStream<Uint8Array> {
341
- // Use the native ReadableStream.from() if available.
342
- if ('from' in ReadableStream && typeof ReadableStream.from === 'function') {
343
- return ReadableStream.from(iterable)
344
- }
345
-
346
- // If you see this error, consider using a polyfill for ReadableStream. For
347
- // example, the "web-streams-polyfill" package:
348
- // https://github.com/MattiasBuelens/web-streams-polyfill
349
-
350
- throw new TypeError(
351
- 'ReadableStream.from() is not supported in this environment. ' +
352
- 'It is required to support using iterables as the request body. ' +
353
- 'Consider using a polyfill or re-write your code to use a different body type.',
354
- )
355
- }
356
-
357
- export function httpResponseBodyParse(
358
- mimeType: string | null,
359
- data: ArrayBuffer | undefined,
360
- ): any {
361
- try {
362
- if (mimeType) {
363
- if (mimeType.includes('application/json')) {
364
- const str = new TextDecoder().decode(data)
365
- return jsonStringToLex(str)
366
- }
367
- if (mimeType.startsWith('text/')) {
368
- return new TextDecoder().decode(data)
369
- }
370
- }
371
- if (data instanceof ArrayBuffer) {
372
- return new Uint8Array(data)
373
- }
374
- return data
375
- } catch (cause) {
376
- throw new XRPCError(
377
- ResponseType.InvalidResponse,
378
- undefined,
379
- `Failed to parse response body: ${String(cause)}`,
380
- undefined,
381
- { cause },
382
- )
383
- }
384
- }
@@ -1,124 +0,0 @@
1
- import { LexiconDoc, Lexicons, ValidationError } from '@atproto/lexicon'
2
- import {
3
- FetchHandler,
4
- FetchHandlerObject,
5
- FetchHandlerOptions,
6
- buildFetchHandler,
7
- } from './fetch-handler.js'
8
- import {
9
- CallOptions,
10
- Gettable,
11
- QueryParams,
12
- ResponseType,
13
- XRPCError,
14
- XRPCInvalidResponseError,
15
- XRPCResponse,
16
- httpResponseCodeToEnum,
17
- } from './types.js'
18
- import {
19
- combineHeaders,
20
- constructMethodCallHeaders,
21
- constructMethodCallUrl,
22
- encodeMethodCallBody,
23
- getMethodSchemaHTTPMethod,
24
- httpResponseBodyParse,
25
- isErrorResponseBody,
26
- } from './util.js'
27
-
28
- export class XrpcClient {
29
- readonly fetchHandler: FetchHandler
30
- readonly headers = new Map<string, Gettable<null | string>>()
31
- readonly lex: Lexicons
32
-
33
- constructor(
34
- fetchHandlerOpts: FetchHandler | FetchHandlerObject | FetchHandlerOptions,
35
- // "Lexicons" is redundant here (because that class implements
36
- // "Iterable<LexiconDoc>") but we keep it for explicitness:
37
- lex: Lexicons | Iterable<LexiconDoc>,
38
- ) {
39
- this.fetchHandler = buildFetchHandler(fetchHandlerOpts)
40
-
41
- this.lex = lex instanceof Lexicons ? lex : new Lexicons(lex)
42
- }
43
-
44
- setHeader(key: string, value: Gettable<null | string>): void {
45
- this.headers.set(key.toLowerCase(), value)
46
- }
47
-
48
- unsetHeader(key: string): void {
49
- this.headers.delete(key.toLowerCase())
50
- }
51
-
52
- clearHeaders(): void {
53
- this.headers.clear()
54
- }
55
-
56
- async call(
57
- methodNsid: string,
58
- params?: QueryParams,
59
- data?: unknown,
60
- opts?: CallOptions,
61
- ): Promise<XRPCResponse> {
62
- const def = this.lex.getDefOrThrow(methodNsid)
63
- if (!def || (def.type !== 'query' && def.type !== 'procedure')) {
64
- throw new TypeError(
65
- `Invalid lexicon: ${methodNsid}. Must be a query or procedure.`,
66
- )
67
- }
68
-
69
- // @TODO: should we validate the params and data here?
70
- // this.lex.assertValidXrpcParams(methodNsid, params)
71
- // if (data !== undefined) {
72
- // this.lex.assertValidXrpcInput(methodNsid, data)
73
- // }
74
-
75
- const reqUrl = constructMethodCallUrl(methodNsid, def, params)
76
- const reqMethod = getMethodSchemaHTTPMethod(def)
77
- const reqHeaders = constructMethodCallHeaders(def, data, opts)
78
- const reqBody = encodeMethodCallBody(reqHeaders, data)
79
-
80
- // The duplex field is required for streaming bodies, but not yet reflected
81
- // anywhere in docs or types. See whatwg/fetch#1438, nodejs/node#46221.
82
- const init: RequestInit & { duplex: 'half' } = {
83
- method: reqMethod,
84
- headers: combineHeaders(reqHeaders, this.headers),
85
- body: reqBody,
86
- duplex: 'half',
87
- redirect: 'follow',
88
- signal: opts?.signal,
89
- }
90
-
91
- try {
92
- const response = await this.fetchHandler.call(undefined, reqUrl, init)
93
-
94
- const resStatus = response.status
95
- const resHeaders = Object.fromEntries(response.headers.entries())
96
- const resBodyBytes = await response.arrayBuffer()
97
- const resBody = httpResponseBodyParse(
98
- response.headers.get('content-type'),
99
- resBodyBytes,
100
- )
101
-
102
- const resCode = httpResponseCodeToEnum(resStatus)
103
- if (resCode !== ResponseType.Success) {
104
- const { error = undefined, message = undefined } =
105
- resBody && isErrorResponseBody(resBody) ? resBody : {}
106
- throw new XRPCError(resCode, error, message, resHeaders)
107
- }
108
-
109
- try {
110
- this.lex.assertValidXrpcOutput(methodNsid, resBody)
111
- } catch (e: unknown) {
112
- if (e instanceof ValidationError) {
113
- throw new XRPCInvalidResponseError(methodNsid, e, resBody)
114
- }
115
-
116
- throw e
117
- }
118
-
119
- return new XRPCResponse(resBody, resHeaders)
120
- } catch (err) {
121
- throw XRPCError.from(err)
122
- }
123
- }
124
- }
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../tsconfig/isomorphic.json",
3
- "compilerOptions": {
4
- "rootDir": "./src",
5
- "outDir": "./dist",
6
- },
7
- "include": ["./src"],
8
- }
@@ -1 +0,0 @@
1
- {"version":"7.0.0-dev.20260614.1","root":["./src/client.ts","./src/fetch-handler.ts","./src/index.ts","./src/types.ts","./src/util.ts","./src/xrpc-client.ts"]}
package/tsconfig.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "include": [],
3
- "references": [{ "path": "./tsconfig.build.json" }],
4
- }