@openeudi/core 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,16 +9,34 @@ npm install @openeudi/core
9
9
  ## Quick Start
10
10
 
11
11
  ```ts
12
- import { VerificationService, DemoMode, VerificationType } from "@openeudi/core";
12
+ import {
13
+ VerificationService,
14
+ DemoMode,
15
+ VerificationType,
16
+ VerificationStatus,
17
+ } from "@openeudi/core";
13
18
 
14
19
  const service = new VerificationService({ mode: new DemoMode() });
15
20
 
21
+ // Typed events -- event name typos are caught at compile time
22
+ service.on("session:created", (session) => {
23
+ console.log("Session ready:", session.walletUrl);
24
+ });
16
25
  service.on("session:verified", (session, result) => {
17
- console.log(`Verified: ${result.country}, age: ${result.ageVerified}`);
26
+ console.log("Verified:", result.country, "age:", result.ageVerified);
27
+ });
28
+ service.on("session:rejected", (session, reason) => {
29
+ console.warn("Rejected:", reason);
30
+ });
31
+ service.on("error", (err, sessionId) => {
32
+ console.error("Background error for session", sessionId, err);
18
33
  });
19
34
 
20
35
  const session = await service.createSession({ type: VerificationType.BOTH });
21
36
  console.log(session.walletUrl); // openid4vp://verify?session=<uuid>
37
+
38
+ // Teardown when done
39
+ service.destroy();
22
40
  ```
23
41
 
24
42
  ## Modes
@@ -31,7 +49,7 @@ Auto-completes sessions with randomized EU citizen data after a configurable del
31
49
  import { DemoMode } from "@openeudi/core";
32
50
 
33
51
  const mode = new DemoMode({
34
- delayMs: 2000, // auto-complete after 2s (default: 3000)
52
+ delayMs: 2000, // auto-complete after 2s (default: 3000)
35
53
  });
36
54
  ```
37
55
 
@@ -43,8 +61,8 @@ Returns deterministic results for integration testing. Supports global defaults
43
61
  import { MockMode } from "@openeudi/core";
44
62
 
45
63
  const mode = new MockMode({
46
- defaultResult: { verified: true, country: "FR", ageVerified: true },
47
- delayMs: 100,
64
+ defaultResult: { verified: true, country: "FR", ageVerified: true },
65
+ delayMs: 100,
48
66
  });
49
67
 
50
68
  const service = new VerificationService({ mode });
@@ -52,11 +70,11 @@ const session = await service.createSession({ type: VerificationType.AGE });
52
70
 
53
71
  // Override result for a specific session
54
72
  mode.setSessionResult(session.id, {
55
- verified: false,
56
- rejectionReason: "underage",
73
+ verified: false,
74
+ rejectionReason: "underage",
57
75
  });
58
76
 
59
- // Clean up override
77
+ // Remove override
60
78
  mode.clearSessionResult(session.id);
61
79
  ```
62
80
 
@@ -65,15 +83,22 @@ mode.clearSessionResult(session.id);
65
83
  Implement `IVerificationMode` to connect to a real EUDI Wallet relying party:
66
84
 
67
85
  ```ts
68
- import type { IVerificationMode, VerificationSession, VerificationResult } from "@openeudi/core";
86
+ import type {
87
+ IVerificationMode,
88
+ BaseSession,
89
+ VerificationResult,
90
+ } from "@openeudi/core";
69
91
 
70
92
  class ProductionMode implements IVerificationMode {
71
- readonly name = "production";
72
-
73
- async processCallback(session: VerificationSession, walletResponse: unknown): Promise<VerificationResult> {
74
- const claims = await verifyVPToken(walletResponse); // your OpenID4VP logic
75
- return { verified: true, country: claims.country, ageVerified: claims.age >= 18 };
76
- }
93
+ readonly name = "production";
94
+
95
+ async processCallback(
96
+ session: BaseSession,
97
+ walletResponse: unknown,
98
+ ): Promise<VerificationResult> {
99
+ const claims = await verifyVPToken(walletResponse); // your OpenID4VP logic
100
+ return { verified: true, country: claims.country, ageVerified: claims.age >= 18 };
101
+ }
77
102
  }
78
103
  ```
79
104
 
@@ -85,47 +110,140 @@ The default `InMemorySessionStore` works for single-process deployments. Impleme
85
110
  import type { ISessionStore, VerificationSession } from "@openeudi/core";
86
111
 
