Skip to content

Commit 4780b9c

Browse files
committed
Added provider types
1 parent 8c12e41 commit 4780b9c

6 files changed

Lines changed: 496 additions & 1 deletion

File tree

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,25 @@ It contains:
1010
- token verification (`verifyAgeToken`)
1111
- gate policy (`isGateRequired`)
1212
- signed verification cookie helpers
13+
- provider mode helpers for Existing Gate Integration
1314
- typed domain errors and models
1415

1516
Most hostmasters should use `@agecheck/node` directly. Consume `@agecheck/core` only if you are building a framework adapter or platform integration.
1617

18+
## Integration model support
19+
20+
Core primitives support all three backend integration patterns:
21+
22+
1. Managed Gate Mode
23+
2. Existing Gate Integration (Provider Mode)
24+
3. Hybrid Mode (AgeCheck + other providers)
25+
26+
Provider Mode helpers:
27+
28+
- `verifyAgeCheckCredential(...)`
29+
- `normalizeExternalProviderAssertion(...)`
30+
- `buildSetCookieFromProviderAssertion(...)`
31+
1732
## Install
1833

1934
```bash

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@agecheck/core",
3-
"version": "0.1.1",
3+
"version": "0.2.0",
44
"description": "Core verification, policy, and cookie logic for AgeCheck SDKs.",
55
"license": "Apache-2.0",
66
"repository": {

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ export * from "./verify.js";
1919
export * from "./cookie.js";
2020
export * from "./webhooks.js";
2121
export * from "./sdk.js";
22+
export * from "./provider.js";

src/provider.ts

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
/*
2+
* AgeCheck-core
3+
* Copyright (c) 2026 ReallyMe LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* SPDX-License-Identifier: Apache-2.0
12+
*/
13+
14+
import { AgeCheckError, ErrorCode } from "./errors.js";
15+
import type {
16+
AgeCheckSdk,
17+
} from "./sdk.js";
18+
import type {
19+
EvidenceType,
20+
NormalizedProviderVerificationResult,
21+
ProviderAssertion,
22+
ProviderFailure,
23+
ProviderVerificationResult,
24+
VerificationAssertion,
25+
VerificationType,
26+
VerifyAgeCheckCredentialInput,
27+
} from "./types.js";
28+
29+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
30+
31+
function isAgeTier(value: string): boolean {
32+
return /^[1-9]\d*\+$/.test(value);
33+
}
34+
35+
function isSessionIdentifier(value: string): boolean {
36+
return UUID_PATTERN.test(value);
37+
}
38+
39+
function isVerificationType(value: string): value is VerificationType {
40+
return value === "passkey" || value === "oid4vp" || value === "other";
41+
}
42+
43+
function isEvidenceType(value: string): value is EvidenceType {
44+
return value === "webauthn_assertion" || value === "sd_jwt" || value === "zk_attestation" || value === "other";
45+
}
46+
47+
function normalizeProviderName(provider: string | undefined): string | null {
48+
if (typeof provider !== "string") {
49+
return "agecheck";
50+
}
51+
const trimmed = provider.trim();
52+
if (trimmed.length === 0) {
53+
return null;
54+
}
55+
return trimmed;
56+
}
57+
58+
function fail(code: string, message: string, detail?: string): ProviderFailure {
59+
const out: ProviderFailure = {
60+
verified: false,
61+
code,
62+
message,
63+
};
64+
if (typeof detail === "string" && detail.length > 0) {
65+
out.detail = detail;
66+
}
67+
return out;
68+
}
69+
70+
export async function verifyAgeCheckCredential(
71+
sdk: AgeCheckSdk,
72+
input: VerifyAgeCheckCredentialInput,
73+
): Promise<NormalizedProviderVerificationResult> {
74+
if (typeof input.jwt !== "string" || input.jwt.length === 0) {
75+
return fail(ErrorCode.INVALID_INPUT, "Missing jwt for agecheck provider.");
76+
}
77+
78+
if (typeof input.expectedSession !== "string" || input.expectedSession.length === 0) {
79+
return fail(ErrorCode.INVALID_INPUT, "expectedSession must be a non-empty string.");
80+
}
81+
if (!isSessionIdentifier(input.expectedSession)) {
82+
return fail(ErrorCode.INVALID_INPUT, "expectedSession must be a UUID.");
83+
}
84+
85+
const provider = normalizeProviderName(input.provider);
86+
if (provider === null) {
87+
return fail(ErrorCode.INVALID_INPUT, "provider must be a non-empty string.");
88+
}
89+
90+
const verify = await sdk.verifyToken(input.jwt, input.expectedSession);
91+
if (!verify.ok) {
92+
return fail(verify.code, "Age validation failed.", verify.detail);
93+
}
94+
95+
const verificationType = input.verificationType ?? "passkey";
96+
if (!isVerificationType(verificationType)) {
97+
return fail(ErrorCode.INVALID_INPUT, "verificationType is invalid.");
98+
}
99+
100+
const evidenceType = input.evidenceType ?? "webauthn_assertion";
101+
if (!isEvidenceType(evidenceType)) {
102+
return fail(ErrorCode.INVALID_INPUT, "evidenceType is invalid.");
103+
}
104+
105+
const now = Math.floor(Date.now() / 1000);
106+
const claims = verify.claims;
107+
const transactionClaim = claims.jti;
108+
const transactionId =
109+
typeof input.providerTransactionId === "string" && input.providerTransactionId.length > 0
110+
? input.providerTransactionId
111+
: typeof transactionClaim === "string" && transactionClaim.length > 0
112+
? transactionClaim
113+
: undefined;
114+
115+
let loa: string | undefined;
116+
if (typeof input.loa === "string" && input.loa.length > 0) {
117+
loa = input.loa;
118+
} else {
119+
const vc = claims.vc;
120+
if (vc && typeof vc === "object") {
121+
const credentialSubject = (vc as { credentialSubject?: unknown }).credentialSubject;
122+
if (credentialSubject && typeof credentialSubject === "object") {
123+
const loaCandidate = (credentialSubject as { loa?: unknown }).loa;
124+
if (typeof loaCandidate === "string" && loaCandidate.length > 0) {
125+
loa = loaCandidate;
126+
}
127+
}
128+
}
129+
}
130+
131+
const assertion: ProviderAssertion = {
132+
provider,
133+
verified: true,
134+
level: verify.ageTier,
135+
session: input.expectedSession,
136+
verifiedAtUnix: now,
137+
verificationType,
138+
evidenceType,
139+
};
140+
if (typeof input.assurance === "string" && input.assurance.length > 0) {
141+
assertion.assurance = input.assurance;
142+
}
143+
if (typeof transactionId === "string" && transactionId.length > 0) {
144+
assertion.providerTransactionId = transactionId;
145+
}
146+
if (typeof loa === "string" && loa.length > 0) {
147+
assertion.loa = loa;
148+
}
149+
return assertion;
150+
}
151+
152+
export function normalizeExternalProviderAssertion(
153+
providerResult: ProviderVerificationResult,
154+
expectedSession: string | undefined,
155+
): NormalizedProviderVerificationResult {
156+
if (expectedSession !== undefined && !isSessionIdentifier(expectedSession)) {
157+
return fail(ErrorCode.INVALID_INPUT, "expected session must be a UUID.");
158+
}
159+
160+
if (!providerResult.verified) {
161+
return providerResult;
162+
}
163+
164+
if (typeof providerResult.provider !== "string" || providerResult.provider.trim().length === 0) {
165+
return fail(ErrorCode.INVALID_INPUT, "provider must be a non-empty string.");
166+
}
167+
168+
if (typeof providerResult.level !== "string" || !isAgeTier(providerResult.level)) {
169+
return fail(ErrorCode.INVALID_INPUT, "provider level must be an age tier like 18+.");
170+
}
171+
172+
if (typeof providerResult.session !== "string" || providerResult.session.length === 0) {
173+
return fail(ErrorCode.SESSION_BINDING_REQUIRED, "Provider session is required.");
174+
}
175+
if (!isSessionIdentifier(providerResult.session)) {
176+
return fail(ErrorCode.INVALID_INPUT, "Provider session must be a UUID.");
177+
}
178+
179+
if (expectedSession !== undefined && providerResult.session !== expectedSession) {
180+
return fail(ErrorCode.SESSION_BINDING_MISMATCH, "Session binding mismatch.");
181+
}
182+
183+
const verifiedAtUnixRaw = providerResult.verifiedAtUnix;
184+
const verifiedAtUnix =
185+
Number.isInteger(verifiedAtUnixRaw) && typeof verifiedAtUnixRaw === "number" && verifiedAtUnixRaw > 0
186+
? verifiedAtUnixRaw
187+
: Math.floor(Date.now() / 1000);
188+
189+
const normalized: ProviderAssertion = {
190+
provider: providerResult.provider.trim(),
191+
verified: true,
192+
level: providerResult.level,
193+
session: providerResult.session,
194+
verifiedAtUnix,
195+
};
196+
197+
if (typeof providerResult.verificationType === "string") {
198+
if (!isVerificationType(providerResult.verificationType)) {
199+
return fail(ErrorCode.INVALID_INPUT, "verificationType is invalid.");
200+
}
201+
normalized.verificationType = providerResult.verificationType;
202+
}
203+
if (typeof providerResult.evidenceType === "string") {
204+
if (!isEvidenceType(providerResult.evidenceType)) {
205+
return fail(ErrorCode.INVALID_INPUT, "evidenceType is invalid.");
206+
}
207+
normalized.evidenceType = providerResult.evidenceType;
208+
}
209+
if (typeof providerResult.providerTransactionId === "string" && providerResult.providerTransactionId.length > 0) {
210+
normalized.providerTransactionId = providerResult.providerTransactionId;
211+
}
212+
if (typeof providerResult.loa === "string" && providerResult.loa.length > 0) {
213+
normalized.loa = providerResult.loa;
214+
}
215+
if (typeof providerResult.assurance === "string" && providerResult.assurance.length > 0) {
216+
normalized.assurance = providerResult.assurance;
217+
}
218+
return normalized;
219+
}
220+
221+
export function toCoreVerificationAssertion(assertion: ProviderAssertion): VerificationAssertion {
222+
const coreAssertion: VerificationAssertion = {
223+
provider: assertion.provider,
224+
verified: true,
225+
level: assertion.level,
226+
verifiedAtUnix: assertion.verifiedAtUnix,
227+
};
228+
if (typeof assertion.assurance === "string" && assertion.assurance.length > 0) {
229+
coreAssertion.assurance = assertion.assurance;
230+
}
231+
if (typeof assertion.verificationType === "string") {
232+
coreAssertion.verificationType = assertion.verificationType;
233+
}
234+
if (typeof assertion.evidenceType === "string") {
235+
coreAssertion.evidenceType = assertion.evidenceType;
236+
}
237+
if (typeof assertion.providerTransactionId === "string" && assertion.providerTransactionId.length > 0) {
238+
coreAssertion.providerTransactionId = assertion.providerTransactionId;
239+
}
240+
if (typeof assertion.loa === "string" && assertion.loa.length > 0) {
241+
coreAssertion.loa = assertion.loa;
242+
}
243+
return coreAssertion;
244+
}
245+
246+
export async function buildSetCookieFromProviderAssertion(
247+
sdk: AgeCheckSdk,
248+
assertion: ProviderAssertion,
249+
): Promise<string> {
250+
try {
251+
return await sdk.buildSetCookieFromAssertion(toCoreVerificationAssertion(assertion));
252+
} catch (error: unknown) {
253+
if (error instanceof AgeCheckError) {
254+
throw error;
255+
}
256+
throw new AgeCheckError(ErrorCode.VERIFY_FAILED, "Failed to issue verification cookie.");
257+
}
258+
}

src/types.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,66 @@ export interface VerifiedCookiePayload {
102102
level: string;
103103
}
104104

105+
export type VerificationType = "passkey" | "oid4vp" | "other";
106+
export type EvidenceType = "webauthn_assertion" | "sd_jwt" | "zk_attestation" | "other";
107+
105108
export interface VerificationAssertion {
106109
provider: string;
107110
level: string;
108111
verified: true;
109112
verifiedAtUnix: number;
110113
assurance?: string;
114+
verificationType?: VerificationType;
115+
evidenceType?: EvidenceType;
116+
providerTransactionId?: string;
117+
loa?: string;
118+
}
119+
120+
export interface ProviderAssertion {
121+
provider: string;
122+
verified: true;
123+
level: string;
124+
session: string;
125+
verifiedAtUnix: number;
126+
assurance?: string;
127+
verificationType?: VerificationType;
128+
evidenceType?: EvidenceType;
129+
providerTransactionId?: string;
130+
loa?: string;
131+
}
132+
133+
export interface ExternalProviderAssertion {
134+
provider: string;
135+
verified: true;
136+
level: string;
137+
session: string;
138+
verifiedAtUnix?: number;
139+
assurance?: string;
140+
verificationType?: VerificationType;
141+
evidenceType?: EvidenceType;
142+
providerTransactionId?: string;
143+
loa?: string;
144+
}
145+
146+
export interface ProviderFailure {
147+
verified: false;
148+
code: string;
149+
message: string;
150+
detail?: string;
151+
}
152+
153+
export type ProviderVerificationResult = ExternalProviderAssertion | ProviderFailure;
154+
export type NormalizedProviderVerificationResult = ProviderAssertion | ProviderFailure;
155+
156+
export interface VerifyAgeCheckCredentialInput {
157+
jwt: string;
158+
expectedSession: string;
159+
provider?: string;
160+
assurance?: string;
161+
verificationType?: VerificationType;
162+
evidenceType?: EvidenceType;
163+
providerTransactionId?: string;
164+
loa?: string;
111165
}
112166

113167
export interface JwtClaims {

0 commit comments

Comments
 (0)