@eudi-verify/client 0.1.0 → 0.1.2

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
@@ -11,36 +11,36 @@ pnpm add @eudi-verify/client
11
11
  ## Quick Start
12
12
 
13
13
  ```ts
14
- import { createVerification } from '@eudi-verify/client';
14
+ import { createVerification } from "@eudi-verify/client";
15
15
 
16
16
  const verification = createVerification({
17
- apiUrl: 'https://your-api.example.com',
17
+ apiUrl: "https://your-api.example.com",
18
18
  });
19
19
 
20
20
  // Subscribe to state changes
21
21
  verification.subscribe((state) => {
22
22
  switch (state.status) {
23
- case 'loading':
23
+ case "loading":
24
24
  showSpinner();
25
25
  break;
26
- case 'showQR':
26
+ case "showQR":
27
27
  // Display QR code for wallet scanning
28
- document.getElementById('qr').src = state.qrDataUrl;
28
+ document.getElementById("qr").src = state.qrDataUrl;
29
29
  break;
30
- case 'waitingForWallet':
31
- showMessage('Approve in your wallet app...');
30
+ case "waitingForWallet":
31
+ showMessage("Approve in your wallet app...");
32
32
  break;
33
- case 'verified':
33
+ case "verified":
34
34
  // Send token to your backend for validation
35
35
  submitToBackend(state.token, state.claims);
36
36
  break;
37
- case 'rejected':
38
- showError('Verification was declined');
37
+ case "rejected":
38
+ showError("Verification was declined");
39
39
  break;
40
- case 'expired':
41
- showError('Session expired, please try again');
40
+ case "expired":
41
+ showError("Session expired, please try again");
42
42
  break;
43
- case 'error':
43
+ case "error":
44
44
  showError(state.error);
45
45
  break;
46
46
  }
@@ -66,14 +66,14 @@ All states are typed as a discriminated union:
66
66
 
67
67
  ```ts
68
68
  type VerificationState =
69
- | { status: 'idle' }
70
- | { status: 'loading' }
71
- | { status: 'showQR'; qrDataUrl: string; qrUrl: string; sessionId: string }
72
- | { status: 'waitingForWallet'; sessionId: string }
73
- | { status: 'verified'; token: string; claims: VerifiedClaims }
74
- | { status: 'rejected'; error?: string }
75
- | { status: 'expired' }
76
- | { status: 'error'; error: string };
69
+ | { status: "idle" }
70
+ | { status: "loading" }
71
+ | { status: "showQR"; qrDataUrl: string; qrUrl: string; sessionId: string }
72
+ | { status: "waitingForWallet"; sessionId: string }
73
+ | { status: "verified"; token: string; claims: VerifiedClaims }
74
+ | { status: "rejected"; error?: string }
75
+ | { status: "expired" }
76
+ | { status: "error"; error: string };
77
77
  ```
78
78
 
79
79
  ## API
@@ -84,27 +84,27 @@ Creates a verification instance.
84
84
 
85
85
  ```ts
86
86
  interface VerificationConfig {
87
- apiUrl: string; // Your verifier API base URL
87
+ apiUrl: string; // Your verifier API base URL
88
88
  polling?: {
89
89
  initialIntervalMs?: number; // Default: 1000
90
- maxIntervalMs?: number; // Default: 10000
90
+ maxIntervalMs?: number; // Default: 10000
91
91
  backoffMultiplier?: number; // Default: 2
92
92
  };
93
93
  qr?: {
94
- size?: number; // Default: 200
95
- errorCorrection?: 'L' | 'M' | 'Q' | 'H'; // Default: 'M'
94
+ size?: number; // Default: 200
95
+ errorCorrection?: "L" | "M" | "Q" | "H"; // Default: 'M'
96
96
  };
97
97
  }
98
98
  ```
99
99
 
100
100
  ### Verification Instance
101
101
 
102
- | Method | Description |
103
- |--------|-------------|
104
- | `state` | Current state (read-only) |
105
- | `start(request)` | Start verification with requested claims |
106
- | `cancel()` | Cancel current verification |
107
- | `destroy()` | Cleanup (stop polling, clear subscribers) |
102
+ | Method | Description |
103
+ | --------------------- | -------------------------------------------------- |
104
+ | `state` | Current state (read-only) |
105
+ | `start(request)` | Start verification with requested claims |
106
+ | `cancel()` | Cancel current verification |
107
+ | `destroy()` | Cleanup (stop polling, clear subscribers) |
108
108
  | `subscribe(callback)` | Subscribe to state changes, returns unsubscribe fn |
109
109
 
110
110
  ### Claims You Can Request
@@ -125,9 +125,9 @@ interface VerificationRequest {
125
125
  For custom integrations, use the API client directly:
126
126
 
127
127
  ```ts