87
112
  class RedisSessionStore implements ISessionStore {
88
- constructor(private redis: Redis) {}
89
-
90
- async get(id: string): Promise<VerificationSession | null> {
91
- const data = await this.redis.get(`session:${id}`);
92
- return data ? JSON.parse(data) : null;
93
- }
94
- async set(session: VerificationSession): Promise<void> {
95
- const ttl = Math.max(0, session.expiresAt.getTime() - Date.now());
96
- await this.redis.set(`session:${session.id}`, JSON.stringify(session), "PX", ttl);
97
- }
98
- async delete(id: string): Promise<void> {
99
- await this.redis.del(`session:${id}`);
100
- }
113
+ constructor(private redis: Redis) {}
114
+
115
+ async get(id: string): Promise<VerificationSession | null> {
116
+ const data = await this.redis.get(`session:${id}`);
117
+ return data ? JSON.parse(data) : null;
118
+ }
119
+ async set(session: VerificationSession): Promise<void> {
120
+ const ttl = Math.max(0, session.expiresAt.getTime() - Date.now());
121
+ await this.redis.set(`session:${session.id}`, JSON.stringify(session), "PX", ttl);
122
+ }
123
+ async delete(id: string): Promise<void> {
124
+ await this.redis.del(`session:${id}`);
125
+ }
101
126
  }
102
127
 
103
128
  const service = new VerificationService({
104
- mode: new DemoMode(),
105
- store: new RedisSessionStore(new Redis()),
129
+ mode: new DemoMode(),
130
+ store: new RedisSessionStore(new Redis()),
106
131
  });
107
132
  ```
108
133
 
134
+ ## Discriminated Union Types
135
+
136
+ `VerificationSession` is a discriminated union. Narrow by `session.status` to access phase-specific fields:
137
+
138
+ ```ts
139
+ import {
140
+ VerificationStatus,
141
+ type VerificationSession,
142
+ type PendingSession,
143
+ type CompletedSession,
144
+ type ExpiredSession,
145
+ } from "@openeudi/core";
146
+
147
+ function inspect(session: VerificationSession) {
148
+ switch (session.status) {
149
+ case VerificationStatus.PENDING:
150
+ // session is PendingSession here
151
+ console.log("Waiting for wallet, url:", session.walletUrl);
152
+ break;
153
+
154
+ case VerificationStatus.VERIFIED:
155
+ case VerificationStatus.REJECTED:
156
+ // session is CompletedSession here
157
+ // .result and .completedAt are available
158
+ console.log("Result:", session.result, "at:", session.completedAt);
159
+ break;
160
+
161
+ case VerificationStatus.EXPIRED:
162
+ // session is ExpiredSession here
163
+ console.log("Expired at:", session.completedAt);
164
+ break;
165
+ }
166
+ }
167
+ ```
168
+
169
+ **Fields by type:**
170
+
171
+ | Field | BaseSession | PendingSession | CompletedSession | ExpiredSession |
172
+ |---|---|---|---|---|
173
+ | `id`, `type`, `status`, `walletUrl`, `createdAt`, `expiresAt` | yes | yes | yes | yes |
174
+ | `result`, `completedAt` | - | - | yes | yes |
175
+
109
176
  ## Events
110
177
 
111
- `VerificationService` extends `EventEmitter`. Subscribe to lifecycle events:
178
+ `VerificationService` extends `EventEmitter` with fully typed events. Listener argument types are inferred from the event name -- no casting needed.
112
179
 
113
- | Event | Handler Signature | Description |
114
- | ------------------ | -------------------------------------------------------------------- | ------------------------------ |
115
- | `session:created` | `(session: VerificationSession) => void` | Session created, QR code ready |
116
- | `session:verified` | `(session: VerificationSession, result: VerificationResult) => void` | Verification passed |
117
- | `session:rejected` | `(session: VerificationSession, reason?: string) => void` | Verification rejected |
118
- | `session:expired` | `(session: VerificationSession) => void` | Session TTL exceeded |
180
+ | Event | Handler Signature | Description |
181
+ | --- | --- | --- |
182
+ | `session:created` | `(session: PendingSession) => void` | Session created, wallet URL ready |
183
+ | `session:verified` | `(session: CompletedSession, result: VerificationResult) => void` | Verification passed |
184
+ | `session:rejected` | `(session: CompletedSession, reason: string) => void` | Verification rejected |
185
+ | `session:expired` | `(session: ExpiredSession) => void` | Session TTL exceeded |
186
+ | `session:cancelled` | `(session: PendingSession) => void` | Session cancelled by caller |
187
+ | `error` | `(err: Error, sessionId?: string) => void` | Background simulation error |
119
188
 
120
189
  ```ts
