Skip to content

Commit 84d87de

Browse files
committed
provider mode
1 parent 726cd65 commit 84d87de

11 files changed

Lines changed: 677 additions & 95 deletions

File tree

README.md

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,23 @@ TypeScript server SDK for age verification, designed to help websites implement
1111
- enforce gate policy server-side
1212
- verify AgeCheck credentials (`did:web:agecheck.me` and optional demo issuer)
1313
- issue and validate signed HttpOnly verification cookies
14-
- keep provider integration pluggable behind one assertion boundary
14+
- support Existing Gate Integration and multi-provider coexistence
1515

1616
## Install
1717

1818
```bash
1919
pnpm add @agecheck/node
2020
```
2121

22-
## Minimal integration path (existing sites)
22+
## Supported integration modes
2323

24-
This is the shortest production path for hostmasters.
24+
1. Managed gate mode: use AgeCheck gate route + verify route + signed cookie.
25+
2. Existing Gate Integration: keep your existing gate and add AgeCheck as one provider option.
26+
3. Hybrid mode: use AgeCheck gate while also supporting other providers in Provider Mode.
2527

26-
1. Build one SDK instance at server startup.
27-
2. Protect routes with `requireVerifiedOrRedirect(...)`.
28-
3. Handle `POST /verify` by verifying JWT + session and setting signed cookie.
29-
4. Trust cookie validation on every protected request.
28+
All modes converge into one normalized provider assertion and one signed cookie pipeline.
29+
30+
## Minimal managed-gate integration
3031

3132
```ts
3233
import { AgeCheckSdk } from "@agecheck/node";
@@ -44,7 +45,7 @@ const sdk = new AgeCheckSdk({
4445
cookie: {
4546
secret: process.env.AGECHECK_COOKIE_SECRET!,
4647
cookieName: "agecheck_verified",
47-
ttlSeconds: 86400,
48+
ttlSeconds: 86400, // hostmaster-controlled (e.g. 31536000 for 1 year)
4849
},
4950
});
5051

@@ -85,6 +86,63 @@ export async function verifyEndpoint(request: Request): Promise<Response> {
8586
}
8687
```
8788

89+
## Existing Gate Integration (Provider Mode)
90+
91+
Use these helpers when a hostmaster already has a gate flow and wants to add AgeCheck as a provider:
92+
93+
```ts
94+
import {
95+
AgeCheckSdk,
96+
buildSetCookieFromProviderAssertion,
97+
normalizeExternalProviderAssertion,
98+
verifyAgeCheckCredential,
99+
type ProviderVerificationResult,
100+
} from "@agecheck/node";
101+
102+
const sdk = new AgeCheckSdk({
103+
deploymentMode: "production",
104+
verify: { requiredAge: 18 },
105+
cookie: { secret: process.env.AGECHECK_COOKIE_SECRET! },
106+
});
107+
108+
export async function verifyProviderEndpoint(body: {
109+
provider?: string;
110+
jwt?: string;
111+
payload?: { agegateway_session?: string };
112+
redirect?: string;
113+
}): Promise<Response> {
114+
const expectedSession = body.payload?.agegateway_session;
115+
if (typeof expectedSession !== "string" || expectedSession.length === 0) {
116+
return Response.json({ verified: false, code: "invalid_input", error: "Missing session." }, { status: 400 });
117+
}
118+
119+
let assertion: ProviderVerificationResult;
120+
if ((body.provider ?? "agecheck") === "agecheck") {
121+
assertion = await verifyAgeCheckCredential(sdk, {
122+
jwt: body.jwt ?? "",
123+
expectedSession,
124+
assurance: "passkey",
125+
});
126+
} else {
127+
const externalResult: ProviderVerificationResult = await verifyOtherProvider(body);
128+
assertion = normalizeExternalProviderAssertion(externalResult, expectedSession);
129+
}
130+
131+
if (!assertion.verified) {
132+
return Response.json(
133+
{ verified: false, code: assertion.code, error: assertion.message, detail: assertion.detail },
134+
{ status: 401 },
135+
);
136+
}
137+
138+
const setCookie = await buildSetCookieFromProviderAssertion(sdk, assertion);
139+
return new Response(JSON.stringify({ verified: true, redirect: body.redirect ?? "/" }), {
140+
status: 200,
141+
headers: { "content-type": "application/json", "set-cookie": setCookie },
142+
});
143+
}
144+
```
145+
88146
## Deployment modes
89147

