Suite of AT Protocol TypeScript libraries built on web standards
1import { z } from "zod";
2import type {
3 AtIdentifierString,
4 AtUriString,
5 CidString,
6 DatetimeString,
7 DidString,
8 HandleString,
9 InferMethodInputBody,
10 InferMethodParams,
11 Procedure,
12 Query,
13 RecordKeyString,
14 TidString,
15} from "@atp/lex";
16
17export type QueryParams = Record<string, unknown>;
18export type HeadersMap = Record<string, string | undefined>;
19export type XrpcMethod = Query | Procedure;
20export type XrpcMethodNamespace<M extends XrpcMethod = XrpcMethod> =
21 | { readonly main: M }
22 | { readonly Main: M };
23export type XrpcMethodLike<M extends XrpcMethod = XrpcMethod> =
24 | M
25 | XrpcMethodNamespace<M>;
26
27type InferXrpcMethod<M extends XrpcMethodLike> = M extends XrpcMethod ? M
28 : M extends { readonly main: infer Inner } ? Inner extends XrpcMethod ? Inner
29 : never
30 : M extends { readonly Main: infer Inner } ? Inner extends XrpcMethod ? Inner
31 : never
32 : never;
33
34type Digit =
35 | "0"
36 | "1"
37 | "2"
38 | "3"
39 | "4"
40 | "5"
41 | "6"
42 | "7"
43 | "8"
44 | "9";
45type LowerAlpha =
46 | "a"
47 | "b"
48 | "c"
49 | "d"
50 | "e"
51 | "f"
52 | "g"
53 | "h"
54 | "i"
55 | "j"
56 | "k"
57 | "l"
58 | "m"
59 | "n"
60 | "o"
61 | "p"
62 | "q"
63 | "r"
64 | "s"
65 | "t"
66 | "u"
67 | "v"
68 | "w"
69 | "x"
70 | "y"
71 | "z";
72type UpperAlpha = Uppercase<LowerAlpha>;
73type Alpha = LowerAlpha | UpperAlpha;
74type DidChar = Alpha | Digit | "." | "_" | ":" | "%" | "-";
75type DidEndChar = Alpha | Digit | "." | "_" | "-";
76type HandleChar = Alpha | Digit | "-";
77type RecordKeyChar = Alpha | Digit | "_" | "~" | "." | ":" | "-";
78type TidInitialChar =
79 | "2"
80 | "3"
81 | "4"
82 | "5"
83 | "6"
84 | "7"
85 | "a"
86 | "b"
87 | "c"
88 | "d"
89 | "e"
90 | "f"
91 | "g"
92 | "h"
93 | "i"
94 | "j";
95type TidChar =
96 | "2"
97 | "3"
98 | "4"
99 | "5"
100 | "6"
101 | "7"
102 | LowerAlpha;
103type Base32Char = LowerAlpha | "2" | "3" | "4" | "5" | "6" | "7";
104type Base58Char =
105 | "1"
106 | "2"
107 | "3"
108 | "4"
109 | "5"
110 | "6"
111 | "7"
112 | "8"
113 | "9"
114 | "A"
115 | "B"
116 | "C"
117 | "D"
118 | "E"
119 | "F"
120 | "G"
121 | "H"
122 | "J"
123 | "K"
124 | "L"
125 | "M"
126 | "N"
127 | "P"
128 | "Q"
129 | "R"
130 | "S"
131 | "T"
132 | "U"
133 | "V"
134 | "W"
135 | "X"
136 | "Y"
137 | "Z"
138 | "a"
139 | "b"
140 | "c"
141 | "d"
142 | "e"
143 | "f"
144 | "g"
145 | "h"
146 | "i"
147 | "j"
148 | "k"
149 | "m"
150 | "n"
151 | "o"
152 | "p"
153 | "q"
154 | "r"
155 | "s"
156 | "t"
157 | "u"
158 | "v"
159 | "w"
160 | "x"
161 | "y"
162 | "z";
163
164type IsLiteralString<S extends string> = string extends S ? false : true;
165
166type IsChars<S extends string, Allowed extends string> =
167 IsLiteralString<S> extends false ? false
168 : S extends "" ? true
169 : S extends `${infer First}${infer Rest}`
170 ? First extends Allowed ? IsChars<Rest, Allowed>
171 : false
172 : false;
173
174type IsDidMethod<S extends string> = IsLiteralString<S> extends false ? false
175 : S extends `${infer First}${infer Rest}`
176 ? First extends LowerAlpha
177 ? Rest extends "" ? true : IsChars<Rest, LowerAlpha>
178 : false
179 : false;
180
181type IsDidIdentifier<S extends string> = IsLiteralString<S> extends false
182 ? false
183 : S extends `${infer First}${infer Rest}`
184 ? Rest extends "" ? First extends DidEndChar ? true : false
185 : First extends DidChar ? IsDidIdentifier<Rest>
186 : false
187 : false;
188
189type IsDidLiteral<S extends string> = S extends
190 `did:${infer Method}:${infer Id}`
191 ? IsDidMethod<Method> extends true ? IsDidIdentifier<Id>
192 : false
193 : false;
194
195type IsHandleLabel<S extends string> = IsLiteralString<S> extends false ? false
196 : S extends `${infer First}${infer Rest}`
197 ? First extends Alpha | Digit
198 ? Rest extends "" ? true : IsHandleLabelTail<Rest>
199 : false
200 : false;
201
202type IsHandleLabelTail<S extends string> = S extends
203 `${infer First}${infer Rest}`
204 ? Rest extends "" ? First extends Alpha | Digit ? true : false
205 : First extends HandleChar ? IsHandleLabelTail<Rest>
206 : false
207 : false;
208
209type IsFinalHandleLabel<S extends string> = IsLiteralString<S> extends false
210 ? false
211 : S extends `${infer First}${infer Rest}`
212 ? First extends Alpha ? Rest extends "" ? true : IsHandleLabelTail<Rest>
213 : false
214 : false;
215
216type IsHandleParts<S extends string> = S extends `${infer Label}.${infer Rest}`
217 ? IsHandleLabel<Label> extends true ? IsHandleParts<Rest> : false
218 : IsFinalHandleLabel<S>;
219
220type IsHandleLiteral<S extends string> = S extends `${string}.${string}`
221 ? IsHandleParts<S>
222 : false;
223
224type IsAtIdentifierLiteral<S extends string> = IsDidLiteral<S> extends true
225 ? true
226 : IsHandleLiteral<S>;
227
228type IsRecordKeyLiteral<S extends string> = IsLiteralString<S> extends false
229 ? false
230 : S extends "." | ".." ? false
231 : S extends `${infer _First}${infer _Rest}` ? IsChars<S, RecordKeyChar>
232 : false;
233
234type IsExactChars<
235 S extends string,
236 Allowed extends string,
237 Length extends number,
238 Count extends unknown[] = [],
239> = Count["length"] extends Length ? (S extends "" ? true : false)
240 : S extends `${infer First}${infer Rest}`
241 ? First extends Allowed
242 ? IsExactChars<Rest, Allowed, Length, [...Count, unknown]>
243 : false
244 : false;
245
246type IsTidLiteral<S extends string> = S extends `${infer First}${infer Rest}`
247 ? First extends TidInitialChar ? IsExactChars<Rest, TidChar, 12> : false
248 : false;
249
250type IsDatetimeDate<S extends string> = S extends
251 `${infer Year}-${infer Month}-${infer Day}`
252 ? IsExactChars<Year, Digit, 4> extends true
253 ? IsExactChars<Month, Digit, 2> extends true ? IsExactChars<Day, Digit, 2>
254 : false
255 : false
256 : false;
257
258type IsDatetimeTime<S extends string> = S extends
259 `${infer Hour}:${infer Minute}:${infer Second}`
260 ? IsExactChars<Hour, Digit, 2> extends true
261 ? IsExactChars<Minute, Digit, 2> extends true
262 ? IsExactChars<Second, Digit, 2>
263 : false
264 : false
265 : false;
266
267type IsDatetimeOffset<S extends string> = S extends
268 `${infer Hour}:${infer Minute}`
269 ? IsExactChars<Hour, Digit, 2> extends true ? IsExactChars<Minute, Digit, 2>
270 : false
271 : false;
272
273type IsDatetimeTail<S extends string> = S extends
274 `${infer Time}.${infer Fraction}Z`
275 ? IsDatetimeTime<Time> extends true ? IsChars<Fraction, Digit> : false
276 : S extends `${infer Time}Z` ? IsDatetimeTime<Time>
277 : S extends `${infer Time}+${infer Offset}`
278 ? IsDatetimeTime<Time> extends true ? IsDatetimeOffset<Offset> : false
279 : S extends `${infer Time}-${infer Offset}`
280 ? IsDatetimeTime<Time> extends true ? IsDatetimeOffset<Offset> : false
281 : S extends `${infer Time}.${infer Fraction}+${infer Offset}`
282 ? IsDatetimeTime<Time> extends true
283 ? IsChars<Fraction, Digit> extends true ? IsDatetimeOffset<Offset> : false
284 : false
285 : S extends `${infer Time}.${infer Fraction}-${infer Offset}`
286 ? IsDatetimeTime<Time> extends true
287 ? IsChars<Fraction, Digit> extends true ? IsDatetimeOffset<Offset> : false
288 : false
289 : false;
290
291type IsDatetimeLiteral<S extends string> = S extends
292 `${infer Date}T${infer Tail}`
293 ? IsDatetimeDate<Date> extends true ? IsDatetimeTail<Tail> : false
294 : false;
295
296type AtUriHost<S extends string> = S extends `at://${infer Host}/${string}`
297 ? Host
298 : S extends `at://${infer Host}?${string}` ? Host
299 : S extends `at://${infer Host}#${string}` ? Host
300 : S extends `at://${infer Host}` ? Host
301 : never;
302
303type IsAtUriLiteral<S extends string> = [AtUriHost<S>] extends [never] ? false
304 : AtUriHost<S> extends infer Host extends string
305 ? Host extends "" ? false : IsAtIdentifierLiteral<Host>
306 : false;
307
308type IsCidLiteral<S extends string> = S extends `b${infer Rest}`
309 ? IsChars<Rest, Base32Char>
310 : S extends `z${infer Rest}` ? IsChars<Rest, Base58Char>
311 : S extends `Qm${infer Rest}` ? IsChars<Rest, Base58Char>
312 : false;
313
314type ValidateStringLiteral<Actual, Expected> = Actual extends string
315 ? IsLiteralString<Actual> extends false ? never
316 : [Expected] extends [DidString] ? IsDidLiteral<Actual> extends true ? Actual
317 : never
318 : [Expected] extends [HandleString]
319 ? IsHandleLiteral<Actual> extends true ? Actual : never
320 : [Expected] extends [AtIdentifierString]
321 ? IsAtIdentifierLiteral<Actual> extends true ? Actual : never
322 : [Expected] extends [RecordKeyString]
323 ? IsRecordKeyLiteral<Actual> extends true ? Actual : never
324 : [Expected] extends [TidString] ? IsTidLiteral<Actual> extends true ? Actual
325 : never
326 : [Expected] extends [DatetimeString]
327 ? IsDatetimeLiteral<Actual> extends true ? Actual : never
328 : [Expected] extends [AtUriString]
329 ? IsAtUriLiteral<Actual> extends true ? Actual : never
330 : [Expected] extends [CidString] ? IsCidLiteral<Actual> extends true ? Actual
331 : never
332 : never
333 : never;
334
335type ValidateCallValue<Actual, Expected> = [Actual] extends [Expected] ? Actual
336 : undefined extends Expected
337 ? ValidateCallValue<Actual, Exclude<Expected, undefined>>
338 : Expected extends string ? ValidateStringLiteral<Actual, Expected>
339 : Expected extends readonly (infer Value)[]
340 ? Actual extends readonly unknown[]
341 ? { [K in keyof Actual]: ValidateCallValue<Actual[K], Value> }
342 : never
343 : Expected extends object
344 ? Actual extends object ? ValidateCallObject<Actual, Expected>
345 : never
346 : never;
347
348type ValidateCallObject<Actual, Expected> =
349 & {
350 [K in keyof Expected]: K extends keyof Actual
351 ? ValidateCallValue<Actual[K], Expected[K]>
352 : Expected[K];
353 }
354 & {
355 [K in Exclude<keyof Actual, keyof Expected>]: never;
356 };
357
358export type {
359 /**
360 * @deprecated not to be confused with the WHATWG Headers constructor.
361 * Use {@linkcode HeadersMap} instead.
362 */
363 HeadersMap as Headers,
364};
365
366export type Gettable<T> = T | (() => T);
367
368export interface CallOptions {
369 encoding?: string;
370 signal?: AbortSignal;
371 headers?: HeadersMap;
372}
373
374export type BinaryBodyInit =
375 | Uint8Array
376 | ArrayBuffer
377 | ArrayBufferView
378 | Blob
379 | ReadableStream<Uint8Array>
380 | AsyncIterable<Uint8Array>
381 | string;
382
383export interface XrpcCallOptions<
384 M extends XrpcMethodLike = XrpcMethod,
385> extends CallOptions {
386 params?: InferMethodParams<InferXrpcMethod<M>>;
387 body?: InferXrpcMethod<M> extends Procedure
388 ? InferMethodInputBody<InferXrpcMethod<M>, BinaryBodyInit>
389 : undefined;
390 validateRequest?: boolean;
391 validateResponse?: boolean;
392}
393
394export type XrpcCallCompatibleOptions<
395 M extends XrpcMethodLike = XrpcMethod,
396 O = XrpcCallOptions<M>,
397> = ValidateCallValue<O, XrpcCallOptions<M>>;
398
399export const errorResponseBody: z.ZodObject<{
400 error: z.ZodOptional<z.ZodString>;
401 message: z.ZodOptional<z.ZodString>;
402}> = z.object({
403 error: z.string().optional(),
404 message: z.string().optional(),
405});
406export type ErrorResponseBody = z.infer<typeof errorResponseBody>;
407
408export enum ResponseType {
409 /**
410 * Network issue, unable to get response from the server.
411 */
412 Unknown = 1,
413 /**
414 * Response failed lexicon validation.
415 */
416 InvalidResponse = 2,
417 Success = 200,
418 InvalidRequest = 400,
419 AuthenticationRequired = 401,
420 Forbidden = 403,
421 XRPCNotSupported = 404,
422 NotAcceptable = 406,
423 PayloadTooLarge = 413,
424 UnsupportedMediaType = 415,
425 RateLimitExceeded = 429,
426 InternalServerError = 500,
427 MethodNotImplemented = 501,
428 UpstreamFailure = 502,
429 NotEnoughResources = 503,
430 UpstreamTimeout = 504,
431}
432
433export function httpResponseCodeToEnum(status: number): ResponseType {
434 if (status in ResponseType) {
435 return status;
436 } else if (status >= 100 && status < 200) {
437 return ResponseType.XRPCNotSupported;
438 } else if (status >= 200 && status < 300) {
439 return ResponseType.Success;
440 } else if (status >= 300 && status < 400) {
441 return ResponseType.XRPCNotSupported;
442 } else if (status >= 400 && status < 500) {
443 return ResponseType.InvalidRequest;
444 } else {
445 return ResponseType.InternalServerError;
446 }
447}
448
449export function httpResponseCodeToName(status: number): string {
450 return ResponseType[httpResponseCodeToEnum(status)];
451}
452
453/**
454 * Error messages corresponding to XRPC error codes.
455 */
456export const ResponseTypeStrings: Record<ResponseType, string> = {
457 [ResponseType.Unknown]: "Unknown",
458 [ResponseType.InvalidResponse]: "Invalid Response",
459 [ResponseType.Success]: "Success",
460 [ResponseType.InvalidRequest]: "Invalid Request",
461 [ResponseType.AuthenticationRequired]: "Authentication Required",
462 [ResponseType.Forbidden]: "Forbidden",
463 [ResponseType.XRPCNotSupported]: "XRPC Not Supported",
464 [ResponseType.NotAcceptable]: "Not Acceptable",
465 [ResponseType.PayloadTooLarge]: "Payload Too Large",
466 [ResponseType.UnsupportedMediaType]: "Unsupported Media Type",
467 [ResponseType.RateLimitExceeded]: "Rate Limit Exceeded",
468 [ResponseType.InternalServerError]: "Internal Server Error",
469 [ResponseType.MethodNotImplemented]: "Method Not Implemented",
470 [ResponseType.UpstreamFailure]: "Upstream Failure",
471 [ResponseType.NotEnoughResources]: "Not Enough Resources",
472 [ResponseType.UpstreamTimeout]: "Upstream Timeout",
473} as const satisfies Record<ResponseType, string>;
474
475export function httpResponseCodeToString(status: number): string {
476 return ResponseTypeStrings[httpResponseCodeToEnum(status)];
477}
478
479/**
480 * Response type of a successful XRPC request.
481 */
482export class XRPCResponse {
483 readonly success: true = true;
484
485 constructor(
486 public data: any,
487 public headers: HeadersMap,
488 ) {}
489}
490
491/**
492 * Response type of a failed XRPC request with details of the error.
493 */
494export class XRPCError extends Error {
495 readonly success: false = false;
496
497 public status: ResponseType;
498
499 constructor(
500 statusCode: number,
501 public error: string = httpResponseCodeToName(statusCode),
502 message?: string,
503 public headers?: HeadersMap,
504 options?: ErrorOptions,
505 ) {
506 super(message || error || httpResponseCodeToString(statusCode), options);
507
508 this.status = httpResponseCodeToEnum(statusCode);
509
510 // Pre 2022 runtimes won't handle the "options" constructor argument
511 const cause = options?.cause;
512 if (this.cause === undefined && cause !== undefined) {
513 this.cause = cause;
514 }
515 }
516
517 static from(cause: unknown, fallbackStatus?: ResponseType): XRPCError {
518 if (cause instanceof XRPCError) {
519 return cause;
520 }
521
522 // Type cast the cause to an Error if it is one
523 const causeErr = cause instanceof Error ? cause : undefined;
524
525 // Try and find a Response object in the cause
526 const causeResponse: Response | undefined = cause instanceof Response
527 ? cause
528 : (cause && typeof cause === "object" && "response" in cause &&
529 cause.response instanceof Response)
530 ? cause.response
531 : undefined;
532
533 const statusCode: unknown =
534 // Extract status code from "http-errors" like errors
535 (causeErr && typeof causeErr === "object" && "statusCode" in causeErr)
536 ? causeErr.statusCode
537 : (causeErr && typeof causeErr === "object" && "status" in causeErr)
538 ? causeErr.status
539 // Use the status code from the response object as fallback
540 : causeResponse?.status;
541
542 // Convert the status code to a ResponseType
543 const status: ResponseType = typeof statusCode === "number"
544 ? httpResponseCodeToEnum(statusCode)
545 : fallbackStatus ?? ResponseType.Unknown;
546
547 const message = causeErr?.message ?? String(cause);
548
549 const headers = causeResponse
550 ? Object.fromEntries(causeResponse.headers.entries())
551 : undefined;
552
553 return new XRPCError(status, undefined, message, headers, { cause });
554 }
555}
556
557/**
558 * Error for an invalid response from an XRPC request.
559 * Caused by a validation error with the lexicon schema
560 * matching the NSID of the endpoint.
561 */
562export class XRPCInvalidResponseError extends XRPCError {
563 constructor(
564 public lexiconNsid: string,
565 public validationError: Error,
566 public responseBody: unknown,
567 ) {
568 super(
569 ResponseType.InvalidResponse,
570 // @NOTE: This is probably wrong and should use ResponseTypeNames instead.
571 // But it would mean a breaking change.
572 ResponseTypeStrings[ResponseType.InvalidResponse],
573 `The server gave an invalid response and may be out of date.`,
574 undefined,
575 { cause: validationError },
576 );
577 }
578}