0.1.1 • Published 4 months ago

@atcute/xrpc-server v0.1.1

Weekly downloads
-
License
MIT
Repository
github
Last release
4 months ago

@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;