Fastify
A plugin that decorates the request, and hooks that gate the route.
Fastify prefers decorators and hooks over middleware, so the integration is a small plugin rather than a pile of functions. The Node page explains what it calls underneath.
Install
npm install @nulvless/nulvless-js fastify fastify-pluginThe plugin
fastify-plugin stops Fastify from encapsulating the decorators, so routes
registered elsewhere can still see them.
import fp from 'fastify-plugin';
import { createServerClient } from '@nulvless/nulvless-js';
import type { FastifyReply, FastifyRequest } from 'fastify';
export default fp(async (app) => {
const nulvless = createServerClient(
process.env.NULVLESS_URL!,
process.env.NULVLESS_SECRET!,
);
app.decorate('nulvless', nulvless);
app.decorateRequest('userId', null);
app.decorateRequest('session', null);
app.decorate('requireAuth', async (request: FastifyRequest, reply: FastifyReply) => {
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) return reply.code(401).send();
const { data, error } = await nulvless.auth.verify(undefined, {
headers: { Authorization: `Bearer ${token}` },
});
if (error) return reply.code(401).send();
request.userId = data.user.id;
request.session = data.session;
});
app.decorate('requireRole', (roles: string[]) =>
async (request: FastifyRequest, reply: FastifyReply) => {
const token = request.headers.authorization?.replace('Bearer ', '');
const { error } = await nulvless.auth.verify(
{ resource: (request.params as { org: string }).org, roles },
{ headers: { Authorization: `Bearer ${token}` } },
);
if (error) return reply.code(403).send();
},
);
});A hook that sends a reply ends the request — returning reply.code(401).send()
is how you stop the handler from running.
Declare the decorators
import type { NulvlessServerClient, Session } from '@nulvless/nulvless-js';
import type { FastifyReply, FastifyRequest } from 'fastify';
declare module 'fastify' {
interface FastifyInstance {
nulvless: NulvlessServerClient;
requireAuth: (request: FastifyRequest, reply: FastifyReply) => Promise<unknown>;
requireRole: (
roles: string[],
) => (request: FastifyRequest, reply: FastifyReply) => Promise<unknown>;
}
interface FastifyRequest {
userId: string | null;
session: Session | null;
}
}Gate the routes
Attach the hook per route rather than globally, so public routes stay public.
import Fastify from 'fastify';
import nulvlessPlugin from './plugins/nulvless';
const app = Fastify();
await app.register(nulvlessPlugin);
app.get('/me', { preHandler: app.requireAuth }, async (request) => {
return { user: request.userId };
});
app.delete(
'/orgs/:org/keys/:id',
{ preHandler: app.requireRole(['owner', 'admin']) },
async (request, reply) => {
reply.code(204).send();
},
);
await app.listen({ port: 3000 });Act on the user's behalf
Pass request.raw
A FastifyRequest is not a Node IncomingMessage — it wraps one. Give
forwardUserHeaders the raw request or the headers will not be found.
import { forwardUserHeaders } from '@nulvless/nulvless-js';
app.post('/orgs/:org/invites', { preHandler: app.requireAuth }, async (request, reply) => {
const body = request.body as { email: string; role_id: string };
const { data, error } = await app.nulvless.auth.grants.invite(
{ invites: [body], resource: (request.params as { org: string }).org },
{ headers: forwardUserHeaders(request.raw) },
);
if (error) return reply.code(400).send({ error });
return data;
});Require a fresh confirmation
app.delete('/keys/:id', { preHandler: app.requireAuth }, async (request, reply) => {
const { error } = await app.nulvless.auth.sessions.revoke({
id: (request.params as { id: string }).id,
nonce: request.headers['nlv-sudo-nonce'] as string,
});
if (error) return reply.code(403).send({ error });
reply.code(204).send();
});See Sudo mode.