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

Configure Feed

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

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 ecd9e76a fe8bbc4e

+1190 -26
+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/*