128
- import { createApiClient } from '@eudi-verify/client';
128
+ import { createApiClient } from "@eudi-verify/client";
129
129
 
130
- const api = createApiClient({ baseUrl: 'https://your-api.example.com' });
130
+ const api = createApiClient({ baseUrl: "https://your-api.example.com" });
131
131
 
132
132
  // Create session
133
133
  const session = await api.createSession({ age_over_18: true });
@@ -145,13 +145,13 @@ await api.cancelSession(session.id);
145
145
  Generate QR codes independently:
146
146
 
147
147
  ```ts
148
- import { generateQRSvg, generateQRDataUrl } from '@eudi-verify/client';
148
+ import { generateQRSvg, generateQRDataUrl } from "@eudi-verify/client";
149
149
 
150
150
  // SVG string
151
- const svg = generateQRSvg('openid4vp://...', { size: 300 });
151
+ const svg = generateQRSvg("openid4vp://...", { size: 300 });
152
152
 
153
153
  // Data URL (for <img src>)
154
- const dataUrl = generateQRDataUrl('openid4vp://...');
154
+ const dataUrl = generateQRDataUrl("openid4vp://...");
155
155
  ```
156
156
 
157
157
  ## Error Boundaries
@@ -162,17 +162,17 @@ The client exposes two integration paths with different error models.
162
162
 
163
163
  `start()`, `cancel()`, and polling **do not throw** on failure. Errors become state transitions:
164
164
 
165
- | State | Cause | Action |
166
- |-------|-------|--------|
167
- | `rejected` | User declined in wallet | Retry UX; usually not an ops alert |
168
- | `expired` | Session timed out | Restart flow |
169
- | `error` | Network failure, API 4xx/5xx, unknown status | Show message; report via `state.error` (string only) |
165
+ | State | Cause | Action |
166
+ | ---------- | -------------------------------------------- | ---------------------------------------------------- |
167
+ | `rejected` | User declined in wallet | Retry UX; usually not an ops alert |
168
+ | `expired` | Session timed out | Restart flow |
169
+ | `error` | Network failure, API 4xx/5xx, unknown status | Show message; report via `state.error` (string only) |
170
170
 
171
171
  **Boundary:** `verification.subscribe(callback)`. This is the hook for UI updates and client-side error reporting.
172
172
 
