Framework-agnostic EUDI Wallet verification protocol engine.
npm install @openeudi/coreimport {
VerificationService,
DemoMode,
VerificationType,
VerificationStatus,
} from "@openeudi/core";
const service = new VerificationService({ mode: new DemoMode() });
// Typed events -- event name typos are caught at compile time
service.on("session:created", (session) => {
console.log("Session ready:", session.walletUrl);
});
service.on("session:verified", (session, result) => {
console.log("Verified:", result.country, "age:", result.ageVerified);
});
service.on("session:rejected", (session, reason) => {
console.warn("Rejected:", reason);
});
service.on("error", (err, sessionId) => {
console.error("Background error for session", sessionId, err);
});
const session = await service.createSession({ type: VerificationType.BOTH });
console.log(session.walletUrl); // openid4vp://verify?session=<uuid>
// Teardown when done
service.destroy();Auto-completes sessions with randomized EU citizen data after a configurable delay. Ideal for product demos and landing pages.
import { DemoMode } from "@openeudi/core";
const mode = new DemoMode({
delayMs: 2000, // auto-complete after 2s (default: 3000)
});Returns deterministic results for integration testing. Supports global defaults and per-session overrides.
import { MockMode } from "@openeudi/core";
const mode = new MockMode({
defaultResult: { verified: true, country: "FR", ageVerified: true },
delayMs: 100,
});
const service = new VerificationService({ mode });
const session = await service.createSession({ type: VerificationType.AGE });
// Override result for a specific session
mode.setSessionResult(session.id, {
verified: false,
rejectionReason: "underage",
});
// Remove override
mode.clearSessionResult(session.id);Implement IVerificationMode to connect to a real EUDI Wallet relying party:
import type {
IVerificationMode,
BaseSession,
VerificationResult,
} from "@openeudi/core";
class ProductionMode implements IVerificationMode {
readonly name = "production";
async processCallback(
session: BaseSession,
walletResponse: unknown,
): Promise<VerificationResult> {
const claims = await verifyVPToken(walletResponse); // your OpenID4VP logic
return { verified: true, country: claims.country, ageVerified: claims.age >= 18 };
}
}The default InMemorySessionStore works for single-process deployments. Implement ISessionStore for Redis, PostgreSQL, etc:
import type { ISessionStore, VerificationSession } from "@openeudi/core";
class RedisSessionStore implements ISessionStore {
constructor(private redis: Redis) {}
async get(id: string): Promise<VerificationSession | null> {
const data = await this.redis.get(`session:${id}`);
return data ? JSON.parse(data) : null;
}
async set(session: VerificationSession): Promise<void> {
const ttl = Math.max(0, session.expiresAt.getTime() - Date.now());
await this.redis.set(`session:${session.id}`, JSON.stringify(session), "PX", ttl);
}
async delete(id: string): Promise<void> {
await this.redis.del(`session:${id}`);
}
}
const service = new VerificationService({
mode: new DemoMode(),
store: new RedisSessionStore(new Redis()),
});VerificationSession is a discriminated union. Narrow by session.status to access phase-specific fields:
import {
VerificationStatus,
type VerificationSession,
type PendingSession,
type CompletedSession,
type ExpiredSession,
} from "@openeudi/core";
function inspect(session: VerificationSession) {
switch (session.status) {
case VerificationStatus.PENDING:
// session is PendingSession here
console.log("Waiting for wallet, url:", session.walletUrl);
break;
case VerificationStatus.VERIFIED:
case VerificationStatus.REJECTED:
// session is CompletedSession here
// .result and .completedAt are available
console.log("Result:", session.result, "at:", session.completedAt);
break;
case VerificationStatus.EXPIRED:
// session is ExpiredSession here
console.log("Expired at:", session.completedAt);
break;
}
}Fields by type:
| Field | BaseSession | PendingSession | CompletedSession | ExpiredSession |
|---|---|---|---|---|
id, type, status, walletUrl, createdAt, expiresAt |
yes | yes | yes | yes |
result, completedAt |
- | - | yes | yes |
VerificationService extends EventEmitter with fully typed events. Listener argument types are inferred from the event name -- no casting needed.
| Event | Handler Signature | Description |
|---|---|---|
session:created |
(session: PendingSession) => void |
Session created, wallet URL ready |
session:verified |
(session: CompletedSession, result: VerificationResult) => void |
Verification passed |
session:rejected |
(session: CompletedSession, reason: string) => void |
Verification rejected |
session:expired |
(session: ExpiredSession) => void |
Session TTL exceeded |
session:cancelled |
(session: PendingSession) => void |
Session cancelled by caller |
error |
(err: Error, sessionId?: string) => void |
Background simulation error |
service.on("session:created", (session) =>
sendSSE(session.id, { walletUrl: session.walletUrl })
);
service.on("session:verified", (session, result) =>
sendSSE(session.id, { status: "verified", country: result.country })
);
service.on("session:rejected", (session, reason) =>
sendSSE(session.id, { status: "rejected", reason })
);
service.on("session:expired", (session) =>
sendSSE(session.id, { status: "expired" })
);
service.on("session:cancelled", (session) =>
sendSSE(session.id, { status: "cancelled" })
);
service.on("error", (err, sessionId) =>
console.error("Service error:", err.message, sessionId)
);createSession() validates inputs and throws synchronously on bad data:
countryWhitelistandcountryBlacklistare mutually exclusive- All country codes must be valid ISO 3166-1 alpha-2 (e.g.
"DE","FR") isValidCountryCode(code)is exported for use in your own validation layer
import { isValidCountryCode } from "@openeudi/core";
isValidCountryCode("DE"); // true
isValidCountryCode("XX"); // false
isValidCountryCode("deu"); // false (must be 2 uppercase letters)Constructor config is also validated:
sessionTtlMsmust be a positive integerwalletBaseUrlmust be a non-empty string
const service = new VerificationService({ mode: new DemoMode() });
// ... normal usage ...
// Teardown: removes all listeners, clears session tracking, prevents future calls
service.destroy();
// All calls after destroy() throw ServiceDestroyedError
await service.createSession({ type: VerificationType.AGE }); // throwsUse destroy() in server shutdown handlers to prevent memory leaks when hot-reloading.
Optional helper to generate QR code data URIs. Requires the qrcode peer dependency:
npm install qrcodeimport { generateQRCode } from "@openeudi/core/qr";
const session = await service.createSession({ type: VerificationType.AGE });
const dataUri = await generateQRCode(session.walletUrl);
// => "data:image/png;base64,..."| Method | Returns | Description |
|---|---|---|
constructor(config) |
VerificationService |
Create service with mode, optional store, TTL, and wallet URL |
createSession(input) |
Promise<PendingSession> |
Create a new verification session |
getSession(id) |
Promise<VerificationSession> |
Retrieve session by ID (throws SessionNotFoundError) |
handleCallback(sessionId, walletResponse) |
Promise<VerificationResult> |
Process wallet callback (throws SessionNotFoundError, SessionExpiredError) |
cancelSession(id) |
Promise<void> |
Cancel a pending session (throws SessionNotPendingError if not pending) |
cleanupExpired() |
Promise<number> |
Remove expired sessions, returns count cleaned |
destroy() |
void |
Permanently destroy the service and prevent further use |
interface VerificationServiceConfig {
mode: IVerificationMode; // Required -- DemoMode, MockMode, or custom
store?: ISessionStore; // Default: InMemorySessionStore
sessionTtlMs?: number; // Default: 300_000 (5 minutes)
walletBaseUrl?: string; // Default: 'openid4vp://verify'
}| Class | Thrown by | Description |
|---|---|---|
SessionNotFoundError |
getSession, handleCallback, cancelSession |
No session with the given ID |
SessionExpiredError |
handleCallback |
Session TTL has elapsed |
SessionNotPendingError |
cancelSession |
Session is not in PENDING status |
ServiceDestroyedError |
All public methods | Service has been destroyed |
All types are exported from the main entry point:
import type {
VerificationSession, // Discriminated union (Pending | Completed | Expired)
BaseSession, // Common fields shared by all session states
PendingSession, // Status: PENDING
CompletedSession, // Status: VERIFIED or REJECTED (.result, .completedAt present)
ExpiredSession, // Status: EXPIRED (.completedAt present)
VerificationResult, // Outcome of a verification
CreateSessionInput, // Input for createSession()
VerificationServiceConfig, // Constructor config
VerificationEvents, // Event map for typed EventEmitter
IVerificationMode, // Strategy interface for modes
ISessionStore, // Storage adapter interface
DemoModeConfig, // DemoMode constructor options
MockModeConfig, // MockMode constructor options
} from "@openeudi/core";
import {
VerificationType, // AGE | COUNTRY | BOTH
VerificationStatus, // PENDING | VERIFIED | REJECTED | EXPIRED
SessionNotFoundError,
SessionExpiredError,
SessionNotPendingError,
ServiceDestroyedError,
isValidCountryCode,
VERSION, // '0.2.0'
} from "@openeudi/core";session.result and session.completedAt no longer exist on all sessions. Narrow by session.status:
// Before (v0.1.x)
if (session.result?.verified) { ... }
// After (v0.2.0)
if (session.status === VerificationStatus.VERIFIED) {
// session.result is available here (TypeScript knows this)
console.log(session.result.country);
}Remove any handling for VerificationStatus.SCANNED -- it no longer exists.
Custom modes must accept BaseSession instead of VerificationSession:
// Before
async processCallback(session: VerificationSession, ...): Promise<VerificationResult>
// After
async processCallback(session: BaseSession, ...): Promise<VerificationResult>createSession() now returns Promise<PendingSession>. This is a narrowing of the previous Promise<VerificationSession> and is backward-compatible in most cases, but update type annotations accordingly.
Listen for the error event to handle DemoMode simulation failures:
service.on("error", (err, sessionId) => {
console.error("Simulation failed:", err.message);
});eIDAS Pro -- managed verification service with WooCommerce/Shopify plugins, admin dashboard, and compliance tools built on @openeudi/core.