Skip to content

Commit 7ad50b5

Browse files
committed
initial commit
1 parent e2680df commit 7ad50b5

29 files changed

Lines changed: 4682 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
branches: ["**"]
6+
pull_request:
7+
8+
jobs:
9+
test-build:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@v4
15+
16+
- name: Setup pnpm
17+
uses: pnpm/action-setup@v4
18+
19+
- name: Setup Node
20+
uses: actions/setup-node@v4
21+
with:
22+
node-version: 20
23+
cache: pnpm
24+
25+
- name: Install
26+
run: pnpm install --frozen-lockfile
27+
28+
- name: Typecheck
29+
run: pnpm typecheck
30+
31+
- name: Test
32+
run: pnpm test
33+
34+
- name: Build
35+
run: pnpm build
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
name: compatibility
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: ["**"]
7+
schedule:
8+
- cron: "0 6 * * 1"
9+
10+
jobs:
11+
adapters-latest:
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Setup pnpm
19+
uses: pnpm/action-setup@v4
20+
21+
- name: Setup Node
22+
uses: actions/setup-node@v4
23+
with:
24+
node-version: 20
25+
cache: pnpm
26+
27+
- name: Install and build package
28+
run: |
29+
pnpm install --frozen-lockfile
30+
pnpm build
31+
32+
- name: Validate adapter compatibility against latest frameworks
33+
shell: bash
34+
run: |
35+
set -euo pipefail
36+
WORKSPACE="${GITHUB_WORKSPACE}"
37+
NODE_TYPES="${WORKSPACE}/dist/index.d.ts"
38+
CORE_TYPES="${WORKSPACE}/node_modules/@agecheck/core/dist/index.d.ts"
39+
40+
check_project() {
41+
local name="$1"
42+
local deps_json="$2"
43+
local code="$3"
44+
45+
local dir
46+
dir="$(mktemp -d)"
47+
48+
cat > "${dir}/package.json" <<JSON
49+
{
50+
"name": "compat-${name}",
51+
"private": true,
52+
"type": "module",
53+
"scripts": {
54+
"typecheck": "tsc --noEmit"
55+
},
56+
"dependencies": {
57+
${deps_json}
58+
},
59+
"devDependencies": {
60+
"typescript": "^5.9.2",
61+
"@types/node": "^24.5.2"
62+
}
63+
}
64+
JSON
65+
66+
cat > "${dir}/tsconfig.json" <<'JSON'
67+
{
68+
"compilerOptions": {
69+
"target": "ES2022",
70+
"module": "NodeNext",
71+
"moduleResolution": "NodeNext",
72+
"strict": true,
73+
"skipLibCheck": true,
74+
"exactOptionalPropertyTypes": true,
75+
"noUncheckedIndexedAccess": true,
76+
"baseUrl": ".",
77+
"paths": {
78+
"@agecheck/node": ["${NODE_TYPES}"],
79+
"@agecheck/core": ["${CORE_TYPES}"]
80+
},
81+
"types": ["node"]
82+
},
83+
"include": ["index.ts"]
84+
}
85+
JSON
86+
87+
printf "%s\n" "$code" > "${dir}/index.ts"
88+
89+
pnpm --dir "${dir}" install --no-frozen-lockfile
90+
pnpm --dir "${dir}" typecheck
91+
}
92+
93+
check_project \
94+
"express" \
95+
'"express": "latest", "@types/express": "latest"' \
96+
'import express from "express";\nimport { AgeCheckSdk, createExpressGateMiddleware, createExpressVerifyHandler } from "@agecheck/node";\nconst app = express();\nconst sdk = new AgeCheckSdk({ deploymentMode: "production", verify: { requiredAge: 18 }, gate: { headerName: "X-Age-Gate", requiredValue: "true" }, cookie: { secret: "s".repeat(32) } });\napp.post("/verify", createExpressVerifyHandler(sdk));\napp.use("/restricted", createExpressGateMiddleware(sdk, { gatePath: "/ageverify" }));'
97+
98+
check_project \
99+
"fastify" \
100+
'"fastify": "latest"' \
101+
'import Fastify from "fastify";\nimport { AgeCheckSdk, createFastifyGateHook, createFastifyVerifyHandler } from "@agecheck/node";\nconst app = Fastify();\nconst sdk = new AgeCheckSdk({ deploymentMode: "production", verify: { requiredAge: 18 }, gate: { headerName: "X-Age-Gate", requiredValue: "true" }, cookie: { secret: "s".repeat(32) } });\napp.post("/verify", createFastifyVerifyHandler(sdk));\napp.addHook("preHandler", async (req, reply) => { if (req.url.startsWith("/restricted")) await createFastifyGateHook(sdk, { gatePath: "/ageverify" })(req, reply); });'
102+
103+
check_project \
104+
"hono" \
105+
'"hono": "latest"' \
106+
'import { Hono } from "hono";\nimport { AgeCheckSdk, createHonoGateMiddleware, createHonoVerifyHandler } from "@agecheck/node";\nconst app = new Hono();\nconst sdk = new AgeCheckSdk({ deploymentMode: "production", verify: { requiredAge: 18 }, gate: { headerName: "X-Age-Gate", requiredValue: "true" }, cookie: { secret: "s".repeat(32) } });\napp.post("/verify", createHonoVerifyHandler(sdk));\napp.use("/restricted/*", createHonoGateMiddleware(sdk));'
107+
108+
check_project \
109+
"nuxt-vue" \
110+
'"nuxt": "latest", "vue": "latest"' \
111+
'import { AgeCheckSdk, createNuxtGateMiddleware, createNuxtVerifyHandler } from "@agecheck/node";\nconst sdk = new AgeCheckSdk({ deploymentMode: "production", verify: { requiredAge: 18 }, gate: { headerName: "X-Age-Gate", requiredValue: "true" }, cookie: { secret: "s".repeat(32) } });\nconst gate = createNuxtGateMiddleware(sdk);\nconst verify = createNuxtVerifyHandler(sdk, { readBody: async () => ({ provider: "other", payload: { agegateway_session: "abc" } }), providerVerifier: async () => ({ verified: true, provider: "other", level: "18+", session: "abc" }) });\nvoid gate;\nvoid verify;'