173
173
  ```ts
174
174
  verification.subscribe((state) => {
175
- if (state.status === 'error') {
175
+ if (state.status === "error") {
176
176
  reportError({ message: state.error });
177
177
  }
178
178
  });
@@ -191,9 +191,9 @@ import {
191
191
  ApiResponseError,
192
192
  SessionNotFoundError,
193
193
  RateLimitError,
194
- } from '@eudi-verify/client';
194
+ } from "@eudi-verify/client";
195
195
 
196
- const api = createApiClient({ baseUrl: 'https://your-api.example.com' });
196
+ const api = createApiClient({ baseUrl: "https://your-api.example.com" });
197
197
 
198
198
  try {
199
199
  await api.createSession({ age_over_18: true });
@@ -201,7 +201,7 @@ try {
201
201
  if (error instanceof RateLimitError) {
202
202
  await sleep(error.retryAfterMs ?? 60_000);
203
203
  } else if (error instanceof NetworkError) {
204
- reportError({ type: 'network', cause: error.cause?.message });
204
+ reportError({ type: "network", cause: error.cause?.message });
205
205
  } else if (error instanceof ApiResponseError) {
206
206
  reportError({ status: error.statusCode, code: error.errorCode });
207
207
  }
@@ -212,11 +212,11 @@ Session-level outcomes (`rejected`, `expired`, `error`) come from `getSession()`
212
212
 
213
213
  ### Which path to use
214
214
 
215
- | Need | Use |
216
- |------|-----|
217
- | Drop-in flow with QR + polling | `createVerification` + `subscribe` |
218
- | Rate-limit retry, custom error codes | `createApiClient` + `try/catch` |
219
- | Both | `createVerification` for UX; `createApiClient` for one-off calls |
215
+ | Need | Use |
216
+ | ------------------------------------ | ---------------------------------------------------------------- |
217
+ | Drop-in flow with QR + polling | `createVerification` + `subscribe` |
218
+ | Rate-limit retry, custom error codes | `createApiClient` + `try/catch` |
219
+ | Both | `createVerification` for UX; `createApiClient` for one-off calls |
220
220
 
221
221
  ## Bundle Size
222
222
 
package/dist/index.d.ts CHANGED
@@ -20,7 +20,7 @@ interface VerificationRequest {
20
20
  /**
21
21
  * Session lifecycle states.
22
22
  */
23
- type SessionStatus = 'pending' | 'waiting_for_wallet' | 'verified' | 'rejected' | 'expired' | 'cancelled' | 'error';
23
+ type SessionStatus = "pending" | "waiting_for_wallet" | "verified" | "rejected" | "expired" | "cancelled" | "error";
24
24
  /**
25
25
  * Terminal states that cannot transition further.
26
26
  */
@@ -114,14 +114,13 @@ declare function createPoller(fn: () => Promise<boolean>, config?: PollingConfig
114
114
  /**
115
115
  * @eudi-verify/client - QR Code Generator
116
116
  *
117
- * Minimal QR code generation for OpenID4VP URLs.
118
- * Implements ISO/IEC 18004 (QR Code) with byte mode encoding.
117
+ * Uses `qrcode` (MIT) for matrix generation; renders crisp SVG locally.
119
118
  */
120
119
  interface QRCodeOptions {
121
120
  /** Size in pixels (default: 200) */
122
121
  size?: number;
123
- /** Error correction level (default: 'M') */
124
- errorCorrection?: 'L' | 'M' | 'Q' | 'H';
122
+ /** Error correction level (default: 'L') */
123
+ errorCorrection?: "L" | "M" | "Q" | "H";
125
124
  /** Quiet zone modules (default: 4) */
126
125
  quietZone?: number;
127
126
  }
@@ -144,28 +143,28 @@ declare function generateQRDataUrl(data: string, options?: QRCodeOptions): strin
144
143
  * All possible verification states.
145
144
  */
146
145
  type VerificationState = {
147
- status: 'idle';
146
+ status: "idle";
148
147
  } | {
149
- status: 'loading';
148
+ status: "loading";
150
149
  } | {
151
- status: 'showQR';
150
+ status: "showQR";
152
151
  qrDataUrl: string;
153
152
  qrUrl: string;
154
153
  sessionId: string;
155
154
  } | {
156
- status: 'waitingForWallet';
155
+ status: "waitingForWallet";
157
156
  sessionId: string;
158
157
  } | {
159
- status: 'verified';
158
+ status: "verified";
160
159
  token: string;
161
160
  claims: VerifiedClaims;
162
161
  } | {
163
- status: 'rejected';
162
+ status: "rejected";
164
163
  error?: string;
165
164
  } | {
166
- status: 'expired';
165
+ status: "expired";
167
166
  } | {
168
- status: 'error';
167
+ status: "error";
169
168
  error: string;
170
169
  };
171
170
  /**
package/dist/index.js CHANGED
@@ -40,7 +40,10 @@ var ApiResponseError = class extends EudiClientError {
40
40
  var SessionNotFoundError = class extends ApiResponseError {
41
41
  sessionId;
42
42
  constructor(sessionId) {
43
- super(404, { error: "not_found", message: `Session ${sessionId} not found` });
43
+ super(404, {
44
+ error: "not_found",
45
+ message: `Session ${sessionId} not found`
46
+ });
44
47
  this.name = "SessionNotFoundError";
45
48
  this.sessionId = sessionId;
46
49
  }
@@ -100,7 +103,10 @@ function createApiClient(config) {
100
103
  if (error.name === "AbortError") {
101
104
  throw new NetworkError(`Request timeout after ${timeoutMs}ms`);
102
105
  }
103
- throw new NetworkError(`Network request failed: ${error.message}`, error);
106
+ throw new NetworkError(
107
+ `Network request failed: ${error.message}`,
108
+ error
109
+ );
104
110
  }
105
111
  throw new NetworkError("Unknown network error");
106
112
  }
@@ -131,10 +137,15 @@ function createApiClient(config) {
131
137
  }
132
138
  return {
133
139
  async createSession(verificationRequest) {
134
- return request("POST", "/sessions", { request: verificationRequest });
140
+ return request("POST", "/sessions", {
141
+ request: verificationRequest
142
+ });
135
143
  },
136
144
  async getSession(sessionId) {
137
- return request("GET", `/sessions/${encodeURIComponent(sessionId)}`);
145
+ return request(
146
+ "GET",
147
+ `/sessions/${encodeURIComponent(sessionId)}`
148
+ );
138
149
  },
139
150
  async cancelSession(sessionId) {
140
151
  return request(
@@ -199,422 +210,25 @@ function createPoller(fn, config = {}) {
199
210
  }
200
211
 
201
212
  // src/qr.ts
202
- var EC_LEVELS = { L: 0, M: 1, Q: 2, H: 3 };
203
- var VERSION_TABLE = {
204
- L: [
205
- { version: 1, totalCodewords: 19, ecCodewordsPerBlock: 7, numBlocks: 1 },
206
- { version: 2, totalCodewords: 34, ecCodewordsPerBlock: 10, numBlocks: 1 },
207
- { version: 3, totalCodewords: 55, ecCodewordsPerBlock: 15, numBlocks: 1 },
208
- { version: 4, totalCodewords: 80, ecCodewordsPerBlock: 20, numBlocks: 1 },
209
- { version: 5, totalCodewords: 108, ecCodewordsPerBlock: 26, numBlocks: 1 },
210
- { version: 6, totalCodewords: 136, ecCodewordsPerBlock: 18, numBlocks: 2 },
211
- { version: 7, totalCodewords: 156, ecCodewordsPerBlock: 20, numBlocks: 2 },
212
- { version: 8, totalCodewords: 194, ecCodewordsPerBlock: 24, numBlocks: 2 },
213
- { version: 9, totalCodewords: 232, ecCodewordsPerBlock: 30, numBlocks: 2 },
214
- { version: 10, totalCodewords: 274, ecCodewordsPerBlock: 18, numBlocks: 4 },
215
- { version: 11, totalCodewords: 401, ecCodewordsPerBlock: 20, numBlocks: 4 },
216
- { version: 12, totalCodewords: 466, ecCodewordsPerBlock: 24, numBlocks: 4 },
217
- { version: 13, totalCodewords: 532, ecCodewordsPerBlock: 26, numBlocks: 4 },
218
- { version: 14, totalCodewords: 588, ecCodewordsPerBlock: 30, numBlocks: 4 }
219
- ],
220
- M: [
221
- { version: 1, totalCodewords: 16, ecCodewordsPerBlock: 10, numBlocks: 1 },
222
- { version: 2, totalCodewords: 28, ecCodewordsPerBlock: 16, numBlocks: 1 },
223
- { version: 3, totalCodewords: 44, ecCodewordsPerBlock: 26, numBlocks: 1 },
224
- { version: 4, totalCodewords: 64, ecCodewordsPerBlock: 18, numBlocks: 2 },
225
- { version: 5, totalCodewords: 86, ecCodewordsPerBlock: 24, numBlocks: 2 },
226
- { version: 6, totalCodewords: 108, ecCodewordsPerBlock: 16, numBlocks: 4 },
227
- { version: 7, totalCodewords: 124, ecCodewordsPerBlock: 18, numBlocks: 4 },
228
- { version: 8, totalCodewords: 154, ecCodewordsPerBlock: 22, numBlocks: 4 },
229
- { version: 9, totalCodewords: 182, ecCodewordsPerBlock: 22, numBlocks: 5 },
230
- { version: 10, totalCodewords: 216, ecCodewordsPerBlock: 26, numBlocks: 5 }
231
- ],
232
- Q: [
233
- { version: 1, totalCodewords: 13, ecCodewordsPerBlock: 13, numBlocks: 1 },
234
- { version: 2, totalCodewords: 22, ecCodewordsPerBlock: 22, numBlocks: 1 },
235
- { version: 3, totalCodewords: 34, ecCodewordsPerBlock: 18, numBlocks: 2 },
236
- { version: 4, totalCodewords: 48, ecCodewordsPerBlock: 26, numBlocks: 2 },
237
- { version: 5, totalCodewords: 62, ecCodewordsPerBlock: 18, numBlocks: 4 },
238
- { version: 6, totalCodewords: 76, ecCodewordsPerBlock: 24, numBlocks: 4 },
239
- { version: 7, totalCodewords: 88, ecCodewordsPerBlock: 18, numBlocks: 6 },
240
- { version: 8, totalCodewords: 110, ecCodewordsPerBlock: 22, numBlocks: 6 },
241
- { version: 9, totalCodewords: 132, ecCodewordsPerBlock: 20, numBlocks: 8 },
242
- { version: 10, totalCodewords: 154, ecCodewordsPerBlock: 24, numBlocks: 8 }
243
- ],
244
- H: [
245
- { version: 1, totalCodewords: 9, ecCodewordsPerBlock: 17, numBlocks: 1 },
246
- { version: 2, totalCodewords: 16, ecCodewordsPerBlock: 28, numBlocks: 1 },
247
- { version: 3, totalCodewords: 26, ecCodewordsPerBlock: 22, numBlocks: 2 },
248
- { version: 4, totalCodewords: 36, ecCodewordsPerBlock: 16, numBlocks: 4 },
249
- { version: 5, totalCodewords: 46, ecCodewordsPerBlock: 22, numBlocks: 4 },
250
- { version: 6, totalCodewords: 60, ecCodewordsPerBlock: 28, numBlocks: 4 },
251
- { version: 7, totalCodewords: 66, ecCodewordsPerBlock: 26, numBlocks: 5 },
252
- { version: 8, totalCodewords: 86, ecCodewordsPerBlock: 26, numBlocks: 6 },
253
- { version: 9, totalCodewords: 100, ecCodewordsPerBlock: 24, numBlocks: 8 },
254
- { version: 10, totalCodewords: 122, ecCodewordsPerBlock: 28, numBlocks: 8 }
255
- ]
256
- };
257
- var FORMAT_INFO_STRINGS = {
258
- 0: [1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0],
259
- 1: [1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1],
260
- 2: [1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0],
261
- 3: [1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1],
262
- 4: [1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1],
263
- 5: [1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0],
264
- 6: [1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
265
- 7: [1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0],
266
- 8: [1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0],
267
- 9: [1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1],
268
- 10: [1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0],
269
- 11: [1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1],
270
- 12: [1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1],
271
- 13: [1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0],
272
- 14: [1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1],
273
- 15: [1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0],
274
- 16: [0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1],
275
- 17: [0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0],
276
- 18: [0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1],
277
- 19: [0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0],
278
- 20: [0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0],
279
- 21: [0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1],
280
- 22: [0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0],
281
- 23: [0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1],
282
- 24: [0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1],
283
- 25: [0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0],
284
- 26: [0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1],
285
- 27: [0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0],
286
- 28: [0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0],
287
- 29: [0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1],
288
- 30: [0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0],
289
- 31: [0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1]
290
- };
291
- var ALIGNMENT_PATTERNS = [
292
- [],
293
- [],
294
- [6, 18],
295
- [6, 22],
296
- [6, 26],
297
- [6, 30],
298
- [6, 34],
299
- [6, 22, 38],
300
- [6, 24, 42],
301
- [6, 26, 46],
302
- [6, 28, 50],
303
- [6, 30, 54],
304
- [6, 32, 58],
305
- [6, 34, 62],
306
- [6, 26, 46, 66]
307
- ];
308
- function getVersionInfo(dataLength, ecLevel) {
309
- const table = VERSION_TABLE[ecLevel];
310
- for (const info of table) {
311
- const dataCodewords = info.totalCodewords - info.ecCodewordsPerBlock * info.numBlocks;
312
- const maxBytes = dataCodewords - (info.version < 10 ? 2 : 3);
313
- if (maxBytes >= dataLength) {
314
- return info;
315
- }
316
- }
317
- throw new Error(`Data too long for QR code (max ~270 bytes with ${ecLevel} EC)`);
318
- }
319
- function createByteData(data, version) {
320
- const bytes = new TextEncoder().encode(data);
321
- const bits = [];
322
- bits.push(0, 1, 0, 0);
323
- const countBits = version < 10 ? 8 : 16;
324
- for (let i = countBits - 1; i >= 0; i--) {
325
- bits.push(bytes.length >> i & 1);
326
- }
327
- for (const byte of bytes) {
328
- for (let i = 7; i >= 0; i--) {
329
- bits.push(byte >> i & 1);
330
- }
331
- }
332
- bits.push(0, 0, 0, 0);
333
- while (bits.length % 8 !== 0) {
334
- bits.push(0);
335
- }
336
- const codewords = [];
337
- for (let i = 0; i < bits.length; i += 8) {
338
- let byte = 0;
339
- for (let j = 0; j < 8; j++) {
340
- byte = byte << 1 | (bits[i + j] ?? 0);
341
- }
342
- codewords.push(byte);
343
- }
344
- return codewords;
345
- }
346
- var GF_EXP = new Uint8Array(512);
347
- var GF_LOG = new Uint8Array(256);
348
- (function initGaloisField() {
349
- let x = 1;
350
- for (let i = 0; i < 255; i++) {
351
- GF_EXP[i] = x;
352
- GF_LOG[x] = i;
353
- x <<= 1;
354
- if (x & 256) x ^= 285;
355
- }
356
- for (let i = 255; i < 512; i++) {
357
- GF_EXP[i] = GF_EXP[i - 255];
358
- }
359
- })();
360
- function gfMul(a, b) {
361
- if (a === 0 || b === 0) return 0;
362
- return GF_EXP[GF_LOG[a] + GF_LOG[b]];
363
- }
364
- function generateECCodewords(data, numECCodewords) {
365
- const gen = [1];
366
- for (let i = 0; i < numECCodewords; i++) {
367
- const newGen = new Array(gen.length + 1).fill(0);
368
- for (let j = 0; j < gen.length; j++) {
369
- newGen[j] ^= gen[j];
370
- newGen[j + 1] ^= gfMul(gen[j], GF_EXP[i]);
371
- }
372
- gen.length = newGen.length;
373
- for (let j = 0; j < newGen.length; j++) gen[j] = newGen[j];
374
- }
375
- const msg = [...data, ...new Array(numECCodewords).fill(0)];
376
- for (let i = 0; i < data.length; i++) {
377
- const coef = msg[i];
378
- if (coef !== 0) {
379
- for (let j = 0; j < gen.length; j++) {
380
- msg[i + j] ^= gfMul(gen[j], coef);
381
- }
382
- }
383
- }
384
- return msg.slice(data.length);
385
- }
386
- function interleaveBlocks(dataCodewords, info) {
387
- const { numBlocks, ecCodewordsPerBlock, totalCodewords } = info;
388
- const totalDataCodewords = totalCodewords - numBlocks * ecCodewordsPerBlock;
389
- const smallBlockSize = Math.floor(totalDataCodewords / numBlocks);
390
- const largeBlockCount = totalDataCodewords % numBlocks;
391
- const dataBlocks = [];
392
- const ecBlocks = [];
393
- let offset = 0;
394
- for (let i = 0; i < numBlocks; i++) {
395
- const blockSize = smallBlockSize + (i >= numBlocks - largeBlockCount ? 1 : 0);
396
- const block = dataCodewords.slice(offset, offset + blockSize);
397
- offset += blockSize;
398
- dataBlocks.push(block);
399
- ecBlocks.push(generateECCodewords(block, ecCodewordsPerBlock));
400
- }
401
- const result = [];
402
- const maxDataLen = Math.max(...dataBlocks.map((b) => b.length));
403
- for (let i = 0; i < maxDataLen; i++) {
404
- for (const block of dataBlocks) {
405
- if (i < block.length) result.push(block[i]);
406
- }
407
- }
408
- for (let i = 0; i < ecCodewordsPerBlock; i++) {
409
- for (const block of ecBlocks) {
410
- result.push(block[i]);
411
- }
412
- }
413
- return result;
414
- }
415
- function createMatrix(version) {
416
- const size = version * 4 + 17;
417
- const matrix = Array.from(
418
- { length: size },
419
- () => new Array(size).fill(null)
420
- );
421
- function setFinderPattern(row, col) {
422
- for (let r = -1; r <= 7; r++) {
423
- for (let c = -1; c <= 7; c++) {
424
- const nr = row + r;
425
- const nc = col + c;
426
- if (nr < 0 || nr >= size || nc < 0 || nc >= size) continue;
427
- const isOuter = r === -1 || r === 7 || c === -1 || c === 7;
428
- const isInner = r >= 1 && r <= 5 && c >= 1 && c <= 5;
429
- const isCore = r >= 2 && r <= 4 && c >= 2 && c <= 4;
430
- matrix[nr][nc] = isOuter ? 0 : isCore ? 1 : isInner ? 0 : 1;
431
- }
432
- }
433
- }
434
- setFinderPattern(0, 0);
435
- setFinderPattern(0, size - 7);
436
- setFinderPattern(size - 7, 0);
437
- for (let i = 8; i < size - 8; i++) {
438
- matrix[6][i] = i % 2 === 0 ? 1 : 0;
439
- matrix[i][6] = i % 2 === 0 ? 1 : 0;
440
- }
441
- matrix[size - 8][8] = 1;
442
- if (version >= 2) {
443
- const positions = ALIGNMENT_PATTERNS[version];
444
- for (const row of positions) {
445
- for (const col of positions) {
446
- if (matrix[row][col] !== null) continue;
447
- for (let r = -2; r <= 2; r++) {
448
- for (let c = -2; c <= 2; c++) {
449
- const isOuter = Math.abs(r) === 2 || Math.abs(c) === 2;
450
- const isCore = r === 0 && c === 0;
451
- matrix[row + r][col + c] = isOuter || isCore ? 1 : 0;
452
- }
453
- }
454
- }
455
- }
456
- }
457
- return matrix;
458
- }
459
- function placeData(matrix, codewords) {
460
- const size = matrix.length;
461
- let bitIndex = 0;
462
- const bits = [];
463
- for (const cw of codewords) {
464
- for (let i = 7; i >= 0; i--) {
465
- bits.push(cw >> i & 1);
466
- }
467
- }
468
- let col = size - 1;
469
- let goingUp = true;
470
- while (col > 0) {
471
- if (col === 6) col--;
472
- for (let row = goingUp ? size - 1 : 0; goingUp ? row >= 0 : row < size; row += goingUp ? -1 : 1) {
473
- for (const offset of [0, -1]) {
474
- const c = col + offset;
475
- if (c < 0 || matrix[row][c] !== null) continue;
476
- matrix[row][c] = bitIndex < bits.length ? bits[bitIndex++] : 0;
477
- }
478
- }
479
- col -= 2;
480
- goingUp = !goingUp;
481
- }
482
- }
483
- function applyMask(matrix, maskPattern) {
484
- const size = matrix.length;
485
- const maskFns = [
486
- (r, c) => (r + c) % 2 === 0,
487
- (r) => r % 2 === 0,
488
- (_, c) => c % 3 === 0,
489
- (r, c) => (r + c) % 3 === 0,
490
- (r, c) => (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0,
491
- (r, c) => r * c % 2 + r * c % 3 === 0,
492
- (r, c) => (r * c % 2 + r * c % 3) % 2 === 0,
493
- (r, c) => ((r + c) % 2 + r * c % 3) % 2 === 0
494
- ];
495
- const maskFn = maskFns[maskPattern];
496
- const reserved = createReservedMask(size);
497
- for (let r = 0; r < size; r++) {
498
- for (let c = 0; c < size; c++) {
499
- if (!reserved[r][c] && maskFn(r, c)) {
500
- matrix[r][c] ^= 1;
501
- }
502
- }
503
- }
504
- }
505
- function createReservedMask(size) {
506
- const reserved = Array.from(
507
- { length: size },
508
- () => new Array(size).fill(false)
509
- );
510
- for (let i = 0; i < 8; i++) {
511
- for (let j = 0; j < 8; j++) {
512
- reserved[i][j] = true;
513
- reserved[i][size - 8 + j] = true;
514
- reserved[size - 8 + i][j] = true;
515
- }
516
- }
517
- for (let i = 0; i < 9; i++) {
518
- reserved[i][8] = true;
519
- reserved[8][i] = true;
520
- reserved[size - 8 + i - 1][8] = true;
521
- reserved[8][size - 8 + i - 1] = true;
522
- }
523
- for (let i = 0; i < size; i++) {
524
- reserved[6][i] = true;
525
- reserved[i][6] = true;
526
- }
527
- return reserved;
528
- }
529
- function addFormatInfo(matrix, ecLevel, maskPattern) {
530
- const formatIndex = EC_LEVELS[ecLevel] * 8 + maskPattern;
531
- const bits = FORMAT_INFO_STRINGS[formatIndex];
532
- const size = matrix.length;
533
- for (let i = 0; i < 6; i++) {
534
- matrix[8][i] = bits[i];
535
- matrix[i][8] = bits[14 - i];
536
- }
537
- matrix[8][7] = bits[6];
538
- matrix[8][8] = bits[7];
539
- matrix[7][8] = bits[8];
540
- for (let i = 0; i < 7; i++) {
541
- matrix[8][size - 1 - i] = bits[14 - i];
542
- matrix[size - 1 - i][8] = bits[i];
543
- }
544
- matrix[size - 8][8] = bits[7];
545
- }
546
- function evaluatePenalty(matrix) {
547
- const size = matrix.length;
548
- let penalty = 0;
549
- for (let r = 0; r < size; r++) {
550
- let count = 1;
551
- for (let c = 1; c < size; c++) {
552
- if (matrix[r][c] === matrix[r][c - 1]) {
553
- count++;
554
- if (count === 5) penalty += 3;
555
- else if (count > 5) penalty++;
556
- } else {
557
- count = 1;
558
- }
559
- }
560
- }
561
- for (let c = 0; c < size; c++) {
562
- let count = 1;
563
- for (let r = 1; r < size; r++) {
564
- if (matrix[r][c] === matrix[r - 1][c]) {
565
- count++;
566
- if (count === 5) penalty += 3;
567
- else if (count > 5) penalty++;
568
- } else {
569
- count = 1;
570
- }
571
- }
572
- }
573
- return penalty;
574
- }
575
- function generateQRMatrix(data, ecLevel) {
576
- const info = getVersionInfo(data.length, ecLevel);
577
- const dataCodewords = createByteData(data, info.version);
578
- const totalDataCodewords = info.totalCodewords - info.numBlocks * info.ecCodewordsPerBlock;
579
- while (dataCodewords.length < totalDataCodewords) {
580
- dataCodewords.push(dataCodewords.length % 2 === 0 ? 236 : 17);
581
- }
582
- const codewords = interleaveBlocks(dataCodewords, info);
583
- const baseMatrix = createMatrix(info.version);
584
- placeData(baseMatrix, codewords);
585
- let bestMatrix = null;
586
- let bestPenalty = Infinity;
587
- for (let mask = 0; mask < 8; mask++) {
588
- const matrix = baseMatrix.map(
589
- (row) => row.map((cell) => cell === null ? 0 : cell)
590
- );
591
- applyMask(matrix, mask);
592
- addFormatInfo(matrix, ecLevel, mask);
593
- const penalty = evaluatePenalty(matrix);
594
- if (penalty < bestPenalty) {
595
- bestPenalty = penalty;
596
- bestMatrix = matrix;
597
- }
598
- }
599
- return bestMatrix;
600
- }
213
+ import { create as createQRCode } from "qrcode/lib/core/qrcode.js";
601
214
  function generateQRSvg(data, options = {}) {
602
215
  const { size = 200, errorCorrection = "L", quietZone = 4 } = options;
603
- const matrix = generateQRMatrix(data, errorCorrection);
604
- const moduleCount = matrix.length;
216
+ const qr = createQRCode(data, { errorCorrectionLevel: errorCorrection });
217
+ const moduleCount = qr.modules.size;
605
218
  const totalModules = moduleCount + quietZone * 2;
606
- const moduleSize = size / totalModules;
219
+ const modulePx = Math.max(1, Math.floor(size / totalModules));
220
+ const pad = Math.floor((size - modulePx * totalModules) / 2);
607
221
  let paths = "";
608
222
  for (let r = 0; r < moduleCount; r++) {
609
223
  for (let c = 0; c < moduleCount; c++) {
610
- if (matrix[r][c] === 1) {
611
- const x = (c + quietZone) * moduleSize;
612
- const y = (r + quietZone) * moduleSize;
613
- paths += `M${x},${y}h${moduleSize}v${moduleSize}h-${moduleSize}z`;
224
+ if (qr.modules.get(r, c)) {
225
+ const x = pad + (c + quietZone) * modulePx;
226
+ const y = pad + (r + quietZone) * modulePx;
227
+ paths += `M${x},${y}h${modulePx}v${modulePx}h-${modulePx}z`;
614
228
  }
615
229
  }
616
230
  }
617
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}"><rect width="100%" height="100%" fill="white"/><path d="${paths}" fill="black"/></svg>`;
231
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}" shape-rendering="crispEdges"><rect width="100%" height="100%" fill="white"/><path d="${paths}" fill="black"/></svg>`;
618
232
  }
