@eudi-verify/server 0.1.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 ADDED
@@ -0,0 +1,968 @@
1
+ // src/types.ts
2
+ var TERMINAL_STATUSES = [
3
+ "verified",
4
+ "rejected",
5
+ "expired",
6
+ "cancelled",
7
+ "error"
8
+ ];
9
+ function isTerminalStatus(status) {
10
+ return TERMINAL_STATUSES.includes(status);
11
+ }
12
+ function sessionToDTO(session) {
13
+ const dto = {
14
+ id: session.id,
15
+ status: session.status,
16
+ createdAt: session.createdAt.toISOString(),
17
+ expiresAt: session.expiresAt.toISOString()
18
+ };
19
+ if (session.qrUrl) dto.qrUrl = session.qrUrl;
20
+ if (session.token) dto.token = session.token;
21
+ if (session.claims) dto.claims = session.claims;
22
+ if (session.error) dto.error = session.error;
23
+ return dto;
24
+ }
25
+ var TOKEN_VERSION = "eudi_v1";
26
+ var DEFAULT_SESSION_TTL_MS = 5 * 60 * 1e3;
27
+ var DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1e3;
28
+
29
+ // src/store.ts
30
+ var MemoryKVStore = class {
31
+ store = /* @__PURE__ */ new Map();
32
+ cleanupInterval = null;
33
+ /**
34
+ * Create a new in-memory store.
35
+ * @param cleanupIntervalMs - How often to run expired key cleanup (default: 60s)
36
+ */
37
+ constructor(cleanupIntervalMs = 6e4) {
38
+ if (cleanupIntervalMs > 0) {
39
+ this.cleanupInterval = setInterval(() => {
40
+ this.cleanup();
41
+ }, cleanupIntervalMs);
42
+ if (this.cleanupInterval.unref) {
43
+ this.cleanupInterval.unref();
44
+ }
45
+ }
46
+ }
47
+ async get(key) {
48
+ const entry = this.store.get(key);
49
+ if (!entry) {
50
+ return void 0;
51
+ }
52
+ if (entry.expiresAt !== null && Date.now() > entry.expiresAt) {
53
+ this.store.delete(key);
54
+ return void 0;
55
+ }
56
+ return entry.value;
57
+ }
58
+ async set(key, value, ttlMs) {
59
+ const expiresAt = ttlMs ? Date.now() + ttlMs : null;
60
+ this.store.set(key, { value, expiresAt });
61
+ }
62
+ async delete(key) {
63
+ return this.store.delete(key);
64
+ }
65
+ async has(key) {
66
+ const value = await this.get(key);
67
+ return value !== void 0;
68
+ }
69
+ async getAndDelete(key) {
70
+ const value = await this.get(key);
71
+ if (value !== void 0) {
72
+ this.store.delete(key);
73
+ }
74
+ return value;
75
+ }
76
+ async clear() {
77
+ this.store.clear();
78
+ }
79
+ /**
80
+ * Remove expired entries. Called automatically on interval.
81
+ */
82
+ cleanup() {
83
+ const now = Date.now();
84
+ for (const [key, entry] of this.store) {
85
+ if (entry.expiresAt !== null && now > entry.expiresAt) {
86
+ this.store.delete(key);
87
+ }
88
+ }
89
+ }
90
+ /**
91
+ * Stop the cleanup interval. Call when disposing the store.
92
+ */
93
+ dispose() {
94
+ if (this.cleanupInterval) {
95
+ clearInterval(this.cleanupInterval);
96
+ this.cleanupInterval = null;
97
+ }
98
+ }
99
+ /**
100
+ * Get the current number of entries (for testing/monitoring).
101
+ */
102
+ get size() {
103
+ return this.store.size;
104
+ }
105
+ };
106
+ var KEY_PREFIX = {
107
+ SESSION: "session:",
108
+ TOKEN: "token:",
109
+ RATE_LIMIT: "rate:"
110
+ };
111
+ function sessionKey(sessionId) {
112
+ return `${KEY_PREFIX.SESSION}${sessionId}`;
113
+ }
114
+ function tokenKey(tokenId) {
115
+ return `${KEY_PREFIX.TOKEN}${tokenId}`;
116
+ }
117
+ function rateLimitKey(ip) {
118
+ return `${KEY_PREFIX.RATE_LIMIT}${ip}`;
119
+ }
120
+
121
+ // src/engine.ts
122
+ var MockEngine = class {
123
+ name = "mock";
124
+ mode = "demo";
125
+ config;
126
+ constructor(config = {}) {
127
+ this.config = {
128
+ verificationDelayMs: config.verificationDelayMs ?? 1e3,
129
+ successRate: config.successRate ?? 1,
130
+ defaultClaims: config.defaultClaims ?? {}
131
+ };
132
+ }
133
+ async createSession(config) {
134
+ const requestedClaims = Object.keys(config.request).filter(
135
+ (k) => config.request[k] === true
136
+ );
137
+ const qrUrl = this.buildMockQrUrl(config.sessionId, config.baseUrl, requestedClaims);
138
+ return {
139
+ qrUrl,
140
+ engineData: {
141
+ requestedClaims,
142
+ createdAt: Date.now()
143
+ }
144
+ };
145
+ }
146
+ async parseCallback(rawBody) {
147
+ const params = new URLSearchParams(rawBody);
148
+ const response = params.get("response");
149
+ const sessionId = params.get("session_id") || params.get("state");
150
+ if (!response || !sessionId) {
151
+ throw new Error("Invalid callback: missing response or session_id");
152
+ }
153
+ return { sessionId, response };
154
+ }
155
+ async handleCallback(data, session) {
156
+ if (this.config.verificationDelayMs > 0) {
157
+ await this.delay(this.config.verificationDelayMs);
158
+ }
159
+ const shouldSucceed = Math.random() < this.config.successRate;
160
+ if (!shouldSucceed) {
161
+ return {
162
+ success: false,
163
+ error: "Simulated verification failure",
164
+ status: "rejected"
165
+ };
166
+ }
167
+ const claims = this.generateMockClaims(session.request);
168
+ return {
169
+ success: true,
170
+ claims,
171
+ status: "verified"
172
+ };
173
+ }
174
+ async getAuthorizationRequest(session) {
175
+ return JSON.stringify({
176
+ type: "mock_authorization_request",
177
+ sessionId: session.id,
178
+ request: session.request
179
+ });
180
+ }
181
+ async cancelSession(_session) {
182
+ }
183
+ buildMockQrUrl(sessionId, baseUrl, claims) {
184
+ const params = new URLSearchParams({
185
+ session_id: sessionId,
186
+ claims: claims.join(","),
187
+ mock: "true"
188
+ });
189
+ return `${baseUrl}/mock-wallet?${params.toString()}`;
190
+ }
191
+ generateMockClaims(request) {
192
+ const claims = { ...this.config.defaultClaims };
193
+ if (request.age_over_18) claims.age_over_18 = true;
194
+ if (request.age_over_21) claims.age_over_21 = true;
195
+ if (request.nationality) claims.nationality = "LU";
196
+ if (request.given_name) claims.given_name = "Max";
197
+ if (request.family_name) claims.family_name = "Mustermann";
198
+ if (request.birth_date) claims.birth_date = "1990-01-15";
199
+ return claims;
200
+ }
201
+ delay(ms) {
202
+ return new Promise((resolve) => setTimeout(resolve, ms));
203
+ }
204
+ };
205
+
206
+ // src/engines/openeudi.ts
207
+ var OpenEudiEngine = class {
208
+ name = "openeudi";
209
+ mode;
210
+ config;
211
+ constructor(options) {
212
+ this.mode = options.mode;
213
+ this.config = {
214
+ mode: options.mode,
215
+ baseUrl: options.baseUrl,
216
+ sessionTtlMs: options.sessionTtlMs ?? 5 * 60 * 1e3,
217
+ demoClaims: options.demoClaims ?? {
218
+ age_over_18: true,
219
+ age_over_21: true,
220
+ nationality: "LU",
221
+ given_name: "Jean",
222
+ family_name: "Dupont",
223
+ birth_date: "1985-03-15"
224
+ },
225
+ demoDelayMs: options.demoDelayMs ?? 0
226
+ };
227
+ }
228
+ async initialize() {
229
+ if (this.mode === "demo") {
230
+ console.warn(
231
+ "[OpenEudiEngine] Running in DEMO mode. Credentials are simulated. Do NOT use in production."
232
+ );
233
+ }
234
+ }
235
+ async createSession(config) {
236
+ const nonce = this.generateNonce();
237
+ const requestedClaims = Object.keys(config.request).filter(
238
+ (k) => config.request[k] === true
239
+ );
240
+ const engineData = {
241
+ nonce,
242
+ requestedClaims,
243
+ createdAt: Date.now()
244
+ };
245
+ const qrUrl = this.buildAuthorizationRequestUrl(config, nonce);
246
+ return { qrUrl, engineData };
247
+ }
248
+ async parseCallback(rawBody) {
249
+ const params = new URLSearchParams(rawBody);
250
+ const response = params.get("response");
251
+ const state = params.get("state") || params.get("session_id");
252
+ if (!response || !state) {
253
+ throw new Error("Invalid callback: missing response or state");
254
+ }
255
+ return { sessionId: state, response };
256
+ }
257
+ async handleCallback(data, session) {
258
+ if (this.config.demoDelayMs > 0) {
259
+ await this.delay(this.config.demoDelayMs);
260
+ }
261
+ if (this.mode === "demo") {
262
+ return this.handleDemoCallback(data, session);
263
+ }
264
+ return this.handleProductionCallback(data, session);
265
+ }
266
+ async getAuthorizationRequest(session) {
267
+ const engineData = session._engineData;
268
+ const nonce = engineData?.nonce ?? this.generateNonce();
269
+ if (this.mode === "demo") {
270
+ return JSON.stringify({
271
+ type: "authorization_request",
272
+ response_type: "vp_token",
273
+ client_id: this.config.baseUrl,
274
+ redirect_uri: `${this.config.baseUrl}/callback`,
275
+ state: session.id,
276
+ nonce,
277
+ presentation_definition: this.buildPresentationDefinition(session.request),
278
+ mode: "demo"
279
+ });
280
+ }
281
+ return JSON.stringify({
282
+ type: "authorization_request",
283
+ response_type: "vp_token",
284
+ client_id: this.config.baseUrl,
285
+ redirect_uri: `${this.config.baseUrl}/callback`,
286
+ state: session.id,
287
+ nonce,
288
+ presentation_definition: this.buildPresentationDefinition(session.request)
289
+ });
290
+ }
291
+ async cancelSession(_session) {
292
+ }
293
+ async shutdown() {
294
+ }
295
+ handleDemoCallback(_data, session) {
296
+ const engineData = session._engineData;
297
+ const requestedClaims = engineData?.requestedClaims ?? Object.keys(session.request);
298
+ const claims = this.generateDemoClaims(requestedClaims);
299
+ return {
300
+ success: true,
301
+ claims,
302
+ status: "verified"
303
+ };
304
+ }
305
+ handleProductionCallback(_data, _session) {
306
+ return {
307
+ success: false,
308
+ error: "Production mode not yet implemented",
309
+ status: "error"
310
+ };
311
+ }
312
+ buildAuthorizationRequestUrl(config, nonce) {
313
+ if (this.mode === "demo") {
314
+ const params2 = new URLSearchParams({
315
+ client_id: this.config.baseUrl,
316
+ response_type: "vp_token",
317
+ state: config.sessionId,
318
+ nonce,
319
+ redirect_uri: `${this.config.baseUrl}/callback`,
320
+ mode: "demo"
321
+ });
322
+ return `openid4vp://authorize?${params2.toString()}`;
323
+ }
324
+ const params = new URLSearchParams({
325
+ client_id: this.config.baseUrl,
326
+ request_uri: `${config.baseUrl}/request/${config.sessionId}`
327
+ });
328
+ return `openid4vp://authorize?${params.toString()}`;
329
+ }
330
+ buildPresentationDefinition(request) {
331
+ const requestedClaims = Object.keys(request).filter((k) => request[k] === true);
332
+ const inputDescriptors = requestedClaims.map((claim) => ({
333
+ id: claim,
334
+ name: this.getClaimDisplayName(claim),
335
+ purpose: `Verify ${this.getClaimDisplayName(claim).toLowerCase()}`,
336
+ constraints: {
337
+ fields: [
338
+ {
339
+ path: [`$.${claim}`, `$.vc.credentialSubject.${claim}`]
340
+ }
341
+ ]
342
+ }
343
+ }));
344
+ return {
345
+ id: `eudi-verify-${Date.now()}`,
346
+ name: "EUDI Verification Request",
347
+ purpose: "Identity verification",
348
+ input_descriptors: inputDescriptors
349
+ };
350
+ }
351
+ getClaimDisplayName(claim) {
352
+ const names = {
353
+ age_over_18: "Age over 18",
354
+ age_over_21: "Age over 21",
355
+ nationality: "Nationality",
356
+ given_name: "Given name",
357
+ family_name: "Family name",
358
+ birth_date: "Birth date"
359
+ };
360
+ return names[claim] ?? claim;
361
+ }
362
+ generateDemoClaims(requestedClaims) {
363
+ const claims = {};
364
+ const defaults = this.config.demoClaims;
365
+ for (const claim of requestedClaims) {
366
+ if (claim === "age_over_18" && defaults.age_over_18 !== void 0) {
367
+ claims.age_over_18 = defaults.age_over_18;
368
+ } else if (claim === "age_over_21" && defaults.age_over_21 !== void 0) {
369
+ claims.age_over_21 = defaults.age_over_21;
370
+ } else if (claim === "nationality" && defaults.nationality) {
371
+ claims.nationality = defaults.nationality;
372
+ } else if (claim === "given_name" && defaults.given_name) {
373
+ claims.given_name = defaults.given_name;
374
+ } else if (claim === "family_name" && defaults.family_name) {
375
+ claims.family_name = defaults.family_name;
376
+ } else if (claim === "birth_date" && defaults.birth_date) {
377
+ claims.birth_date = defaults.birth_date;
378
+ }
379
+ }
380
+ return claims;
381
+ }
382
+ generateNonce() {
383
+ const array = new Uint8Array(16);
384
+ crypto.getRandomValues(array);
385
+ return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
386
+ }
387
+ delay(ms) {
388
+ return new Promise((resolve) => setTimeout(resolve, ms));
389
+ }
390
+ };
391
+
392
+ // src/token.ts
393
+ import { createHmac, timingSafeEqual, randomUUID } from "crypto";
394
+ function createTokenService(config) {
395
+ const { secret, store, keyId = "k1", ttlMs = DEFAULT_TOKEN_TTL_MS } = config;
396
+ if (secret.length < 32) {
397
+ throw new Error("Token secret must be at least 32 characters");
398
+ }
399
+ return {
400
+ async mint(sessionId, claims) {
401
+ const tokenId = randomUUID();
402
+ const exp = Math.floor((Date.now() + ttlMs) / 1e3);
403
+ const claimsHash = hashClaims(claims, secret);
404
+ const payload = {
405
+ sid: sessionId,
406
+ kid: keyId,
407
+ exp,
408
+ hash: claimsHash
409
+ };
410
+ const payloadB64 = base64UrlEncode(JSON.stringify(payload));
411
+ const signature = createSignature(payloadB64, secret);
412
+ const token = `${TOKEN_VERSION}.${payloadB64}.${signature}`;
413
+ const tokenData = {
414
+ sessionId,
415
+ claims,
416
+ createdAt: Date.now()
417
+ };
418
+ await store.set(tokenKey(tokenId), tokenData, ttlMs);
419
+ const fullPayload = { ...payload, tid: tokenId };
420
+ const fullPayloadB64 = base64UrlEncode(JSON.stringify(fullPayload));
421
+ const fullSignature = createSignature(fullPayloadB64, secret);
422
+ return `${TOKEN_VERSION}.${fullPayloadB64}.${fullSignature}`;
423
+ },
424
+ async verify(token) {
425
+ const parsed = parseToken(token);
426
+ if (!parsed) {
427
+ return { valid: false, error: "invalid_token" };
428
+ }
429
+ const { version, payloadB64, signature, payload } = parsed;
430
+ if (version !== TOKEN_VERSION) {
431
+ return { valid: false, error: "invalid_token" };
432
+ }
433
+ if (!payload.tid || !payload.sid || !payload.exp || !payload.hash) {
434
+ return { valid: false, error: "invalid_token" };
435
+ }
436
+ const expectedSignature = createSignature(payloadB64, secret);
437
+ if (!constantTimeCompare(signature, expectedSignature)) {
438
+ return { valid: false, error: "invalid_signature" };
439
+ }
440
+ const nowSec = Math.floor(Date.now() / 1e3);
441
+ if (payload.exp <= nowSec) {
442
+ await store.delete(tokenKey(payload.tid));
443
+ return { valid: false, error: "expired" };
444
+ }
445
+ const storedData = await store.getAndDelete(
446
+ tokenKey(payload.tid)
447
+ );
448
+ if (!storedData) {
449
+ const currentSec = Math.floor(Date.now() / 1e3);
450
+ if (payload.exp <= currentSec) {
451
+ return { valid: false, error: "expired" };
452
+ }
453
+ return { valid: false, error: "already_consumed" };
454
+ }
455
+ if (storedData.sessionId !== payload.sid) {
456
+ return { valid: false, error: "invalid_token" };
457
+ }
458
+ const expectedHash = hashClaims(storedData.claims, secret);
459
+ if (payload.hash !== expectedHash) {
460
+ return { valid: false, error: "invalid_token" };
461
+ }
462
+ return { valid: true, claims: storedData.claims };
463
+ }
464
+ };
465
+ }
466
+ function parseToken(token) {
467
+ const parts = token.split(".");
468
+ if (parts.length !== 3) {
469
+ return null;
470
+ }
471
+ const [version, payloadB64, signature] = parts;
472
+ try {
473
+ const payloadJson = base64UrlDecode(payloadB64);
474
+ const payload = JSON.parse(payloadJson);
475
+ return { version, payloadB64, signature, payload };
476
+ } catch {
477
+ return null;
478
+ }
479
+ }
480
+ function createSignature(data, secret) {
481
+ return createHmac("sha256", secret).update(data).digest("base64url");
482
+ }
483
+ function constantTimeCompare(a, b) {
484
+ if (a.length !== b.length) {
485
+ const dummy = Buffer.alloc(a.length);
486
+ timingSafeEqual(dummy, dummy);
487
+ return false;
488
+ }
489
+ const bufA = Buffer.from(a);
490
+ const bufB = Buffer.from(b);
491
+ return timingSafeEqual(bufA, bufB);
492
+ }
493
+ function hashClaims(claims, secret) {
494
+ const sorted = JSON.stringify(claims, Object.keys(claims).sort());
495
+ return createHmac("sha256", secret).update(sorted).digest("base64url").slice(0, 16);
496
+ }
497
+ function base64UrlEncode(str) {
498
+ return Buffer.from(str).toString("base64url");
499
+ }
500
+ function base64UrlDecode(str) {
501
+ return Buffer.from(str, "base64url").toString("utf-8");
502
+ }
503
+
504
+ // src/rate-limit.ts
505
+ function createRateLimiter(config) {
506
+ const { maxRequests = 10, windowMs = 6e4, store } = config;
507
+ async function getOrCreateWindow(ip) {
508
+ const key = rateLimitKey(ip);
509
+ const now = Date.now();
510
+ const existing = await store.get(key);
511
+ if (existing && now - existing.windowStart < windowMs) {
512
+ return existing;
513
+ }
514
+ const data = {
515
+ count: 0,
516
+ windowStart: now
517
+ };
518
+ await store.set(key, data, windowMs);
519
+ return data;
520
+ }
521
+ async function incrementWindow(ip) {
522
+ const key = rateLimitKey(ip);
523
+ const now = Date.now();
524
+ const existing = await store.get(key);
525
+ if (existing && now - existing.windowStart < windowMs) {
526
+ const updated = {
527
+ count: existing.count + 1,
528
+ windowStart: existing.windowStart
529
+ };
530
+ const remainingTtl = windowMs - (now - existing.windowStart);
531
+ await store.set(key, updated, remainingTtl);
532
+ } else {
533
+ const data = {
534
+ count: 1,
535
+ windowStart: now
536
+ };
537
+ await store.set(key, data, windowMs);
538
+ }
539
+ }
540
+ return {
541
+ async check(ip) {
542
+ const window = await getOrCreateWindow(ip);
543
+ const remaining = Math.max(0, maxRequests - window.count);
544
+ const allowed = window.count < maxRequests;
545
+ const result = {
546
+ allowed,
547
+ remaining,
548
+ limit: maxRequests
549
+ };
550
+ if (!allowed) {
551
+ const elapsed = Date.now() - window.windowStart;
552
+ result.retryAfter = Math.ceil((windowMs - elapsed) / 1e3);
553
+ }
554
+ return result;
555
+ },
556
+ async consume(ip) {
557
+ await incrementWindow(ip);
558
+ },
559
+ async checkAndConsume(ip) {
560
+ const window = await getOrCreateWindow(ip);
561
+ const wouldBeCount = window.count + 1;
562
+ const allowed = wouldBeCount <= maxRequests;
563
+ if (allowed) {
564
+ await incrementWindow(ip);
565
+ }
566
+ const remaining = Math.max(0, maxRequests - wouldBeCount);
567
+ const result = {
568
+ allowed,
569
+ remaining: allowed ? remaining : Math.max(0, maxRequests - window.count),
570
+ limit: maxRequests
571
+ };
572
+ if (!allowed) {
573
+ const elapsed = Date.now() - window.windowStart;
574
+ result.retryAfter = Math.ceil((windowMs - elapsed) / 1e3);
575
+ }
576
+ return result;
577
+ }
578
+ };
579
+ }
580
+
581
+ // src/handlers.ts
582
+ import { randomUUID as randomUUID2 } from "crypto";
583
+ function createVerifierHandlers(config) {
584
+ const {
585
+ engine,
586
+ store,
587
+ baseUrl,
588
+ mode,
589
+ sessionTtlMs = DEFAULT_SESSION_TTL_MS,
590
+ tokenSecret,
591
+ tokenKeyId,
592
+ rateLimit: rateLimitConfig,
593
+ allowedOrigins = []
594
+ } = config;
595
+ const tokenService = createTokenService({
596
+ secret: tokenSecret,
597
+ keyId: tokenKeyId,
598
+ ttlMs: sessionTtlMs,
599
+ store
600
+ });
601
+ const rateLimiter = rateLimitConfig ? createRateLimiter({
602
+ maxRequests: rateLimitConfig.maxRequests,
603
+ windowMs: rateLimitConfig.windowMs,
604
+ store
605
+ }) : null;
606
+ function modeHeader() {
607
+ return { "X-Eudi-Mode": mode };
608
+ }
609
+ function logDemoWarning() {
610
+ if (mode === "demo") {
611
+ console.warn(
612
+ "[eudi-verify] WARNING: Running in demo mode. Credentials are simulated. Do not use in production."
613
+ );
614
+ }
615
+ }
616
+ function checkOrigin(origin) {
617
+ if (allowedOrigins.length === 0) return true;
618
+ if (!origin) return false;
619
+ return allowedOrigins.includes(origin);
620
+ }
621
+ async function getStoredSession(sessionId) {
622
+ const session = await store.get(sessionKey(sessionId));
623
+ if (!session) return null;
624
+ if (!isTerminalStatus(session.status) && /* @__PURE__ */ new Date() > new Date(session.expiresAt)) {
625
+ const expired = { ...session, status: "expired" };
626
+ await store.set(sessionKey(sessionId), expired);
627
+ return expired;
628
+ }
629
+ return session;
630
+ }
631
+ return {
632
+ async createSession(ctx) {
633
+ logDemoWarning();
634
+ if (!checkOrigin(ctx.origin)) {
635
+ return {
636
+ status: 403,
637
+ headers: modeHeader(),
638
+ body: {
639
+ error: "forbidden",
640
+ message: "Origin not allowed"
641
+ }
642
+ };
643
+ }
644
+ if (rateLimiter) {
645
+ const rateResult = await rateLimiter.checkAndConsume(ctx.ip);
646
+ if (!rateResult.allowed) {
647
+ return {
648
+ status: 429,
649
+ headers: {
650
+ ...modeHeader(),
651
+ "Retry-After": String(rateResult.retryAfter ?? 60)
652
+ },
653
+ body: {
654
+ error: "rate_limited",
655
+ message: "Too many requests, please retry later"
656
+ }
657
+ };
658
+ }
659
+ }
660
+ const input = ctx.body;
661
+ if (!input?.request || typeof input.request !== "object") {
662
+ return {
663
+ status: 400,
664
+ headers: modeHeader(),
665
+ body: {
666
+ error: "bad_request",
667
+ message: "Invalid verification request"
668
+ }
669
+ };
670
+ }
671
+ const sessionId = randomUUID2();
672
+ const now = /* @__PURE__ */ new Date();
673
+ const expiresAt = new Date(now.getTime() + sessionTtlMs);
674
+ try {
675
+ const engineResult = await engine.createSession({
676
+ sessionId,
677
+ request: input.request,
678
+ baseUrl,
679
+ ttlMs: sessionTtlMs
680
+ });
681
+ const session = {
682
+ id: sessionId,
683
+ status: "pending",
684
+ request: input.request,
685
+ qrUrl: engineResult.qrUrl,
686
+ createdAt: now,
687
+ expiresAt,
688
+ _engineData: engineResult.engineData
689
+ };
690
+ await store.set(sessionKey(sessionId), session, sessionTtlMs);
691
+ return {
692
+ status: 201,
693
+ headers: modeHeader(),
694
+ body: sessionToDTO(session)
695
+ };
696
+ } catch (err) {
697
+ console.error("[eudi-verify] createSession error:", err);
698
+ return {
699
+ status: 500,
700
+ headers: modeHeader(),
701
+ body: {
702
+ error: "internal_error",
703
+ message: "Failed to create session"
704
+ }
705
+ };
706
+ }
707
+ },
708
+ async getSession(ctx) {
709
+ const { sessionId } = ctx.params;
710
+ if (!sessionId) {
711
+ return {
712
+ status: 400,
713
+ headers: modeHeader(),
714
+ body: {
715
+ error: "bad_request",
716
+ message: "Missing session ID"
717
+ }
718
+ };
719
+ }
720
+ const session = await getStoredSession(sessionId);
721
+ if (!session) {
722
+ return {
723
+ status: 404,
724
+ headers: modeHeader(),
725
+ body: {
726
+ error: "not_found",
727
+ message: "Session not found"
728
+ }
729
+ };
730
+ }
731
+ return {
732
+ status: 200,
733
+ headers: modeHeader(),
734
+ body: sessionToDTO(session)
735
+ };
736
+ },
737
+ async cancelSession(ctx) {
738
+ const { sessionId } = ctx.params;
739
+ if (!sessionId) {
740
+ return {
741
+ status: 400,
742
+ headers: modeHeader(),
743
+ body: {
744
+ error: "bad_request",
745
+ message: "Missing session ID"
746
+ }
747
+ };
748
+ }
749
+ const session = await getStoredSession(sessionId);
750
+ if (!session) {
751
+ return {
752
+ status: 404,
753
+ headers: modeHeader(),
754
+ body: {
755
+ error: "not_found",
756
+ message: "Session not found"
757
+ }
758
+ };
759
+ }
760
+ if (isTerminalStatus(session.status)) {
761
+ return {
762
+ status: 409,
763
+ headers: modeHeader(),
764
+ body: {
765
+ error: "conflict",
766
+ message: `Session already in terminal state: ${session.status}`
767
+ }
768
+ };
769
+ }
770
+ const cancelled = { ...session, status: "cancelled" };
771
+ if (engine.cancelSession) {
772
+ try {
773
+ await engine.cancelSession(session);
774
+ } catch (err) {
775
+ console.error("[eudi-verify] cancelSession engine error:", err);
776
+ }
777
+ }
778
+ await store.set(sessionKey(sessionId), cancelled, sessionTtlMs);
779
+ return {
780
+ status: 200,
781
+ headers: modeHeader(),
782
+ body: sessionToDTO(cancelled)
783
+ };
784
+ },
785
+ async verifyToken(ctx) {
786
+ const input = ctx.body;
787
+ if (!input?.token || typeof input.token !== "string") {
788
+ return {
789
+ status: 400,
790
+ headers: modeHeader(),
791
+ body: {
792
+ error: "bad_request",
793
+ message: "Missing or invalid token"
794
+ }
795
+ };
796
+ }
797
+ const result = await tokenService.verify(input.token);
798
+ return {
799
+ status: 200,
800
+ headers: modeHeader(),
801
+ body: result
802
+ };
803
+ },
804
+ async handleCallback(ctx) {
805
+ logDemoWarning();
806
+ if (!ctx.rawBody) {
807
+ return {
808
+ status: 400,
809
+ headers: modeHeader(),
810
+ body: {
811
+ error: "bad_request",
812
+ message: "Missing callback body"
813
+ }
814
+ };
815
+ }
816
+ let callbackData;
817
+ try {
818
+ callbackData = await engine.parseCallback(ctx.rawBody);
819
+ } catch (err) {
820
+ console.error("[eudi-verify] parseCallback error:", err);
821
+ return {
822
+ status: 400,
823
+ headers: modeHeader(),
824
+ body: {
825
+ error: "bad_request",
826
+ message: "Invalid callback format"
827
+ }
828
+ };
829
+ }
830
+ const session = await getStoredSession(callbackData.sessionId);
831
+ if (!session) {
832
+ return {
833
+ status: 400,
834
+ headers: modeHeader(),
835
+ body: {
836
+ error: "bad_request",
837
+ message: "Session not found for callback"
838
+ }
839
+ };
840
+ }
841
+ if (isTerminalStatus(session.status)) {
842
+ return {
843
+ status: 200,
844
+ headers: modeHeader(),
845
+ body: { status: "ok" }
846
+ };
847
+ }
848
+ try {
849
+ const result = await engine.handleCallback(callbackData, session);
850
+ const updated = {
851
+ ...session,
852
+ status: result.status,
853
+ claims: result.claims,
854
+ error: result.error
855
+ };
856
+ if (result.success && result.claims) {
857
+ const token = await tokenService.mint(session.id, result.claims);
858
+ updated.token = token;
859
+ }
860
+ await store.set(sessionKey(session.id), updated, sessionTtlMs);
861
+ return {
862
+ status: 200,
863
+ headers: modeHeader(),
864
+ body: { status: "ok" }
865
+ };
866
+ } catch (err) {
867
+ console.error("[eudi-verify] handleCallback error:", err);
868
+ const errorSession = {
869
+ ...session,
870
+ status: "error",
871
+ error: "Verification failed"
872
+ };
873
+ await store.set(sessionKey(session.id), errorSession, sessionTtlMs);
874
+ return {
875
+ status: 200,
876
+ headers: modeHeader(),
877
+ body: { status: "ok" }
878
+ };
879
+ }
880
+ },
881
+ async getRequest(ctx) {
882
+ const { requestId } = ctx.params;
883
+ if (!requestId) {
884
+ return {
885
+ status: 400,
886
+ headers: modeHeader(),
887
+ body: {
888
+ error: "bad_request",
889
+ message: "Missing request ID"
890
+ }
891
+ };
892
+ }
893
+ const session = await getStoredSession(requestId);
894
+ if (!session) {
895
+ return {
896
+ status: 404,
897
+ headers: {
898
+ ...modeHeader(),
899
+ "Content-Type": "application/json"
900
+ },
901
+ body: {
902
+ error: "not_found",
903
+ message: "Request not found"
904
+ }
905
+ };
906
+ }
907
+ if (!engine.getAuthorizationRequest) {
908
+ return {
909
+ status: 501,
910
+ headers: {
911
+ ...modeHeader(),
912
+ "Content-Type": "application/json"
913
+ },
914
+ body: {
915
+ error: "not_implemented",
916
+ message: "PAR not supported by this engine"
917
+ }
918
+ };
919
+ }
920
+ try {
921
+ const jwt = await engine.getAuthorizationRequest(session);
922
+ return {
923
+ status: 200,
924
+ headers: {
925
+ ...modeHeader(),
926
+ "Content-Type": "application/oauth-authz-req+jwt"
927
+ },
928
+ body: jwt
929
+ };
930
+ } catch (err) {
931
+ console.error("[eudi-verify] getRequest error:", err);
932
+ return {
933
+ status: 500,
934
+ headers: {
935
+ ...modeHeader(),
936
+ "Content-Type": "application/json"
937
+ },
938
+ body: {
939
+ error: "internal_error",
940
+ message: "Failed to generate authorization request"
941
+ }
942
+ };
943
+ }
944
+ }
945
+ };
946
+ }
947
+
948
+ // src/index.ts
949
+ var VERSION = "0.1.0";
950
+ export {
951
+ DEFAULT_SESSION_TTL_MS,
952
+ DEFAULT_TOKEN_TTL_MS,
953
+ KEY_PREFIX,
954
+ MemoryKVStore,
955
+ MockEngine,
956
+ OpenEudiEngine,
957
+ TERMINAL_STATUSES,
958
+ TOKEN_VERSION,
959
+ VERSION,
960
+ createRateLimiter,
961
+ createTokenService,
962
+ createVerifierHandlers,
963
+ isTerminalStatus,
964
+ rateLimitKey,
965
+ sessionKey,
966
+ sessionToDTO,
967
+ tokenKey
968
+ };