Server-side SDK
The @aero2/sdk/server subpath gives you the OAuth2 server-side primitives you need to:
- Verify access tokens issued by your Aero2 application (resource-server flow).
- Mint and refresh
client_credentialstokens for backend-to-backend calls (M2M flow). - Introspect tokens via the RFC 7662 endpoint when you need real-time revocation checks.
Safe to import from Node 18+, Cloudflare Workers, Bun, Deno — no browser-only globals are referenced.
Install
npm install @aero2/sdkcreateResourceServer
Verifies the JWT signature, issuer, audience, and expiry against the issuer's JWKS. The discovery doc and JWKS are cached in memory — only the very first request per (issuer) does a network round-trip.
import { createResourceServer } from "@aero2/sdk/server";
const rs = createResourceServer({
issuer: "https://acme.aero2.dev",
audience: "my-service", // optional but recommended
clockToleranceSec: 30, // optional, default 30
});
const claims = await rs.verifyToken(bearerToken);
// claims.sub, claims.scope, claims.client_id, claims.aud, ...verifyToken throws Aero2TokenError for any failure (signature, issuer, audience, exp, malformed). Resource servers should treat that as a 401 and not surface the error message to clients (it's intentionally generic, but still a leakage surface).
Per-call audience override
When one service handles multiple audiences:
await rs.verifyToken(token, { audience: "orders-api" });
await rs.verifyToken(token, { audience: "billing-api" });createM2MClient
Mints client_credentials access tokens for service-to-service calls. The token is cached in memory until ~60s before its exp, then refreshed transparently. Concurrent callers are de-duplicated to a single fetch.
import { createM2MClient } from "@aero2/sdk/server";
const m2m = createM2MClient({
issuer: "https://acme.aero2.dev",
clientId: process.env.M2M_CLIENT_ID!,
clientSecret: process.env.M2M_CLIENT_SECRET!,
defaultScope: "api:read api:write",
});
// Single call to get a token. Cache hit on subsequent calls within the
// validity window — no extra network traffic.
const token = await m2m.getAccessToken();Per-call options
// Override the default scope.
const narrower = await m2m.getAccessToken({ scope: "api:read" });
// Force a fresh fetch (skips cache).
const fresh = await m2m.getAccessToken({ forceRefresh: true });
// Drop the cached token. Next call re-fetches.
m2m.clearCache();A scope change forces a fresh fetch — different scopes are tracked independently.
introspectToken
Round-trips the issuer's introspection endpoint (RFC 7662). Returns { active: false } for any token the IdP cannot validate — this function never throws on active: false.
import { introspectToken } from "@aero2/sdk/server";
const result = await introspectToken({
issuer: "https://acme.aero2.dev",
clientId: "my-service",
clientSecret: process.env.RS_CLIENT_SECRET!,
token,
});
if (!result.active) return new Response("invalid_token", { status: 401 });
console.log(result.scope, result.client_id, result.sub);Aero2's introspection endpoint enforces that the calling client matches the token's audience (access tokens) or the issuing client (refresh tokens). Calls from unrelated clients receive { active: false } — see the introspection docs for details.
verifyToken vs introspectToken
verifyToken | introspectToken | |
|---|---|---|
| Network calls | None per request (cached JWKS) | One per request |
| Catches revoked tokens | No (stateless) | Yes |
| Latency | Microseconds | Tens of ms |
| Right call when... | Token freshness > revocation latency is OK | Real-time revocation matters |
Most APIs use verifyToken for the hot path and introspectToken only for sensitive endpoints.
Error handling
Every SDK throw extends Aero2Error:
import {
Aero2Error,
Aero2NetworkError,
Aero2OAuthError,
Aero2TokenError,
} from "@aero2/sdk/server";
try {
await m2m.getAccessToken();
} catch (err) {
if (err instanceof Aero2OAuthError) {
// IdP returned an OAuth error response — err.errorCode is the
// RFC 6749 `error` field (e.g. "invalid_client", "invalid_scope").
} else if (err instanceof Aero2NetworkError) {
// fetch itself failed — DNS, TLS, connection refused.
} else if (err instanceof Aero2TokenError) {
// Token verification failed — treat as 401.
}
throw err;
}Custom fetch
Inject a custom fetch implementation for tests, proxies, or environments where globalThis.fetch isn't appropriate:
const rs = createResourceServer({
issuer,
fetchImpl: customFetch,
});
const m2m = createM2MClient({
issuer,
clientId,
clientSecret,
fetchImpl: customFetch,
});
await introspectToken({ issuer, clientId, clientSecret, token, fetchImpl: customFetch });