a collection of lightweight TypeScript packages for AT Protocol, the protocol powering Bluesky
atproto bluesky typescript npm

feat(xrpc-server): initial commit

Squashed commit of the following:

commit f9ea5d0dca042804a0e1e2a1e1e49ea568e059b6
Author: Mary <git@mary.my.id>
Date: Tue May 27 07:49:35 2025 +0700

wip

commit 2782c60f2176d5349cb3efade4f824f4279de8ab
Author: Mary <git@mary.my.id>
Date: Mon May 26 16:13:20 2025 +0700

wip

commit 0e7f468cf5ba20931fdbb729fbf3b735e224382a
Author: Mary <git@mary.my.id>
Date: Sat May 24 16:44:21 2025 +0700

wip

commit e81c4c1a4a84b8ddfb5fcb5e004e14fa6b21d6db
Author: Mary <git@mary.my.id>
Date: Fri May 23 17:39:33 2025 +0700

wip

commit 71fb9ebce01cd1dc0f9188b7736ff033e428c089
Author: Mary <git@mary.my.id>
Date: Mon May 19 06:14:57 2025 +0700

wip

commit dd46d0c58a52c4cec19ad7cb45b4ca73532781c5
Author: Mary <git@mary.my.id>
Date: Sat May 17 08:03:59 2025 +0700

wip

commit 8571bf1d7910cb5e545e1051f6c6f5480eacee99
Author: Mary <git@mary.my.id>
Date: Thu May 15 22:02:03 2025 +0700

wip

mary.my.id ecd9e76a fe8bbc4e