121
- service.on("session:created", (session) => sendSSE(session.id, { walletUrl: session.walletUrl }));
190
+ service.on("session:created", (session) =>
191
+ sendSSE(session.id, { walletUrl: session.walletUrl })
192
+ );
122
193
  service.on("session:verified", (session, result) =>
123
- sendSSE(session.id, { status: "verified", country: result.country })
194
+ sendSSE(session.id, { status: "verified", country: result.country })
195
+ );
196
+ service.on("session:rejected", (session, reason) =>
197
+ sendSSE(session.id, { status: "rejected", reason })
198
+ );
199
+ service.on("session:expired", (session) =>
200
+ sendSSE(session.id, { status: "expired" })
201
+ );
202
+ service.on("session:cancelled", (session) =>
203
+ sendSSE(session.id, { status: "cancelled" })
204
+ );
205
+ service.on("error", (err, sessionId) =>
206
+ console.error("Service error:", err.message, sessionId)
124
207
  );
125
- service.on("session:rejected", (session, reason) => sendSSE(session.id, { status: "rejected", reason }));
126
- service.on("session:expired", (session) => sendSSE(session.id, { status: "expired" }));
127
208
  ```
128
209
 
210
+ ## Input Validation
211
+
212
+ `createSession()` validates inputs and throws synchronously on bad data:
213
+
214
+ - `countryWhitelist` and `countryBlacklist` are mutually exclusive
215
+ - All country codes must be valid ISO 3166-1 alpha-2 (e.g. `"DE"`, `"FR"`)
216
+ - `isValidCountryCode(code)` is exported for use in your own validation layer
217
+
218
+ ```ts
219
+ import { isValidCountryCode } from "@openeudi/core";
220
+
221
+ isValidCountryCode("DE"); // true
222
+ isValidCountryCode("XX"); // false
223
+ isValidCountryCode("deu"); // false (must be 2 uppercase letters)
224
+ ```
225
+
226
+ Constructor config is also validated:
227
+
228
+ - `sessionTtlMs` must be a positive integer
229
+ - `walletBaseUrl` must be a non-empty string
230
+
231
+ ## Service Lifecycle
232
+
233
+ ```ts
234
+ const service = new VerificationService({ mode: new DemoMode() });
235
+
236
+ // ... normal usage ...
237
+
238
+ // Teardown: removes all listeners, clears session tracking, prevents future calls
239
+ service.destroy();
240
+
241
+ // All calls after destroy() throw ServiceDestroyedError
242
+ await service.createSession({ type: VerificationType.AGE }); // throws
243
+ ```
244
+
245
+ Use `destroy()` in server shutdown handlers to prevent memory leaks when hot-reloading.
246
+
129
247
  ## QR Code
130
248
 
131
249
  Optional helper to generate QR code data URIs. Requires the `qrcode` peer dependency:
@@ -146,49 +264,116 @@ const dataUri = await generateQRCode(session.walletUrl);
146
264
 
147
265
  ### `VerificationService`
148
266
 
149
- | Method | Returns | Description |
150
- | ------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------ |
151
- | `constructor(config)` | `VerificationService` | Create service with mode, optional store and TTL |
152
- | `createSession(input)` | `Promise<VerificationSession>` | Create a new verification session |
153
- | `getSession(id)` | `Promise<VerificationSession>` | Retrieve session by ID (throws `SessionNotFoundError`) |
154
- | `handleCallback(sessionId, walletResponse)` | `Promise<VerificationResult>` | Process wallet callback (throws `SessionNotFoundError`, `SessionExpiredError`) |
155
- | `cleanupExpired()` | `Promise<number>` | Remove expired sessions, returns count cleaned |
267
+ | Method | Returns | Description |
268
+ | --- | --- | --- |
269
+ | `constructor(config)` | `VerificationService` | Create service with mode, optional store, TTL, and wallet URL |
270
+ | `createSession(input)` | `Promise<PendingSession>` | Create a new verification session |
271
+ | `getSession(id)` | `Promise<VerificationSession>` | Retrieve session by ID (throws `SessionNotFoundError`) |
272
+ | `handleCallback(sessionId, walletResponse)` | `Promise<VerificationResult>` | Process wallet callback (throws `SessionNotFoundError`, `SessionExpiredError`) |
273
+ | `cancelSession(id)` | `Promise<void>` | Cancel a pending session (throws `SessionNotPendingError` if not pending) |
274
+ | `cleanupExpired()` | `Promise<number>` | Remove expired sessions, returns count cleaned |
275
+ | `destroy()` | `void` | Permanently destroy the service and prevent further use |
156
276
 
157
277
  ### Configuration
158
278
 
159
279
  ```ts
160
280
  interface VerificationServiceConfig {
161
- mode: IVerificationMode; // Required DemoMode, MockMode, or custom
162
- store?: ISessionStore; // Default: InMemorySessionStore
163
- sessionTtlMs?: number; // Default: 300_000 (5 minutes)
164
- walletBaseUrl?: string; // Default: 'openid4vp://verify'
281
+ mode: IVerificationMode; // Required -- DemoMode, MockMode, or custom
282
+ store?: ISessionStore; // Default: InMemorySessionStore
283
+ sessionTtlMs?: number; // Default: 300_000 (5 minutes)
284
+ walletBaseUrl?: string; // Default: 'openid4vp://verify'
165
285
  }
