Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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_credentials tokens 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/sdk

createResourceServer

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

verifyTokenintrospectToken
Network callsNone per request (cached JWKS)One per request
Catches revoked tokensNo (stateless)Yes
LatencyMicrosecondsTens of ms
Right call when...Token freshness > revocation latency is OKReal-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 });