@openeudi/core 0.1.0 → 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/dist/index.js CHANGED
@@ -10,7 +10,6 @@ var VerificationType = /* @__PURE__ */ ((VerificationType2) => {
10
10
  })(VerificationType || {});
11
11
  var VerificationStatus = /* @__PURE__ */ ((VerificationStatus2) => {
12
12
  VerificationStatus2["PENDING"] = "PENDING";
13
- VerificationStatus2["SCANNED"] = "SCANNED";
14
13
  VerificationStatus2["VERIFIED"] = "VERIFIED";
15
14
  VerificationStatus2["REJECTED"] = "REJECTED";
16
15
  VerificationStatus2["EXPIRED"] = "EXPIRED";
@@ -32,6 +31,26 @@ var SessionExpiredError = class extends Error {
32
31
  Object.setPrototypeOf(this, new.target.prototype);
33
32
  }
34
33
  };
34
+ var SessionNotPendingError = class extends Error {
35
+ /** The session ID that was not in PENDING status */
36
+ sessionId;
37
+ /** The status the session was actually in */
38
+ currentStatus;
39
+ constructor(sessionId, currentStatus) {
40
+ super(`Session ${sessionId} is not pending (current status: ${currentStatus})`);
41
+ this.name = "SessionNotPendingError";
42
+ this.sessionId = sessionId;
43
+ this.currentStatus = currentStatus;
44
+ Object.setPrototypeOf(this, new.target.prototype);
45
+ }
46
+ };
47
+ var ServiceDestroyedError = class extends Error {
48
+ constructor() {
49
+ super("VerificationService has been destroyed and cannot accept new operations");
50
+ this.name = "ServiceDestroyedError";
51
+ Object.setPrototypeOf(this, new.target.prototype);
52
+ }
53
+ };
35
54
 
36
55
  // src/storage/memory.store.ts
37
56
  var InMemorySessionStore = class {
@@ -141,6 +160,299 @@ var MockMode = class {
141
160
  return this.processCallback(session, {});
142
161
  }
143
162
  };
163
+
164
+ // src/validation.ts
165
+ var ISO_3166_1_ALPHA2 = /* @__PURE__ */ new Set([
166
+ "AF",
167
+ "AX",
168
+ "AL",
169
+ "DZ",
170
+ "AS",
171
+ "AD",
172
+ "AO",
173
+ "AI",
174
+ "AQ",
175
+ "AG",
176
+ "AR",
177
+ "AM",
178
+ "AW",
179
+ "AU",
180
+ "AT",
181
+ "AZ",
182
+ "BS",
183
+ "BH",
184
+ "BD",
185
+ "BB",
186
+ "BY",
187
+ "BE",
188
+ "BZ",
189
+ "BJ",
190
+ "BM",
191
+ "BT",
192
+ "BO",
193
+ "BQ",
194
+ "BA",
195
+ "BW",
196
+ "BV",
197
+ "BR",
198
+ "IO",
199
+ "BN",
200
+ "BG",
201
+ "BF",
202
+ "BI",
203
+ "CV",
204
+ "KH",
205
+ "CM",
206
+ "CA",
207
+ "KY",
208
+ "CF",
209
+ "TD",
210
+ "CL",
211
+ "CN",
212
+ "CX",
213
+ "CC",
214
+ "CO",
215
+ "KM",
216
+ "CG",
217
+ "CD",
218
+ "CK",
219
+ "CR",
220
+ "CI",
221
+ "HR",
222
+ "CU",
223
+ "CW",
224
+ "CY",
225
+ "CZ",
226
+ "DK",
227
+ "DJ",
228
+ "DM",
229
+ "DO",
230
+ "EC",
231
+ "EG",
232
+ "SV",
233
+ "GQ",
234
+ "ER",
235
+ "EE",
236
+ "SZ",
237
+ "ET",
238
+ "FK",
239
+ "FO",
240
+ "FJ",
241
+ "FI",
242
+ "FR",
243
+ "GF",
244
+ "PF",
245
+ "TF",
246
+ "GA",
247
+ "GM",
248
+ "GE",
249
+ "DE",
250
+ "GH",
251
+ "GI",
252
+ "GR",
253
+ "GL",
254
+ "GD",
255
+ "GP",
256
+ "GU",
257
+ "GT",
258
+ "GG",
259
+ "GN",
260
+ "GW",
261
+ "GY",
262
+ "HT",
263
+ "HM",
264
+ "VA",
265
+ "HN",
266
+ "HK",
267
+ "HU",
268
+ "IS",
269
+ "IN",
270
+ "ID",
271
+ "IR",
272
+ "IQ",
273
+ "IE",
274
+ "IM",
275
+ "IL",
276
+ "IT",
277
+ "JM",
278
+ "JP",
279
+ "JE",
280
+ "JO",
281
+ "KZ",
282
+ "KE",
283
+ "KI",
284
+ "KP",
285
+ "KR",
286
+ "KW",
287
+ "KG",
288
+ "LA",
289
+ "LV",
290
+ "LB",
291
+ "LS",
292
+ "LR",
293
+ "LY",
294
+ "LI",
295
+ "LT",
296
+ "LU",
297
+ "MO",
298
+ "MG",
299
+ "MW",
300
+ "MY",
301
+ "MV",
302
+ "ML",
303
+ "MT",
304
+ "MH",
305
+ "MQ",
306
+ "MR",
307
+ "MU",
308
+ "YT",
309
+ "MX",
310
+ "FM",
311
+ "MD",
312
+ "MC",
313
+ "MN",
314
+ "ME",
315
+ "MS",
316
+ "MA",
317
+ "MZ",
318
+ "MM",
319
+ "NA",
320
+ "NR",
321
+ "NP",
322
+ "NL",
323
+ "NC",
324
+ "NZ",
325
+ "NI",
326
+ "NE",
327
+ "NG",
328
+ "NU",
329
+ "NF",
330
+ "MK",
331
+ "MP",
332
+ "NO",
333
+ "OM",
334
+ "PK",
335
+ "PW",
336
+ "PS",
337
+ "PA",
338
+ "PG",
339
+ "PY",
340
+ "PE",
341
+ "PH",
342
+ "PN",
343
+ "PL",
344
+ "PT",
345
+ "PR",
346
+ "QA",
347
+ "RE",
348
+ "RO",
349
+ "RU",
350
+ "RW",
351
+ "BL",
352
+ "SH",
353
+ "KN",
354
+ "LC",
355
+ "MF",
356
+ "PM",
357
+ "VC",
358
+ "WS",
359
+ "SM",
360
+ "ST",
361
+ "SA",
362
+ "SN",
363
+ "RS",
364
+ "SC",
365
+ "SL",
366
+ "SG",
367
+ "SX",
368
+ "SK",
369
+ "SI",
370
+ "SB",
371
+ "SO",
372
+ "ZA",
373
+ "GS",
374
+ "SS",
375
+ "ES",
376
+ "LK",
377
+ "SD",
378
+ "SR",
379
+ "SJ",
380
+ "SE",
381
+ "CH",
382
+ "SY",
383
+ "TW",
384
+ "TJ",
385
+ "TZ",
386
+ "TH",
387
+ "TL",
388
+ "TG",
389
+ "TK",
390
+ "TO",
391
+ "TT",
392
+ "TN",
393
+ "TR",
394
+ "TM",
395
+ "TC",
396
+ "TV",
397
+ "UG",
398
+ "UA",
399
+ "AE",
400
+ "GB",
401
+ "US",
402
+ "UM",
403
+ "UY",
404
+ "UZ",
405
+ "VU",
406
+ "VE",
407
+ "VN",
408
+ "VG",
409
+ "VI",
410
+ "WF",
411
+ "EH",
412
+ "YE",
413
+ "ZM",
414
+ "ZW"
415
+ ]);
416
+ function isValidCountryCode(code) {
417
+ return ISO_3166_1_ALPHA2.has(code);
418
+ }
419
+ function validateConfig(config) {
420
+ if (config.sessionTtlMs !== void 0) {
421
+ if (config.sessionTtlMs <= 0) {
422
+ throw new Error(
423
+ `sessionTtlMs must be a positive number, received: ${config.sessionTtlMs}`
424
+ );
425
+ }
426
+ }
427
+ if (config.walletBaseUrl !== void 0) {
428
+ if (config.walletBaseUrl.trim().length === 0) {
429
+ throw new Error("walletBaseUrl must be a non-empty string");
430
+ }
431
+ }
432
+ }
433
+ function validateSessionInput(input) {
434
+ if (input.countryWhitelist !== void 0 && input.countryBlacklist !== void 0) {
435
+ throw new Error(
436
+ "countryWhitelist and countryBlacklist cannot both be provided; use one or the other"
437
+ );
438
+ }
439
+ if (input.countryWhitelist !== void 0) {
440
+ const invalid = input.countryWhitelist.filter((code) => !isValidCountryCode(code));
441
+ if (invalid.length > 0) {
442
+ throw new Error(
443
+ `countryWhitelist contains invalid ISO 3166-1 alpha-2 codes: ${invalid.join(", ")}`
444
+ );
445
+ }
446
+ }
447
+ if (input.countryBlacklist !== void 0) {
448
+ const invalid = input.countryBlacklist.filter((code) => !isValidCountryCode(code));
449
+ if (invalid.length > 0) {
450
+ throw new Error(
451
+ `countryBlacklist contains invalid ISO 3166-1 alpha-2 codes: ${invalid.join(", ")}`
452
+ );
453
+ }
454
+ }
455
+ }
144
456
  var DEFAULT_TTL_MS = 3e5;
145
457
  var DEFAULT_WALLET_BASE_URL = "openid4vp://verify";
146
458
  var VerificationService = class extends EventEmitter {
@@ -150,25 +462,110 @@ var VerificationService = class extends EventEmitter {
150
462
  walletBaseUrl;
151
463
  /** Track session IDs for cleanup (store interface has no list method) */
152
464
  sessionIds = /* @__PURE__ */ new Set();
465
+ /** Whether {@link destroy} has been called */
466
+ destroyed = false;
467
+ /**
468
+ * Create a new VerificationService instance.
469
+ *
470
+ * @param config - Service configuration including mode, store, TTL, and wallet URL
471
+ * @throws {Error} If config.sessionTtlMs is not positive or config.walletBaseUrl is empty
472
+ *
473
+ * @example
474
+ * ```typescript
475
+ * const service = new VerificationService({
476
+ * mode: new DemoMode(),
477
+ * store: new InMemorySessionStore(),
478
+ * sessionTtlMs: 300_000,
479
+ * walletBaseUrl: 'openid4vp://verify',
480
+ * });
481
+ * ```
482
+ */
153
483
  constructor(config) {
154
484
  super();
485
+ validateConfig(config);
155
486
  this.mode = config.mode;
156
487
  this.store = config.store ?? new InMemorySessionStore();
157
488
  this.sessionTtlMs = config.sessionTtlMs ?? DEFAULT_TTL_MS;
158
489
  this.walletBaseUrl = config.walletBaseUrl ?? DEFAULT_WALLET_BASE_URL;
159
490
  }
491
+ // -----------------------------------------------------------------------
492
+ // Typed EventEmitter overrides
493
+ // -----------------------------------------------------------------------
494
+ /**
495
+ * Register a listener for a typed event.
496
+ *
497
+ * @param event - Event name from {@link VerificationEvents}
498
+ * @param listener - Callback receiving the event's typed arguments
499
+ * @returns this (for chaining)
500
+ */
501
+ on(event, listener) {
502
+ return super.on(event, listener);
503
+ }
504
+ /**
505
+ * Register a one-time listener for a typed event.
506
+ *
507
+ * @param event - Event name from {@link VerificationEvents}
508
+ * @param listener - Callback receiving the event's typed arguments (called at most once)
509
+ * @returns this (for chaining)
510
+ */
511
+ once(event, listener) {
512
+ return super.once(event, listener);
513
+ }
514
+ /**
515
+ * Remove a previously registered listener for a typed event.
516
+ *
517
+ * @param event - Event name from {@link VerificationEvents}
518
+ * @param listener - The exact function reference that was registered
519
+ * @returns this (for chaining)
520
+ */
521
+ off(event, listener) {
522
+ return super.off(event, listener);
523
+ }
524
+ /**
525
+ * Emit a typed event.
526
+ *
527
+ * @param event - Event name from {@link VerificationEvents}
528
+ * @param args - Arguments matching the event's type signature
529
+ * @returns true if the event had listeners, false otherwise
530
+ */
531
+ emit(event, ...args) {
532
+ return super.emit(event, ...args);
533
+ }
534
+ // -----------------------------------------------------------------------
535
+ // Public API
536
+ // -----------------------------------------------------------------------
160
537
  /**
161
- * Create a new verification session
538
+ * Create a new verification session.
539
+ *
540
+ * Builds the wallet URL, persists the session, emits `session:created`,
541
+ * and (if the mode supports it) kicks off background auto-completion.
542
+ *
543
+ * @param input - Session creation parameters (type, country filters, metadata)
544
+ * @returns The newly created pending session with a populated walletUrl
545
+ * @throws {ServiceDestroyedError} If the service has been destroyed
546
+ * @throws {Error} If input validation fails (e.g. invalid country codes)
547
+ * @emits session:created When the session is persisted
548
+ *
549
+ * @example
550
+ * ```typescript
551
+ * const session = await service.createSession({
552
+ * type: VerificationType.AGE,
553
+ * countryWhitelist: ['DE', 'FR'],
554
+ * });
555
+ * console.log(session.walletUrl); // 'openid4vp://verify?session=...'
556
+ * ```
162
557
  */
163
558
  async createSession(input) {
559
+ this.assertNotDestroyed();
560
+ validateSessionInput(input);
164
561
  const id = v4();
165
562
  const now = /* @__PURE__ */ new Date();
563
+ const walletUrl = this.mode.buildWalletUrl ? await this.mode.buildWalletUrl(id, input) : `${this.walletBaseUrl}?session=${id}`;
166
564
  const session = {
167
565
  id,
168
566
  type: input.type,
169
567
  status: "PENDING" /* PENDING */,
170
- walletUrl: "",
171
- // set below, may be async from mode
568
+ walletUrl,
172
569
  countryWhitelist: input.countryWhitelist,
173
570
  countryBlacklist: input.countryBlacklist,
174
571
  redirectUrl: input.redirectUrl,
@@ -176,7 +573,6 @@ var VerificationService = class extends EventEmitter {
176
573
  createdAt: now,
177
574
  expiresAt: new Date(now.getTime() + this.sessionTtlMs)
178
575
  };
179
- session.walletUrl = this.mode.buildWalletUrl ? await this.mode.buildWalletUrl(id, input) : `${this.walletBaseUrl}?session=${id}`;
180
576
  await this.store.set(session);
181
577
  this.sessionIds.add(id);
182
578
  this.emit("session:created", session);
@@ -186,16 +582,30 @@ var VerificationService = class extends EventEmitter {
186
582
  if (current && current.status === "PENDING" /* PENDING */) {
187
583
  await this.completeSession(current, result);
188
584
  }
189
- }).catch(() => {
585
+ }).catch((error) => {
586
+ this.emit("error", error instanceof Error ? error : new Error(String(error)), id);
190
587
  });
191
588
  }
192
589
  return session;
193
590
  }
194
591
  /**
195
- * Get a session by ID
196
- * @throws SessionNotFoundError
592
+ * Retrieve a session by its ID.
593
+ *
594
+ * @param id - The session UUID to look up
595
+ * @returns The session in its current state
596
+ * @throws {ServiceDestroyedError} If the service has been destroyed
597
+ * @throws {SessionNotFoundError} If no session exists with the given ID
598
+ *
599
+ * @example
600
+ * ```typescript
601
+ * const session = await service.getSession('550e8400-e29b-41d4-a716-446655440000');
602
+ * if (session.status === VerificationStatus.VERIFIED) {
603
+ * console.log('Already verified:', session.result);
604
+ * }
605
+ * ```
197
606
  */
198
607
  async getSession(id) {
608
+ this.assertNotDestroyed();
199
609
  const session = await this.store.get(id);
200
610
  if (!session) {
201
611
  throw new SessionNotFoundError(id);
@@ -203,11 +613,30 @@ var VerificationService = class extends EventEmitter {
203
613
  return session;
204
614
  }
205
615
  /**
206
- * Handle a callback from the wallet
207
- * @throws SessionNotFoundError
208
- * @throws SessionExpiredError
616
+ * Handle a callback from the wallet containing credential data.
617
+ *
618
+ * Delegates to the mode's `processCallback` to evaluate the wallet response,
619
+ * then transitions the session to VERIFIED or REJECTED.
620
+ *
621
+ * @param sessionId - The session UUID the callback belongs to
622
+ * @param walletResponse - Raw credential data from the wallet
623
+ * @returns The verification result
624
+ * @throws {ServiceDestroyedError} If the service has been destroyed
625
+ * @throws {SessionNotFoundError} If no session exists with the given ID
626
+ * @throws {SessionExpiredError} If the session has passed its TTL
627
+ * @emits session:verified When verification succeeds
628
+ * @emits session:rejected When verification fails
629
+ *
630
+ * @example
631
+ * ```typescript
632
+ * const result = await service.handleCallback(sessionId, walletData);
633
+ * if (result.verified) {
634
+ * grantAccess(result.country);
635
+ * }
636
+ * ```
209
637
  */
210
638
  async handleCallback(sessionId, walletResponse) {
639
+ this.assertNotDestroyed();
211
640
  const session = await this.store.get(sessionId);
212
641
  if (!session) {
213
642
  throw new SessionNotFoundError(sessionId);
@@ -220,10 +649,55 @@ var VerificationService = class extends EventEmitter {
220
649
  return result;
221
650
  }
222
651
  /**
223
- * Remove expired sessions from the store
224
- * @returns Number of sessions cleaned up
652
+ * Cancel a pending verification session.
653
+ *
654
+ * Only sessions in PENDING status can be cancelled. Completed or expired
655
+ * sessions will cause a {@link SessionNotPendingError}.
656
+ *
657
+ * @param id - The session UUID to cancel
658
+ * @throws {ServiceDestroyedError} If the service has been destroyed
659
+ * @throws {SessionNotFoundError} If no session exists with the given ID
660
+ * @throws {SessionNotPendingError} If the session is not in PENDING status
661
+ * @emits session:cancelled When the session is removed
662
+ *
663
+ * @example
664
+ * ```typescript
665
+ * await service.cancelSession(session.id);
666
+ * // session is now deleted from the store
667
+ * ```
668
+ */
669
+ async cancelSession(id) {
670
+ this.assertNotDestroyed();
671
+ const session = await this.store.get(id);
672
+ if (!session) {
673
+ throw new SessionNotFoundError(id);
674
+ }
675
+ if (session.status !== "PENDING" /* PENDING */) {
676
+ throw new SessionNotPendingError(id, session.status);
677
+ }
678
+ await this.store.delete(id);
679
+ this.sessionIds.delete(id);
680
+ this.emit("session:cancelled", session);
681
+ }
682
+ /**
683
+ * Remove expired sessions from the store.
684
+ *
685
+ * Iterates all tracked session IDs and transitions any pending session
686
+ * whose TTL has elapsed to EXPIRED status before deleting it.
687
+ *
688
+ * @returns Number of sessions that were expired and removed
689
+ * @throws {ServiceDestroyedError} If the service has been destroyed
690
+ * @emits session:expired For each session that is expired
691
+ *
692
+ * @example
693
+ * ```typescript
694
+ * // Run periodically (e.g. every 60 seconds)
695
+ * const count = await service.cleanupExpired();
696
+ * console.log(`Cleaned up ${count} expired sessions`);
697
+ * ```
225
698
  */
226
699
  async cleanupExpired() {
700
+ this.assertNotDestroyed();
227
701
  const now = /* @__PURE__ */ new Date();
228
702
  let count = 0;
229
703
  for (const id of this.sessionIds) {
@@ -233,7 +707,11 @@ var VerificationService = class extends EventEmitter {
233
707
  continue;
234
708
  }
235
709
  if (now > session.expiresAt && session.status === "PENDING" /* PENDING */) {
236
- const expired = { ...session, status: "EXPIRED" /* EXPIRED */ };
710
+ const expired = {
711
+ ...session,
712
+ status: "EXPIRED" /* EXPIRED */,
713
+ completedAt: now
714
+ };
237
715
  await this.store.set(expired);
238
716
  await this.store.delete(id);
239
717
  this.sessionIds.delete(id);
@@ -243,6 +721,40 @@ var VerificationService = class extends EventEmitter {
243
721
  }
244
722
  return count;
245
723
  }
724
+ /**
725
+ * Permanently destroy this service instance.
726
+ *
727
+ * Removes all event listeners, clears tracked session IDs, and marks
728
+ * the instance as destroyed. All subsequent public method calls will
729
+ * throw {@link ServiceDestroyedError}.
730
+ *
731
+ * @example
732
+ * ```typescript
733
+ * service.destroy();
734
+ * // Any further call throws ServiceDestroyedError
735
+ * await service.createSession({ type: VerificationType.AGE }); // throws
736
+ * ```
737
+ */
738
+ destroy() {
739
+ this.removeAllListeners();
740
+ this.sessionIds.clear();
741
+ this.destroyed = true;
742
+ }
743
+ // -----------------------------------------------------------------------
744
+ // Private helpers
745
+ // -----------------------------------------------------------------------
746
+ /**
747
+ * Guard that throws if the service has been destroyed.
748
+ * Called at the top of every public method.
749
+ */
750
+ assertNotDestroyed() {
751
+ if (this.destroyed) {
752
+ throw new ServiceDestroyedError();
753
+ }
754
+ }
755
+ /**
756
+ * Transition a session to VERIFIED or REJECTED and emit the appropriate event.
757
+ */
246
758
  async completeSession(session, result) {
247
759
  const status = result.verified ? "VERIFIED" /* VERIFIED */ : "REJECTED" /* REJECTED */;
248
760
  const updated = {
@@ -252,17 +764,18 @@ var VerificationService = class extends EventEmitter {
252
764
  result
253
765
  };
254
766
  await this.store.set(updated);
767
+ this.sessionIds.delete(session.id);
255
768
  if (result.verified) {
256
769
  this.emit("session:verified", updated, result);
257
770
  } else {
258
- this.emit("session:rejected", updated, result.rejectionReason);
771
+ this.emit("session:rejected", updated, result.rejectionReason ?? "");
259
772
  }
260
773
  }
261
774
  };
262
775
 
263
776
  // src/index.ts
264
- var VERSION = "0.1.0";
777
+ var VERSION = "0.2.0";
265
778
 
266
- export { DemoMode, InMemorySessionStore, MockMode, SessionExpiredError, SessionNotFoundError, VERSION, VerificationService, VerificationStatus, VerificationType };
779
+ export { DemoMode, InMemorySessionStore, MockMode, ServiceDestroyedError, SessionExpiredError, SessionNotFoundError, SessionNotPendingError, VERSION, VerificationService, VerificationStatus, VerificationType, isValidCountryCode };
267
780
  //# sourceMappingURL=index.js.map
268
781
  //# sourceMappingURL=index.js.map