@eudi-verify/client 0.1.0 → 0.1.1
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 +50 -50
- package/dist/index.d.ts +12 -13
- package/dist/index.js +33 -413
- package/package.json +6 -1
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
|
|
14
|
+
import { createVerification } from "@eudi-verify/client";
|
|
15
15
|
|
|
16
16
|
const verification = createVerification({
|
|
17
|
-
apiUrl:
|
|
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
|
|
23
|
+
case "loading":
|
|
24
24
|
showSpinner();
|
|
25
25
|
break;
|
|
26
|
-
case
|
|
26
|
+
case "showQR":
|
|
27
27
|
// Display QR code for wallet scanning
|
|
28
|
-
document.getElementById(
|
|
28
|
+
document.getElementById("qr").src = state.qrDataUrl;
|
|
29
29
|
break;
|
|
30
|
-
case
|
|
31
|
-
showMessage(
|
|
30
|
+
case "waitingForWallet":
|
|
31
|
+
showMessage("Approve in your wallet app...");
|
|
32
32
|
break;
|
|
33
|
-
case
|
|
33
|
+
case "verified":
|
|
34
34
|
// Send token to your backend for validation
|
|
35
35
|
submitToBackend(state.token, state.claims);
|
|
36
36
|
break;
|
|
37
|
-
case
|
|
38
|
-
showError(
|
|
37
|
+
case "rejected":
|
|
38
|
+
showError("Verification was declined");
|
|
39
39
|
break;
|
|
40
|
-
case
|
|
41
|
-
showError(
|
|
40
|
+
case "expired":
|
|
41
|
+
showError("Session expired, please try again");
|
|
42
42
|
break;
|
|
43
|
-
case
|
|
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:
|
|
70
|
-
| { status:
|
|
71
|
-
| { status:
|
|
72
|
-
| { status:
|
|
73
|
-
| { status:
|
|
74
|
-
| { status:
|
|
75
|
-
| { status:
|
|
76
|
-
| { status:
|
|
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;
|
|
87
|
+
apiUrl: string; // Your verifier API base URL
|
|
88
88
|
polling?: {
|
|
89
89
|
initialIntervalMs?: number; // Default: 1000
|
|
90
|
-
maxIntervalMs?: number;
|
|
90
|
+
maxIntervalMs?: number; // Default: 10000
|
|
91
91
|
backoffMultiplier?: number; // Default: 2
|
|
92
92
|
};
|
|
93
93
|
qr?: {
|
|
94
|
-
size?: number;
|
|
95
|
-
errorCorrection?:
|
|
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
|
|
103
|
-
|
|
104
|
-
| `state`
|
|
105
|
-
| `start(request)`
|
|
106
|
-
| `cancel()`
|
|
107
|
-
| `destroy()`
|
|
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
|
|
128
|
+
import { createApiClient } from "@eudi-verify/client";
|
|
129
129
|
|
|
130
|
-
const api = createApiClient({ baseUrl:
|
|
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
|
|
148
|
+
import { generateQRSvg, generateQRDataUrl } from "@eudi-verify/client";
|
|
149
149
|
|
|
150
150
|
// SVG string
|
|
151
|
-
const svg = generateQRSvg(
|
|
151
|
+
const svg = generateQRSvg("openid4vp://...", { size: 300 });
|
|
152
152
|
|
|
153
153
|
// Data URL (for <img src>)
|
|
154
|
-
const dataUrl = generateQRDataUrl(
|
|
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
|
|
166
|
-
|
|
167
|
-
| `rejected` | User declined in wallet
|
|
168
|
-
| `expired`
|
|
169
|
-
| `error`
|
|
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 ===
|
|
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
|
|
194
|
+
} from "@eudi-verify/client";
|
|
195
195
|
|
|
196
|
-
const api = createApiClient({ baseUrl:
|
|
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:
|
|
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
|
|
216
|
-
|
|
217
|
-
| Drop-in flow with QR + polling
|
|
218
|
-
| Rate-limit retry, custom error codes | `createApiClient` + `try/catch`
|
|
219
|
-
| Both
|
|
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 =
|
|
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
|
-
*
|
|
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: '
|
|
124
|
-
errorCorrection?:
|
|
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:
|
|
146
|
+
status: "idle";
|
|
148
147
|
} | {
|
|
149
|
-
status:
|
|
148
|
+
status: "loading";
|
|
150
149
|
} | {
|
|
151
|
-
status:
|
|
150
|
+
status: "showQR";
|
|
152
151
|
qrDataUrl: string;
|
|
153
152
|
qrUrl: string;
|
|
154
153
|
sessionId: string;
|
|
155
154
|
} | {
|
|
156
|
-
status:
|
|
155
|
+
status: "waitingForWallet";
|
|
157
156
|
sessionId: string;
|
|
158
157
|
} | {
|
|
159
|
-
status:
|
|
158
|
+
status: "verified";
|
|
160
159
|
token: string;
|
|
161
160
|
claims: VerifiedClaims;
|
|
162
161
|
} | {
|
|
163
|
-
status:
|
|
162
|
+
status: "rejected";
|
|
164
163
|
error?: string;
|
|
165
164
|
} | {
|
|
166
|
-
status:
|
|
165
|
+
status: "expired";
|
|
167
166
|
} | {
|
|
168
|
-
status:
|
|
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, {
|
|
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(
|
|
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", {
|
|
140
|
+
return request("POST", "/sessions", {
|
|
141
|
+
request: verificationRequest
|
|
142
|
+
});
|
|
135
143
|
},
|
|
136
144
|
async getSession(sessionId) {
|
|
137
|
-
return request(
|
|
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
|
-
|
|
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
|
|
604
|
-
const moduleCount =
|
|
216
|
+
const qr = createQRCode(data, { errorCorrectionLevel: errorCorrection });
|
|
217
|
+
const moduleCount = qr.modules.size;
|
|
605
218
|
const totalModules = moduleCount + quietZone * 2;
|
|
606
|
-
const
|
|
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 (
|
|
611
|
-
const x = (c + quietZone) *
|
|
612
|
-
const y = (r + quietZone) *
|
|
613
|
-
paths += `M${x},${y}h${
|
|
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({
|
|
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 {
|
|
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.
|
|
3
|
+
"version": "0.1.1",
|
|
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": {
|