166
286
  ```
167
287
 
288
+ ### Error Classes
289
+
290
+ | Class | Thrown by | Description |
291
+ | --- | --- | --- |
292
+ | `SessionNotFoundError` | `getSession`, `handleCallback`, `cancelSession` | No session with the given ID |
293
+ | `SessionExpiredError` | `handleCallback` | Session TTL has elapsed |
294
+ | `SessionNotPendingError` | `cancelSession` | Session is not in PENDING status |
295
+ | `ServiceDestroyedError` | All public methods | Service has been destroyed |
296
+
168
297
  ## Types
169
298
 
170
299
  All types are exported from the main entry point:
171
300
 
172
301
  ```ts
173
302
  import type {
174
- VerificationSession, // Full session object
175
- VerificationResult, // Outcome of a verification
176
- CreateSessionInput, // Input for createSession()
177
- VerificationServiceConfig, // Constructor config
178
- IVerificationMode, // Strategy interface for modes
179
- ISessionStore, // Storage adapter interface
180
- DemoModeConfig, // DemoMode constructor options
181
- MockModeConfig, // MockMode constructor options
303
+ VerificationSession, // Discriminated union (Pending | Completed | Expired)
304
+ BaseSession, // Common fields shared by all session states
305
+ PendingSession, // Status: PENDING
306
+ CompletedSession, // Status: VERIFIED or REJECTED (.result, .completedAt present)
307
+ ExpiredSession, // Status: EXPIRED (.completedAt present)
308
+ VerificationResult, // Outcome of a verification
309
+ CreateSessionInput, // Input for createSession()
310
+ VerificationServiceConfig, // Constructor config
311
+ VerificationEvents, // Event map for typed EventEmitter
312
+ IVerificationMode, // Strategy interface for modes
313
+ ISessionStore, // Storage adapter interface
314
+ DemoModeConfig, // DemoMode constructor options
315
+ MockModeConfig, // MockMode constructor options
182
316
  } from "@openeudi/core";
183
317
 
184
318
  import {
185
- VerificationType, // AGE | COUNTRY | BOTH
186
- VerificationStatus, // PENDING | SCANNED | VERIFIED | REJECTED | EXPIRED
187
- SessionNotFoundError, // Thrown by getSession / handleCallback
188
- SessionExpiredError, // Thrown by handleCallback
319
+ VerificationType, // AGE | COUNTRY | BOTH
320
+ VerificationStatus, // PENDING | VERIFIED | REJECTED | EXPIRED
321
+ SessionNotFoundError,
322
+ SessionExpiredError,
323
+ SessionNotPendingError,
324
+ ServiceDestroyedError,
325
+ isValidCountryCode,
326
+ VERSION, // '0.2.0'
189
327
  } from "@openeudi/core";
190
328
  ```
191
329
 
330
+ ## Migration from v0.1.x
331
+
332
+ ### Discriminated union sessions
333
+
334
+ `session.result` and `session.completedAt` no longer exist on all sessions. Narrow by `session.status`:
335
+
336
+ ```ts
337
+ // Before (v0.1.x)
338
+ if (session.result?.verified) { ... }
339
+
340
+ // After (v0.2.0)
341
+ if (session.status === VerificationStatus.VERIFIED) {
342
+ // session.result is available here (TypeScript knows this)
343
+ console.log(session.result.country);
344
+ }
345
+ ```
346
+
347
+ ### VerificationStatus.SCANNED removed
348
+
349
+ Remove any handling for `VerificationStatus.SCANNED` -- it no longer exists.
350
+
351
+ ### IVerificationMode.processCallback signature
352
+
353
+ Custom modes must accept `BaseSession` instead of `VerificationSession`:
354
+
355
+ ```ts
356
+ // Before
357
+ async processCallback(session: VerificationSession, ...): Promise<VerificationResult>
358
+
359
+ // After
360
+ async processCallback(session: BaseSession, ...): Promise<VerificationResult>
361
+ ```
362
+
363
+ ### createSession return type
364
+
365
+ `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.
366
+
367
+ ### New required error handling
368
+
369
+ Listen for the `error` event to handle DemoMode simulation failures:
370
+
371
+ ```ts
372
+ service.on("error", (err, sessionId) => {
373
+ console.error("Simulation failed:", err.message);
374
+ });
375
+ ```
376
+
192
377
  ## License
193
378
 
194
379
  [Apache 2.0](./LICENSE)