619
233
  function generateQRDataUrl(data, options = {}) {
620
234
  const svg = generateQRSvg(data, options);
@@ -625,7 +239,10 @@ function generateQRDataUrl(data, options = {}) {
625
239
  // src/verification.ts
626
240
  function createVerification(config) {
627
241
  const { apiUrl, polling, qr, fetch: fetchFn } = config;
628
- const client = createApiClient({ baseUrl: apiUrl, fetch: fetchFn });
242
+ const client = createApiClient({
243
+ baseUrl: apiUrl,
244
+ fetch: fetchFn
245
+ });
629
246
  const subscribers = /* @__PURE__ */ new Set();
630
247
  let currentState = { status: "idle" };
631
248
  let currentSessionId = null;
@@ -666,7 +283,10 @@ function createVerification(config) {
666
283
  case "cancelled":
667
284
  return { status: "idle" };
668
285
  case "error":
669
- return { status: "error", error: session.error ?? "Verification failed" };
286
+ return {
287
+ status: "error",
288
+ error: session.error ?? "Verification failed"
289
+ };
670
290
  default:
671
291
  return { status: "error", error: "Unknown session status" };
672
292
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eudi-verify/client",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Typed API client and state machine for EUDI Wallet verification",
5
5
  "type": "module",
6
6
  "engines": {
@@ -33,7 +33,12 @@
33
33
  "url": "https://github.com/eudi-verify/eudi-verify.git",
34
34
  "directory": "packages/client"
35
35
  },
36
+ "dependencies": {
37
+ "qrcode": "1.5.4"
38
+ },
36
39
  "devDependencies": {
40
+ "@types/qrcode": "^1.5.5",
41
+ "jsqr": "1.4.0",
37
42
  "tsup": "^8.0.0"
38
43
  },
39
44
  "scripts": {