Suite of AT Protocol TypeScript libraries built on web standards
1/**
2 * # XRPC Server implementation for atproto services.
3 *
4 * This module provides a Hono-based server implementation for atproto's XRPC protocol,
5 * with support for Lexicon schema validation, authentication, rate limiting, and streaming.
6 * Written in TypeScript with full type safety and designed to work across JavaScript runtimes.
7 *
8 * ## Features
9 * - Full Lexicon schema validation
10 * - Built on Hono for high performance and runtime compatibility
11 * - Authentication (Basic Auth, Bearer tokens, JWT verification)
12 * - Rate limiting (global, shared, and per-route)
13 * - WebSocket streaming support
14 * - Server timing utilities for performance monitoring
15 * - Comprehensive error handling with XRPC error types
16 * - TypeScript-first with complete type definitions
17 *
18 * NOTE: streamMethods (WebSocket streaming endpoints) are currently
19 * only supported in Deno and Cloudflare Workers. This doesn't
20 * include the client-side StreamConnection function, which can be
21 * used in any runtime or browser.
22 *
23 * @example Basic server setup with a simple endpoint
24 * ```ts
25 * import { createServer } from "jsr:@atp/xrpc-server";
26 * import type { LexiconDoc } from "@atp/lexicon";
27 *
28 * const lexicons: LexiconDoc[] = [{
29 * lexicon: 1,
30 * id: "com.example.ping",
31 * defs: {
32 * main: {
33 * type: "query",
34 * parameters: {
35 * type: "params",
36 * properties: { message: { type: "string" } },
37 * },
38 * output: {
39 * encoding: "application/json",
40 * },
41 * },
42 * },
43 * }];
44 *
45 * const server = createServer(lexicons);
46 * server.method("com.example.ping", {
47 * handler: ({ params }) => ({
48 * encoding: "application/json",
49 * body: { message: params.message || "Hello World!" }
50 * })
51 * });
52 *
53 * // Deno
54 * Deno.serve(server.handler.fetch);
55 * ```
56 *
57 * @example Authentication with custom auth verifiers
58 * ```ts
59 * import { createServer, AuthRequiredError } from "jsr:@atp/xrpc-server";
60 *
61 * const server = createServer(lexicons);
62 *
63 * // Basic Auth verification
64 * server.method("com.example.protected", {
65 * auth: async ({ req }) => {
66 * const auth = req.headers.get("Authorization");
67 * if (!auth?.startsWith("Basic ")) {
68 * throw new AuthRequiredError("Basic auth required");
69 * }
70 * const [username, password] = atob(auth.slice(6)).split(":");
71 * if (username !== "admin" || password !== "secret") {
72 * throw new AuthRequiredError("Invalid credentials");
73 * }
74 * return { credentials: { username } };
75 * },
76 * handler: ({ auth }) => ({
77 * encoding: "application/json",
78 * body: { user: auth?.credentials?.username }
79 * })
80 * });
81 *
82 * // Bearer token verification
83 * server.method("com.example.tokenProtected", {
84 * auth: async ({ req }) => {
85 * const token = req.headers.get("Authorization")?.replace("Bearer ", "");
86 * if (!token) throw new AuthRequiredError("Bearer token required");
87 *
88 * // Verify token (implement your own logic)
89 * const user = await verifyToken(token);
90 * return { credentials: user };
91 * },
92 * handler: ({ auth }) => ({
93 * encoding: "application/json",
94 * body: { userId: auth?.credentials?.id }
95 * })
96 * });
97 * ```
98 *
99 * @example Rate limiting configuration
100 * ```ts
101 * import { createServer } from "jsr:@atp/xrpc-server";
102 * import { MemoryRateLimiter } from "@atp/xrpc-server";
103 *
104 * const server = createServer(lexicons, {
105 * rateLimits: {
106 * creator: (opts) => new MemoryRateLimiter(opts),
107 * global: [{
108 * name: "global",
109 * durationMs: 60000, // 1 minute
110 * points: 100 // 100 requests per minute
111 * }],
112 * shared: [{
113 * name: "auth-heavy",
114 * durationMs: 300000, // 5 minutes
115 * points: 20 // 20 requests per 5 minutes
116 * }],
117 * bypass: (ctx) => ctx.auth?.credentials?.isAdmin === true
118 * }
119 * });
120 *
121 * // Per-route rate limiting
122 * server.method("com.example.limited", {
123 * rateLimit: [
124 * { name: "auth-heavy" }, // Use shared rate limiter
125 * { durationMs: 60000, points: 10 } // Additional route-specific limit
126 * ],
127 * handler: () => ({
128 * encoding: "application/json",
129 * body: { status: "ok" }
130 * })
131 * });
132 * ```
133 *
134 * @example Streaming endpoint with proper error handling
135 * ```ts
136 * import { createServer, ErrorFrame } from "jsr:@atp/xrpc-server";
137 *
138 * const server = createServer(lexicons);
139 *
140 * server.streamMethod("com.example.events", {
141 * auth: async ({ req }) => {
142 * // Authenticate streaming connections
143 * const token = req.headers.get("Authorization")?.replace("Bearer ", "");
144 * if (!token) throw new AuthRequiredError("Authentication required");
145 * return { credentials: await verifyToken(token) };
146 * },
147 * handler: async function* ({ auth, signal }) {
148 * try {
149 * const eventStream = subscribeToEvents(auth.credentials.userId);
150 *
151 * while (!signal.aborted) {
152 * const event = await eventStream.next();
153 * if (event.done) break;
154 *
155 * yield {
156 * timestamp: new Date().toISOString(),
157 * event: event.value
158 * };
159 * }
160 * } catch (error) {
161 * yield new ErrorFrame({
162 * error: "StreamError",
163 * message: error.message
164 * });
165 * }
166 * }
167 * });
168 * ```
169 *
170 * @example Error handling and custom error conversion
171 * ```ts
172 * import { createServer, XRPCError, InternalServerError } from "jsr:@atp/xrpc-server";
173 *
174 * const server = createServer(lexicons, {
175 * errorParser: (err) => {
176 * if (err instanceof MyCustomError) {
177 * return new InvalidRequestError(err.message, "CustomError");
178 * }
179 * return XRPCError.fromError(err);
180 * }
181 * });
182 * ```
183 *
184 * @module
185 */
186
187export * from "./types.ts";
188export * from "./auth.ts";
189export * from "./server.ts";
190export * from "./errors.ts";
191
192export * from "./stream/index.ts";
193export * from "./rate-limiter.ts";
194export {
195 parseReqNsid,
196 ServerTimer,
197 type ServerTiming,
198 serverTimingHeader,
199} from "./util.ts";