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

Configure Feed

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

xrpc-server documentation

+150 -53
+7
identity/did/memory-cache.ts
··· 11 11 updatedAt: number; 12 12 }; 13 13 14 + /** 15 + * DID cache using memory, recommended for low scale 16 + * use cases. 17 + * 18 + * Not compatible with Cloudflare Workers or serverless/edge 19 + * setups 20 + */ 14 21 export class MemoryCache implements DidCache { 15 22 public staleTTL: number; 16 23 public maxTTL: number;
+4
identity/handle/index.ts
··· 4 4 const SUBDOMAIN = "_atproto"; 5 5 const PREFIX = "did="; 6 6 7 + /** 8 + * Identity resolver for resolving handles to DIDs 9 + * using DNS 10 + */ 7 11 export class HandleResolver { 8 12 public timeout: number; 9 13 private backupNameservers: string[] | undefined;
+57 -13
xrpc-server/auth.ts
··· 4 4 import * as crypto from "@atp/crypto"; 5 5 import { AuthRequiredError } from "./errors.ts"; 6 6 7 - type ServiceJwtParams = { 7 + /** 8 + * Parameters for service JWT creation 9 + * @prop iss The issuer of the key (corresponds to user DID) 10 + * @prop aud The intended audience of the key, the service it's intended for 11 + * @prop iat When the key was issued at 12 + * @prop exp When the key expires 13 + * @prop lxm Lexicon (XRPC) endpoints the key is allowed to be used for 14 + * @prop keypair Signing key to be used to create the JWT token 15 + */ 16 + export type ServiceJwtParams = { 8 17 iss: string; 9 18 aud: string; 10 19 iat?: number; ··· 13 22 keypair: crypto.Keypair; 14 23 }; 15 24 16 - type ServiceJwtHeaders = { 25 + /** 26 + * Headers of a service JWT token 27 + * @prop alg Algorithm used for the JWT token's encoding 28 + */ 29 + export type ServiceJwtHeaders = { 17 30 alg: string; 18 31 } & Record<string, unknown>; 19 32 20 - type ServiceJwtPayload = { 33 + /** 34 + * Parameters for service JWT creation 35 + * @prop iss The issuer of the token (corresponds to user DID) 36 + * @prop aud The intended audience of the token, the service it's intended for 37 + * @prop exp When the key expires 38 + * @prop lxm Lexicon (XRPC) endpoints the token is allowed to be used for 39 + * @prop jti JWT Identifier 40 + */ 41 + export type ServiceJwtPayload = { 21 42 iss: string; 22 43 aud: string; 23 44 exp: number; ··· 25 46 jti?: string; 26 47 }; 27 48 28 - export const createServiceJwt = async ( 49 + /** 50 + * Create a JWT token string for service auth 51 + * @param params Information and permissions given to the service JWT token 52 + */ 53 + export const createServiceJwt = ( 29 54 params: ServiceJwtParams, 30 - ): Promise<string> => { 55 + ): string => { 31 56 const { iss, aud, keypair } = params; 32 57 const iat = params.iat ?? Math.floor(Date.now() / 1e3); 33 58 const exp = params.exp ?? iat + MINUTE / 1e3; ··· 47 72 }); 48 73 const toSignStr = `${jsonToB64Url(header)}.${jsonToB64Url(payload)}`; 49 74 const toSign = ui8.fromString(toSignStr, "utf8"); 50 - const sig = await keypair.sign(toSign); 75 + const sig = keypair.sign(toSign); 51 76 return `${toSignStr}.${ui8.toString(sig, "base64url")}`; 52 77 }; 53 78 ··· 68 93 * fetch(url, { headers: auth.headers }); 69 94 * ``` 70 95 */ 71 - export const createServiceAuthHeaders = async ( 96 + export const createServiceAuthHeaders = ( 72 97 params: ServiceJwtParams, 73 - ): Promise<{ headers: { authorization: string } }> => { 74 - const jwt = await createServiceJwt(params); 98 + ): { headers: { authorization: string } } => { 99 + const jwt = createServiceJwt(params); 75 100 return { 76 101 headers: { authorization: `Bearer ${jwt}` }, 77 102 }; ··· 81 106 return common.utf8ToB64Url(JSON.stringify(json)); 82 107 }; 83 108 109 + /** Verify a message signature against a key */ 84 110 export type VerifySignatureWithKeyFn = ( 85 111 key: string, 86 112 msgBytes: Uint8Array, ··· 88 114 alg: string, 89 115 ) => boolean; 90 116 117 + /** 118 + * Verify a JWT token is valid against the context in which 119 + * it's being used, including the lxm matching the current endpoint, 120 + * the aud matching the service DID, and the key itself matching 121 + * the signing key of the DID who claims to have issued it 122 + * @param jwtStr The JWT token being used 123 + * @param ownDid The DID of the current service, null indicates to skip the audience check 124 + * @param lxm The lexicon permissions of the JWT token, null indicates to skip the lxm check 125 + * @param getSigningKey A function to get the signing key of the issuer 126 + * @param verifySignatureWithKey A method to verify the signature with the JWT token, 127 + */ 91 128 export const verifyJwt = async ( 92 129 jwtStr: string, 93 - ownDid: string | null, // null indicates to skip the audience check 94 - lxm: string | null, // null indicates to skip the lxm check 130 + ownDid: string | null, 131 + lxm: string | null, 95 132 getSigningKey: ( 96 133 iss: string, 97 134 forceRefresh: boolean, ··· 153 190 154 191 let validSig: boolean; 155 192 try { 156 - validSig = await verifySignatureWithKey( 193 + validSig = verifySignatureWithKey( 157 194 signingKey, 158 195 msgBytes, 159 196 sigBytes, ··· 171 208 const freshSigningKey = await getSigningKey(payload.iss, true); 172 209 try { 173 210 validSig = freshSigningKey !== signingKey 174 - ? await verifySignatureWithKey( 211 + ? verifySignatureWithKey( 175 212 freshSigningKey, 176 213 msgBytes, 177 214 sigBytes, ··· 196 233 return payload; 197 234 }; 198 235 236 + /** 237 + * Default method to verify a JWT signature against a key. 238 + * @param key to verify JWT token against 239 + * @param msgBytes Corresponding message 240 + * @param sigBytes JWT signature bytes to verify 241 + * @param alg Encoding algorithm for JWT signature 242 + */ 199 243 export const cryptoVerifySignatureWithKey: VerifySignatureWithKeyFn = ( 200 244 key: string, 201 245 msgBytes: Uint8Array,
+47
xrpc-server/rate-limiter.ts
··· 7 7 import { ResponseType, XRPCError } from "./errors.ts"; 8 8 import { logger } from "./logger.ts"; 9 9 10 + /** Context about the request and response */ 10 11 export interface RateLimiterContext { 11 12 req: Request; 12 13 res?: Response; 13 14 } 14 15 16 + /** Method type to calculate the group key of the request */ 15 17 export type CalcKeyFn<C extends RateLimiterContext = RateLimiterContext> = ( 16 18 ctx: C, 17 19 ) => string | null; 20 + 21 + /** Method function to calculate the amount of points at a given time for a duration */ 18 22 export type CalcPointsFn<C extends RateLimiterContext = RateLimiterContext> = ( 19 23 ctx: C, 20 24 ) => number; 21 25 26 + /** Generic rate limiter interface used for all rate limiters */ 22 27 export interface RateLimiterI< 23 28 C extends RateLimiterContext = RateLimiterContext, 24 29 > { ··· 26 31 reset: RateLimiterReset<C>; 27 32 } 28 33 34 + /** Method options for {@link RateLimiterConsume} */ 29 35 export type RateLimiterConsumeOptions< 30 36 C extends RateLimiterContext = RateLimiterContext, 31 37 > = { ··· 33 39 calcPoints?: CalcPointsFn<C>; 34 40 }; 35 41 42 + /** Generic rate limiter request consume method type */ 36 43 export type RateLimiterConsume< 37 44 C extends RateLimiterContext = RateLimiterContext, 38 45 > = ( ··· 40 47 opts?: RateLimiterConsumeOptions<C>, 41 48 ) => Promise<RateLimiterStatus | RateLimitExceededError | null>; 42 49 50 + /** Information about the current status of the rate limiter 51 + * @prop limit Current point limit 52 + * @prop duration Duration limit is applied 53 + * @prop remainingPoints The remaining points before the limit is reached 54 + * @prop msBeforeNext Milliseconds before the next duration begins 55 + * @prop consumedPoints Amount of points consumed in this duration 56 + * @prop isFirstInDuration Is this the first point in this duration? 57 + */ 43 58 export type RateLimiterStatus = { 44 59 limit: number; 45 60 duration: number; ··· 49 64 isFirstInDuration: boolean; 50 65 }; 51 66 67 + /** Options for {@link RateLimiterReset} */ 52 68 export type RateLimiterResetOptions< 53 69 C extends RateLimiterContext = RateLimiterContext, 54 70 > = { 55 71 calcKey?: CalcKeyFn<C>; 56 72 }; 57 73 74 + /** Generic method type to reset any rate limiter **/ 58 75 export type RateLimiterReset< 59 76 C extends RateLimiterContext = RateLimiterContext, 60 77 > = (ctx: C, opts?: RateLimiterResetOptions<C>) => Promise<void>; 61 78 79 + /** 80 + * Generic options for {@link RateLimiter} 81 + * @prop keyPrefix A prefix to differentiate multiple rate limiters 82 + * @prop durationMs Millisecond duration of the period the limit is applied on 83 + * @prop points Amount of points allowed over the duration 84 + * @prop calcKey Method to calculate what rate limits apply to the request 85 + * @prop calcPoints Method to calculate the group key for the request 86 + * @prop failClosed Should all requests be rejected if the rate limiter fails? 87 + */ 62 88 export type RateLimiterOptions< 63 89 C extends RateLimiterContext = RateLimiterContext, 64 90 > = { ··· 140 166 } 141 167 } 142 168 169 + /** 170 + * Rate limiter implementation using memory, 171 + * recommended for low-scale uses. 172 + * 173 + * Should not be used with Cloudflare Workers 174 + * or serverless/edge use cases. 175 + */ 143 176 export class MemoryRateLimiter< 144 177 C extends RateLimiterContext = RateLimiterContext, 145 178 > extends RateLimiter<C> { ··· 153 186 } 154 187 } 155 188 189 + /** 190 + * Rate limiter implementation using Redis, 191 + * recommended for medium or high scale 192 + * or any serverless/edge use case. 193 + */ 156 194 export class RedisRateLimiter< 157 195 C extends RateLimiterContext = RateLimiterContext, 158 196 > extends RateLimiter<C> { ··· 167 205 } 168 206 } 169 207 208 + /** 209 + * Method type to get rate limiter status 210 + * from a response and a rate limiter instance 211 + * @param limiter Rate limiter instance 212 + * @param res Response returned by rate limiter 213 + */ 170 214 export const formatLimiterStatus = ( 171 215 limiter: RateLimiterAbstract, 172 216 res: RateLimiterRes, ··· 181 225 }; 182 226 }; 183 227 228 + /** Options for {@link WrappedRateLimiter} */ 184 229 export type WrappedRateLimiterOptions< 185 230 C extends RateLimiterContext = RateLimiterContext, 186 231 > = { ··· 276 321 return lowest; 277 322 }; 278 323 324 + /** Options for {@link RouteRateLimiter} */ 279 325 export type RouteRateLimiterOptions< 280 326 C extends RateLimiterContext = RateLimiterContext, 281 327 > = { ··· 349 395 ); 350 396 } 351 397 398 + /** XRPC error returned by rate limiter when rate limit has been exceeded */ 352 399 export class RateLimitExceededError extends XRPCError { 353 400 constructor( 354 401 public status: RateLimiterStatus,
+2 -1
xrpc-server/server.ts
··· 80 80 81 81 /** 82 82 * XRPC server implementation that handles HTTP and WebSocket requests. 83 - * Manages method registration, authentication, rate limiting, and streaming. 83 + * Manages method registration, authentication, rate limiting, and streaming 84 + * with automatic schema validation. 84 85 */ 85 86 export class Server { 86 87 /** The underlying Hono HTTP server instance */
+3
xrpc-server/stream/stream.ts
··· 129 129 } 130 130 } 131 131 132 + /** Ensure that a chunk adheres to the message frame type, 133 + * and isn't an error or unknown frame type 134 + */ 132 135 export function ensureChunkIsMessage(chunk: Uint8Array): MessageFrame<unknown> { 133 136 const frame = Frame.fromBytes(chunk); 134 137 if (frame.isMessage()) {
+1
xrpc-server/stream/types.ts
··· 106 106 } 107 107 } 108 108 109 + /** Different closure codes for normal, abnormal and policy violation closures. */ 109 110 export enum CloseCode { 110 111 /** Normal closure, meaning the purpose for which the connection was established has been fulfilled */ 111 112 Normal = 1000,
+17 -6
xrpc-server/stream/websocket-keepalive.ts
··· 5 5 import { CloseCode, DisconnectError } from "./types.ts"; 6 6 import { iterateBinary } from "./stream.ts"; 7 7 8 - // Public options are web-standard and protocol-safe. 8 + /** 9 + * Options for a {@link WebSocketKeepAlive} instance 10 + * 11 + * @prop getUrl Method to get the current URL of the websocket endpoint 12 + * @prop maxReconnectSeconds Maximum time a request can take to reconnect 13 + * @prop signal Abort signal to send when aborting connection 14 + * 15 + * @prop heartbeatIntervalMs Interval to send provided heartbeatPayload on, 16 + * @prop heartbeatPayload Method to create payload to send for heartbeat 17 + * @prop isPong If provided, we mark alive only when it returns true for a message 18 + * if omitted, *any* message is considered proof of life 19 + * 20 + * @prop onReconnectError Reconnect hook 21 + * 22 + * @prop createSocket Socket factory override (lets you use custom client if needed) 23 + * @prop protocols Override value for accepted protocols 24 + */ 9 25 export type KeepAliveOptions = { 10 26 getUrl: () => Promise<string>; 11 27 maxReconnectSeconds?: number; 12 28 signal?: AbortSignal; 13 29 14 - // Heartbeat (optional, protocol-safe): 15 - // - If provided, we'll send this payload periodically. 16 - // - If `isPong` is provided, we mark alive only when it returns true for a message. 17 - // - If omitted, we consider *any* incoming message as proof of life. 18 30 heartbeatIntervalMs?: number; // default 10 * SECOND 19 31 heartbeatPayload?: () => string | ArrayBuffer | Uint8Array | Blob; 20 32 isPong?: (data: unknown) => boolean; ··· 22 34 // Reconnect hook 23 35 onReconnectError?: (error: unknown, n: number, initialSetup: boolean) => void; 24 36 25 - // Socket factory override (lets you use custom client if needed) 26 37 createSocket?: (url: string, protocols?: string | string[]) => WebSocket; 27 38 protocols?: string | string[]; 28 39 };
+12 -33
xrpc-server/types.ts
··· 80 80 body: unknown; 81 81 }; 82 82 83 - /** 84 - * Result of successful authentication. 85 - */ 83 + /** Result of successful authentication. */ 86 84 export type AuthResult = { 87 85 /** Authentication credentials (e.g., user info, tokens) */ 88 86 credentials: unknown; ··· 90 88 artifacts?: unknown; 91 89 }; 92 90 91 + /** Zod Schema for request headers. */ 93 92 export const headersSchema: z.ZodRecord<z.ZodString, z.ZodString> = z.record( 94 93 z.string(), 95 94 z.string(), 96 95 ); 97 96 98 - /** 99 - * HTTP headers as a record of string key-value pairs. 100 - */ 97 + /** HTTP headers as a record of string key-value pairs. */ 101 98 export type Headers = z.infer<typeof headersSchema>; 102 99 103 100 export const handlerSuccess: z.ZodObject<{ ··· 110 107 headers: headersSchema.optional(), 111 108 }); 112 109 113 - /** 114 - * Successful response from a method handler. 115 - */ 110 + /** Successful response from a method handler. */ 116 111 export type HandlerSuccess = z.infer<typeof handlerSuccess>; 117 112 118 - /** 119 - * Handler response that pipes through a buffer. 120 - */ 113 + /** Handler response that pipes through a buffer. */ 121 114 export type HandlerPipeThroughBuffer = { 122 115 /** Content encoding of the response */ 123 116 encoding: string; ··· 127 120 headers?: Headers; 128 121 }; 129 122 130 - /** 131 - * Handler response that pipes through a stream. 132 - */ 123 + /** Handler response that pipes through a stream. */ 133 124 export type HandlerPipeThroughStream = { 134 125 /** Content encoding of the response */ 135 126 encoding: string; ··· 139 130 headers?: Headers; 140 131 }; 141 132 142 - /** 143 - * Union type for handler responses that pipe data through either a buffer or stream. 144 - */ 133 + /** Union type for handler responses that pipe data through either a buffer or stream. */ 145 134 export type HandlerPipeThrough = 146 135 | HandlerPipeThroughBuffer 147 136 | HandlerPipeThroughStream; 148 137 149 - /** 150 - * Authentication state for a handler context. 151 - */ 138 + /** Authentication state for a handler context. */ 152 139 export type Auth = void | AuthResult; 153 140 154 - /** 155 - * Input data for a handler context. 156 - */ 141 + /** Input data for a handler context. */ 157 142 export type Input = void | HandlerInput; 158 143 159 - /** 160 - * Output data from a handler. 161 - */ 144 + /** Output data from a handler. */ 162 145 export type Output = void | HandlerSuccess | ErrorResult; 163 146 164 147 /** ··· 364 347 return "name" in opts && typeof opts.name === "string"; 365 348 } 366 349 367 - /** 368 - * Options for configuring payload size limits by content type. 369 - */ 350 + /** Options for configuring payload size limits by content type. */ 370 351 export type RouteOptions = { 371 352 /** Maximum size for binary/blob payloads in bytes */ 372 353 blobLimit?: number; ··· 376 357 textLimit?: number; 377 358 }; 378 359 379 - /** 380 - * Simplified route options with only blob limit configuration. 381 - */ 360 + /** Simplified route options with only blob limit configuration. */ 382 361 export type RouteOpts = { 383 362 /** Maximum size for binary/blob payloads in bytes */ 384 363 blobLimit?: number;