@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 +249 -64
- package/dist/index.cjs +532 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +352 -57
- package/dist/index.d.ts +352 -57
- package/dist/index.js +530 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,16 +9,34 @@ npm install @openeudi/core
|
|
|
9
9
|
## Quick Start
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
56
|
-
|
|
73
|
+
verified: false,
|
|
74
|
+
rejectionReason: "underage",
|
|
57
75
|
});
|
|
58
76
|
|
|
59
|
-
//
|
|
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 {
|
|
86
|
+
import type {
|
|
87
|
+
IVerificationMode,
|
|
88
|
+
BaseSession,
|
|
89
|
+
VerificationResult,
|
|
90
|
+
} from "@openeudi/core";
|
|
69
91
|
|
|
70
92
|
class ProductionMode implements IVerificationMode {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
|
178
|
+
`VerificationService` extends `EventEmitter` with fully typed events. Listener argument types are inferred from the event name -- no casting needed.
|
|
112
179
|
|
|
113
|
-
| Event
|
|
114
|
-
|
|
|
115
|
-
| `session:created`
|
|
116
|
-
| `session:verified` | `(session:
|
|
117
|
-
| `session:rejected` | `(session:
|
|
118
|
-
| `session:expired`
|
|
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) =>
|
|
190
|
+
service.on("session:created", (session) =>
|
|
191
|
+
sendSSE(session.id, { walletUrl: session.walletUrl })
|
|
192
|
+
);
|
|
122
193
|
service.on("session:verified", (session, result) =>
|
|
123
|
-
|
|
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
|
|
150
|
-
|
|
|
151
|
-
| `constructor(config)`
|
|
152
|
-
| `createSession(input)`
|
|
153
|
-
| `getSession(id)`
|
|
154
|
-
| `handleCallback(sessionId, walletResponse)` | `Promise<VerificationResult>`
|
|
155
|
-
| `
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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)
|