0.1.1 • Published 4 months ago
@atcute/xrpc-server v0.1.1
@atcute/xrpc-server
a small web framework for handling XRPC operations.
quick start
this framework relies on schemas generated by @atcute/lex-cli
, you'd need to follow its
quick start guide on how to set it up.
for this example, we'll define a very simple query operation, one that returns a message greeting the name that's provided to it:
// file: lexicons/com/example/greet.json
{
"lexicon": 1,
"id": "com.example.greet",
"defs": {
"main": {
"type": "query",
"parameters": {
"type": "params",
"required": ["name"],
"properties": {
"name": {
"type": "string"
}
}
},
"output": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["message"],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
}
}
now we can build a server using the TypeScript schemas:
// file: src/index.js
import { XRPCRouter, json } from '@atcute/xrpc-server';
import { cors } from '@atucte/xrpc-server/middlewares/cors';
import { ComExampleGreet } from './lexicons/index.js';
const router = new XRPCRouter({ middlewares: [cors()] });
router.add(ComExampleGreet.mainSchema, {
async handler({ params: { name } }) {
return json({ message: `hello ${name}!` });
},
});
export default router;
on Deno, Bun or Cloudflare Workers, you can export the router directly and expect it to work out of the box.
but for Node.js, you'll need the @hono/node-server
adapter as the router works
with standard Web Request/Response:
// file: src/index.js
import { XRPCRouter } from '@atcute/xrpc-server';
import { serve } from '@hono/node-server';
const router = new XRPCRouter();
// ... handler code ...
serve(
{
fetch: router.fetch,
port: 3000,
},
(info) => {
console.log(`listening on port ${info.port}`);
},
);
kitchen sink
Bluesky feed generator example
import { parseCanonicalResourceUri, type Nsid } from '@atcute/lexicons';
import { AuthRequiredError, InvalidRequestError, XRPCRouter, json } from '@atcute/xrpc-server';
import { ServiceJwtVerifier, type VerifiedJwt } from '@atcute/xrpc-server/auth';
import { cors } from '@atucte/xrpc-server/middlewares/cors';
import {
CompositeDidDocumentResolver,
PlcDidDocumentResolver,
WebDidDocumentResolver,
} from '@atcute/identity-resolver';
import { AppBskyFeedGetFeedSkeleton } from '@atcute/bluesky';
const SERVICE_DID = 'did:web:feedgen.example.com';
const router = new XRPCRouter({
middlewares: [cors()],
});
const jwtVerifier = new ServiceJwtVerifier({
serviceDid: SERVICE_DID,
resolver: new CompositeDidDocumentResolver({
methods: {
plc: new PlcDidDocumentResolver(),
web: new WebDidDocumentResolver(),
},
}),
});
const requireAuth = async (request: Request, lxm: Nsid): Promise<VerifiedJwt> => {
const auth = request.headers.get('authorization');
if (auth === null) {
throw new AuthRequiredError({ description: `missing authorization header` });
}
if (!auth.startsWith('Bearer ')) {
throw new AuthRequiredError({ description: `invalid authorization scheme` });
}
const jwtString = auth.slice('Bearer '.length).trim();
const result = await jwtVerifier.verify(jwtString, { lxm });
if (!result.ok) {
throw new AuthRequiredError(result.error);
}
return result.value;
};
router.add(AppBskyFeedGetFeedSkeleton.mainSchema, {
async handler({ params: { feed }, request }) {
await requireAuth(request, 'app.bsky.feed.getFeedSkeleton');
const feedUri = parseCanonicalResourceUri(feed);
if (
!feedUri.ok ||
feedUri.value.collection !== 'app.bsky.feed.generator' ||
feedUri.value.repo !== SERVICE_DID ||
feedUri.value.rkey !== 'feed'
) {
throw new InvalidRequestError({
error: 'InvalidFeed',
description: `invalid feed`,
});
}
return json({
feed: [
{ post: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3l6oveex3ii2l' },
{ post: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3lpk2lf7k6k2t' },
],
});
},
});
export default router;