@atproto/xrpc 0.4.3 → 0.5.1-rc.0

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.
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.XrpcClient = void 0;
4
+ const lexicon_1 = require("@atproto/lexicon");
5
+ const fetch_handler_1 = require("./fetch-handler");
6
+ const types_1 = require("./types");
7
+ const util_1 = require("./util");
8
+ class XrpcClient {
9
+ constructor(fetchHandlerOpts,
10
+ // "Lexicons" is redundant here (because that class implements
11
+ // "Iterable<LexiconDoc>") but we keep it for explicitness:
12
+ lex) {
13
+ Object.defineProperty(this, "fetchHandler", {
14
+ enumerable: true,
15
+ configurable: true,
16
+ writable: true,
17
+ value: void 0
18
+ });
19
+ Object.defineProperty(this, "lex", {
20
+ enumerable: true,
21
+ configurable: true,
22
+ writable: true,
23
+ value: void 0
24
+ });
25
+ this.fetchHandler = (0, fetch_handler_1.buildFetchHandler)(fetchHandlerOpts);
26
+ this.lex = lex instanceof lexicon_1.Lexicons ? lex : new lexicon_1.Lexicons(lex);
27
+ }
28
+ async call(methodNsid, params, data, opts) {
29
+ const def = this.lex.getDefOrThrow(methodNsid);
30
+ if (!def || (def.type !== 'query' && def.type !== 'procedure')) {
31
+ throw new TypeError(`Invalid lexicon: ${methodNsid}. Must be a query or procedure.`);
32
+ }
33
+ // @TODO: should we validate the params and data here?
34
+ // this.lex.assertValidXrpcParams(methodNsid, params)
35
+ // if (data !== undefined) {
36
+ // this.lex.assertValidXrpcInput(methodNsid, data)
37
+ // }
38
+ const reqUrl = (0, util_1.constructMethodCallUrl)(methodNsid, def, params);
39
+ const reqMethod = (0, util_1.getMethodSchemaHTTPMethod)(def);
40
+ const reqHeaders = (0, util_1.constructMethodCallHeaders)(def, data, opts);
41
+ const reqBody = (0, util_1.encodeMethodCallBody)(reqHeaders, data);
42
+ // The duplex field is required for streaming bodies, but not yet reflected
43
+ // anywhere in docs or types. See whatwg/fetch#1438, nodejs/node#46221.
44
+ const init = {
45
+ method: reqMethod,
46
+ headers: reqHeaders,
47
+ body: reqBody,
48
+ duplex: 'half',
49
+ signal: opts?.signal,
50
+ };
51
+ try {
52
+ const response = await this.fetchHandler.call(undefined, reqUrl, init);
53
+ const resStatus = response.status;
54
+ const resHeaders = Object.fromEntries(response.headers.entries());
55
+ const resBodyBytes = await response.arrayBuffer();
56
+ const resBody = (0, util_1.httpResponseBodyParse)(response.headers.get('content-type'), resBodyBytes);
57
+ const resCode = (0, types_1.httpResponseCodeToEnum)(resStatus);
58
+ if (resCode !== types_1.ResponseType.Success) {
59
+ const { error = undefined, message = undefined } = resBody && (0, util_1.isErrorResponseBody)(resBody) ? resBody : {};
60
+ throw new types_1.XRPCError(resCode, error, message, resHeaders);
61
+ }
62
+ try {
63
+ this.lex.assertValidXrpcOutput(methodNsid, resBody);
64
+ }
65
+ catch (e) {
66
+ if (e instanceof lexicon_1.ValidationError) {
67
+ throw new types_1.XRPCInvalidResponseError(methodNsid, e, resBody);
68
+ }
69
+ throw e;
70
+ }
71
+ return new types_1.XRPCResponse(resBody, resHeaders);
72
+ }
73
+ catch (err) {
74
+ throw types_1.XRPCError.from(err);
75
+ }
76
+ }
77
+ }
78
+ exports.XrpcClient = XrpcClient;
79
+ //# sourceMappingURL=xrpc-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xrpc-client.js","sourceRoot":"","sources":["../src/xrpc-client.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/xrpc",
3
- "version": "0.4.3",
3
+ "version": "0.5.1-rc.0",
4
4
  "license": "MIT",
5
5
  "description": "atproto HTTP API (XRPC) client library",
6
6
  "keywords": [
@@ -14,14 +14,15 @@
14
14
  "directory": "packages/xrpc"
15
15
  },
16
16
  "main": "dist/index.js",
17
+ "types": "dist/index.d.ts",
17
18
  "dependencies": {
18
- "zod": "^3.21.4",
19
- "@atproto/lexicon": "^0.3.3"
19
+ "zod": "^3.23.8",
20
+ "@atproto/lexicon": "^0.4.0"
20
21
  },
