@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/dist/xrpc.js ADDED
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.xrpc = xrpc;
4
+ exports.xrpcRequestUrl = xrpcRequestUrl;
5
+ exports.xrpcRequestParams = xrpcRequestParams;
6
+ exports.xrpcRequestInit = xrpcRequestInit;
7
+ exports.xrpcRequestHeaders = xrpcRequestHeaders;
8
+ exports.xrpcResponseHandler = xrpcResponseHandler;
9
+ exports.extractEncoding = extractEncoding;
10
+ exports.readResponseBody = readResponseBody;
11
+ const lex_json_1 = require("@atproto/lex-json");
12
+ const lex_schema_1 = require("@atproto/lex-schema");
13
+ const error_js_1 = require("./error.js");
14
+ const response_js_1 = require("./response.js");
15
+ const types_js_1 = require("./types.js");
16
+ async function xrpc(agent, ns, options = {}) {
17
+ options.signal?.throwIfAborted();
18
+ const method = (0, types_js_1.getMain)(ns);
19
+ const url = xrpcRequestUrl(method, options);
20
+ const request = xrpcRequestInit(method, options);
21
+ const response = await agent.fetchHandler(url, request);
22
+ return xrpcResponseHandler(response, method, options);
23
+ }
24
+ function xrpcRequestUrl(method, options) {
25
+ const path = `/xrpc/${method.nsid}`;
26
+ const queryString = options.params
27
+ ? xrpcRequestParams(method.parameters, options.params, options)
28
+ : undefined;
29
+ return queryString ? `${path}?${queryString}` : path;
30
+ }
31
+ function xrpcRequestParams(schema, params, options) {
32
+ const urlSearchParams = schema?.toURLSearchParams(options.validateRequest ? schema.parse(params) : params);
33
+ return urlSearchParams?.size ? urlSearchParams.toString() : undefined;
34
+ }
35
+ function xrpcRequestInit(schema, options) {
36
+ const headers = xrpcRequestHeaders(options);
37
+ // Requests with body
38
+ if ('input' in schema && schema.input?.encoding) {
39
+ if (options.validateRequest &&
40
+ schema.input == null &&
41
+ options.body !== undefined) {
42
+ throw new TypeError(`XRPC method ${schema.nsid} does not accept a request body`);
43
+ }
44
+ headers.set('content-type', schema.input.encoding);
45
+ return {
46
+ duplex: 'half',
47
+ redirect: 'follow',
48
+ referrerPolicy: 'strict-origin-when-cross-origin', // (default)
49
+ mode: 'cors', // (default)
50
+ signal: options.signal,
51
+ method: 'POST',
52
+ headers,
53
+ body: xrpcRequestBody(schema.input?.encoding, options.validateRequest
54
+ ? schema.input?.body.parse(options.body)
55
+ : options.body),
56
+ };
57
+ }
58
+ // Requests without body
59
+ return {
60
+ duplex: 'half',
61
+ redirect: 'follow',
62
+ referrerPolicy: 'strict-origin-when-cross-origin', // (default)
63
+ mode: 'cors', // (default)
64
+ signal: options.signal,
65
+ method: schema instanceof lex_schema_1.Query ? 'GET' : 'POST',
66
+ headers,
67
+ };
68
+ }
69
+ function xrpcRequestHeaders(options) {
70
+ const headers = new Headers(options.headers);
71
+ if (options.service && !headers.has('atproto-proxy')) {
72
+ headers.set('atproto-proxy', options.service);
73
+ }
74
+ if (options.labelers) {
75
+ headers.set('atproto-accept-labelers', [...options.labelers, headers.get('atproto-accept-labelers')?.trim()]
76
+ .filter(Boolean)
77
+ .join(', '));
78
+ }
79
+ return headers;
80
+ }
81
+ function xrpcRequestBody(encoding, body) {
82
+ if (encoding === undefined) {
83
+ return null;
84
+ }
85
+ if (encoding === 'application/json') {
86
+ if (body !== undefined)
87
+ return (0, lex_json_1.lexStringify)(body);
88
+ }
89
+ else if (encoding.startsWith('text/')) {
90
+ if (typeof body === 'string')
91
+ return body;
92
+ }
93
+ else {
94
+ if (ArrayBuffer.isView(body) || body instanceof ArrayBuffer)
95
+ return body;
96
+ }
97
+ throw new TypeError(`Invalid ${typeof body} body for ${encoding} encoding`);
98
+ }
99
+ async function xrpcResponseHandler(response, schema, options) {
100
+ // @NOTE The body MUST either be read or canceled to avoid resource leaks.
101
+ // Since nothing should cause an exception before "readXrpcResponseBody" is
102
+ // called, we can safely not use a try/finally here.
103
+ const encoding = extractEncoding(response.headers);
104
+ const body = await readResponseBody(response, encoding).catch((cause) => {
105
+ throw new error_js_1.XrpcServiceError(error_js_1.KnownError.InvalidResponse, response.status, response.headers, undefined, 'Failed to read XRPC response', { cause });
106
+ });
107
+ // @NOTE redirect is set to 'follow', so we shouldn't get 3xx responses here
108
+ if (response.status < 200 || response.status >= 300) {
109
+ // All unsuccessful responses should follow a standard error response
110
+ // schema. The Content-Type should be application/json, and the payload
111
+ // should be a JSON object with the following fields:
112
+ // - error (string, required): type name of the error (generic ASCII
113
+ // constant, no whitespace)
114
+ // - message (string, optional): description of the error, appropriate for
115
+ // display to humans
116
+ if (body != null &&
117
+ encoding === 'application/json' &&
118
+ error_js_1.xrpcErrorBodySchema.check(body)) {
119
+ throw new error_js_1.XrpcResponseError(response.status, response.headers, encoding, body);
120
+ }
121
+ throw new error_js_1.XrpcServiceError(response.status >= 500
122
+ ? error_js_1.KnownError.InternalServerError
123
+ : error_js_1.KnownError.InvalidResponse, response.status, response.headers, body);
124
+ }
125
+ // Check response encoding
126
+ if (schema.output.encoding !== encoding) {
127
+ throw new error_js_1.XrpcServiceError(error_js_1.KnownError.InvalidResponse, response.status, response.headers, body, `Expected response with content-type ${schema.output.encoding}, got ${encoding}`);
128
+ }
129
+ if (schema.output.encoding == null) {
130
+ if (body !== undefined) {
131
+ throw new error_js_1.XrpcServiceError(error_js_1.KnownError.InvalidResponse, response.status, response.headers, body, `Expected empty response body`);
132
+ }
133
+ return new response_js_1.XrpcResponse(schema, response.status, response.headers, undefined);
134
+ }
135
+ else {
136
+ // @NOTE this should already be enforced by readXrpcResponseBody
137
+ if (body === undefined) {
138
+ throw new error_js_1.XrpcServiceError(error_js_1.KnownError.InvalidResponse, response.status, response.headers, body, `Expected non-empty response body`);
139
+ }
140
+ return new response_js_1.XrpcResponse(schema, response.status, response.headers, schema.output.schema == null || options?.validateResponse === false
141
+ ? body
142
+ : schema.output.schema.parse(body));
143
+ }
144
+ }
145
+ function extractEncoding(headers) {
146
+ const contentType = headers.get('content-type');
147
+ if (!contentType)
148
+ return undefined;
149
+ return contentType.split(';')[0].trim();
150
+ }
151
+ async function readResponseBody(response, encoding) {
152
+ // When encoding is undefined or empty, we expect no body
153
+ if (encoding == null) {
154
+ if (response.body == null)
155
+ return undefined;
156
+ // Let's make sure the body is empty (while avoiding reading it all).
157
+ if (!('getReader' in response.body)) {
158
+ // Some environments may not support body.getReader(), fall back to
159
+ // reading the whole body.
160
+ const buffer = await response.arrayBuffer();
161
+ if (buffer.byteLength === 0)
162
+ return undefined;
163
+ }
164
+ else {
165
+ const reader = response.body.getReader();
166
+ const next = await reader.read();
167
+ if (next.done)
168
+ return undefined;
169
+ await reader.cancel(); // Drain the rest of the (non-empty) body stream
170
+ }
171
+ throw new SyntaxError('Content-type is undefined but body is not empty');
172
+ }
173
+ if (encoding === 'application/json') {
174
+ // @NOTE Using `lexParse(text)` (instead of `jsonToLex(json)`) here as using
175
+ // a reviver function during JSON.parse should be faster than parsing to
176
+ // JSON then converting to Lex (?)
177
+ // @TODO verify statement above
178
+ return (0, lex_json_1.lexParse)(await response.text());
179
+ }
180
+ if (encoding.startsWith('text/')) {
181
+ return response.text();
182
+ }
183
+ return new Uint8Array(await response.arrayBuffer());
184
+ }
185
+ //# sourceMappingURL=xrpc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xrpc.js","sourceRoot":"","sources":["../src/xrpc.ts"],"names":[],"mappings":"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","sourcesContent":["import { LexValue } from '@atproto/lex-data'\nimport { lexParse, lexStringify } from '@atproto/lex-json'\nimport {\n Did,\n InferParamsSchema,\n InferPayloadBody,\n Params,\n ParamsSchema,\n Procedure,\n Query,\n Restricted,\n Subscription,\n} from '@atproto/lex-schema'\nimport { Agent } from './agent.js'\nimport {\n KnownError,\n XrpcResponseError,\n XrpcServiceError,\n xrpcErrorBodySchema,\n} from './error.js'\nimport { XrpcResponse, XrpcResponseBody } from './response.js'\nimport { CallOptions, Namespace, Service, getMain } from './types.js'\n\nexport type XrpcOptions<M extends Procedure | Query = Procedure | Query> =\n CallOptions & XrpcRequestUrlOptions<M> & XrpcRequestInitOptions<M>\n\nexport async function xrpc<const M extends Query | Procedure>(\n agent: Agent,\n ns: NonNullable<unknown> extends XrpcOptions<M>\n ? Namespace<M>\n : Restricted<'This XRPC method requires an \"options\" argument'>,\n): Promise<XrpcResponse<M>>\nexport async function xrpc<const M extends Query | Procedure>(\n agent: Agent,\n ns: Namespace<M>,\n options: XrpcOptions<M>,\n): Promise<XrpcResponse<M>>\nexport async function xrpc<const M extends Query | Procedure>(\n agent: Agent,\n ns: Namespace<M>,\n options: XrpcOptions<M> = {} as XrpcOptions<M>,\n): Promise<XrpcResponse<M>> {\n options.signal?.throwIfAborted()\n const method = getMain(ns)\n const url = xrpcRequestUrl(method, options)\n const request = xrpcRequestInit(method, options)\n const response = await agent.fetchHandler(url, request)\n return xrpcResponseHandler<M>(response, method, options)\n}\n\nexport type XrpcRequestUrlOptions<M extends Query | Procedure | Subscription> =\n CallOptions &\n (undefined extends InferParamsSchema<M['parameters']>\n ? { params?: InferParamsSchema<M['parameters']> }\n : { params: InferParamsSchema<M['parameters']> })\n\nexport function xrpcRequestUrl<M extends Procedure | Query | Subscription>(\n method: M,\n options: XrpcRequestUrlOptions<M>,\n) {\n const path = `/xrpc/${method.nsid}`\n\n const queryString = options.params\n ? xrpcRequestParams(method.parameters, options.params, options)\n : undefined\n\n return queryString ? `${path}?${queryString}` : path\n}\n\nexport function xrpcRequestParams(\n schema: ParamsSchema | undefined,\n params: Params | undefined,\n options: CallOptions,\n): undefined | string {\n const urlSearchParams = schema?.toURLSearchParams(\n options.validateRequest ? schema.parse(params) : (params as any),\n )\n return urlSearchParams?.size ? urlSearchParams.toString() : undefined\n}\n\nexport type XrpcRequestInitOptions<T extends Query | Procedure> = CallOptions &\n (T extends Procedure\n ? never extends InferPayloadBody<T['input']>\n ? { body?: InferPayloadBody<T['input']> }\n : { body: InferPayloadBody<T['input']> }\n : { body?: never })\n\nexport function xrpcRequestInit<T extends Procedure | Query>(\n schema: T,\n options: XrpcRequestInitOptions<T>,\n): RequestInit & { duplex?: 'half' } {\n const headers = xrpcRequestHeaders(options)\n\n // Requests with body\n if ('input' in schema && schema.input?.encoding) {\n if (\n options.validateRequest &&\n schema.input == null &&\n options.body !== undefined\n ) {\n throw new TypeError(\n `XRPC method ${schema.nsid} does not accept a request body`,\n )\n }\n\n headers.set('content-type', schema.input.encoding)\n return {\n duplex: 'half',\n redirect: 'follow',\n referrerPolicy: 'strict-origin-when-cross-origin', // (default)\n mode: 'cors', // (default)\n signal: options.signal,\n method: 'POST',\n headers,\n body: xrpcRequestBody(\n schema.input?.encoding,\n options.validateRequest\n ? schema.input?.body.parse(options.body)\n : options.body,\n ),\n }\n }\n\n // Requests without body\n return {\n duplex: 'half',\n redirect: 'follow',\n referrerPolicy: 'strict-origin-when-cross-origin', // (default)\n mode: 'cors', // (default)\n signal: options.signal,\n method: schema instanceof Query ? 'GET' : 'POST',\n headers,\n }\n}\n\nexport function xrpcRequestHeaders(options: {\n headers?: HeadersInit\n service?: Service\n labelers?: Iterable<Did>\n}): Headers {\n const headers = new Headers(options.headers)\n\n if (options.service && !headers.has('atproto-proxy')) {\n headers.set('atproto-proxy', options.service)\n }\n\n if (options.labelers) {\n headers.set(\n 'atproto-accept-labelers',\n [...options.labelers, headers.get('atproto-accept-labelers')?.trim()]\n .filter(Boolean)\n .join(', '),\n )\n }\n\n return headers\n}\n\nfunction xrpcRequestBody(\n encoding: string | undefined,\n body: LexValue | undefined,\n): BodyInit | null {\n if (encoding === undefined) {\n return null\n }\n\n if (encoding === 'application/json') {\n if (body !== undefined) return lexStringify(body)\n } else if (encoding.startsWith('text/')) {\n if (typeof body === 'string') return body\n } else {\n if (ArrayBuffer.isView(body) || body instanceof ArrayBuffer) return body\n }\n\n throw new TypeError(`Invalid ${typeof body} body for ${encoding} encoding`)\n}\n\nexport async function xrpcResponseHandler<M extends Procedure | Query>(\n response: Response,\n schema: M,\n options?: { validateResponse?: boolean },\n): Promise<XrpcResponse<M>> {\n // @NOTE The body MUST either be read or canceled to avoid resource leaks.\n // Since nothing should cause an exception before \"readXrpcResponseBody\" is\n // called, we can safely not use a try/finally here.\n\n const encoding = extractEncoding(response.headers)\n\n const body = await readResponseBody(response, encoding).catch((cause) => {\n throw new XrpcServiceError(\n KnownError.InvalidResponse,\n response.status,\n response.headers,\n undefined,\n 'Failed to read XRPC response',\n { cause },\n )\n })\n\n // @NOTE redirect is set to 'follow', so we shouldn't get 3xx responses here\n if (response.status < 200 || response.status >= 300) {\n // All unsuccessful responses should follow a standard error response\n // schema. The Content-Type should be application/json, and the payload\n // should be a JSON object with the following fields:\n // - error (string, required): type name of the error (generic ASCII\n // constant, no whitespace)\n // - message (string, optional): description of the error, appropriate for\n // display to humans\n if (\n body != null &&\n encoding === 'application/json' &&\n xrpcErrorBodySchema.check(body)\n ) {\n throw new XrpcResponseError(\n response.status,\n response.headers,\n encoding,\n body,\n )\n }\n\n throw new XrpcServiceError(\n response.status >= 500\n ? KnownError.InternalServerError\n : KnownError.InvalidResponse,\n response.status,\n response.headers,\n body,\n )\n }\n\n // Check response encoding\n if (schema.output.encoding !== encoding) {\n throw new XrpcServiceError(\n KnownError.InvalidResponse,\n response.status,\n response.headers,\n body,\n `Expected response with content-type ${schema.output.encoding}, got ${encoding}`,\n )\n }\n\n if (schema.output.encoding == null) {\n if (body !== undefined) {\n throw new XrpcServiceError(\n KnownError.InvalidResponse,\n response.status,\n response.headers,\n body,\n `Expected empty response body`,\n )\n }\n\n return new XrpcResponse<M>(\n schema,\n response.status,\n response.headers,\n undefined as XrpcResponseBody<M>,\n )\n } else {\n // @NOTE this should already be enforced by readXrpcResponseBody\n if (body === undefined) {\n throw new XrpcServiceError(\n KnownError.InvalidResponse,\n response.status,\n response.headers,\n body,\n `Expected non-empty response body`,\n )\n }\n\n return new XrpcResponse<M>(\n schema,\n response.status,\n response.headers,\n schema.output.schema == null || options?.validateResponse === false\n ? (body as XrpcResponseBody<M>)\n : (schema.output.schema.parse(body) as XrpcResponseBody<M>),\n )\n }\n}\n\nexport function extractEncoding(headers: Headers): string | undefined {\n const contentType = headers.get('content-type')\n if (!contentType) return undefined\n return contentType.split(';')[0].trim()\n}\n\nexport async function readResponseBody(\n response: Response,\n encoding: string,\n): Promise<LexValue>\nexport async function readResponseBody(\n response: Response,\n encoding: string | undefined,\n): Promise<LexValue | undefined>\nexport async function readResponseBody(\n response: Response,\n encoding: string | undefined,\n): Promise<LexValue | undefined> {\n // When encoding is undefined or empty, we expect no body\n if (encoding == null) {\n if (response.body == null) return undefined\n\n // Let's make sure the body is empty (while avoiding reading it all).\n if (!('getReader' in response.body)) {\n // Some environments may not support body.getReader(), fall back to\n // reading the whole body.\n const buffer = await response.arrayBuffer()\n if (buffer.byteLength === 0) return undefined\n } else {\n const reader = response.body.getReader()\n const next = await reader.read()\n if (next.done) return undefined\n await reader.cancel() // Drain the rest of the (non-empty) body stream\n }\n\n throw new SyntaxError('Content-type is undefined but body is not empty')\n }\n\n if (encoding === 'application/json') {\n // @NOTE Using `lexParse(text)` (instead of `jsonToLex(json)`) here as using\n // a reviver function during JSON.parse should be faster than parsing to\n // JSON then converting to Lex (?)\n\n // @TODO verify statement above\n return lexParse(await response.text())\n }\n\n if (encoding.startsWith('text/')) {\n return response.text()\n }\n\n return new Uint8Array(await response.arrayBuffer())\n}\n"]}
package/jest.config.js ADDED
@@ -0,0 +1,5 @@
1
+ /** @type {import('jest').Config} */
2
+ module.exports = {
3
+ transform: { '^.+\\.(t|j)s$': ['@swc/jest'] },
4
+ moduleNameMapper: { '^(\\.\\.?\\/.+)\\.js$': ['$1.ts', '$1.js'] },
5
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@atproto/lex-client",
3
+ "version": "0.0.0",
4
+ "license": "MIT",
5
+ "description": "HTTP client for interacting with Lexicon based APIs",
6
+ "keywords": [
7
+ "atproto",
8
+ "lexicon",
9
+ "xrpc",
10
+ "client"
11
+ ],
12
+ "homepage": "https://atproto.com",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/bluesky-social/atproto",
16
+ "directory": "packages/lex/lex-client"
17
+ },
18
+ "dependencies": {
19
+ "@atproto/lex-data": "workspace:*",
20
+ "@atproto/lex-json": "workspace:*",
21
+ "@atproto/lex-schema": "workspace:*",
22
+ "tslib": "^2.8.1"
23
+ },
24
+ "devDependencies": {
25
+ "@atproto/lex-cbor": "workspace:*",
26
+ "@atproto/lex-builder": "workspace:*",
27
+ "jest": "^28.1.2"
28
+ },
29
+ "scripts": {
30
+ "prebuild": "node ./scripts/lex-build.mjs",
31
+ "build": "tsc --build tsconfig.build.json",
32
+ "test": "jest"
33
+ },
34
+ "sideEffects": false,
35
+ "type": "commonjs",
36
+ "main": "./dist/index.js",
37
+ "types": "./dist/index.d.ts",
38
+ "exports": {
39
+ ".": {
40
+ "browser": "./dist/index.js",
41
+ "import": "./dist/index.js",
42
+ "require": "./dist/index.js",
43
+ "types": "./dist/index.d.ts"
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,40 @@
1
+ /* eslint-env node */
2
+
3
+ import { build } from '@atproto/lex-builder'
4
+
5
+ Promise.all([
6
+ // For src
7
+ build({
8
+ lexicons: '../../../lexicons',
9
+ out: './src/lexicons',
10
+ override: true,
11
+ include: [
12
+ 'com.atproto.repo.createRecord',
13
+ 'com.atproto.repo.deleteRecord',
14
+ 'com.atproto.repo.getRecord',
15
+ 'com.atproto.repo.putRecord',
16
+ 'com.atproto.repo.listRecords',
17
+ 'com.atproto.repo.uploadBlob',
18
+ ],
19
+ lib: '@atproto/lex-schema',
20
+ pretty: true,
21
+ pureAnnotations: true,
22
+ }),
23
+
24
+ // For tests
25
+ build({
26
+ lexicons: '../../../lexicons',
27
+ out: './tests/lexicons',
28
+ override: true,
29
+ include: [
30
+ 'app.bsky.*',
31
+ 'com.atproto.repo.createRecord',
32
+ 'com.atproto.repo.getRecord',
33
+ ],
34
+ lib: '@atproto/lex-schema',
35
+ pretty: true,
36
+ }),
37
+ ]).catch((err) => {
38
+ console.error('Error building lexicon schemas:', err)
39
+ process.exit(1)
40
+ })
package/src/agent.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { Did } from '@atproto/lex-schema'
2
+
3
+ export interface Agent {
4
+ did?: Did
5
+
6
+ fetchHandler: (
7
+ this: Agent,
8
+ /**
9
+ * The URL (pathname + query parameters) to make the request to, without the
10
+ * origin. The origin (protocol, hostname, and port) must be added by this
11
+ * {@link FetchHandler}, typically based on authentication or other factors.
12
+ */
13
+ path: string,
14
+ init: RequestInit,
15
+ ) => Promise<Response>
16
+ }
17
+
18
+ export type AgentConfig = {
19
+ /**
20
+ * The identifier (DID) of the user represented by this agent.
21
+ */
22
+ did?: Did
23
+
24
+ /**
25
+ * The service URL to make requests to. This can be a string, URL, or a
26
+ * function that returns a string or URL. This is useful for dynamic URLs,
27
+ * such as a service URL that changes based on authentication.
28
+ */
29
+ service: string | URL
30
+
31
+ /**
32
+ * Bring your own fetch implementation. Typically useful for testing, logging,
33
+ * mocking, or adding retries, session management, signatures, proof of
34
+ * possession (DPoP), SSRF protection, etc. Defaults to the global `fetch`
35
+ * function.
36
+ */
37
+ fetch?: typeof globalThis.fetch
38
+ }
39
+
40
+ export type AgentOptions = AgentConfig | string | URL
41
+
42
+ export function buildAgent(options: AgentOptions): Agent {
43
+ const config: AgentConfig =
44
+ typeof options === 'string' || options instanceof URL
45
+ ? { did: undefined, service: options }
46
+ : options
47
+
48
+ const { service, fetch = globalThis.fetch } = config
49
+
50
+ if (typeof fetch !== 'function') {
51
+ throw new TypeError('fetch() is not available in this environment')
52
+ }
53
+
54
+ return {
55
+ get did() {
56
+ return config.did
57
+ },
58
+
59
+ async fetchHandler(path, init) {
60
+ return fetch(new URL(path, service), init)
61
+ },
62
+ }
63
+ }