TypeScript server SDK for age verification, designed to help websites implement compliant age-assurance flows across jurisdictions.
- enforce gate policy server-side
- verify AgeCheck credentials (
did:web:agecheck.meand optional demo issuer) - issue and validate signed HttpOnly verification cookies
- support Existing Gate Integration and multi-provider coexistence
pnpm add @agecheck/nodeQuickstart: see /docs/QUICKSTART.md.
- Managed gate mode: use AgeCheck gate route + verify route + signed cookie.
- Existing Gate Integration: keep your existing gate and add AgeCheck as one provider option.
- Hybrid mode: use AgeCheck gate while also supporting other providers in Provider Mode.
All modes converge into one normalized provider assertion and one signed cookie pipeline.
import express from "express";
import { AgeCheckSdk, createExpressGateMiddleware, createExpressVerifyHandler } from "@agecheck/node";
const app = express();
app.use(express.json());
const sdk = new AgeCheckSdk({
deploymentMode: (process.env.AGECHECK_DEPLOYMENT_MODE === "demo" ? "demo" : "production"),
verify: {
requiredAge: Number(process.env.AGECHECK_REQUIRED_AGE ?? "18"),
allowCustomIssuer: false,
},
gate: {
headerName: process.env.AGECHECK_GATE_HEADER_NAME ?? "X-Age-Gate",
requiredValue: process.env.AGECHECK_GATE_HEADER_REQUIRED_VALUE ?? "true",
},
cookie: {
secret: process.env.AGECHECK_COOKIE_SECRET!,
cookieName: "agecheck_verified",
ttlSeconds: 86400,
},
});
app.post("/verify", createExpressVerifyHandler(sdk));
app.use("/restricted", createExpressGateMiddleware(sdk, { gatePath: "/ageverify" }));This route pair handles token verification and signed verification cookie issuance.
Most hostmasters should not call buildSetCookieFromAssertion(...) directly.
Use these helpers when a hostmaster already has a gate flow and wants to add AgeCheck as a provider:
import {
AgeCheckSdk,
buildSetCookieFromProviderAssertion,
normalizeExternalProviderAssertion,
verifyAgeCheckCredential,
type ProviderVerificationResult,
} from "@agecheck/node";
const sdk = new AgeCheckSdk({
deploymentMode: "production",
verify: { requiredAge: 18 },
cookie: { secret: process.env.AGECHECK_COOKIE_SECRET! },
});
export async function verifyProviderEndpoint(body: {
provider?: string;
jwt?: string;
payload?: { agegateway_session?: string };
redirect?: string;
}): Promise<Response> {
const expectedSession = body.payload?.agegateway_session;
if (typeof expectedSession !== "string" || expectedSession.length === 0) {
return Response.json({ verified: false, code: "invalid_input", error: "Missing session." }, { status: 400 });
}
let assertion: ProviderVerificationResult;
if ((body.provider ?? "agecheck") === "agecheck") {
assertion = await verifyAgeCheckCredential(sdk, {
jwt: body.jwt ?? "",
expectedSession,
assurance: "passkey",
});
} else {
const externalResult: ProviderVerificationResult = await verifyOtherProvider(body);
assertion = normalizeExternalProviderAssertion(externalResult, expectedSession);
}
if (!assertion.verified) {
return Response.json(
{ verified: false, code: assertion.code, error: assertion.message, detail: assertion.detail },
{ status: 401 },
);
}
const setCookie = await buildSetCookieFromProviderAssertion(sdk, assertion);
return new Response(JSON.stringify({ verified: true, redirect: body.redirect ?? "/" }), {
status: 200,
headers: { "content-type": "application/json", "set-cookie": setCookie },
});
}production- accepts production issuer credentials
- gate is raised only when policy header requires it
demo- accepts demo + production issuer credentials
- gate is always raised
Provider results normalize to this shape:
{
provider: string;
verified: true;
level: "18+" | "21+" | `${number}+`;
session: string; // UUID
verifiedAtUnix: number;
assurance?: string;
verificationType?: "passkey" | "oid4vp" | "other";
evidenceType?: "webauthn_assertion" | "sd_jwt" | "zk_attestation" | "other";
providerTransactionId?: string;
loa?: string;
}This keeps provider internals isolated while preserving one site-level cookie and enforcement model.
payload.agegateway_session and provider assertion session are treated as required UUID values.
@agecheck/node includes framework adapter helpers:
createExpressGateMiddleware,createExpressVerifyHandlercreateFastifyGateHook,createFastifyVerifyHandlercreateHonoGateMiddleware,createHonoVerifyHandlercreateNuxtGateMiddleware,createNuxtVerifyHandler
See /docs/ADAPTERS.md for adapter mapping and behavior notes.
If you see:
{
"verified": false,
"code": "verify_failed",
"error": "Failed to issue verification cookie"
}check:
- Node runtime is
>=20 AGECHECK_COOKIE_SECRETis present in the running process and at least 32 bytespayload.agegateway_sessionis a UUID- your verify route is using adapter helpers (
createExpressVerifyHandleror equivalent)
worker-demo/ is a reference backend implementation (verify endpoint + gate page + cookie endpoints). It is useful for validation and demos, but production adopters should wire the SDK into their own server routes/middleware.
See /docs/EXISTING_SITES.md for the migration pattern that keeps your existing templates/content and moves enforcement into middleware.
See /docs/HOSTMASTER_E2E.md for the full request lifecycle and production enforcement model.
See /docs/COMPATIBILITY.md for latest-framework compatibility coverage and CI validation scope.
See /docs/VERSIONING.md.
pnpm typecheck
pnpm test
pnpm buildApache-2.0