21
- "scripts": {
22
- "build": "node ./build.js",
23
- "postbuild": "tsc --build tsconfig.build.json",
24
- "update-main-to-dist": "node ../../update-main-to-dist.js packages/xrpc"
22
+ "devDependencies": {
23
+ "typescript": "^5.3.3"
25
24
  },
26
- "types": "dist/index.d.ts"
25
+ "scripts": {
26
+ "build": "tsc --build tsconfig.build.json"
27
+ }
27
28
  }
package/src/client.ts CHANGED
@@ -1,29 +1,24 @@
1
- import { LexiconDoc, Lexicons, ValidationError } from '@atproto/lexicon'
2
- import {
3
- getMethodSchemaHTTPMethod,
4
- constructMethodCallUri,
5
- constructMethodCallHeaders,
6
- encodeMethodCallBody,
7
- httpResponseCodeToEnum,
8
- httpResponseBodyParse,
9
- normalizeHeaders,
10
- } from './util'
11
- import {
12
- FetchHandler,
13
- FetchHandlerResponse,
14
- Headers,
15
- CallOptions,
16
- QueryParams,
17
- ResponseType,
18
- errorResponseBody,
19
- ErrorResponseBody,
20
- XRPCResponse,
21
- XRPCError,
22
- XRPCInvalidResponseError,
23
- } from './types'
1
+ import { LexiconDoc, Lexicons } from '@atproto/lexicon'
2
+ import { CallOptions, QueryParams } from './types'
3
+ import { XrpcClient } from './xrpc-client'
4
+ import { combineHeaders } from './util'
24
5
 
