npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@openeudi/core

v0.9.0

Published

Framework-agnostic EUDI Wallet verification protocol engine

Readme

@openeudi/core

Framework-agnostic EUDI Wallet verification protocol engine.

npm install @openeudi/core

Quick Start

import {
    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> (placeholder, see below)

// Teardown when done
service.destroy();

Modes

DemoMode

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)
});

MockMode

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);

Custom Mode

Implement IVerificationMode to connect to a real EUDI Wallet relying party:

import type {
    IVerificationMode,
    BaseSession,
    CreateSessionInput,
    VerificationResult,
} from "@openeudi/core";

/** Whole years between an ISO `YYYY-MM-DD` birth date and today (UTC). */
function ageInYears(birthdate: string, today = new Date()): number {
    const [year, month, day] = birthdate.split("-").map(Number);
    const beforeBirthday =
        today.getUTCMonth() + 1 < month ||
        (today.getUTCMonth() + 1 === month && today.getUTCDate() < day);
    return today.getUTCFullYear() - year - (beforeBirthday ? 1 : 0);
}

class ProductionMode implements IVerificationMode {
    readonly name = "production";

    async buildWalletUrl(sessionId: string, input: CreateSessionInput): Promise<string> {
        return buildAuthorizationRequest(sessionId, input); // your OpenID4VP request (client_id, request_uri, ...)
    }

    async processCallback(
        session: BaseSession,
        walletResponse: unknown,
    ): Promise<VerificationResult> {
        // Your OpenID4VP logic. Here: a PID as SD-JWT VC (vct "urn:eudi:pid:1").
        const claims = await verifyVPToken(walletResponse);
        // The PID has no age_over_18 attribute: derive the age from `birthdate`
        // (`birth_date` in the mdoc PID, namespace eu.europa.ec.eudi.pid.1), or
        // request a proof-of-age attestation (mdoc eu.europa.ec.av.1, age_over_NN).
        const ageVerified = ageInYears(claims.birthdate) >= 18;
        return { verified: ageVerified, country: claims.address?.country, ageVerified };
    }
}

A mode that talks to real wallets must implement buildWalletUrl. Without it the service falls back to <walletBaseUrl>?session=<id> (openid4vp://verify?session=<id> by default). That link is a placeholder for demos and tests: it carries no client_id, request_uri or dcql_query, so it is not an OpenID4VP Authorization Request.

Custom Storage

The default InMemorySessionStore works for single-process deployments. Implement ISessionStore for Redis, PostgreSQL, etc:

import type {
    ISessionStore,
    StoredSession,
    StoredSessionStatus,
    VerificationSession,
} from "@openeudi/core";

// Replace the session only if its current status matches, in one atomic step.
const COMPARE_AND_SET = `
local current = redis.call('GET', KEYS[1])
if not current or cjson.decode(current).status ~= ARGV[1] then return 0 end
redis.call('SET', KEYS[1], ARGV[2], 'PX', ARGV[3])
return 1`;

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}`);
    }
    // Optional. Needed when several processes share the store.
    async compareAndSet(
        id: string,
        expectedStatus: StoredSessionStatus,
        next: StoredSession,
    ): Promise<boolean> {
        const ttl = Math.max(1, next.expiresAt.getTime() - Date.now());
        const replaced = await this.redis.eval(
            COMPARE_AND_SET, 1, `session:${id}`, expectedStatus, JSON.stringify(next), ttl,
        );
        return replaced === 1;
    }
}

const service = new VerificationService({
    mode: new DemoMode(),
    store: new RedisSessionStore(new Redis()),
});

A session accepts exactly one wallet callback: handleCallback claims the session before the mode processes the response, and every other callback for it gets SessionNotPendingError. Two optional store methods matter when several processes share a store:

  • compareAndSet(id, expectedStatus, next) makes that claim atomic across all of them. Without it, the one-callback guarantee holds only inside one process. While a callback is processed, the stored status is PROCESSING (exported as PROCESSING_STATUS); your schema must accept it. getSession still reports such a session as PENDING.
  • listExpired(now) returns the IDs of PENDING sessions past expiresAt. Without it, cleanupExpired() only sees sessions created by the same instance since it started.

Discriminated Union Types

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 |

Events

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)
);

Input Validation

createSession() validates inputs and throws synchronously on bad data:

  • countryWhitelist and countryBlacklist are 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:

  • sessionTtlMs must be a positive integer
  • walletBaseUrl must be a non-empty string

Service Lifecycle

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 }); // throws

Use destroy() in server shutdown handlers to prevent memory leaks when hot-reloading.

QR Code

Optional helper to generate QR code data URIs. Requires the qrcode peer dependency:

npm install qrcode
import { generateQRCode } from "@openeudi/core/qr";

const session = await service.createSession({ type: VerificationType.AGE });
const dataUri = await generateQRCode(session.walletUrl);
// => "data:image/png;base64,..."

API Reference

VerificationService

| 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 the wallet callback; accepted once per session (throws SessionNotFoundError, SessionNotPendingError, 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 |

Configuration

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' (placeholder link, used only without buildWalletUrl)
}

Error Classes

| Class | Thrown by | Description | | --- | --- | --- | | SessionNotFoundError | getSession, handleCallback, cancelSession | No session with the given ID | | SessionExpiredError | handleCallback | Session TTL has elapsed | | SessionNotPendingError | handleCallback, cancelSession | Session is not in PENDING status (already completed, or a callback is being processed) | | ServiceDestroyedError | All public methods | Service has been destroyed |

Types

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
    StoredSession,          // What a store holds: VerificationSession | ProcessingSession
    StoredSessionStatus,    // VerificationStatus values plus 'PROCESSING'
    ProcessingSession,      // Stored while a callback is processed (compareAndSet stores only)
    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,
    PROCESSING_STATUS,      // 'PROCESSING' (storage-only marker)
    VERSION,                // Package version string, same as package.json
} from "@openeudi/core";

Migration from v0.1.x

Discriminated union sessions

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);
}

VerificationStatus.SCANNED removed

Remove any handling for VerificationStatus.SCANNED -- it no longer exists.

IVerificationMode.processCallback signature

Custom modes must accept BaseSession instead of VerificationSession:

// Before
async processCallback(session: VerificationSession, ...): Promise<VerificationResult>

// After
async processCallback(session: BaseSession, ...): Promise<VerificationResult>

createSession return type

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.

New required error handling

Listen for the error event to handle DemoMode simulation failures:

service.on("error", (err, sessionId) => {
    console.error("Simulation failed:", err.message);
});

License

Apache 2.0

Related

eIDAS Pro -- managed verification service with WooCommerce/Shopify plugins, admin dashboard, and compliance tools built on @openeudi/core.

Community integrations

Third-party projects built on this library. These are not maintained or audited by OpenEUDI -- evaluate them on their own merits.

Built something on @openeudi/core? Open an issue and we're happy to consider listing it here.