verified
+1 -1
packages/identity/identity-resolver/lib/types.ts
··· 6 6 noCache?: boolean; 7 7 } 8 8 9 - export interface DidDocumentResolver<TMethod extends string> { 9 + export interface DidDocumentResolver<TMethod extends string = string> { 10 10 resolve(did: Did<TMethod>, options?: ResolveDidDocumentOptions): Promise<DidDocument>; 11 11 } 12 12
+77
packages/servers/xrpc-server/README.md
··· 1 + # @atcute/xrpc-server 2 + 3 + ```ts 4 + import { parseCanonicalResourceUri, type Nsid } from '@atcute/lexicons'; 5 + 6 + import { AuthRequiredError, InvalidRequestError, XRPCRouter, json } from '@atcute/xrpc-server'; 7 + import { ServiceJwtVerifier, type VerifiedJwt } from '@atcute/xrpc-server/auth'; 8 + 9 + import { 10 + CompositeDidDocumentResolver, 11 + PlcDidDocumentResolver, 12 + WebDidDocumentResolver, 13 + } from '@atcute/identity-resolver'; 14 + 15 + import { AppBskyFeedGetFeedSkeleton } from '@atcute/bluesky'; 16 + 17 + const SERVICE_DID = 'did:web:feedgen.example.com'; 18 + 19 + const router = new XRPCRouter(); 20 + const jwtVerifier = new ServiceJwtVerifier({ 21 + serviceDid: SERVICE_DID, 22 + resolver: new CompositeDidDocumentResolver({ 23 + methods: { 24 + plc: new PlcDidDocumentResolver(), 25 + web: new WebDidDocumentResolver(), 26 + }, 27 + }), 28 + }); 29 + 30 + const requireAuth = async (request: Request, lxm: Nsid): Promise<VerifiedJwt> => { 31 + const auth = request.headers.get('authorization'); 32 + if (auth === null) { 33 + throw new AuthRequiredError({ description: `missing authorization header` }); 34 + } 35 + if (!auth.startsWith('Bearer ')) { 36 + throw new AuthRequiredError({ description: `invalid authorization scheme` }); 37 + } 38 + 39 + const jwtString = auth.slice('Bearer '.length).trim(); 40 + 41 + const result = await jwtVerifier.verify(jwtString, { lxm }); 42 + if (!result.ok) { 43 + throw new AuthRequiredError(result.error); 44 + } 45 + 46 + return result.value; 47 + }; 48 + 49 + router.add(AppBskyFeedGetFeedSkeleton.mainSchema, { 50 + async handler({ params: { feed }, request }) { 51 + await requireAuth(request, 'app.bsky.feed.getFeedSkeleton'); 52 + 53 + const feedUri = parseCanonicalResourceUri(feed); 54 + 55 + if ( 56 + !feedUri.ok || 57 + feedUri.value.collection !== 'app.bsky.feed.generator' || 58 + feedUri.value.repo !== SERVICE_DID || 59 + feedUri.value.rkey !== 'feed' 60 + ) { 61 + throw new InvalidRequestError({ 62 + error: 'InvalidFeed', 63 + description: `invalid feed`, 64 + }); 65 + } 66 + 67 + return json({ 68 + feed: [ 69 + { post: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3l6oveex3ii2l' }, 70 + { post: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3lpk2lf7k6k2t' }, 71 + ], 72 + }); 73 + }, 74 + }); 75 + 76 + export default router; 77 + ```
+1
packages/servers/xrpc-server/lib/auth/index.ts
··· 1 + export * from './jwt-verifier.js';
+213
packages/servers/xrpc-server/lib/auth/jwt-verifier.ts
··· 1 + import { getPublicKeyFromDidController, verifySig, type FoundPublicKey } from '@atcute/crypto'; 2 + import { getAtprotoVerificationMaterial, type DidDocument } from '@atcute/identity'; 3 + import { type DidDocumentResolver } from '@atcute/identity-resolver'; 4 + import type { Did, Nsid } from '@atcute/lexicons'; 5 + import * as uint8arrays from '@atcute/uint8array'; 6 + 7 + import type { Result } from '../types/misc.js'; 8 + 9 + import { parseJwt, type ParsedJwt } from './jwt.js'; 10 + import type { AuthError } from './types.js'; 11 + 12 + export interface ServiceJwtVerifierOptions { 13 + serviceDid: Did | null; 14 + resolver: DidDocumentResolver; 15 + } 16 + 17 + export interface VerifyJwtOptions { 18 + lxm: Nsid | Nsid[] | null; 19 + } 20 + 21 + export interface VerifiedJwt { 22 + issuer: Did; 23 + lxm: string | undefined; 24 + } 25 + 26 + export class ServiceJwtVerifier { 27 + didDocResolver: DidDocumentResolver; 28 + serviceDid: Did | null; 29 + 30 + constructor(options: ServiceJwtVerifierOptions) { 31 + this.didDocResolver = options.resolver; 32 + this.serviceDid = options.serviceDid; 33 + } 34 + 35 + async #getSigningKey(issuer: Did, noCache: boolean): Promise<Result<FoundPublicKey, AuthError>> { 36 + let didDocument: DidDocument; 37 + let key: FoundPublicKey; 38 + 39 + try { 40 + didDocument = await this.didDocResolver.resolve(issuer, { noCache }); 41 + } catch { 42 + return { 43 + ok: false, 44 + error: { 45 + error: 'UnresolvedDidDocument', 46 + description: `failed to retrieve did document for ${issuer}`, 47 + }, 48 + }; 49 + } 50 + 51 + const controller = getAtprotoVerificationMaterial(didDocument); 52 + if (!controller) { 53 + return { 54 + ok: false, 55 + error: { 56 + error: 'BadJwtIssuer', 57 + description: `${issuer} does not have an atproto verification material`, 58 + }, 59 + }; 60 + } 61 + 62 + try { 63 + key = getPublicKeyFromDidController(controller); 64 + } catch { 65 + return { 66 + ok: false, 67 + error: { 68 + error: 'BadJwtIssuer', 69 + description: `${issuer} has invalid atproto verification material`, 70 + }, 71 + }; 72 + } 73 + 74 + return { ok: true, value: key }; 75 + } 76 + 77 + async #verifySignature(key: FoundPublicKey, jwt: ParsedJwt): Promise<Result<boolean, AuthError>> { 78 + try { 79 + return { 80 + ok: true, 81 + value: await verifySig(key, jwt.signature, jwt.message, { allowMalleableSig: true }), 82 + }; 83 + } catch { 84 + return { 85 + ok: false, 86 + error: { 87 + error: 'BadJwtSignature', 88 + description: `could not verify jwt signature`, 89 + }, 90 + }; 91 + } 92 + } 93 + 94 + async verify(jwtString: string, options?: VerifyJwtOptions): Promise<Result<VerifiedJwt, AuthError>> { 95 + const parsed = parseJwt(jwtString); 96 + if (!parsed.ok) { 97 + return parsed; 98 + } 99 + 100 + const { header, payload } = parsed.value; 101 + 102 + switch (header.typ) { 103 + case 'at+jwt': 104 + case 'refresh+jwt': 105 + case 'dpop+jwt': { 106 + return { 107 + ok: false, 108 + error: { 109 + error: 'BadJwtType', 110 + description: `invalid jwt type`, 111 + }, 112 + }; 113 + } 114 + } 115 + 116 + if (Date.now() / 1_000 > payload.exp) { 117 + return { 118 + ok: false, 119 + error: { 120 + error: 'JwtExpired', 121 + description: `jwt is expired`, 122 + }, 123 + }; 124 + } 125 + 126 + if (this.serviceDid !== undefined && this.serviceDid !== payload.aud) { 127 + return { 128 + ok: false, 129 + error: { 130 + error: 'BadJwtAudience', 131 + description: `jwt audience does not match (expected ${this.serviceDid})`, 132 + }, 133 + }; 134 + } 135 + 136 + if ( 137 + options?.lxm != null && 138 + (typeof options.lxm === 'string' ? options.lxm !== payload.lxm : !options.lxm.includes(payload.lxm!)) 139 + ) { 140 + return { 141 + ok: false, 142 + error: { 143 + error: `BadJwtLexiconMethod`, 144 + description: `jwt lexicon method does not match (expected ${options.lxm})`, 145 + }, 146 + }; 147 + } 148 + 149 + const key = await this.#getSigningKey(payload.iss, false); 150 + if (!key.ok) { 151 + return key; 152 + } 153 + 154 + let isValid = false; 155 + 156 + if (key.value.jwtAlg === header.alg) { 157 + const result = await this.#verifySignature(key.value, parsed.value); 158 + if (!result.ok) { 159 + return result; 160 + } 161 + 162 + isValid = result.value; 163 + } 164 + 165 + if (!isValid) { 166 + // try again, uncached 167 + const freshKey = await this.#getSigningKey(payload.iss, true); 168 + if (!freshKey.ok) { 169 + return freshKey; 170 + } 171 + 172 + // at this point we can't ignore the jwt alg difference 173 + if (freshKey.value.jwtAlg !== header.alg) { 174 + return { 175 + ok: false, 176 + error: { 177 + error: 'BadJwtIssuer', 178 + description: `mismatching cryptographic key format (jwt is ${header.alg})`, 179 + }, 180 + }; 181 + } 182 + 183 + // only revalidate if it's a different key 184 + if (!uint8arrays.equals(freshKey.value.publicKeyBytes, key.value.publicKeyBytes)) { 185 + const result = await this.#verifySignature(key.value, parsed.value); 186 + if (!result.ok) { 187 + return result; 188 + } 189 + 190 + isValid = result.value; 191 + } 192 + } 193 + 194 + if (!isValid) { 195 + // too bad 196 + return { 197 + ok: false, 198 + error: { 199 + error: 'BadJwtSignature', 200 + description: `invalid jwt signature`, 201 + }, 202 + }; 203 + } 204 + 205 + return { 206 + ok: true, 207 + value: { 208 + issuer: payload.iss, 209 + lxm: payload.lxm, 210 + }, 211 + }; 212 + } 213 + }
+120
packages/servers/xrpc-server/lib/auth/jwt.ts
··· 1 + import * as v from '@badrap/valita'; 2 + 3 + import { isDid, isNsid } from '@atcute/lexicons/syntax'; 4 + import { fromBase64 } from '@atcute/multibase'; 5 + import { decodeUtf8From } from '@atcute/uint8array'; 6 + 7 + import type { Result } from '../types/misc.js'; 8 + 9 + import type { AuthError } from './types.js'; 10 + 11 + const encoder = new TextEncoder(); 12 + 13 + const didString = v.string().assert(isDid, `must be a did`); 14 + const nsidString = v.string().assert(isNsid, `must be an nsid`); 15 + 16 + const integer = v.number().assert((input) => input >= 0 && Number.isSafeInteger(input), `must be an integer`); 17 + 18 + const jwtHeader = v.object({ 19 + typ: v.string().optional(), 20 + alg: v.string(), 21 + }); 22 + 23 + export interface JwtHeader extends v.Infer<typeof jwtHeader> {} 24 + 25 + const jwtPayload = v 26 + .object({ 27 + iss: didString, 28 + aud: didString, 29 + exp: integer, 30 + iat: integer.optional(), 31 + lxm: nsidString.optional(), 32 + jti: v.string().optional(), 33 + }) 34 + .assert(({ iat, exp }) => iat === undefined || exp > iat, { 35 + message: `expiry time must be greater than issued time`, 36 + path: ['exp'], 37 + }); 38 + 39 + export interface JwtPayload extends v.Infer<typeof jwtPayload> {} 40 + 41 + export interface ParsedJwt { 42 + header: JwtHeader; 43 + payload: JwtPayload; 44 + message: Uint8Array; 45 + signature: Uint8Array; 46 + } 47 + 48 + const readJwtPortion = <T>(schema: v.Type<T>, input: string): Result<T, AuthError> => { 49 + try { 50 + const raw = decodeUtf8From(fromBase64(input)); 51 + const json = JSON.parse(raw); 52 + 53 + const result = schema.try(json); 54 + if (result.ok) { 55 + return result; 56 + } 57 + } catch {} 58 + 59 + return { 60 + ok: false, 61 + error: { 62 + error: `MalformedJwt`, 63 + description: `jwt is malformed`, 64 + }, 65 + }; 66 + }; 67 + 68 + const readJwtSignature = (input: string): Result<Uint8Array, AuthError> => { 69 + try { 70 + return { ok: true, value: fromBase64(input) }; 71 + } catch {} 72 + 73 + return { 74 + ok: false, 75 + error: { 76 + error: `MalformedJwt`, 77 + description: `jwt is malformed`, 78 + }, 79 + }; 80 + }; 81 + 82 + export const parseJwt = (jwtString: string): Result<ParsedJwt, AuthError> => { 83 + const parts = jwtString.split('.'); 84 + if (parts.length !== 3) { 85 + return { 86 + ok: false, 87 + error: { 88 + error: `MalformedJwt`, 89 + description: `jwt is malformed`, 90 + }, 91 + }; 92 + } 93 + 94 + const [headerString, payloadString, signatureString] = parts; 95 + 96 + const header = readJwtPortion(jwtHeader, headerString); 97 + if (!header.ok) { 98 + return header; 99 + } 100 + 101 + const payload = readJwtPortion(jwtPayload, payloadString); 102 + if (!payload.ok) { 103 + return payload; 104 + } 105 + 106 + const signature = readJwtSignature(signatureString); 107 + if (!signature.ok) { 108 + return signature; 109 + } 110 + 111 + return { 112 + ok: true, 113 + value: { 114 + header: header.value, 115 + payload: payload.value, 116 + message: encoder.encode(`${headerString}.${payloadString}`), 117 + signature: signature.value, 118 + }, 119 + }; 120 + };
+4
packages/servers/xrpc-server/lib/auth/types.ts
··· 1 + export type AuthError = { 2 + error: string; 3 + description: string; 4 + };
+3
packages/servers/xrpc-server/lib/main/index.ts
··· 1 + export * from './response.js'; 2 + export * from './router.js'; 3 + export * from './xrpc-error.js';
+9
packages/servers/xrpc-server/lib/main/response.ts
··· 1 + declare const kJson: unique symbol; 2 + 3 + export type JSONResponse<TData> = Response & { [kJson]: TData }; 4 + 5 + export const json: { 6 + <TData>(data: NoInfer<TData>, init?: ResponseInit): JSONResponse<TData>; 7 + } = (data: any, init?: ResponseInit): any => { 8 + return Response.json(data, init); 9 + };
+246
packages/servers/xrpc-server/lib/main/router.ts
··· 1 + import { safeParse, type XRPCProcedureMetadata, type XRPCQueryMetadata } from '@atcute/lexicons/validations'; 2 + 3 + import type { Literal, Promisable } from '../types/misc.js'; 4 + 5 + import type { ProcedureConfig, QueryConfig, UnknownOperationContext } from './types/operation.js'; 6 + import { createAsyncMiddlewareRunner, type Middleware } from './utils/middlewares.js'; 7 + import { constructParamsHandler } from './utils/request-params.js'; 8 + import { invalidRequest, validationError } from './utils/response.js'; 9 + 10 + import { XRPCError } from './xrpc-error.js'; 11 + 12 + const JSON_TYPE_RE = /^\s*application\/json\s*(?:$|;)/i; 13 + 14 + type InternalRequestContext = { 15 + url: URL; 16 + request: Request; 17 + }; 18 + 19 + type InternalRequestHandler = (context: InternalRequestContext) => Promise<Response>; 20 + 21 + type InternalRouteData = { 22 + method: 'GET' | 'POST'; 23 + handler: InternalRequestHandler; 24 + }; 25 + 26 + export type FetchMiddleware = Middleware<[request: Request], Promise<Response>>; 27 + 28 + export type NotFoundHandler = (request: Request) => Promisable<Response>; 29 + export type ExceptionHandler = (error: unknown, request: Request) => Promisable<Response>; 30 + 31 + export const defaultExceptionHandler: ExceptionHandler = (error: unknown) => { 32 + if (error instanceof XRPCError) { 33 + return error.toResponse(); 34 + } 35 + 36 + if (error instanceof Response) { 37 + return error; 38 + } 39 + 40 + return Response.json( 41 + { error: 'InternalServerError', message: `an exception happened whilst processing this request` }, 42 + { status: 500 }, 43 + ); 44 + }; 45 + 46 + export const defaultNotFoundHandler: NotFoundHandler = () => { 47 + return new Response('Not Found', { status: 404 }); 48 + }; 49 + 50 + export interface XRPCRouterOptions { 51 + middlewares?: FetchMiddleware[]; 52 + handleNotFound?: NotFoundHandler; 53 + handleException?: ExceptionHandler; 54 + } 55 + 56 + export class XRPCRouter { 57 + #handlers: Record<string, InternalRouteData> = {}; 58 + #handleNotFound: NotFoundHandler; 59 + #handleException: ExceptionHandler; 60 + 61 + fetch: (request: Request) => Promise<Response>; 62 + 63 + constructor({ 64 + middlewares = [], 65 + handleException = defaultExceptionHandler, 66 + handleNotFound = defaultNotFoundHandler, 67 + }: XRPCRouterOptions = {}) { 68 + this.fetch = createAsyncMiddlewareRunner([...middlewares, (request) => this.#dispatch(request)]); 69 + this.#handleException = handleException; 70 + this.#handleNotFound = handleNotFound; 71 + } 72 + 73 + async #dispatch(request: Request): Promise<Response> { 74 + const url = new URL(request.url); 75 + const pathname = url.pathname; 76 + 77 + if (!pathname.startsWith('/xrpc/')) { 78 + return this.#handleNotFound(request); 79 + } 80 + 81 + const nsid = pathname.slice('/xrpc/'.length); 82 + const route = this.#handlers[nsid]; 83 + 84 + if (route === undefined) { 85 + return this.#handleNotFound(request); 86 + } 87 + 88 + if (request.method !== route.method) { 89 + return Response.json( 90 + { error: 'InvalidHttpMethod', message: `invalid http method (expected ${route.method})` }, 91 + { status: 405, headers: { allow: `${route.method}` } }, 92 + ); 93 + } 94 + 95 + try { 96 + const response = await route.handler({ 97 + request: request, 98 + url: url, 99 + }); 100 + 101 + return response; 102 + } catch (err) { 103 + return this.#handleException(err, request); 104 + } 105 + } 106 + 107 + add<TQuery extends XRPCQueryMetadata>(query: TQuery, config: QueryConfig<TQuery>): void; 108 + add<TProcedure extends XRPCProcedureMetadata>( 109 + procedure: TProcedure, 110 + config: ProcedureConfig<TProcedure>, 111 + ): void; 112 + add(operation: XRPCQueryMetadata | XRPCProcedureMetadata, config: any): void { 113 + switch (operation.type) { 114 + case 'xrpc_query': { 115 + return this.#addQuery(operation, config); 116 + } 117 + case 'xrpc_procedure': { 118 + return this.#addProcedure(operation, config); 119 + } 120 + } 121 + } 122 + 123 + #addQuery<TQuery extends XRPCQueryMetadata>(query: TQuery, config: QueryConfig<TQuery>): void { 124 + const handleParams = query.params ? constructParamsHandler(query.params) : null; 125 + 126 + const handler = config.handler; 127 + 128 + this.#handlers[query.nsid] = { 129 + method: 'GET', 130 + handler: async ({ request, url }) => { 131 + let params: Record<string, Literal | Literal[]>; 132 + 133 + if (handleParams !== null) { 134 + const result = handleParams(url.searchParams); 135 + if (!result.ok) { 136 + return validationError('params', result); 137 + } 138 + 139 + params = result.value; 140 + } else { 141 + params = {}; 142 + } 143 + 144 + const context: UnknownOperationContext = { 145 + request: request, 146 + params: params, 147 + }; 148 + 149 + const output = await handler(context as any); 150 + 151 + if (output instanceof Response) { 152 + return output; 153 + } 154 + 155 + return new Response(null); 156 + }, 157 + }; 158 + } 159 + 160 + #addProcedure<TProcedure extends XRPCProcedureMetadata>( 161 + procedure: TProcedure, 162 + config: ProcedureConfig<TProcedure>, 163 + ): void { 164 + const handleParams = procedure.params ? constructParamsHandler(procedure.params) : null; 165 + 166 + const requiresInput = procedure.input !== null; 167 + const inputSchema = procedure.input?.type === 'lex' ? procedure.input.schema : null; 168 + 169 + const handler = config.handler; 170 + 171 + this.#handlers[procedure.nsid] = { 172 + method: 'POST', 173 + handler: async ({ request, url }) => { 174 + let params: Record<string, Literal | Literal[]>; 175 + let input: Record<string, unknown> | undefined; 176 + 177 + if (handleParams !== null) { 178 + const result = handleParams(url.searchParams); 179 + if (!result.ok) { 180 + return validationError('params', result); 181 + } 182 + 183 + params = result.value; 184 + } else { 185 + params = {}; 186 + } 187 + 188 + const headers = request.headers; 189 + if (requiresInput) { 190 + if (!isBodyPresent(headers)) { 191 + return invalidRequest(`request body is expected but none was provided`); 192 + } 193 + 194 + if (inputSchema !== null) { 195 + { 196 + const type = headers.get('content-type'); 197 + if (type === null) { 198 + return invalidRequest(`request encoding not provided`); 199 + } 200 + 201 + if (!JSON_TYPE_RE.test(type)) { 202 + return invalidRequest(`invalid request encoding (expected application/json)`); 203 + } 204 + } 205 + 206 + let raw: any; 207 + try { 208 + raw = await request.json(); 209 + } catch (err) { 210 + return invalidRequest(`invalid request body (failed to parse json)`); 211 + } 212 + 213 + const result = safeParse(inputSchema, raw); 214 + if (!result.ok) { 215 + return validationError('input', result); 216 + } 217 + 218 + input = result.value; 219 + } 220 + } else { 221 + if (isBodyPresent(headers)) { 222 + return invalidRequest(`request body is provided when none was expected`); 223 + } 224 + } 225 + 226 + const context: UnknownOperationContext = { 227 + request: request, 228 + params: params, 229 + input: input, 230 + }; 231 + 232 + const output = await handler(context as any); 233 + 234 + if (output instanceof Response) { 235 + return output; 236 + } 237 + 238 + return new Response(null); 239 + }, 240 + }; 241 + } 242 + } 243 + 244 + const isBodyPresent = (headers: Headers): boolean => { 245 + return headers.get('content-length') !== null && headers.get('transfer-encoding') !== null; 246 + };
+76
packages/servers/xrpc-server/lib/main/types/operation.ts
··· 1 + import type { 2 + InferOutput, 3 + ObjectSchema, 4 + XRPCLexBodyParam, 5 + XRPCProcedureMetadata, 6 + XRPCQueryMetadata, 7 + } from '@atcute/lexicons/validations'; 8 + 9 + import type { Literal, Promisable } from '../../types/misc.js'; 10 + 11 + import type { JSONResponse } from '../response.js'; 12 + 13 + export type UnknownOperationContext = { 14 + request: Request; 15 + params: Record<string, Literal | Literal[]>; 16 + input?: Record<string, unknown>; 17 + }; 18 + 19 + // #region Query 20 + 21 + export type QueryContext<TQuery extends XRPCQueryMetadata> = { 22 + request: Request; 23 + } & (TQuery['params'] extends ObjectSchema 24 + ? { 25 + params: InferOutput<TQuery['params']>; 26 + } 27 + : { 28 + // params 29 + }); 30 + 31 + export type QueryHandler<TQuery extends XRPCQueryMetadata> = ( 32 + context: QueryContext<TQuery>, 33 + ) => Promisable< 34 + TQuery['output'] extends null 35 + ? Response | void 36 + : TQuery['output'] extends XRPCLexBodyParam 37 + ? Response | JSONResponse<InferOutput<TQuery['output']['schema']>> 38 + : Response 39 + >; 40 + 41 + export type QueryConfig<TQuery extends XRPCQueryMetadata = XRPCQueryMetadata> = { 42 + handler: QueryHandler<TQuery>; 43 + }; 44 + 45 + // #region Procedure 46 + 47 + export type ProcedureContext<TProcedure extends XRPCProcedureMetadata> = { 48 + request: Request; 49 + } & (TProcedure['params'] extends ObjectSchema 50 + ? { 51 + params: InferOutput<TProcedure['params']>; 52 + } 53 + : { 54 + // params 55 + }) & 56 + (TProcedure['input'] extends XRPCLexBodyParam 57 + ? { 58 + input: InferOutput<TProcedure['input']['schema']>; 59 + } 60 + : { 61 + // input 62 + }); 63 + 64 + export type ProcedureHandler<TProcedure extends XRPCProcedureMetadata> = ( 65 + context: ProcedureContext<TProcedure>, 66 + ) => Promisable< 67 + TProcedure['output'] extends null 68 + ? Response | void 69 + : TProcedure['output'] extends XRPCLexBodyParam 70 + ? Response | JSONResponse<InferOutput<TProcedure['output']['schema']>> 71 + : Response 72 + >; 73 + 74 + export type ProcedureConfig<TProcedure extends XRPCProcedureMetadata = XRPCProcedureMetadata> = { 75 + handler: ProcedureHandler<TProcedure>; 76 + };
+13
packages/servers/xrpc-server/lib/main/utils/middlewares.ts
··· 1 + export type Middleware<TParams extends any[], TReturn> = ( 2 + ...params: [...TParams, next: (...params: TParams) => TReturn] 3 + ) => TReturn; 4 + 5 + export const createAsyncMiddlewareRunner = <TParams extends any[], TReturn>( 6 + middlewares: [...Middleware<TParams, Promise<TReturn>>[], Middleware<TParams, Promise<TReturn>>], 7 + ) => { 8 + // prettier-ignore 9 + return middlewares.reduceRight<(...params: TParams) => Promise<TReturn>>( 10 + (next, run) => (...args) => run(...args, next), 11 + () => Promise.reject(new Error(`middleware chain exhausted`)), 12 + ); 13 + };
+103
packages/servers/xrpc-server/lib/main/utils/request-params.ts
··· 1 + import { 2 + safeParse, 3 + type ArraySchema, 4 + type BaseSchema, 5 + type ObjectSchema, 6 + type OptionalSchema, 7 + type ValidationResult, 8 + } from '@atcute/lexicons/validations'; 9 + 10 + import type { Literal } from '../../types/misc.js'; 11 + 12 + const isArraySchema = (schema: BaseSchema): schema is ArraySchema => { 13 + return schema.type === 'array'; 14 + }; 15 + 16 + const isOptionalSchema = (schema: BaseSchema): schema is OptionalSchema => { 17 + return schema.type === 'optional'; 18 + }; 19 + 20 + const unwrapOptional = (schema: BaseSchema): BaseSchema => { 21 + return isOptionalSchema(schema) ? schema.wrapped : schema; 22 + }; 23 + 24 + const unwrapArray = (schema: BaseSchema): BaseSchema => { 25 + return isArraySchema(schema) ? schema.item : schema; 26 + }; 27 + 28 + const coerceBoolean = (str: string): boolean => { 29 + return str === 'true'; 30 + }; 31 + 32 + const coerceInteger = (str: string): number => { 33 + return Number(str); 34 + }; 35 + 36 + export const constructParamsHandler = <TSchema extends ObjectSchema>(schema: TSchema) => { 37 + const entries = Object.entries(schema.shape).map(([key, schema]) => { 38 + const nonnullable = unwrapOptional(schema); 39 + const singular = unwrapArray(nonnullable); 40 + 41 + let coerce: ((x: string) => Literal) | undefined; 42 + switch (singular.type) { 43 + case 'boolean': { 44 + coerce = coerceBoolean; 45 + break; 46 + } 47 + case 'integer': { 48 + coerce = coerceInteger; 49 + break; 50 + } 51 + } 52 + 53 + return { 54 + key: key, 55 + coerce: coerce, 56 + multiple: isArraySchema(nonnullable), 57 + optional: isOptionalSchema(schema), 58 + }; 59 + }); 60 + 61 + const len = entries.length; 62 + 63 + return (searchParams: URLSearchParams): ValidationResult<Record<string, Literal | Literal[]>> => { 64 + const input: Record<string, Literal | Literal[]> = {}; 65 + 66 + for (let idx = 0; idx < len; idx++) { 67 + const entry = entries[idx]; 68 + const key = entry.key; 69 + const coerce = entry.coerce; 70 + 71 + const raw = searchParams.getAll(key); 72 + const count = raw.length; 73 + 74 + let value: Literal | Literal[]; 75 + 76 + if (entry.multiple) { 77 + if (count === 0 && entry.optional) { 78 + continue; 79 + } 80 + 81 + value = coerce !== undefined ? raw.map(coerce) : raw; 82 + } else { 83 + if (count === 0) { 84 + continue; 85 + } 86 + 87 + value = coerce !== undefined ? coerce(raw[0]) : raw[0]; 88 + } 89 + 90 + /*#__INLINE__*/ set(input, key, value); 91 + } 92 + 93 + return safeParse(schema, input); 94 + }; 95 + }; 96 + 97 + const set = <K extends PropertyKey, V>(obj: Record<K, V>, key: NoInfer<K>, value: NoInfer<V>): void => { 98 + if (key === '__proto__') { 99 + Object.defineProperty(obj, key, { value }); 100 + } else { 101 + obj[key] = value; 102 + } 103 + };
+14
packages/servers/xrpc-server/lib/main/utils/response.ts
··· 1 + import type { Err } from '@atcute/lexicons/validations'; 2 + 3 + export const invalidRequest = (message: string) => { 4 + return Response.json({ error: 'InvalidRequest', message }, { status: 400 }); 5 + }; 6 + 7 + export const validationError = (kind: 'params' | 'input', err: Err): Response => { 8 + const message = `invalid ${kind}: ${err.message}`; 9 + 10 + return Response.json( 11 + { error: 'InvalidRequest', message: message, 'net.kelinci.atcute.issues': err.issues }, 12 + { status: 400 }, 13 + ); 14 + };
+80
packages/servers/xrpc-server/lib/main/xrpc-error.ts
··· 1 + export interface XRPCErrorOptions { 2 + status: number; 3 + error: string; 4 + description?: string; 5 + } 6 + 7 + export class XRPCError extends Error { 8 + /** response status */ 9 + readonly status: number; 10 + 11 + /** error name */ 12 + readonly error: string; 13 + /** error message */ 14 + readonly description?: string; 15 + 16 + constructor({ status, error, description }: XRPCErrorOptions) { 17 + super(`${error} > ${description ?? '(unspecified description)'}`); 18 + 19 + this.status = status; 20 + 21 + this.error = error; 22 + this.description = description; 23 + } 24 + 25 + toResponse(): Response { 26 + return Response.json({ error: this.error, message: this.description }, { status: this.status }); 27 + } 28 + } 29 + 30 + export class InvalidRequestError extends XRPCError { 31 + constructor({ status = 400, error = 'InvalidRequest', description }: Partial<XRPCErrorOptions> = {}) { 32 + super({ status, error, description }); 33 + } 34 + } 35 + 36 + export class AuthRequiredError extends XRPCError { 37 + constructor({ 38 + status = 401, 39 + error = 'AuthenticationRequired', 40 + description, 41 + }: Partial<XRPCErrorOptions> = {}) { 42 + super({ status, error, description }); 43 + } 44 + } 45 + 46 + export class ForbiddenError extends XRPCError { 47 + constructor({ status = 403, error = 'Forbidden', description }: Partial<XRPCErrorOptions> = {}) { 48 + super({ status, error, description }); 49 + } 50 + } 51 + 52 + export class RateLimitExceededError extends XRPCError { 53 + constructor({ status = 429, error = 'RateLimitExceeded', description }: Partial<XRPCErrorOptions> = {}) { 54 + super({ status, error, description }); 55 + } 56 + } 57 + 58 + export class InternalServerError extends XRPCError { 59 + constructor({ status = 500, error = 'InternalServerError', description }: Partial<XRPCErrorOptions> = {}) { 60 + super({ status, error, description }); 61 + } 62 + } 63 + 64 + export class UpstreamFailureError extends XRPCError { 65 + constructor({ status = 502, error = 'UpstreamFailure', description }: Partial<XRPCErrorOptions> = {}) { 66 + super({ status, error, description }); 67 + } 68 + } 69 + 70 + export class NotEnoughResourcesError extends XRPCError { 71 + constructor({ status = 503, error = 'NotEnoughResources', description }: Partial<XRPCErrorOptions> = {}) { 72 + super({ status, error, description }); 73 + } 74 + } 75 + 76 + export class UpstreamTimeoutError extends XRPCError { 77 + constructor({ status = 504, error = 'UpstreamTimeout', description }: Partial<XRPCErrorOptions> = {}) { 78 + super({ status, error, description }); 79 + } 80 + }
+68
packages/servers/xrpc-server/lib/middlewares/cors.ts
··· 1 + import type { FetchMiddleware } from '../main/router.js'; 2 + 3 + export interface CORSOptions { 4 + /** Additional headers to expose to the client */ 5 + exposedHeaders?: string[]; 6 + /** Additional headers to allow */ 7 + allowedHeaders?: string[]; 8 + } 9 + 10 + const DEFAULT_EXPOSED_HEADERS = [ 11 + 'dpop-nonce', 12 + 'www-authenticate', 13 + 14 + 'ratelimit-limit', 15 + 'ratelimit-policy', 16 + 'ratelimit-remaining', 17 + 'ratelimit-reset', 18 + ]; 19 + 20 + const DEFAULT_ALLOWED_HEADERS = [ 21 + 'content-type', 22 + 23 + 'authorization', 24 + 'dpop', 25 + 26 + 'atproto-accept-labelers', 27 + 'atproto-proxy', 28 + ]; 29 + 30 + export const cors = (options: CORSOptions = {}): FetchMiddleware => { 31 + const exposedHeaders = Array.from( 32 + new Set([...DEFAULT_EXPOSED_HEADERS, ...(options.exposedHeaders?.map((h) => h.toLowerCase()) || [])]), 33 + ).sort(); 34 + 35 + const allowedHeaders = Array.from( 36 + new Set([...DEFAULT_ALLOWED_HEADERS, ...(options.allowedHeaders?.map((h) => h.toLowerCase()) || [])]), 37 + ) 38 + .sort() 39 + .join(','); 40 + 41 + return async (request, next) => { 42 + const origin = request.headers.get('origin') || '*'; 43 + 44 + // Handle preflight requests 45 + if (request.method === 'OPTIONS') { 46 + const headers = new Headers(); 47 + headers.set('access-control-max-age', '86400'); 48 + headers.set('access-control-allow-origin', origin); 49 + 50 + if (allowedHeaders) { 51 + headers.set('access-control-allow-headers', allowedHeaders); 52 + } 53 + 54 + return new Response(null, { status: 204, headers: headers }); 55 + } 56 + 57 + const response = await next(request); 58 + 59 + const expose = exposedHeaders.filter((h) => response.headers.has(h)).join(','); 60 + 61 + response.headers.set('access-control-allow-origin', origin); 62 + if (expose.length > 0) { 63 + response.headers.append('access-control-expose-headers', expose); 64 + } 65 + 66 + return response; 67 + }; 68 + };
+4
packages/servers/xrpc-server/lib/types/misc.ts
··· 1 + export type Promisable<T> = T | Promise<T>; 2 + export type Literal = string | number | boolean; 3 + 4 + export type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
+43
packages/servers/xrpc-server/package.json
··· 1 + { 2 + "type": "module", 3 + "name": "@atcute/xrpc-server", 4 + "version": "1.0.0", 5 + "description": "xrpc server", 6 + "license": "MIT", 7 + "repository": { 8 + "url": "https://github.com/mary-ext/atcute", 9 + "directory": "packages/servers/xrpc-server" 10 + }, 11 + "files": [ 12 + "dist/", 13 + "lib/", 14 + "!lib/**/*.bench.ts", 15 + "!lib/**/*.test.ts" 16 + ], 17 + "exports": { 18 + ".": "./dist/main/index.js", 19 + "./auth": "./dist/auth/index.js", 20 + "./middlewares/cors": "./dist/middlewares/cors.js" 21 + }, 22 + "scripts": { 23 + "build": "tsc --project tsconfig.build.json", 24 + "test": "vitest run --coverage", 25 + "prepublish": "rm -rf dist; pnpm run build" 26 + }, 27 + "dependencies": { 28 + "@atcute/crypto": "workspace:^", 29 + "@atcute/identity": "workspace:^", 30 + "@atcute/identity-resolver": "workspace:^", 31 + "@atcute/lexicons": "workspace:^", 32 + "@atcute/multibase": "workspace:^", 33 + "@atcute/uint8array": "workspace:^", 34 + "@badrap/valita": "^0.4.4" 35 + }, 36 + "devDependencies": { 37 + "@atcute/atproto": "workspace:^", 38 + "@atcute/bluesky": "workspace:^", 39 + "@atcute/xrpc-server": "file:", 40 + "@vitest/coverage-v8": "^3.0.4", 41 + "vitest": "^3.0.4" 42 + } 43 + }
+4
packages/servers/xrpc-server/tsconfig.build.json
··· 1 + { 2 + "extends": "./tsconfig.json", 3 + "exclude": ["**/*.test.ts"] 4 + }
+23
packages/servers/xrpc-server/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "types": [], 4 + "outDir": "dist/", 5 + "esModuleInterop": true, 6 + "skipLibCheck": true, 7 + "target": "ESNext", 8 + "allowJs": true, 9 + "resolveJsonModule": true, 10 + "moduleDetection": "force", 11 + "isolatedModules": true, 12 + "verbatimModuleSyntax": true, 13 + "strict": true, 14 + "noImplicitOverride": true, 15 + "noUnusedLocals": true, 16 + "noUnusedParameters": true, 17 + "noFallthroughCasesInSwitch": true, 18 + "module": "NodeNext", 19 + "sourceMap": true, 20 + "declaration": true, 21 + }, 22 + "include": ["lib"], 23 + }
+87 -25
pnpm-lock.yaml
··· 47 47 version: 1.2.13 48 48 vitest: 49 49 specifier: ^3.1.3 50 - version: 3.1.3(@types/node@22.15.17) 50 + version: 3.1.3(@types/node@22.15.17)(yaml@2.8.0) 51 51 52 52 packages/bluesky/richtext-builder: 53 53 dependencies: ··· 138 138 version: link:../../internal/dev-env 139 139 '@vitest/coverage-v8': 140 140 specifier: ^3.1.3 141 - version: 3.1.3(vitest@3.1.3(@types/node@22.15.17)) 141 + version: 3.1.3(vitest@3.1.3(@types/node@22.15.17)(yaml@2.8.0)) 142 142 vitest: 143 143 specifier: ^3.1.3 144 - version: 3.1.3(@types/node@22.15.17) 144 + version: 3.1.3(@types/node@22.15.17)(yaml@2.8.0) 145 145 146 146 packages/clients/jetstream: 147 147 dependencies: ··· 169 169 devDependencies: 170 170 '@vitest/coverage-v8': 171 171 specifier: ^3.0.4 172 - version: 3.1.3(vitest@3.1.3(@types/node@22.15.17)) 172 + version: 3.1.3(vitest@3.1.3(@types/node@22.15.17)(yaml@2.8.0)) 173 173 vitest: 174 174 specifier: ^3.0.4 175 - version: 3.1.3(@types/node@22.15.17) 175 + version: 3.1.3(@types/node@22.15.17)(yaml@2.8.0) 176 176 177 177 packages/definitions/atproto: 178 178 dependencies: ··· 226 226 version: 0.15.6 227 227 vitest: 228 228 specifier: ^3.1.3 229 - version: 3.1.3(@types/node@22.15.17) 229 + version: 3.1.3(@types/node@22.15.17)(yaml@2.8.0) 230 230 231 231 packages/definitions/frontpage: 232 232 dependencies: ··· 245 245 version: link:../../lexicons/lex-cli 246 246 vitest: 247 247 specifier: ^3.1.3 248 - version: 3.1.3(@types/node@22.15.17) 248 + version: 3.1.3(@types/node@22.15.17)(yaml@2.8.0) 249 249 250 250 packages/definitions/leaflet: 251 251 dependencies: ··· 315 315 version: file:packages/definitions/tangled 316 316 vitest: 317 317 specifier: ^3.1.3 318 - version: 3.1.3(@types/node@22.15.17) 318 + version: 3.1.3(@types/node@22.15.17)(yaml@2.8.0) 319 319 320 320 packages/definitions/whitewind: 321 321 dependencies: ··· 485 485 devDependencies: 486 486 '@vitest/coverage-v8': 487 487 specifier: ^3.1.3 488 - version: 3.1.3(vitest@3.1.3(@types/node@22.15.17)) 488 + version: 3.1.3(vitest@3.1.3(@types/node@22.15.17)(yaml@2.8.0)) 489 489 vitest: 490 490 specifier: ^3.1.3 491 - version: 3.1.3(@types/node@22.15.17) 491 + version: 3.1.3(@types/node@22.15.17)(yaml@2.8.0) 492 492 493 493 packages/misc/util-fetch: 494 494 dependencies: ··· 522 522 specifier: workspace:^ 523 523 version: link:../../definitions/atproto 524 524 525 + packages/servers/xrpc-server: 526 + dependencies: 527 + '@atcute/crypto': 528 + specifier: workspace:^ 529 + version: link:../../utilities/crypto 530 + '@atcute/identity': 531 + specifier: workspace:^ 532 + version: link:../../identity/identity 533 + '@atcute/identity-resolver': 534 + specifier: workspace:^ 535 + version: link:../../identity/identity-resolver 536 + '@atcute/lexicons': 537 + specifier: workspace:^ 538 + version: link:../../lexicons/lexicons 539 + '@atcute/multibase': 540 + specifier: workspace:^ 541 + version: link:../../utilities/multibase 542 + '@atcute/uint8array': 543 + specifier: workspace:^ 544 + version: link:../../utilities/uint8array 545 + '@badrap/valita': 546 + specifier: ^0.4.4 547 + version: 0.4.4 548 + devDependencies: 549 + '@atcute/atproto': 550 + specifier: workspace:^ 551 + version: link:../../definitions/atproto 552 + '@atcute/bluesky': 553 + specifier: workspace:^ 554 + version: link:../../definitions/bluesky 555 + '@atcute/xrpc-server': 556 + specifier: 'file:' 557 + version: file:packages/servers/xrpc-server 558 + '@vitest/coverage-v8': 559 + specifier: ^3.0.4 560 + version: 3.1.3(vitest@3.1.3(@types/node@22.15.17)(yaml@2.8.0)) 561 + vitest: 562 + specifier: ^3.0.4 563 + version: 3.1.3(@types/node@22.15.17)(yaml@2.8.0) 564 + 525 565 packages/utilities/car: 526 566 dependencies: 527 567 '@atcute/cbor': ··· 602 642 version: 1.2.13 603 643 '@vitest/coverage-v8': 604 644 specifier: ^3.1.3 605 - version: 3.1.3(vitest@3.1.3(@types/node@22.15.17)) 645 + version: 3.1.3(vitest@3.1.3(@types/node@22.15.17)(yaml@2.8.0)) 606 646 vitest: 607 647 specifier: ^3.1.3 608 - version: 3.1.3(@types/node@22.15.17) 648 + version: 3.1.3(@types/node@22.15.17)(yaml@2.8.0) 609 649 610 650 packages/utilities/multibase: 611 651 dependencies: ··· 621 661 devDependencies: 622 662 vitest: 623 663 specifier: ^3.1.3 624 - version: 3.1.3(@types/node@22.15.17) 664 + version: 3.1.3(@types/node@22.15.17)(yaml@2.8.0) 625 665 626 666 packages/utilities/uint8array: 627 667 devDependencies: ··· 633 673 devDependencies: 634 674 vitest: 635 675 specifier: ^3.1.3 636 - version: 3.1.3(@types/node@22.15.17) 676 + version: 3.1.3(@types/node@22.15.17)(yaml@2.8.0) 637 677 638 678 packages: 639 679 ··· 667 707 668 708 '@atcute/whitewind@file:packages/definitions/whitewind': 669 709 resolution: {directory: packages/definitions/whitewind, type: directory} 710 + 711 + '@atcute/xrpc-server@file:packages/servers/xrpc-server': 712 + resolution: {directory: packages/servers/xrpc-server, type: directory} 670 713 671 714 '@atproto-labs/fetch-node@0.1.8': 672 715 resolution: {integrity: sha512-OOTIhZNPEDDm7kaYU8iYRgzM+D5n3mP2iiBSyKuLakKTaZBL5WwYlUsJVsqX26SnUXtGEroOJEVJ6f66OcG80w==} ··· 3360 3403 resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} 3361 3404 engines: {node: '>=0.4'} 3362 3405 3406 + yaml@2.8.0: 3407 + resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} 3408 + engines: {node: '>= 14.6'} 3409 + hasBin: true 3410 + 3363 3411 yocto-queue@1.2.1: 3364 3412 resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} 3365 3413 engines: {node: '>=12.20'} ··· 3418 3466 dependencies: 3419 3467 '@atcute/lexicons': link:packages/lexicons/lexicons 3420 3468 3469 + '@atcute/xrpc-server@file:packages/servers/xrpc-server': 3470 + dependencies: 3471 + '@atcute/crypto': link:packages/utilities/crypto 3472 + '@atcute/identity': link:packages/identity/identity 3473 + '@atcute/identity-resolver': link:packages/identity/identity-resolver 3474 + '@atcute/lexicons': link:packages/lexicons/lexicons 3475 + '@atcute/multibase': link:packages/utilities/multibase 3476 + '@atcute/uint8array': link:packages/utilities/uint8array 3477 + '@badrap/valita': 0.4.4 3478 + 3421 3479 '@atproto-labs/fetch-node@0.1.8': 3422 3480 dependencies: 3423 3481 '@atproto-labs/fetch': 0.2.2 ··· 5135 5193 dependencies: 5136 5194 undici-types: 6.21.0 5137 5195 5138 - '@vitest/coverage-v8@3.1.3(vitest@3.1.3(@types/node@22.15.17))': 5196 + '@vitest/coverage-v8@3.1.3(vitest@3.1.3(@types/node@22.15.17)(yaml@2.8.0))': 5139 5197 dependencies: 5140 5198 '@ampproject/remapping': 2.3.0 5141 5199 '@bcoe/v8-coverage': 1.0.2 ··· 5149 5207 std-env: 3.9.0 5150 5208 test-exclude: 7.0.1 5151 5209 tinyrainbow: 2.0.0 5152 - vitest: 3.1.3(@types/node@22.15.17) 5210 + vitest: 3.1.3(@types/node@22.15.17)(yaml@2.8.0) 5153 5211 transitivePeerDependencies: 5154 5212 - supports-color 5155 5213 ··· 5160 5218 chai: 5.2.0 5161 5219 tinyrainbow: 2.0.0 5162 5220 5163 - '@vitest/mocker@3.1.3(vite@6.3.5(@types/node@22.15.17))': 5221 + '@vitest/mocker@3.1.3(vite@6.3.5(@types/node@22.15.17)(yaml@2.8.0))': 5164 5222 dependencies: 5165 5223 '@vitest/spy': 3.1.3 5166 5224 estree-walker: 3.0.3 5167 5225 magic-string: 0.30.17 5168 5226 optionalDependencies: 5169 - vite: 6.3.5(@types/node@22.15.17) 5227 + vite: 6.3.5(@types/node@22.15.17)(yaml@2.8.0) 5170 5228 5171 5229 '@vitest/pretty-format@3.1.3': 5172 5230 dependencies: ··· 6684 6742 6685 6743 vary@1.1.2: {} 6686 6744 6687 - vite-node@3.1.3(@types/node@22.15.17): 6745 + vite-node@3.1.3(@types/node@22.15.17)(yaml@2.8.0): 6688 6746 dependencies: 6689 6747 cac: 6.7.14 6690 6748 debug: 4.4.0 6691 6749 es-module-lexer: 1.7.0 6692 6750 pathe: 2.0.3 6693 - vite: 6.3.5(@types/node@22.15.17) 6751 + vite: 6.3.5(@types/node@22.15.17)(yaml@2.8.0) 6694 6752 transitivePeerDependencies: 6695 6753 - '@types/node' 6696 6754 - jiti ··· 6705 6763 - tsx 6706 6764 - yaml 6707 6765 6708 - vite@6.3.5(@types/node@22.15.17): 6766 + vite@6.3.5(@types/node@22.15.17)(yaml@2.8.0): 6709 6767 dependencies: 6710 6768 esbuild: 0.25.4 6711 6769 fdir: 6.4.4(picomatch@4.0.2) ··· 6716 6774 optionalDependencies: 6717 6775 '@types/node': 22.15.17 6718 6776 fsevents: 2.3.3 6777 + yaml: 2.8.0 6719 6778 6720 - vitest@3.1.3(@types/node@22.15.17): 6779 + vitest@3.1.3(@types/node@22.15.17)(yaml@2.8.0): 6721 6780 dependencies: 6722 6781 '@vitest/expect': 3.1.3 6723 - '@vitest/mocker': 3.1.3(vite@6.3.5(@types/node@22.15.17)) 6782 + '@vitest/mocker': 3.1.3(vite@6.3.5(@types/node@22.15.17)(yaml@2.8.0)) 6724 6783 '@vitest/pretty-format': 3.1.3 6725 6784 '@vitest/runner': 3.1.3 6726 6785 '@vitest/snapshot': 3.1.3 ··· 6737 6796 tinyglobby: 0.2.13 6738 6797 tinypool: 1.0.2 6739 6798 tinyrainbow: 2.0.0 6740 - vite: 6.3.5(@types/node@22.15.17) 6741 - vite-node: 3.1.3(@types/node@22.15.17) 6799 + vite: 6.3.5(@types/node@22.15.17)(yaml@2.8.0) 6800 + vite-node: 3.1.3(@types/node@22.15.17)(yaml@2.8.0) 6742 6801 why-is-node-running: 2.3.0 6743 6802 optionalDependencies: 6744 6803 '@types/node': 22.15.17 ··· 6784 6843 ws@8.18.2: {} 6785 6844 6786 6845 xtend@4.0.2: {} 6846 + 6847 + yaml@2.8.0: 6848 + optional: true 6787 6849 6788 6850 yocto-queue@1.2.1: {} 6789 6851
+1
pnpm-workspace.yaml
··· 7 7 - packages/lexicons/* 8 8 - packages/misc/* 9 9 - packages/oauth/* 10 + - packages/servers/* 10 11 - packages/utilities/*