.github/workflows/publish.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: publish
2+
3+
on:
4+
workflow_dispatch:
5+
release:
6+
types: [published]
7+
8+
jobs:
9+
publish:
10+
if: github.event_name == 'workflow_dispatch' || github.event.release.prerelease == false
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: read
14+
id-token: write
15+
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v4
19+
20+
- name: Setup pnpm
21+
uses: pnpm/action-setup@v4
22+
23+
- name: Setup Node
24+
uses: actions/setup-node@v4
25+
with:
26+
node-version: 20
27+
registry-url: https://registry.npmjs.org
28+
cache: pnpm
29+
30+
- name: Install
31+
run: pnpm install --frozen-lockfile
32+
33+
- name: Typecheck
34+
run: pnpm typecheck
35+
36+
- name: Test
37+
run: pnpm test
38+
39+
- name: Build
40+
run: pnpm build
41+
42+
- name: Publish
43+
run: npm publish --provenance --access public
44+
env:
45+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,11 @@ dist
137137
# Vite logs files
138138
vite.config.js.timestamp-*
139139
vite.config.ts.timestamp-*
140+
141+
# macOS
142+
.DS_Store
143+
**/.DS_Store
144+
145+
# Cloudflare local state
146+
.wrangler/
147+
.mf/

README.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# AgeCheck Node SDK (`@agecheck/node`)
2+
3+
[![CI](https://github.com/agecheck/agecheck-node/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/agecheck/agecheck-node/actions/workflows/ci.yml)
4+
[![Compatibility](https://github.com/agecheck/agecheck-node/actions/workflows/compatibility.yml/badge.svg?branch=main)](https://github.com/agecheck/agecheck-node/actions/workflows/compatibility.yml)
5+
[![Publish](https://github.com/agecheck/agecheck-node/actions/workflows/publish.yml/badge.svg)](https://github.com/agecheck/agecheck-node/actions/workflows/publish.yml)
6+
[![npm](https://img.shields.io/npm/v/%40agecheck%2Fnode?label=npm)](https://www.npmjs.com/package/@agecheck/node)
7+
8+
Production-grade server SDK for AgeCheck age-gate policy, JWT verification, and stateless signed verification cookies.
9+
10+
## What this package is for
11+
12+
- enforce gate policy server-side
13+
- verify AgeCheck credentials (`did:web:agecheck.me` and optional demo issuer)
14+
- issue and validate signed HttpOnly verification cookies
15+
- keep provider integration pluggable behind one assertion boundary
16+
17+
## Install
18+
19+
```bash
20+
pnpm add @agecheck/node
21+
```
22+
23+
## Minimal integration path (existing sites)
24+
25+
This is the shortest production path for hostmasters.
26+
27+
1. Build one SDK instance at server startup.
28+
2. Protect routes with `requireVerifiedOrRedirect(...)`.
29+
3. Handle `POST /verify` by verifying JWT + session and setting signed cookie.
30+
4. Trust cookie validation on every protected request.
31+
32+
```ts
33+
import { AgeCheckSdk } from "@agecheck/node";
34+
35+
const sdk = new AgeCheckSdk({
36+
deploymentMode: "production", // "demo" for demo deployments
37+
verify: {
38+
requiredAge: 18,
39+
allowCustomIssuer: false,
40+
},
41+
gate: {
42+
headerName: "X-Age-Gate",
43+
requiredValue: "true",
44+
},
45+
cookie: {
46+
secret: process.env.AGECHECK_COOKIE_SECRET!,
47+
cookieName: "agecheck_verified",
48+
ttlSeconds: 86400,
49+
},
50+
});
51+
52+
export async function enforce(request: Request): Promise<Response | null> {
53+
return sdk.requireVerifiedOrRedirect(request, { gatePath: "/ageverify" });
54+
}
55+
56+
export async function verifyEndpoint(request: Request): Promise<Response> {
57+
const body = (await request.json()) as {
58+
jwt?: string;
59+
payload?: { agegateway_session?: string };
60+
redirect?: string;
61+
};
62+
63+
const result = await sdk.verifyToken(body.jwt ?? "", body.payload?.agegateway_session ?? "");
64+
if (!result.ok) {
65+
return Response.json(
66+
{ verified: false, error: "Age validation failed.", code: result.code },
67+
{ status: 401 },
68+
);
69+
}
70+
71+
const setCookie = await sdk.buildSetCookieFromAssertion({
72+
provider: "agecheck",
73+
verified: true,
74+
level: result.ageTier,
75+
verifiedAtUnix: Math.floor(Date.now() / 1000),
76+
assurance: "passkey",
77+
});
78+
79+
const headers = new Headers({ "content-type": "application/json" });
80+
headers.append("set-cookie", setCookie);
81+
82+
return new Response(
83+
JSON.stringify({ verified: true, redirect: body.redirect ?? "/" }),
84+
{ status: 200, headers },
85+
);
86+
}
87+
```
88+
89+
## Deployment modes
90+
91+
- `production`
92+
- accepts production issuer credentials
93+
- gate is raised only when policy header requires it
94+
- `demo`
95+
- accepts demo + production issuer credentials
96+
- gate is always raised
97+
98+
## Provider boundary
99+
100+
AgeCheck is the default provider, but you can normalize other provider results into the same assertion model and keep one cookie pipeline.
101+
102+
```ts
103+
const cookie = await sdk.buildSetCookieFromAssertion({
104+
provider: "my-provider",
105+
verified: true,
106+
level: "21+",
107+
verifiedAtUnix: Math.floor(Date.now() / 1000),
108+
});
109+
```
110+
111+
## Framework adapters
112+
113+
`@agecheck/node` includes framework adapter helpers:
114+
115+
- `createExpressGateMiddleware`, `createExpressVerifyHandler`
116+
- `createFastifyGateHook`, `createFastifyVerifyHandler`
117+
- `createHonoGateMiddleware`, `createHonoVerifyHandler`
118+
- `createNuxtGateMiddleware`, `createNuxtVerifyHandler`
119+
120+
See `/docs/ADAPTERS.md` for adapter mapping and behavior notes.
121+
122+
## Worker reference
123+
124+
`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.
125+
126+
## Existing sites
127+
128+
See `/docs/EXISTING_SITES.md` for the migration pattern that keeps your existing templates/content and moves enforcement into middleware.
129+
130+
## Full hostmaster guide
131+
132+
See `/docs/HOSTMASTER_E2E.md` for the full request lifecycle and production enforcement model.
133+
134+
## Framework compatibility
135+
136+
See `/docs/COMPATIBILITY.md` for latest-framework compatibility coverage and CI validation scope.
137+
138+
## Versioning and releases
139+
140+
See `/docs/VERSIONING.md`.
141+
142+
## Quality gates
143+
144+
```bash
145+
pnpm typecheck
146+
pnpm test
147+
pnpm build
148+
```
149+
150+
## License
151+
152+
Apache-2.0

0 commit comments

Comments
 (0)