6
+ /** @deprecated Use {@link XrpcClient} instead */
25
7
  export class Client {
26
- fetch: FetchHandler = defaultFetchHandler
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
+
27
22
  lex = new Lexicons()
28
23
 
29
24
  // method calls
@@ -33,7 +28,7 @@ export class Client {
33
28
  serviceUri: string | URL,
34
29
  methodNsid: string,
35
30
  params?: QueryParams,
36
- data?: unknown,
31
+ data?: BodyInit | null,
37
32
  opts?: CallOptions,
38
33
  ) {
39
34
  return this.service(serviceUri).call(methodNsid, params, data, opts)
@@ -61,13 +56,19 @@ export class Client {
61
56
  }
62
57
  }
63
58
 
64
- export class ServiceClient {
65
- baseClient: Client
59
+ /** @deprecated Use {@link XrpcClient} instead */
60
+ export class ServiceClient extends XrpcClient {
66
61
  uri: URL
67
62
  headers: Record<string, string> = {}
68
63
 
69
- constructor(baseClient: Client, serviceUri: string | URL) {
70
- this.baseClient = baseClient
64
+ constructor(
65
+ public baseClient: Client,
66
+ serviceUri: string | URL,
67
+ ) {
68
+ super(async (input, init) => {
69
+ const headers = combineHeaders(init.headers, Object.entries(this.headers))
70
+ return fetch(new URL(input, this.uri), { ...init, headers })
71
+ }, baseClient.lex)
71
72
  this.uri = typeof serviceUri === 'string' ? new URL(serviceUri) : serviceUri
72
73
  }
73
74
 
@@ -78,92 +79,4 @@ export class ServiceClient {
78
79
  unsetHeader(key: string): void {
79
80
  delete this.headers[key]
80
81
  }
81
-
82
- async call(
83
- methodNsid: string,
84
- params?: QueryParams,
85
- data?: unknown,
86
- opts?: CallOptions,
87
- ) {
88
- const def = this.baseClient.lex.getDefOrThrow(methodNsid)
89
- if (!def || (def.type !== 'query' && def.type !== 'procedure')) {
90
- throw new Error(
91
- `Invalid lexicon: ${methodNsid}. Must be a query or procedure.`,
92
- )
93
- }
94
-
95
- const httpMethod = getMethodSchemaHTTPMethod(def)
96
- const httpUri = constructMethodCallUri(methodNsid, def, this.uri, params)
97
- const httpHeaders = constructMethodCallHeaders(def, data, {
98
- headers: {
99
- ...this.headers,
100
- ...opts?.headers,
101
- },
102
- encoding: opts?.encoding,
103
- })
104
-
105
- const res = await this.baseClient.fetch(
106
- httpUri,
107
- httpMethod,
108
- httpHeaders,
109
- data,
110
- )
111
-
112
- const resCode = httpResponseCodeToEnum(res.status)
113
- if (resCode === ResponseType.Success) {
114
- try {
115
- this.baseClient.lex.assertValidXrpcOutput(methodNsid, res.body)
116
- } catch (e: any) {
117
- if (e instanceof ValidationError) {
118
- throw new XRPCInvalidResponseError(methodNsid, e, res.body)
119
- } else {
120
- throw e
121
- }
122
- }
123
- return new XRPCResponse(res.body, res.headers)
124
- } else {
125
- if (res.body && isErrorResponseBody(res.body)) {
126
- throw new XRPCError(
127
- resCode,
128
- res.body.error,
129
- res.body.message,
130
- res.headers,
131
- )
132
- } else {
133
- throw new XRPCError(resCode)
134
- }
135
- }
136
- }
137
- }
138
-
139
- export async function defaultFetchHandler(
140
- httpUri: string,
141
- httpMethod: string,
142
- httpHeaders: Headers,
143
- httpReqBody: unknown,
144
- ): Promise<FetchHandlerResponse> {
145
- try {
146
- // The duplex field is now required for streaming bodies, but not yet reflected
147
- // anywhere in docs or types. See whatwg/fetch#1438, nodejs/node#46221.
148
- const headers = normalizeHeaders(httpHeaders)
149
- const reqInit: RequestInit & { duplex: string } = {
150
- method: httpMethod,
151
- headers,
152
- body: encodeMethodCallBody(headers, httpReqBody),
153
- duplex: 'half',
154
- }
155
- const res = await fetch(httpUri, reqInit)
156
- const resBody = await res.arrayBuffer()
157
- return {
158
- status: res.status,
159
- headers: Object.fromEntries(res.headers.entries()),
160
- body: httpResponseBodyParse(res.headers.get('content-type'), resBody),
161
- }
162
- } catch (e) {
163
- throw new XRPCError(ResponseType.Unknown, String(e))
164
- }
165
- }
166
-
167
- function isErrorResponseBody(v: unknown): v is ErrorResponseBody {
168
- return errorResponseBody.safeParse(v).success
169
82
  }
@@ -0,0 +1,73 @@
1
+ import { Gettable } from './types'
2
+ import { combineHeaders } from './util'
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), etc. Defaults to the global `fetch` function.
38
+ */
39
+ fetch?: typeof globalThis.fetch
40
+ }
41
+
42
+ export function buildFetchHandler(
43
+ options: FetchHandler | FetchHandlerOptions,
44
+ ): FetchHandler {
45
+ // Already a fetch handler (allowed for convenience)
46
+ if (typeof options === 'function') return options
47
+
48
+ const {
49
+ service,
50
+ headers: defaultHeaders = undefined,
51
+ fetch = globalThis.fetch,
52
+ } = typeof options === 'string' || options instanceof URL
53
+ ? { service: options }
54
+ : options
55
+
56
+ if (typeof fetch !== 'function') {
57
+ throw new TypeError(
58
+ 'XrpcDispatcher requires fetch() to be available in your environment.',
59
+ )
60
+ }
61
+
62
+ const defaultHeadersEntries =
63
+ defaultHeaders != null ? Object.entries(defaultHeaders) : undefined
64
+
65
+ return async function (url, init) {
66
+ const base = typeof service === 'function' ? service() : service
67
+ const fullUrl = new URL(url, base)
68
+
69
+ const headers = combineHeaders(init.headers, defaultHeadersEntries)
70
+
71
+ return fetch(fullUrl, { ...init, headers })
72
+ }
73
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,10 @@
1
- export * from './types'
2
1
  export * from './client'
2
+ export * from './fetch-handler'
3
+ export * from './types'
4
+ export * from './util'
5
+ export * from './xrpc-client'
3
6
 
4
7
  import { Client } from './client'
8
+ /** @deprecated create a local {@link XrpcClient} instance instead */
5
9
  const defaultInst = new Client()
6
10
  export default defaultInst
package/src/types.ts CHANGED
@@ -2,26 +2,19 @@ import { z } from 'zod'
2
2
  import { ValidationError } from '@atproto/lexicon'
3
3
 
4
4
  export type QueryParams = Record<string, any>
5
- export type Headers = Record<string, string>
5
+ export type HeadersMap = Record<string, string>
6
+
7
+ /** @deprecated not to be confused with the WHATWG Headers constructor */
8
+ export type Headers = HeadersMap
9
+
10
+ export type Gettable<T> = T | (() => T)
6
11
 
7
12
  export interface CallOptions {
8
13
  encoding?: string
9
- headers?: Headers
10
- }
11
-
12
- export interface FetchHandlerResponse {
13
- status: number
14
- headers: Headers
15
- body: ArrayBuffer | undefined
14
+ signal?: AbortSignal
15
+ headers?: HeadersMap
16
16
  }
17
17
 
18
- export type FetchHandler = (
19
- httpUri: string,
20
- httpMethod: string,
21
- httpHeaders: Headers,
22
- httpReqBody: any,
23
- ) => Promise<FetchHandlerResponse>
24
-
25
18
  export const errorResponseBody = z.object({
26
19
  error: z.string().optional(),
27
20
  message: z.string().optional(),
@@ -45,7 +38,24 @@ export enum ResponseType {
45
38
  UpstreamTimeout = 504,
46
39
  }
47
40
 
41
+ export function httpResponseCodeToEnum(status: number): ResponseType {
42
+ if (status in ResponseType) {
43
+ return status
44
+ } else if (status >= 100 && status < 200) {
45
+ return ResponseType.XRPCNotSupported
46
+ } else if (status >= 200 && status < 300) {
47
+ return ResponseType.Success
48
+ } else if (status >= 300 && status < 400) {
49
+ return ResponseType.XRPCNotSupported
50
+ } else if (status >= 400 && status < 500) {
51
+ return ResponseType.InvalidRequest
52
+ } else {
53
+ return ResponseType.InternalServerError
54
+ }
55
+ }
56
+
48
57
  export const ResponseTypeNames = {
58
+ [ResponseType.Unknown]: 'Unknown',
49
59
  [ResponseType.InvalidResponse]: 'InvalidResponse',
50
60
  [ResponseType.Success]: 'Success',
51
61
  [ResponseType.InvalidRequest]: 'InvalidRequest',
@@ -61,7 +71,12 @@ export const ResponseTypeNames = {
61
71
  [ResponseType.UpstreamTimeout]: 'UpstreamTimeout',
62
72
  }
63
73
 
74
+ export function httpResponseCodeToName(status: number): string {
75
+ return ResponseTypeNames[httpResponseCodeToEnum(status)]
76
+ }
77
+
64
78
  export const ResponseTypeStrings = {
79
+ [ResponseType.Unknown]: 'Unknown',
65
80
  [ResponseType.InvalidResponse]: 'Invalid Response',
66
81
  [ResponseType.Success]: 'Success',
67
82
  [ResponseType.InvalidRequest]: 'Invalid Request',
@@ -77,27 +92,63 @@ export const ResponseTypeStrings = {
77
92
  [ResponseType.UpstreamTimeout]: 'Upstream Timeout',
78
93
  }
79
94
 
95
+ export function httpResponseCodeToString(status: number): string {
96
+ return ResponseTypeStrings[httpResponseCodeToEnum(status)]
97
+ }
98
+
80
99
  export class XRPCResponse {
81
100
  success = true
82
101
 
83
- constructor(public data: any, public headers: Headers) {}
102
+ constructor(
103
+ public data: any,
104
+ public headers: Headers,
105
+ ) {}
84
106
  }
85
107
 
86
108
  export class XRPCError extends Error {
87
109
  success = false
88
- headers?: Headers
110
+
111
+ public status: ResponseType
89
112
 
90
113
  constructor(
91
- public status: ResponseType,
92
- public error?: string,
114
+ statusCode: number,
115
+ public error: string = httpResponseCodeToName(statusCode),
93
116
  message?: string,
94
- headers?: Headers,
117
+ public headers?: Headers,
118
+ options?: ErrorOptions,
95
119
  ) {
96
- super(message || error || ResponseTypeStrings[status])
97
- if (!this.error) {
98
- this.error = ResponseTypeNames[status]
120
+ super(message || error || httpResponseCodeToString(statusCode), options)
121
+
122
+ this.status = httpResponseCodeToEnum(statusCode)
123
+
124
+ // Pre 2022 runtimes won't handle the "options" constructor argument
125
+ const cause = options?.cause
126
+ if (this.cause === undefined && cause !== undefined) {
127
+ this.cause = cause
99
128
  }
100
- this.headers = headers
129
+ }
130
+
131
+ static from(cause: unknown, fallbackStatus?: ResponseType): XRPCError {
132
+ if (cause instanceof XRPCError) {
133
+ return cause
134
+ }
135
+
136
+ // Extract status code from "http-errors" like errors
137
+ const statusCode: unknown =
138
+ cause instanceof Error
139
+ ? ('statusCode' in cause ? cause.statusCode : undefined) ??
140
+ ('status' in cause ? cause.status : undefined)
141
+ : undefined
142
+
143
+ const status: ResponseType =
144
+ typeof statusCode === 'number'
145
+ ? httpResponseCodeToEnum(statusCode)
146
+ : fallbackStatus ?? ResponseType.Unknown
147
+
148
+ const error = ResponseTypeNames[status]
149
+ const message = cause instanceof Error ? cause.message : String(cause)
150
+
151
+ return new XRPCError(status, error, message, undefined, { cause })
101
152
  }
102
153
  }
103
154
 
@@ -111,6 +162,8 @@ export class XRPCInvalidResponseError extends XRPCError {
111
162
  ResponseType.InvalidResponse,
112
163
  ResponseTypeStrings[ResponseType.InvalidResponse],
113
164
  `The server gave an invalid response and may be out of date.`,
165
+ undefined,
166
+ { cause: validationError },
114
167
  )
115
168
  }
116
169
  }