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