JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 3395
  • Score
    100M100P100Q116964F
  • License MIT

xrpc server

Package Exports

  • @atcute/xrpc-server
  • @atcute/xrpc-server/auth
  • @atcute/xrpc-server/middlewares/cors

Readme

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