90148
- `production`
@@ -94,19 +152,28 @@ export async function verifyEndpoint(request: Request): Promise<Response> {
94152
- accepts demo + production issuer credentials
95153
- gate is always raised
96154

97-
## Provider boundary
155+
## Provider assertion contract
98156

99-
AgeCheck is the default provider, but you can normalize other provider results into the same assertion model and keep one cookie pipeline.
157+
Provider results normalize to this shape:
100158

101159
```ts
102-
const cookie = await sdk.buildSetCookieFromAssertion({
103-
provider: "my-provider",
104-
verified: true,
105-
level: "21+",
106-
verifiedAtUnix: Math.floor(Date.now() / 1000),
107-
});
160+
{
161+
provider: string;
162+
verified: true;
163+
level: "18+" | "21+" | `${number}+`;
164+
session: string; // UUID
165+
verifiedAtUnix: number;
166+
assurance?: string;
167+
verificationType?: "passkey" | "oid4vp" | "other";
168+
evidenceType?: "webauthn_assertion" | "sd_jwt" | "zk_attestation" | "other";
169+
providerTransactionId?: string;
170+
loa?: string;
171+
}
108172
```
109173

174+
This keeps provider internals isolated while preserving one site-level cookie and enforcement model.
175+
`payload.agegateway_session` and provider assertion `session` are treated as required UUID values.
176+
110177
## Framework adapters
111178

112179
`@agecheck/node` includes framework adapter helpers:

docs/ADAPTERS.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,21 @@ All adapters support the same provider boundary:
3333

3434
Provider verifier output is normalized into a single signed-cookie issuance path, so enforcement logic remains identical across providers.
3535

36+
Provider verifier success shape:
37+
38+
```ts
39+
{
40+
verified: true;
41+
provider: string;
42+
level: `${number}+`;
43+
session: string; // UUID
44+
verifiedAtUnix?: number;
45+
assurance?: string;
46+
}
47+
```
48+
49+
`session` is required and must be a UUID. `payload.agegateway_session` is required and must match the provider `session`.
50+
3651
## End-to-end deployment docs
3752

3853
- Hostmaster flow: `/docs/HOSTMASTER_E2E.md`

docs/EXISTING_SITES.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ This guide describes how to add AgeCheck to an existing adult website without re
99
3. Redirect unverified users to your gate route (for example `/ageverify`).
1010
4. Verify JWT at `POST /verify`, set signed HttpOnly cookie, redirect back.
1111

12+
## Existing Gate Integration (Provider Mode)
13+
14+
If your site already has a gate and multiple providers, add AgeCheck as one provider option:
15+
16+
1. Keep your gate UI and policy engine unchanged.
17+
2. Generate or forward the provider session identifier (AgeCheck expects `agegateway_session`).
18+
3. For AgeCheck, call `verifyAgeCheckCredential(...)`.
19+
4. For other providers, map result with `normalizeExternalProviderAssertion(...)`.
20+
5. Issue one canonical cookie with `buildSetCookieFromProviderAssertion(...)`.
21+
1222
## Why this pattern
1323

1424
- enforcement remains server-side
@@ -33,7 +43,18 @@ Set with:
3343

3444
## Provider coexistence
3545

36-
If you support more than one verifier, normalize each verification result into the SDK `VerificationAssertion` shape and reuse the same signed-cookie issuance and enforcement logic.
46+
If you support more than one verifier, normalize each verification result into the provider assertion shape and reuse the same signed-cookie issuance and enforcement logic:
47+
48+
```ts
49+
{
50+
provider: string;
51+
verified: true;
52+
level: `${number}+`;
53+
session: string;
54+
verifiedAtUnix: number;
55+
assurance?: string;
56+
}
57+
```
3758

3859
## Framework adapters
3960

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@agecheck/node",
3-
"version": "0.1.1",
3+
"version": "0.2.0",
44
"description": "AgeCheck Node SDK for gate policy, JWT verification, and signed verification cookies.",
55
"license": "Apache-2.0",
66
"repository": {
@@ -36,7 +36,7 @@
3636
"typecheck": "tsc --noEmit"
3737
},
3838
"dependencies": {
39-
"@agecheck/core": "^0.1.0"
39+
"@agecheck/core": "^0.2.0"
4040
},
4141
"devDependencies": {
4242
"@agecheck/core": "^0.1.1",

pnpm-lock.yaml

Lines changed: 7 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
packages:
2+
- "."
23
- "worker-demo"
34

45
onlyBuiltDependencies:

src/adapters/common.ts

Lines changed: 26 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,18 @@
1111
* SPDX-License-Identifier: Apache-2.0
1212
*/
1313

14-
import { AgeCheckError, ErrorCode, type AgeCheckSdk, type VerificationAssertion } from "@agecheck/core";
14+
import {
15+
AgeCheckError,
16+
ErrorCode,
17+
type AgeCheckSdk,
18+
} from "@agecheck/core";
19+
import {
20+
buildSetCookieFromProviderAssertion,
21+
normalizeExternalProviderAssertion,
22+
type NormalizedProviderVerificationResult,
23+
type ProviderVerificationResult,
24+
verifyAgeCheckCredential,
25+
} from "../provider-mode.js";
1526

1627
export interface AdapterVerifyBody {
1728
provider?: string;
@@ -22,24 +33,6 @@ export interface AdapterVerifyBody {
2233
redirect?: string;
2334
}
2435

25-
export interface ProviderVerificationSuccess {
26-
verified: true;
27-
provider: string;
28-
level: string;
29-
session: string;
30-
verifiedAtUnix?: number;
31-
assurance?: string;
32-
}
33-
34-
export interface ProviderVerificationFailure {
35-
verified: false;
36-
code: string;
37-
message: string;
38-
detail?: string;
39-
}
40-
41-
export type ProviderVerificationResult = ProviderVerificationSuccess | ProviderVerificationFailure;
42-
4336
export type ProviderVerifier = (body: AdapterVerifyBody) => Promise<ProviderVerificationResult>;
4437

4538
export interface AdapterOptions {
@@ -210,56 +203,37 @@ export async function verifyAndBuildOutcome(
210203
}
211204

212205
const expectedSession = body.payload?.agegateway_session;
206+
if (typeof expectedSession !== "string" || expectedSession.length === 0) {
207+
return failure(400, ErrorCode.INVALID_INPUT, "Missing payload.agegateway_session.");
208+
}
213209
const redirect = normalizeRedirect(body.redirect);
214210
const provider = body.provider ?? "agecheck";
215211

216-
let assertion: VerificationAssertion;
212+
let assertion: NormalizedProviderVerificationResult;
217213
if (provider === "agecheck") {
218-
if (typeof body.jwt !== "string") {
219-
return failure(400, ErrorCode.INVALID_INPUT, "Missing jwt for agecheck provider");
220-
}
221-
222-
const verify = await sdk.verifyToken(body.jwt, expectedSession);
223-
if (!verify.ok) {
224-
return failure(401, verify.code, "Age validation failed.", verify.detail);
225-
}
226-
227-
assertion = {
214+
const ageCheckResult = await verifyAgeCheckCredential(sdk, {
215+
jwt: body.jwt ?? "",
216+
expectedSession,
228217
provider: "agecheck",
229-
verified: true,
230-
level: verify.ageTier,
231-
verifiedAtUnix: Math.floor(Date.now() / 1000),
232218
assurance: "passkey",
233-
};
219+
});
220+
assertion = ageCheckResult;
234221
} else {
235222
if (typeof options.providerVerifier !== "function") {
236223
return failure(400, ErrorCode.INVALID_INPUT, "Unknown provider and no provider verifier configured");
237224
}
238225

239226
const providerResult = await options.providerVerifier(body);
240-
if (!providerResult.verified) {
241-
return failure(401, providerResult.code, providerResult.message, providerResult.detail);
242-
}
243-
244-
if (expectedSession !== undefined && providerResult.session !== expectedSession) {
245-
return failure(401, ErrorCode.SESSION_BINDING_MISMATCH, "Session binding mismatch.");
246-
}
227+
assertion = normalizeExternalProviderAssertion(providerResult, expectedSession);
228+
}
247229

248-
const providerAssertion: VerificationAssertion = {
249-
provider: providerResult.provider,
250-
verified: true,
251-
level: providerResult.level,
252-
verifiedAtUnix: providerResult.verifiedAtUnix ?? Math.floor(Date.now() / 1000),
253-
};
254-
if (providerResult.assurance !== undefined) {
255-
providerAssertion.assurance = providerResult.assurance;
256-
}
257-
assertion = providerAssertion;
230+
if (!assertion.verified) {
231+
return failure(401, assertion.code, assertion.message, assertion.detail);
258232
}
259233

260234
let setCookie: string;
261235
try {
262-
setCookie = await sdk.buildSetCookieFromAssertion(assertion);
236+
setCookie = await buildSetCookieFromProviderAssertion(sdk, assertion);
263237
} catch (error: unknown) {
264238
if (error instanceof AgeCheckError) {
265239
return failure(500, error.code, "Failed to issue verification cookie", error.message);

0 commit comments

Comments
 (0)