Documentation Index Fetch the complete documentation index at: https://docs.grantex.dev/llms.txt
Use this file to discover all available pages before exploring further.
Install
npm install @grantex/express @grantex/sdk express
Quick Start
Two lines of middleware protect your entire API:
import express from 'express' ;
import { requireGrantToken , requireScopes } from '@grantex/express' ;
const app = express ();
const JWKS_URI = 'https://grantex-auth-dd4mtrt2gq-uc.a.run.app/.well-known/jwks.json' ;
// 1. Verify the grant token
app . use ( '/api' , requireGrantToken ({ jwksUri: JWKS_URI }));
// 2. Enforce scopes per route
app . get ( '/api/calendar' , requireScopes ( 'calendar:read' ), ( req , res ) => {
res . json ({
principalId: req . grant . principalId ,
scopes: req . grant . scopes ,
});
});
app . post ( '/api/email/send' , requireScopes ( 'email:send' ), ( req , res ) => {
res . json ({ sent: true });
});
app . listen ( 3000 );
After requireGrantToken() succeeds, every downstream handler can access req.grant — a fully typed VerifiedGrant object containing the principal, agent, scopes, and timestamps.
How It Works
Client → Authorization: Bearer <grantToken> → Express app
│
requireGrantToken()
│ │
Valid JWT? Invalid/Missing?
│ │
Set req.grant Return 401 JSON
│
requireScopes()
│ │
Has scopes? Missing scopes?
│ │
Call next() Return 403 JSON
requireGrantToken() extracts the Bearer token from the Authorization header
It verifies the RS256 signature against the Grantex JWKS endpoint (offline — cached after first fetch)
On success, req.grant is populated and next() is called
requireScopes() checks that req.grant.scopes contains every required scope
If any scope is missing, it returns a 403 Forbidden response
Factory Pattern
Use createGrantex() to avoid repeating the same options on every route:
import { createGrantex } from '@grantex/express' ;
const grantex = createGrantex ({
jwksUri: 'https://grantex-auth-dd4mtrt2gq-uc.a.run.app/.well-known/jwks.json' ,
clockTolerance: 5 ,
});
// Clean, concise route definitions
app . get ( '/api/calendar' , grantex . requireToken (), grantex . requireScopes ( 'calendar:read' ), handler );
app . get ( '/api/email' , grantex . requireToken (), grantex . requireScopes ( 'email:read' ), handler );
app . post ( '/api/calendar' , grantex . requireToken (), grantex . requireScopes ( 'calendar:write' ), handler );
You can still override options per-route:
app . get ( '/api/special' , grantex . requireToken ({ audience: 'special-app' }), handler );
By default, the middleware reads the Authorization: Bearer <token> header. You can override this:
Cookie
Custom Header
Query Parameter
app . use ( '/api' , requireGrantToken ({
jwksUri: JWKS_URI ,
tokenExtractor : ( req ) => req . cookies ?. grantToken ,
}));
Custom Error Handling
Provide an onError callback to customize error responses:
app . use ( '/api' , requireGrantToken ({
jwksUri: JWKS_URI ,
onError : ( err , req , res , next ) => {
if ( err . code === 'TOKEN_EXPIRED' ) {
res . status ( 401 ). json ({
error: 'session_expired' ,
message: 'Your session has expired. Please re-authorize.' ,
refreshUrl: '/auth/refresh' ,
});
} else {
res . status ( err . statusCode ). json ({
error: err . code ,
message: err . message ,
});
}
},
}));
Error Codes
Code HTTP Status When TOKEN_MISSING401 No token found in the request TOKEN_INVALID401 JWT signature or format is invalid TOKEN_EXPIRED401 JWT exp claim is in the past SCOPE_INSUFFICIENT403 Token is missing one or more required scopes
req.grant Reference
After requireGrantToken() succeeds, req.grant contains a VerifiedGrant:
Field Type Description tokenIdstringJWT jti claim — unique token identifier grantIdstringGrantex grant record ID principalIdstringEnd-user who authorized the agent agentDidstringAgent’s decentralized identifier developerIdstringDeveloper organization ID scopesstring[]Scopes the agent was granted issuedAtnumberToken issued-at (seconds since epoch) expiresAtnumberToken expiry (seconds since epoch) parentAgentDid?stringParent agent DID (delegated grants only) parentGrantId?stringParent grant ID (delegated grants only) delegationDepth?numberDelegation depth (0 = root grant)
Full Example
A complete Express API protected by Grantex:
import express from 'express' ;
import { createGrantex } from '@grantex/express' ;
import type { GrantexRequest } from '@grantex/express' ;
const app = express ();
app . use ( express . json ());
const grantex = createGrantex ({
jwksUri: 'https://grantex-auth-dd4mtrt2gq-uc.a.run.app/.well-known/jwks.json' ,
});
// Public health check — no auth required
app . get ( '/health' , ( req , res ) => {
res . json ({ status: 'ok' });
});
// Protected routes
const api = express . Router ();
api . use ( grantex . requireToken ());
api . get ( '/me' , ( req , res ) => {
const { principalId , agentDid , scopes } = ( req as GrantexRequest ). grant ;
res . json ({ principalId , agentDid , scopes });
});
api . get ( '/calendar' , grantex . requireScopes ( 'calendar:read' ), ( req , res ) => {
const { principalId } = ( req as GrantexRequest ). grant ;
res . json ({ events: getCalendarEvents ( principalId ) });
});
api . post ( '/calendar/events' , grantex . requireScopes ( 'calendar:write' ), ( req , res ) => {
const { principalId } = ( req as GrantexRequest ). grant ;
const event = createEvent ( principalId , req . body );
res . status ( 201 ). json ( event );
});
api . post ( '/email/send' ,
grantex . requireScopes ( 'email:read' , 'email:send' ),
( req , res ) => {
res . json ({ sent: true });
},
);
app . use ( '/api' , api );
app . listen ( 3000 , () => {
console . log ( 'API server running on http://localhost:3000' );
});
function getCalendarEvents ( principalId : string ) {
return [{ id: '1' , title: 'Team standup' , principalId }];
}
function createEvent ( principalId : string , body : unknown ) {
return { id: '2' , principalId , ... ( body as Record < string , unknown >) };
}
TypeScript Support
Import GrantexRequest for typed route handlers:
import type { GrantexRequest } from '@grantex/express' ;
import type { Response } from 'express' ;
function calendarHandler ( req : GrantexRequest , res : Response ) {
// req.grant is fully typed — no casting needed
const { principalId , scopes , agentDid } = req . grant ;
res . json ({ principalId , scopes , agentDid });
}
All types are exported:
import type {
GrantexMiddlewareOptions ,
GrantexRequest ,
GrantexExpressError ,
GrantexExpressErrorCode ,
VerifiedGrant ,
} from '@grantex/express' ;
Requirements
Node.js 18+
Express 4.18+
@grantex/sdk >= 0.1.0