@atproto/lex-client 0.0.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.
Files changed (111) hide show
  1. package/dist/agent.d.ts +33 -0
  2. package/dist/agent.d.ts.map +1 -0
  3. package/dist/agent.js +21 -0
  4. package/dist/agent.js.map +1 -0
  5. package/dist/client.d.ts +456 -0
  6. package/dist/client.d.ts.map +1 -0
  7. package/dist/client.js +236 -0
  8. package/dist/client.js.map +1 -0
  9. package/dist/error.d.ts +70 -0
  10. package/dist/error.d.ts.map +1 -0
  11. package/dist/error.js +98 -0
  12. package/dist/error.js.map +1 -0
  13. package/dist/index.d.ts +7 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +10 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/lexicons/com/atproto/repo/createRecord.d.ts +3 -0
  18. package/dist/lexicons/com/atproto/repo/createRecord.d.ts.map +1 -0
  19. package/dist/lexicons/com/atproto/repo/createRecord.defs.d.ts +100 -0
  20. package/dist/lexicons/com/atproto/repo/createRecord.defs.d.ts.map +1 -0
  21. package/dist/lexicons/com/atproto/repo/createRecord.defs.js +42 -0
  22. package/dist/lexicons/com/atproto/repo/createRecord.defs.js.map +1 -0
  23. package/dist/lexicons/com/atproto/repo/createRecord.js +10 -0
  24. package/dist/lexicons/com/atproto/repo/createRecord.js.map +1 -0
  25. package/dist/lexicons/com/atproto/repo/defs.d.ts +3 -0
  26. package/dist/lexicons/com/atproto/repo/defs.d.ts.map +1 -0
  27. package/dist/lexicons/com/atproto/repo/defs.defs.d.ts +12 -0
  28. package/dist/lexicons/com/atproto/repo/defs.defs.d.ts.map +1 -0
  29. package/dist/lexicons/com/atproto/repo/defs.defs.js +16 -0
  30. package/dist/lexicons/com/atproto/repo/defs.defs.js.map +1 -0
  31. package/dist/lexicons/com/atproto/repo/defs.js +10 -0
  32. package/dist/lexicons/com/atproto/repo/defs.js.map +1 -0
  33. package/dist/lexicons/com/atproto/repo/deleteRecord.d.ts +3 -0
  34. package/dist/lexicons/com/atproto/repo/deleteRecord.d.ts.map +1 -0
  35. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.d.ts +70 -0
  36. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.d.ts.map +1 -0
  37. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.js +33 -0
  38. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.js.map +1 -0
  39. package/dist/lexicons/com/atproto/repo/deleteRecord.js +10 -0
  40. package/dist/lexicons/com/atproto/repo/deleteRecord.js.map +1 -0
  41. package/dist/lexicons/com/atproto/repo/getRecord.d.ts +3 -0
  42. package/dist/lexicons/com/atproto/repo/getRecord.d.ts.map +1 -0
  43. package/dist/lexicons/com/atproto/repo/getRecord.defs.d.ts +82 -0
  44. package/dist/lexicons/com/atproto/repo/getRecord.defs.d.ts.map +1 -0
  45. package/dist/lexicons/com/atproto/repo/getRecord.defs.js +30 -0
  46. package/dist/lexicons/com/atproto/repo/getRecord.defs.js.map +1 -0
  47. package/dist/lexicons/com/atproto/repo/getRecord.js +10 -0
  48. package/dist/lexicons/com/atproto/repo/getRecord.js.map +1 -0
  49. package/dist/lexicons/com/atproto/repo/listRecords.d.ts +3 -0
  50. package/dist/lexicons/com/atproto/repo/listRecords.d.ts.map +1 -0
  51. package/dist/lexicons/com/atproto/repo/listRecords.defs.d.ts +75 -0
  52. package/dist/lexicons/com/atproto/repo/listRecords.defs.d.ts.map +1 -0
  53. package/dist/lexicons/com/atproto/repo/listRecords.defs.js +42 -0
  54. package/dist/lexicons/com/atproto/repo/listRecords.defs.js.map +1 -0
  55. package/dist/lexicons/com/atproto/repo/listRecords.js +10 -0
  56. package/dist/lexicons/com/atproto/repo/listRecords.js.map +1 -0
  57. package/dist/lexicons/com/atproto/repo/putRecord.d.ts +3 -0
  58. package/dist/lexicons/com/atproto/repo/putRecord.d.ts.map +1 -0
  59. package/dist/lexicons/com/atproto/repo/putRecord.defs.d.ts +110 -0
  60. package/dist/lexicons/com/atproto/repo/putRecord.defs.d.ts.map +1 -0
  61. package/dist/lexicons/com/atproto/repo/putRecord.defs.js +46 -0
  62. package/dist/lexicons/com/atproto/repo/putRecord.defs.js.map +1 -0
  63. package/dist/lexicons/com/atproto/repo/putRecord.js +10 -0
  64. package/dist/lexicons/com/atproto/repo/putRecord.js.map +1 -0
  65. package/dist/lexicons/com/atproto/repo/uploadBlob.d.ts +3 -0
  66. package/dist/lexicons/com/atproto/repo/uploadBlob.d.ts.map +1 -0
  67. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.d.ts +25 -0
  68. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.d.ts.map +1 -0
  69. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.js +22 -0
  70. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.js.map +1 -0
  71. package/dist/lexicons/com/atproto/repo/uploadBlob.js +10 -0
  72. package/dist/lexicons/com/atproto/repo/uploadBlob.js.map +1 -0
  73. package/dist/lexicons/com/atproto/repo.d.ts +8 -0
  74. package/dist/lexicons/com/atproto/repo.d.ts.map +1 -0
  75. package/dist/lexicons/com/atproto/repo.js +15 -0
  76. package/dist/lexicons/com/atproto/repo.js.map +1 -0
  77. package/dist/lexicons/com/atproto.d.ts +2 -0
  78. package/dist/lexicons/com/atproto.d.ts.map +1 -0
  79. package/dist/lexicons/com/atproto.js +9 -0
  80. package/dist/lexicons/com/atproto.js.map +1 -0
  81. package/dist/lexicons/com.d.ts +2 -0
  82. package/dist/lexicons/com.d.ts.map +1 -0
  83. package/dist/lexicons/com.js +9 -0
  84. package/dist/lexicons/com.js.map +1 -0
  85. package/dist/response.d.ts +21 -0
  86. package/dist/response.d.ts.map +1 -0
  87. package/dist/response.js +31 -0
  88. package/dist/response.js.map +1 -0
  89. package/dist/types.d.ts +17 -0
  90. package/dist/types.d.ts.map +1 -0
  91. package/dist/types.js +7 -0
  92. package/dist/types.js.map +1 -0
  93. package/dist/xrpc.d.ts +37 -0
  94. package/dist/xrpc.d.ts.map +1 -0
  95. package/dist/xrpc.js +185 -0
  96. package/dist/xrpc.js.map +1 -0
  97. package/jest.config.js +5 -0
  98. package/package.json +46 -0
  99. package/scripts/lex-build.mjs +40 -0
  100. package/src/agent.ts +63 -0
  101. package/src/client.ts +513 -0
  102. package/src/error.ts +154 -0
  103. package/src/index.ts +6 -0
  104. package/src/response.ts +42 -0
  105. package/src/types.ts +21 -0
  106. package/src/xrpc.ts +335 -0
  107. package/tests/client.test.ts +370 -0
  108. package/tsconfig.build.json +12 -0
  109. package/tsconfig.build.tsbuildinfo +1 -0
  110. package/tsconfig.json +7 -0
  111. package/tsconfig.tests.json +12 -0
