···117117 };
118118 },
119119 );
120120-121120 s = await createServer(server);
122121 const port = (s as Deno.HttpServer & { port: number }).port;
123122 client = new Client(`http://localhost:${port}`, LEXICONS);
+57
xrpc-server/types.ts
···11import type { Context, HonoRequest, Next } from "hono";
22+import type {
33+ InferMethodInput,
44+ InferMethodMessage,
55+ InferMethodOutput,
66+ InferMethodParams,
77+ Procedure,
88+ Query,
99+ Subscription,
1010+} from "@atp/lex";
211import { z } from "zod";
312import type { ErrorResult, XRPCError } from "./errors.ts";
413import type { CalcKeyFn, CalcPointsFn } from "./rate-limiter.ts";
···277286 A extends AuthResult = AuthResult,
278287 P extends Params = Params,
279288> = AuthVerifier<StreamAuthContext<P>, A>;
289289+290290+export type LexMethod = Procedure | Query | Subscription;
291291+export type LexMethodNamespace<M extends LexMethod = LexMethod> =
292292+ | { readonly main: M }
293293+ | { readonly Main: M };
294294+export type LexMethodLike<M extends LexMethod = LexMethod> =
295295+ | M
296296+ | LexMethodNamespace<M>;
297297+298298+export type LexMethodParams<M extends Procedure | Query | Subscription> =
299299+ InferMethodParams<M>;
300300+301301+export type LexMethodInput<M extends Procedure | Query> = InferMethodInput<
302302+ M,
303303+ ReadableStream<Uint8Array>
304304+>;
305305+306306+export type LexMethodOutput<M extends Procedure | Query> =
307307+ InferMethodOutput<M, Uint8Array | ReadableStream<Uint8Array>> extends
308308+ undefined
309309+ ? InferMethodOutput<M, Uint8Array | ReadableStream<Uint8Array>> | void
310310+ : InferMethodOutput<M, Uint8Array | ReadableStream<Uint8Array>>;
311311+312312+export type LexMethodMessage<M extends Subscription> = InferMethodMessage<M>;
313313+314314+export type LexMethodHandler<
315315+ M extends Procedure | Query,
316316+ A extends Auth = Auth,
317317+> = MethodHandler<A, LexMethodParams<M>, LexMethodInput<M>, LexMethodOutput<M>>;
318318+319319+export type LexMethodConfig<
320320+ M extends Procedure | Query,
321321+ A extends Auth = Auth,
322322+> = MethodConfig<A, LexMethodParams<M>, LexMethodInput<M>, LexMethodOutput<M>>;
323323+324324+export type LexSubscriptionHandler<
325325+ M extends Subscription,
326326+ A extends Auth = Auth,
327327+> = StreamHandler<
328328+ A,
329329+ LexMethodParams<M>,
330330+ LexMethodMessage<M>
331331+>;
332332+333333+export type LexSubscriptionConfig<
334334+ M extends Subscription,
335335+ A extends Auth = Auth,
336336+> = StreamConfig<A, LexMethodParams<M>, LexMethodMessage<M>>;
280337281338/**
282339 * Configuration for server-level rate limits.
+265-35
xrpc-server/util.ts
···66 type LexXrpcQuery,
77 type LexXrpcSubscription,
88} from "@atp/lexicon";
99+import { Procedure, type Query, type Subscription } from "@atp/lex";
910import {
1011 InternalServerError,
1112 InvalidRequestError,
···1718 Awaitable,
1819 HandlerSuccess,
1920 Input,
2121+ LexMethodInput,
2222+ LexMethodOutput,
2323+ LexMethodParams,
2024 Params,
2125 RouteOptions,
2226} from "./types.ts";
···105109 return result;
106110}
107111112112+function getSearchParams(url = ""): URLSearchParams {
113113+ return new URL(url ?? "", "http://x").searchParams;
114114+}
115115+108116/**
109117 * Represents a request-like object with essential HTTP request properties.
110118 * Used for handling both standard HTTP requests and custom request implementations.
···174182 }
175183}
176184185185+export function createLexiconParamsVerifier(
186186+ nsid: string,
187187+ def: LexXrpcQuery | LexXrpcProcedure | LexXrpcSubscription,
188188+ lexicons: Lexicons,
189189+): (req: Request) => Params {
190190+ return (req) => {
191191+ const queryParams = getQueryParams(req.url);
192192+ const params = decodeQueryParams(def, queryParams);
193193+ try {
194194+ return lexicons.assertValidXrpcParams(nsid, params) as Params;
195195+ } catch (e) {
196196+ throw new InvalidRequestError(String(e));
197197+ }
198198+ };
199199+}
200200+201201+export function createSchemaParamsVerifier<
202202+ M extends Procedure | Query | Subscription,
203203+>(
204204+ method: M,
205205+): (req: Request) => LexMethodParams<M> {
206206+ return (req) => {
207207+ try {
208208+ return method.parameters.fromURLSearchParams(
209209+ getSearchParams(req.url),
210210+ ) as LexMethodParams<M>;
211211+ } catch (e) {
212212+ throw new InvalidRequestError(
213213+ e instanceof Error ? e.message : String(e),
214214+ );
215215+ }
216216+ };
217217+}
218218+177219const ENCODING_ANY = "*/*";
178220179221function parseDefEncoding({ encoding }: LexXrpcBody) {
···230272 inputEncoding: string,
231273 options: RouteOptions,
232274): ((req: Request, encoding: string) => Promise<unknown>) | undefined {
233233- if (inputEncoding === ENCODING_ANY) {
234234- // When the lexicon's input encoding is */*, the handler will determine how to process it
275275+ if (
276276+ inputEncoding === ENCODING_ANY ||
277277+ (
278278+ inputEncoding !== "application/json" &&
279279+ inputEncoding !== "json" &&
280280+ !inputEncoding.startsWith("text/") &&
281281+ inputEncoding !== "application/x-www-form-urlencoded"
282282+ )
283283+ ) {
235284 return;
236285 }
237286 const { jsonLimit, textLimit } = options;
···264313 };
265314}
266315316316+async function parseBodyForSchemaValidation(
317317+ req: Request,
318318+ encoding: string,
319319+ options: RouteOptions,
320320+): Promise<unknown> {
321321+ const contentLength = req.headers.get("content-length");
322322+ const bodySize = contentLength ? parseInt(contentLength, 10) : 0;
323323+324324+ if (encoding === "application/json" || encoding === "json") {
325325+ if (options.jsonLimit && bodySize > options.jsonLimit) {
326326+ throw new InvalidRequestError(
327327+ `Request body too large: ${bodySize} bytes exceeds JSON limit of ${options.jsonLimit} bytes`,
328328+ );
329329+ }
330330+ return JSON.parse(await req.text());
331331+ }
332332+333333+ if (
334334+ encoding.startsWith("text/") ||
335335+ encoding === "application/x-www-form-urlencoded"
336336+ ) {
337337+ if (options.textLimit && bodySize > options.textLimit) {
338338+ throw new InvalidRequestError(
339339+ `Request body too large: ${bodySize} bytes exceeds text limit of ${options.textLimit} bytes`,
340340+ );
341341+ }
342342+ return await req.text();
343343+ }
344344+345345+ const body = decodeBodyStream(req, options.blobLimit);
346346+ if (body === null) {
347347+ return new Uint8Array(0);
348348+ }
349349+ return new Uint8Array(await new Response(body).arrayBuffer());
350350+}
351351+352352+function toLexBody(value: unknown): unknown {
353353+ if (value === undefined || value instanceof Uint8Array) {
354354+ return value;
355355+ }
356356+ return jsonToLex(value);
357357+}
358358+267359function decodeBodyStream(
268360 req: Request,
269361 maxSize: number | undefined,
···273365274366 if (!req.body) {
275367 return null;
276276- }
277277-278278- if (!contentEncoding) {
279279- return req.body;
280280- }
281281-282282- if (!contentLength) {
283283- throw new XRPCError(
284284- ResponseType.UnsupportedMediaType,
285285- "unsupported content-encoding",
286286- );
287368 }
288369289370 const contentLengthParsed = contentLength
···305386 );
306387 }
307388308308- let transforms: TransformStream[];
309309- try {
310310- transforms = createDecoders(contentEncoding);
311311- } catch (cause) {
312312- throw new XRPCError(
313313- ResponseType.UnsupportedMediaType,
314314- "unsupported content-encoding",
315315- undefined,
316316- { cause },
317317- );
389389+ let stream: ReadableStream = req.body;
390390+391391+ if (contentEncoding) {
392392+ if (!contentLength) {
393393+ throw new XRPCError(
394394+ ResponseType.UnsupportedMediaType,
395395+ "unsupported content-encoding",
396396+ );
397397+ }
398398+399399+ let transforms: TransformStream[];
400400+ try {
401401+ transforms = createDecoders(contentEncoding);
402402+ } catch (cause) {
403403+ throw new XRPCError(
404404+ ResponseType.UnsupportedMediaType,
405405+ "unsupported content-encoding",
406406+ undefined,
407407+ { cause },
408408+ );
409409+ }
410410+411411+ for (const transform of transforms) {
412412+ stream = stream.pipeThrough(transform);
413413+ }
318414 }
319415320416 if (maxSize !== undefined) {
321321- const maxSizeChecker = new MaxSizeChecker(
322322- maxSize,
323323- () =>
324324- new XRPCError(ResponseType.PayloadTooLarge, "request entity too large"),
417417+ stream = stream.pipeThrough(
418418+ new MaxSizeChecker(
419419+ maxSize,
420420+ () =>
421421+ new XRPCError(
422422+ ResponseType.PayloadTooLarge,
423423+ "request entity too large",
424424+ ),
425425+ ),
325426 );
326326- transforms.push(maxSizeChecker);
327327- }
328328-329329- let stream: ReadableStream = req.body;
330330- for (const transform of transforms) {
331331- stream = stream.pipeThrough(transform);
332427 }
333428334429 return stream;
···508603 * @param def - The lexicon definition for the method
509604 * @returns A function that verifies request input
510605 */
511511-export function createInputVerifier(
606606+export function createLexiconInputVerifier(
512607 nsid: string,
513608 def: LexXrpcProcedure | LexXrpcQuery,
514609 options: RouteOptions,
···569664 // Validate against schema if defined
570665 if (input.schema) {
571666 try {
572572- const lexBody = parsedBody ? jsonToLex(parsedBody) : parsedBody;
667667+ if (parsedBody === undefined) {
668668+ parsedBody = await parseBodyForSchemaValidation(
669669+ req,
670670+ reqEncoding,
671671+ options,
672672+ );
673673+ }
674674+ const lexBody = toLexBody(parsedBody);
573675 parsedBody = lexicons.assertValidXrpcInput(nsid, lexBody);
574676 } catch (e) {
575677 throw new InvalidRequestError(
···587689 return { encoding: reqEncoding, body };
588690 };
589691}
692692+693693+export function createSchemaInputVerifier<M extends Procedure | Query>(
694694+ method: M,
695695+ options: RouteOptions,
696696+): (req: Request) => Awaitable<LexMethodInput<M>> {
697697+ const input = method instanceof Procedure ? method.input : undefined;
698698+699699+ if (!input?.encoding) {
700700+ return (req) => {
701701+ if (getBodyPresence(req) === "present") {
702702+ throw new InvalidRequestError(
703703+ `A request body was provided when none was expected`,
704704+ );
705705+ }
706706+707707+ return undefined as LexMethodInput<M>;
708708+ };
709709+ }
710710+711711+ const { blobLimit } = options;
712712+ const allowedEncodings = parseDefEncoding(input as LexXrpcBody);
713713+ const checkEncoding = allowedEncodings.includes(ENCODING_ANY)
714714+ ? undefined
715715+ : (encoding: string) => allowedEncodings.includes(encoding);
716716+ const bodyParser = createBodyParser(input.encoding, options);
717717+718718+ return async (req) => {
719719+ if (getBodyPresence(req) === "missing") {
720720+ throw new InvalidRequestError(
721721+ `A request body is expected but none was provided`,
722722+ );
723723+ }
724724+725725+ const reqEncoding = parseReqEncoding(req);
726726+ if (checkEncoding && !checkEncoding(reqEncoding)) {
727727+ throw new InvalidRequestError(
728728+ `Wrong request encoding (Content-Type): ${reqEncoding}`,
729729+ );
730730+ }
731731+732732+ let parsedBody: unknown = undefined;
733733+734734+ if (bodyParser) {
735735+ try {
736736+ parsedBody = await bodyParser(req, reqEncoding);
737737+ } catch (e) {
738738+ throw new InvalidRequestError(
739739+ e instanceof Error ? e.message : String(e),
740740+ );
741741+ }
742742+ }
743743+744744+ if (input.schema) {
745745+ try {
746746+ if (parsedBody === undefined) {
747747+ parsedBody = await parseBodyForSchemaValidation(
748748+ req,
749749+ reqEncoding,
750750+ options,
751751+ );
752752+ }
753753+ const lexBody = toLexBody(parsedBody);
754754+ parsedBody = input.schema.parse(lexBody);
755755+ } catch (e) {
756756+ throw new InvalidRequestError(
757757+ e instanceof Error ? e.message : String(e),
758758+ );
759759+ }
760760+ }
761761+762762+ const body = parsedBody !== undefined
763763+ ? parsedBody
764764+ : decodeBodyStream(req, blobLimit);
765765+766766+ return { encoding: reqEncoding, body } as LexMethodInput<M>;
767767+ };
768768+}
769769+770770+export function createSchemaOutputVerifier<M extends Procedure | Query>(
771771+ method: M,
772772+): (output: LexMethodOutput<M>) => void {
773773+ const output = method.output;
774774+775775+ if (!output.encoding) {
776776+ return (handlerOutput) => {
777777+ if (handlerOutput !== undefined) {
778778+ throw new InternalServerError(
779779+ `A response body was provided when none was expected`,
780780+ );
781781+ }
782782+ };
783783+ }
784784+785785+ return (handlerOutput) => {
786786+ if (handlerOutput === undefined) {
787787+ throw new InternalServerError(
788788+ `A response body is expected but none was provided`,
789789+ );
790790+ }
791791+792792+ const result = handlerSuccess.safeParse(handlerOutput);
793793+ if (!result.success) {
794794+ throw new InternalServerError(`Invalid handler output`, undefined, {
795795+ cause: result.error,
796796+ });
797797+ }
798798+799799+ const successOutput = handlerOutput as HandlerSuccess;
800800+801801+ if (!isValidEncoding(output as LexXrpcBody, successOutput.encoding)) {
802802+ throw new InternalServerError(
803803+ `Invalid response encoding: ${successOutput.encoding}`,
804804+ );
805805+ }
806806+807807+ if (output.schema) {
808808+ const bodyResult = output.schema.safeParse(successOutput.body);
809809+ if (!bodyResult.success) {
810810+ throw new InternalServerError(bodyResult.error.message, undefined, {
811811+ cause: bodyResult.error,
812812+ });
813813+ }
814814+ successOutput.body = bodyResult.value;
815815+ }
816816+ };
817817+}
818818+819819+export { createLexiconInputVerifier as createInputVerifier };
590820591821/**
592822 * Sets headers on a Hono context response.