Suite of AT Protocol TypeScript libraries built on web standards
21
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 199 lines 6.0 kB view raw
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";