package/src/types.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { Did, UnknownString } from '@atproto/lex-schema'
2
+
3
+ export type { Did, UnknownString }
4
+
5
+ export type DidServiceIdentifier = 'atproto_labeler' | UnknownString
6
+ export type Service = `${Did}#${DidServiceIdentifier}`
7
+
8
+ export type CallOptions = {
9
+ labelers?: Iterable<Did>
10
+ signal?: AbortSignal
11
+ headers?: HeadersInit
12
+ service?: Service
13
+ validateRequest?: boolean
14
+ validateResponse?: boolean
15
+ }
16
+
17
+ export type Namespace<T> = T | { main: T }
18
+
19
+ export function getMain<T extends object>(ns: Namespace<T>): T {
20
+ return 'main' in ns ? ns.main : ns
21
+ }
package/src/xrpc.ts ADDED
@@ -0,0 +1,335 @@
1
+ import { LexValue } from '@atproto/lex-data'
2
+ import { lexParse, lexStringify } from '@atproto/lex-json'
3
+ import {
4
+ Did,
5
+ InferParamsSchema,
6
+ InferPayloadBody,
7
+ Params,
8
+ ParamsSchema,
9
+ Procedure,
10
+ Query,
11
+ Restricted,
12
+ Subscription,
13
+ } from '@atproto/lex-schema'
14
+ import { Agent } from './agent.js'
15
+ import {
16
+ KnownError,
17
+ XrpcResponseError,
18
+ XrpcServiceError,
19
+ xrpcErrorBodySchema,
20
+ } from './error.js'
21
+ import { XrpcResponse, XrpcResponseBody } from './response.js'
22
+ import { CallOptions, Namespace, Service, getMain } from './types.js'
23
+
24
+ export type XrpcOptions<M extends Procedure | Query = Procedure | Query> =
25
+ CallOptions & XrpcRequestUrlOptions<M> & XrpcRequestInitOptions<M>
26
+
27
+ export async function xrpc<const M extends Query | Procedure>(
28
+ agent: Agent,
29
+ ns: NonNullable<unknown> extends XrpcOptions<M>
30
+ ? Namespace<M>
31
+ : Restricted<'This XRPC method requires an "options" argument'>,
32
+ ): Promise<XrpcResponse<M>>
33
+ export async function xrpc<const M extends Query | Procedure>(
34
+ agent: Agent,
35
+ ns: Namespace<M>,
36
+ options: XrpcOptions<M>,
37
+ ): Promise<XrpcResponse<M>>
38
+ export async function xrpc<const M extends Query | Procedure>(
39
+ agent: Agent,
40
+ ns: Namespace<M>,
41
+ options: XrpcOptions<M> = {} as XrpcOptions<M>,
42
+ ): Promise<XrpcResponse<M>> {
43
+ options.signal?.throwIfAborted()
44
+ const method = getMain(ns)
45
+ const url = xrpcRequestUrl(method, options)
46
+ const request = xrpcRequestInit(method, options)
47
+ const response = await agent.fetchHandler(url, request)
48
+ return xrpcResponseHandler<M>(response, method, options)
49
+ }
50
+
51
+ export type XrpcRequestUrlOptions<M extends Query | Procedure | Subscription> =
52
+ CallOptions &
53
+ (undefined extends InferParamsSchema<M['parameters']>
54
+ ? { params?: InferParamsSchema<M['parameters']> }
55
+ : { params: InferParamsSchema<M['parameters']> })
56
+
57
+ export function xrpcRequestUrl<M extends Procedure | Query | Subscription>(
58
+ method: M,
59
+ options: XrpcRequestUrlOptions<M>,
60
+ ) {
61
+ const path = `/xrpc/${method.nsid}`
62
+
63
+ const queryString = options.params
64
+ ? xrpcRequestParams(method.parameters, options.params, options)
65
+ : undefined
66
+
67
+ return queryString ? `${path}?${queryString}` : path
68
+ }
69
+
70
+ export function xrpcRequestParams(
71
+ schema: ParamsSchema | undefined,
72
+ params: Params | undefined,
73
+ options: CallOptions,
74
+ ): undefined | string {
75
+ const urlSearchParams = schema?.toURLSearchParams(
76
+ options.validateRequest ? schema.parse(params) : (params as any),
77
+ )
78
+ return urlSearchParams?.size ? urlSearchParams.toString() : undefined
79
+ }
80
+
81
+ export type XrpcRequestInitOptions<T extends Query | Procedure> = CallOptions &
82
+ (T extends Procedure
83
+ ? never extends InferPayloadBody<T['input']>
84
+ ? { body?: InferPayloadBody<T['input']> }
85
+ : { body: InferPayloadBody<T['input']> }
86
+ : { body?: never })
87
+
88
+ export function xrpcRequestInit<T extends Procedure | Query>(
89
+ schema: T,
90
+ options: XrpcRequestInitOptions<T>,
91
+ ): RequestInit & { duplex?: 'half' } {
92
+ const headers = xrpcRequestHeaders(options)
93
+
94
+ // Requests with body
95
+ if ('input' in schema && schema.input?.encoding) {
96
+ if (
97
+ options.validateRequest &&
98
+ schema.input == null &&
99
+ options.body !== undefined
100
+ ) {
101
+ throw new TypeError(
102
+ `XRPC method ${schema.nsid} does not accept a request body`,
103
+ )
104
+ }
105
+
106
+ headers.set('content-type', schema.input.encoding)
107
+ return {
108
+ duplex: 'half',
109
+ redirect: 'follow',
110
+ referrerPolicy: 'strict-origin-when-cross-origin', // (default)
111
+ mode: 'cors', // (default)
112
+ signal: options.signal,
113
+ method: 'POST',
114
+ headers,
115
+ body: xrpcRequestBody(
116
+ schema.input?.encoding,
117
+ options.validateRequest
118
+ ? schema.input?.body.parse(options.body)
119
+ : options.body,
120
+ ),
121
+ }
122
+ }
123
+
124
+ // Requests without body
125
+ return {
126
+ duplex: 'half',
127
+ redirect: 'follow',
128
+ referrerPolicy: 'strict-origin-when-cross-origin', // (default)
129
+ mode: 'cors', // (default)
130
+ signal: options.signal,
131
+ method: schema instanceof Query ? 'GET' : 'POST',
132
+ headers,
133
+ }
134
+ }
135
+
136
+ export function xrpcRequestHeaders(options: {
137
+ headers?: HeadersInit
138
+ service?: Service
139
+ labelers?: Iterable<Did>
140
+ }): Headers {
141
+ const headers = new Headers(options.headers)
142
+
143
+ if (options.service && !headers.has('atproto-proxy')) {
144
+ headers.set('atproto-proxy', options.service)
145
+ }
146
+
147
+ if (options.labelers) {
148
+ headers.set(
149
+ 'atproto-accept-labelers',
150
+ [...options.labelers, headers.get('atproto-accept-labelers')?.trim()]
151
+ .filter(Boolean)
152
+ .join(', '),
153
+ )
154
+ }
155
+
156
+ return headers
157
+ }
158
+
159
+ function xrpcRequestBody(
160
+ encoding: string | undefined,
161
+ body: LexValue | undefined,
162
+ ): BodyInit | null {
163
+ if (encoding === undefined) {
164
+ return null
165
+ }
166
+
167
+ if (encoding === 'application/json') {
168
+ if (body !== undefined) return lexStringify(body)
169
+ } else if (encoding.startsWith('text/')) {
170
+ if (typeof body === 'string') return body
171
+ } else {
172
+ if (ArrayBuffer.isView(body) || body instanceof ArrayBuffer) return body
173
+ }
174
+
175
+ throw new TypeError(`Invalid ${typeof body} body for ${encoding} encoding`)
176
+ }
177
+
178
+ export async function xrpcResponseHandler<M extends Procedure | Query>(
179
+ response: Response,
180
+ schema: M,
181
+ options?: { validateResponse?: boolean },
182
+ ): Promise<XrpcResponse<M>> {
183
+ // @NOTE The body MUST either be read or canceled to avoid resource leaks.
184
+ // Since nothing should cause an exception before "readXrpcResponseBody" is
185
+ // called, we can safely not use a try/finally here.
186
+
187
+ const encoding = extractEncoding(response.headers)
188
+
189
+ const body = await readResponseBody(response, encoding).catch((cause) => {
190
+ throw new XrpcServiceError(
191
+ KnownError.InvalidResponse,
192
+ response.status,
193
+ response.headers,
194
+ undefined,
195
+ 'Failed to read XRPC response',
196
+ { cause },
197
+ )
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
+ // All unsuccessful responses should follow a standard error response
203
+ // schema. The Content-Type should be application/json, and the payload
204
+ // should be a JSON object with the following fields:
205
+ // - error (string, required): type name of the error (generic ASCII
206
+ // constant, no whitespace)
207
+ // - message (string, optional): description of the error, appropriate for
208
+ // display to humans
209
+ if (
210
+ body != null &&
211
+ encoding === 'application/json' &&
212
+ xrpcErrorBodySchema.check(body)
213
+ ) {
214
+ throw new XrpcResponseError(
215
+ response.status,
216
+ response.headers,
217
+ encoding,
218
+ body,
219
+ )
220
+ }
221
+
222
+ throw new XrpcServiceError(
223
+ response.status >= 500
224
+ ? KnownError.InternalServerError
225
+ : KnownError.InvalidResponse,
226
+ response.status,
227
+ response.headers,
228
+ body,
229
+ )
230
+ }
231
+
232
+ // Check response encoding
233
+ if (schema.output.encoding !== encoding) {
234
+ throw new XrpcServiceError(
235
+ KnownError.InvalidResponse,
236
+ response.status,
237
+ response.headers,
238
+ body,
239
+ `Expected response with content-type ${schema.output.encoding}, got ${encoding}`,
240
+ )
241
+ }
242
+
243
+ if (schema.output.encoding == null) {
244
+ if (body !== undefined) {
245
+ throw new XrpcServiceError(
246
+ KnownError.InvalidResponse,
247
+ response.status,
248
+ response.headers,
249
+ body,
250
+ `Expected empty response body`,
251
+ )
252
+ }
253
+
254
+ return new XrpcResponse<M>(
255
+ schema,
256
+ response.status,
257
+ response.headers,
258
+ undefined as XrpcResponseBody<M>,
259
+ )
260
+ } else {
261
+ // @NOTE this should already be enforced by readXrpcResponseBody
262
+ if (body === undefined) {
263
+ throw new XrpcServiceError(
264
+ KnownError.InvalidResponse,
265
+ response.status,
266
+ response.headers,
267
+ body,
268
+ `Expected non-empty response body`,
269
+ )
270
+ }
271
+
272
+ return new XrpcResponse<M>(
273
+ schema,
274
+ response.status,
275
+ response.headers,
276
+ schema.output.schema == null || options?.validateResponse === false
277
+ ? (body as XrpcResponseBody<M>)
278
+ : (schema.output.schema.parse(body) as XrpcResponseBody<M>),
279
+ )
280
+ }
281
+ }
282
+
283
+ export function extractEncoding(headers: Headers): string | undefined {
284
+ const contentType = headers.get('content-type')
285
+ if (!contentType) return undefined
286
+ return contentType.split(';')[0].trim()
287
+ }
288
+
289
+ export async function readResponseBody(
290
+ response: Response,
291
+ encoding: string,
292
+ ): Promise<LexValue>
293
+ export async function readResponseBody(
294
+ response: Response,
295
+ encoding: string | undefined,
296
+ ): Promise<LexValue | undefined>
297
+ export async function readResponseBody(
298
+ response: Response,
299
+ encoding: string | undefined,
300
+ ): Promise<LexValue | undefined> {
301
+ // When encoding is undefined or empty, we expect no body
302
+ if (encoding == null) {
303
+ if (response.body == null) return undefined
304
+
305
+ // Let's make sure the body is empty (while avoiding reading it all).
306
+ if (!('getReader' in response.body)) {
307
+ // Some environments may not support body.getReader(), fall back to
308
+ // reading the whole body.
309
+ const buffer = await response.arrayBuffer()
310
+ if (buffer.byteLength === 0) return undefined
311
+ } else {
312
+ const reader = response.body.getReader()
313
+ const next = await reader.read()
314
+ if (next.done) return undefined
315
+ await reader.cancel() // Drain the rest of the (non-empty) body stream
316
+ }
317
+
318
+ throw new SyntaxError('Content-type is undefined but body is not empty')
319
+ }
320
+
321
+ if (encoding === 'application/json') {
322
+ // @NOTE Using `lexParse(text)` (instead of `jsonToLex(json)`) here as using
323
+ // a reviver function during JSON.parse should be faster than parsing to
324
+ // JSON then converting to Lex (?)
325
+
326
+ // @TODO verify statement above
327
+ return lexParse(await response.text())
328
+ }
329
+
330
+ if (encoding.startsWith('text/')) {
331
+ return response.text()
332
+ }
333
+
334
+ return new Uint8Array(await response.arrayBuffer())
335
+ }