@zkproofport-app/sdk 0.2.8 → 0.2.9

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
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@zkproofport-app/sdk)](https://www.npmjs.com/package/@zkproofport-app/sdk)
4
4
  [![license](https://img.shields.io/npm/l/@zkproofport-app/sdk)](./LICENSE)
5
5
 
6
- TypeScript SDK for requesting zero-knowledge proofs from the [ZKProofport](https://zkproofport.com) mobile app and verifying them on-chain. Supports Coinbase KYC/Country attestations and OIDC domain attestations (Google, Microsoft 365).
6
+ TypeScript SDK for requesting zero-knowledge proofs from the [ZKProofport](https://zkproofport.com) mobile app and verifying them on-chain. Six circuits are supported: Coinbase KYC and country attestations, OIDC email-domain attestations (Google, Microsoft 365), and the three Korean mobile ID (mDL) circuits.
7
7
 
8
8
  ## How It Works
9
9
 
@@ -21,22 +21,22 @@ TypeScript SDK for requesting zero-knowledge proofs from the [ZKProofport](https
21
21
  │ │ relay callback
22
22
  │ v
23
23
  │ ┌──────────────────────────────────────────────────┐
24
- │ │ SDK receives result (WebSocket / polling) │
24
+ │ │ SDK receives result (WebSocket / polling) │
25
25
  │ │ (proof, publicInputs, status) │
26
26
  │ └─────────────────────┬────────────────────────────┘
27
27
  │ │
28
28
  v v
29
29
  ┌──────────────┐ ┌──────────────────┐ ┌───────────────────┐
30
30
  │ Verify │────>│ On-chain verify │────>│ Access granted │
31
- │ on-chain │ │ (Base Mainnet) │ │ or denied │
31
+ │ on-chain │ │ (Base) │ │ or denied │
32
32
  └──────────────┘ └──────────────────┘ └───────────────────┘
33
33
  ```
34
34
 
35
- 1. Your app sets a wallet signer and creates a proof request via the SDK
36
- 2. The SDK authenticates with the relay using challenge-signature (EIP-191) and gets a tracked request ID
37
- 3. The SDK displays a QR code (desktop) or opens the deep link (mobile)
38
- 4. The user opens the ZKProofport app, which generates the ZK proof
39
- 5. The proof result flows back through the relay to your app via WebSocket (or polling)
35
+ 1. Your app creates a proof request through the SDK
36
+ 2. The SDK asks the relay for a one-time challenge and a tracked request ID. For the Coinbase circuits it also signs that challenge with your wallet signer (EIP-191); the OIDC and mDL circuits need no signature
37
+ 3. Your app shows the returned deep link as a QR code (desktop) or navigates to it (mobile)
38
+ 4. The user opens the ZKProofport app, which generates the ZK proof on-device
39
+ 5. The proof result flows back through the relay to your app over WebSocket (HTTP polling as fallback)
40
40
  6. Your app verifies the proof on-chain
41
41
 
42
42
  ## Installation
@@ -51,16 +51,19 @@ npm install @zkproofport-app/sdk
51
51
  npm install ethers
52
52
  ```
53
53
 
54
+ `ethers` v6 is recommended; v5 also works. Real-time delivery uses `socket.io-client`, which ships as a dependency of this package — nothing extra to install.
55
+
54
56
  ## Quick Start
55
57
 
56
58
  ```typescript
57
59
  import { ProofportSDK } from '@zkproofport-app/sdk';
60
+ import type { CircuitType, ProofResponse } from '@zkproofport-app/sdk';
58
61
  import { BrowserProvider } from 'ethers';
59
62
 
60
63
  // 1. Initialize
61
64
  const sdk = ProofportSDK.create();
62
65
 
63
- // 2. Set wallet signer (ethers v6 Signer)
66
+ // 2. Set wallet signer (ethers v6 Signer) — required for the Coinbase circuits
64
67
  const provider = new BrowserProvider(window.ethereum);
65
68
  const signer = await provider.getSigner();
66
69
  sdk.setSigner(signer);
@@ -72,31 +75,41 @@ const relay = await sdk.createRelayRequest('coinbase_attestation', {
72
75
 
73
76
  // 4. Show QR code to user
74
77
  const qrDataUrl = await sdk.generateQRCode(relay.deepLink);
75
- document.getElementById('qr').src = qrDataUrl;
78
+ (document.getElementById('qr') as HTMLImageElement).src = qrDataUrl;
76
79
 
77
80
  // 5. Wait for proof (WebSocket primary, HTTP polling fallback)
78
81
  const result = await sdk.waitForProof(relay.requestId);
79
82
 
80
83
  if (result.status === 'completed') {
81
- // 6. Verify on-chain
82
- const verification = await sdk.verifyOnChain(
83
- result.circuit,
84
- result.proof,
85
- result.publicInputs
86
- );
84
+ // 6. Verify on-chain — the relay result carries the verifier to call
85
+ const response: ProofResponse = {
86
+ requestId: result.requestId,
87
+ circuit: result.circuit as CircuitType,
88
+ status: 'completed',
89
+ proof: result.proof,
90
+ publicInputs: result.publicInputs,
91
+ verifierAddress: result.verifierAddress,
92
+ chainId: result.chainId,
93
+ };
94
+
95
+ const verification = await sdk.verifyResponseOnChain(response);
87
96
  console.log('Valid:', verification.valid);
88
97
  }
89
98
  ```
90
99
 
91
100
  ## Supported Circuits
92
101
 
102
+ Circuit IDs are canonical and case-sensitive — pass them exactly as written here. `sdk.getSupportedCircuits()` returns the same list at runtime.
103
+
93
104
  ### `coinbase_attestation`
94
105
 
95
- Proves that a user has completed Coinbase KYC identity verification without revealing any personal information.
106
+ Proves that a user has completed Coinbase KYC identity verification without revealing any personal information. Requires a wallet signer.
96
107
 
97
108
  | Field | Type | Required | Description |
98
109
  |-------|------|----------|-------------|
99
110
  | `scope` | `string` | Yes | Application-specific identifier (e.g., your domain). Ensures proof uniqueness per app. |
111
+ | `userAddress` | `string` | No | Address to prove the attestation for. Omit it and the mobile app asks the user to connect a wallet. |
112
+ | `rawTransaction` | `string` | No | Pre-fetched attestation transaction data. Omit it and the app fetches the attestation itself. |
100
113
 
101
114
  ```typescript
102
115
  const relay = await sdk.createRelayRequest('coinbase_attestation', {
@@ -106,13 +119,15 @@ const relay = await sdk.createRelayRequest('coinbase_attestation', {
106
119
 
107
120
  ### `coinbase_country_attestation`
108
121
 
109
- Proves a user's country based on Coinbase verification, supporting inclusion and exclusion checks, without revealing the actual country.
122
+ Proves a user's country based on Coinbase verification, supporting inclusion and exclusion checks, without revealing the actual country. Requires a wallet signer.
110
123
 
111
124
  | Field | Type | Required | Description |
112
125
  |-------|------|----------|-------------|
113
126
  | `scope` | `string` | Yes | Application-specific identifier |
114
127
  | `countryList` | `string[]` | Yes | ISO 3166-1 alpha-2 country codes (e.g., `['US', 'KR']`) |
115
128
  | `isIncluded` | `boolean` | Yes | `true` = prove user IS from listed countries; `false` = prove user is NOT |
129
+ | `userAddress` | `string` | No | Address to prove the attestation for (optional, as above) |
130
+ | `rawTransaction` | `string` | No | Pre-fetched attestation transaction data (optional, as above) |
116
131
 
117
132
  ```typescript
118
133
  const relay = await sdk.createRelayRequest('coinbase_country_attestation', {
@@ -124,7 +139,7 @@ const relay = await sdk.createRelayRequest('coinbase_country_attestation', {
124
139
 
125
140
  ### `oidc_domain_attestation`
126
141
 
127
- Prove email domain affiliation via OIDC Sign-In. The mobile app handles authentication and proof generation entirely on-device — the user's email is never revealed.
142
+ Prove email domain affiliation via OIDC Sign-In. The mobile app handles authentication and proof generation entirely on-device — the user's email is never revealed. No wallet signer is required.
128
143
 
129
144
  | Field | Type | Required | Description |
130
145
  |-------|------|----------|-------------|
@@ -165,12 +180,12 @@ const relay = await sdk.createRelayRequest('oidc_domain_attestation', {
165
180
 
166
181
  ### `mdl_kr_ownership`
167
182
 
168
- Proves the user holds a valid Korean mobile driver's license (모바일 신분증). The license data stays on-device; only the attributes selected by `discloseFlags` are revealed. No wallet signature is required.
183
+ Proves the user holds a valid Korean mobile driver's license (모바일 신분증). The license data stays on-device; only the attributes selected by `discloseFlags` are revealed. No wallet signer is required.
169
184
 
170
185
  | Field | Type | Required | Description |
171
186
  |-------|------|----------|-------------|
172
- | `scope` | `string` | Yes | Application-specific identifier for proof uniqueness |
173
- | `discloseFlags` | `number` | No | Attribute disclosure bitmask `0x00`–`0x0F` (`0x01` name, `0x02` birth, `0x04` sex, `0x08` phone). Omit or `0` for a fully anonymous ownership proof. |
187
+ | `scope` | `string` | Yes | Application-specific identifier for proof uniqueness. Non-empty, 256 characters or fewer, no control characters. |
188
+ | `discloseFlags` | `number` | No | Attribute disclosure bitmask, integer 0–15 (`0x01` name, `0x02` birth, `0x04` sex, `0x08` phone). Omit or `0` for a fully anonymous ownership proof. |
174
189
 
175
190
  ```typescript
176
191
  // Anonymous "holds a valid Korean mobile ID" proof
@@ -181,12 +196,12 @@ const relay = await sdk.createRelayRequest('mdl_kr_ownership', {
181
196
 
182
197
  ### `mdl_kr_age`
183
198
 
184
- Proves the user is at least `ageThreshold` years old according to their Korean mobile driver's license, without revealing the birth date.
199
+ Proves the user is at least `ageThreshold` years old according to their Korean mobile driver's license, without revealing the birth date. No wallet signer is required.
185
200
 
186
201
  | Field | Type | Required | Description |
187
202
  |-------|------|----------|-------------|
188
- | `scope` | `string` | Yes | Application-specific identifier |
189
- | `ageThreshold` | `number` | Yes | Minimum age to prove (integer 1–150, e.g. `19` for Korean adult verification) |
203
+ | `scope` | `string` | Yes | Application-specific identifier (same rules as above) |
204
+ | `ageThreshold` | `number` | Yes | Minimum age to prove — integer between 1 and 150 (e.g. `19` for Korean adult verification) |
190
205
 
191
206
  ```typescript
192
207
  const relay = await sdk.createRelayRequest('mdl_kr_age', {
@@ -197,12 +212,12 @@ const relay = await sdk.createRelayRequest('mdl_kr_age', {
197
212
 
198
213
  ### `mdl_kr_region`
199
214
 
200
- Proves the user's registered address is in the specified si/do region without revealing the full address.
215
+ Proves the user's registered address is in the specified si/do region without revealing the full address. No wallet signer is required.
201
216
 
202
217
  | Field | Type | Required | Description |
203
218
  |-------|------|----------|-------------|
204
- | `scope` | `string` | Yes | Application-specific identifier |
205
- | `targetRegion` | `string` | Yes | Region (si/do) to prove residency in, in Korean (e.g. `'경기도'`, `'서울특별시'`) |
219
+ | `scope` | `string` | Yes | Application-specific identifier (same rules as above) |
220
+ | `targetRegion` | `string` | Yes | Region (si/do) to prove residency in, in Korean (e.g. `'경기도'`, `'서울특별시'`). Non-empty, 256 characters or fewer, no control characters. |
206
221
 
207
222
  ```typescript
208
223
  const relay = await sdk.createRelayRequest('mdl_kr_region', {
@@ -211,7 +226,9 @@ const relay = await sdk.createRelayRequest('mdl_kr_region', {
211
226
  });
212
227
  ```
213
228
 
214
- > The mDL circuits do not require `setSigner()` — the proof is bound to the license via an on-device nullifier (`nullifier_value` public input), not a wallet address. Use `sdk.extractNullifier(result.publicInputs, result.circuit)` for sybil-resistant user identification.
229
+ > mDL inputs are checked before the request leaves your process: a missing `scope`, an out-of-range `discloseFlags`, a non-integer `ageThreshold` or an empty `targetRegion` throws locally instead of coming back as a relay error.
230
+
231
+ > The mDL circuits do not require `setSigner()` — the proof is bound to the license via an on-device nullifier (`nullifier_value` public input), not a wallet address. Use `sdk.extractNullifier(publicInputs, circuit)` for sybil-resistant user identification.
215
232
 
216
233
  ## Integration Guide
217
234
 
@@ -223,11 +240,11 @@ import { ProofportSDK } from '@zkproofport-app/sdk';
223
240
  const sdk = ProofportSDK.create();
224
241
  ```
225
242
 
226
- `ProofportSDK.create()` returns an SDK instance pre-configured with the relay server and verifier contracts. No configuration needed.
243
+ `ProofportSDK.create()` returns an SDK instance pre-configured with the relay server. No configuration needed.
227
244
 
228
245
  ### Step 2: Set Wallet Signer
229
246
 
230
- The SDK uses challenge-signature authentication (EIP-191). Set a wallet signer that can sign messages:
247
+ The Coinbase circuits (`coinbase_attestation`, `coinbase_country_attestation`) authenticate with a signed challenge (EIP-191). Set a wallet signer that can sign messages:
231
248
 
232
249
  ```typescript
233
250
  import { BrowserProvider } from 'ethers';
@@ -241,14 +258,14 @@ The `WalletSigner` interface requires two methods:
241
258
 
242
259
  ```typescript
243
260
  interface WalletSigner {
244
- signMessage(message: string): Promise<string>;
261
+ signMessage(message: string | Uint8Array): Promise<string>;
245
262
  getAddress(): Promise<string>;
246
263
  }
247
264
  ```
248
265
 
249
266
  Any ethers v5/v6 `Signer` is compatible.
250
267
 
251
- > **OIDC Domain note:** Wallet signer is not required for OIDC Domain proofs. See Step 3 for OIDC-specific usage.
268
+ > **When you can skip this step:** the OIDC Domain and the three mDL circuits never sign a challenge. Calling `createRelayRequest` for those circuits without a signer works. Calling it for a Coinbase circuit without one throws `Signer not set. Call setSigner() first. Wallet signature is required for this circuit.`
252
269
 
253
270
  #### About challenge-signature
254
271
 
@@ -265,7 +282,7 @@ sdk.setSigner(Wallet.createRandom());
265
282
 
266
283
  ### Step 3: Create Request (via Relay)
267
284
 
268
- `createRelayRequest` authenticates with the relay (challenge-signature), creates a tracked proof request, and returns a deep link.
285
+ `createRelayRequest` fetches a challenge and request ID from the relay, signs the challenge when the circuit needs it, creates a tracked proof request, and returns a deep link.
269
286
 
270
287
  ```typescript
271
288
  const relay = await sdk.createRelayRequest('coinbase_attestation', {
@@ -275,6 +292,7 @@ const relay = await sdk.createRelayRequest('coinbase_attestation', {
275
292
  dappIcon: 'https://myapp.com/icon.png',
276
293
  message: 'Verify your identity to continue',
277
294
  nonce: 'unique-nonce-123', // Optional: replay prevention
295
+ returnScheme: 'mydapp://', // Optional: mobile-only, see below
278
296
  });
279
297
 
280
298
  // relay.requestId — Relay-issued UUID
@@ -283,6 +301,51 @@ const relay = await sdk.createRelayRequest('coinbase_attestation', {
283
301
  // relay.pollUrl — Relative URL for HTTP polling
284
302
  ```
285
303
 
304
+ Every field of the third argument is optional: `message`, `dappName` and `dappIcon` are shown to the user in the ZKProofport app, `nonce` is a value of your own choosing that the relay refuses to accept twice, and `returnScheme` is described next.
305
+
306
+ #### Switching back to your app: `returnScheme`
307
+
308
+ By default the user stays in the ZKProofport app after generating a proof and switches back to your app themselves. Pass `returnScheme` and the ZKProofport app brings your app to the foreground once the proof has been delivered — the same handoff a wallet performs after a signature.
309
+
310
+ ```typescript
311
+ // Native app: your own registered scheme
312
+ await sdk.createRelayRequest('coinbase_attestation', { scope: 'myapp.com' }, {
313
+ returnScheme: 'mydapp://',
314
+ });
315
+
316
+ // Web app: your own origin
317
+ await sdk.createRelayRequest('coinbase_attestation', { scope: 'myapp.com' }, {
318
+ returnScheme: 'https://myapp.com',
319
+ });
320
+ ```
321
+
322
+ **Only set it in the mobile deep-link flow.** The switch happens on the phone that generated the proof, so it makes sense only when that phone is also where the user started — i.e. when you navigated the browser to `relay.deepLink` on the same device. In the desktop QR flow the user is watching the desktop screen while the proof happens on their phone; opening a page or an app on the phone at that moment surfaces something nobody asked for. Gate it:
323
+
324
+ ```typescript
325
+ const relay = await sdk.createRelayRequest('coinbase_attestation', { scope: 'myapp.com' },
326
+ ProofportSDK.isMobile() ? { returnScheme: 'https://myapp.com' } : {});
327
+ ```
328
+
329
+ `returnScheme` is **not** a return address. It says which app to open, nothing more — the proof still reaches you the usual way, through `waitForProof()` / `waitForResult()`, and the ZKProofport app never posts anything to it.
330
+
331
+ Two shapes are accepted, and nothing else:
332
+
333
+ | Form | Example | Notes |
334
+ |------|---------|-------|
335
+ | Bare custom scheme | `'mydapp://'` | An RFC 3986 scheme followed by exactly `://` |
336
+ | https origin | `'https://myapp.com'`, `'https://myapp.com:8443'` | Host (and optional port) only |
337
+
338
+ Anything else throws before the SDK makes a single network call — no challenge is consumed, so nothing is wasted on a typo:
339
+
340
+ - a path, query string or fragment (`'mydapp://transfer?to=0x...'`, `'https://myapp.com/done'`), which is what keeps a request from driving your app to a specific action
341
+ - an empty or whitespace-containing value, or one longer than 128 characters
342
+ - userinfo (`'https://user:pass@myapp.com'`) and non-ASCII hosts (use punycode)
343
+ - `http://` and other schemes the OS treats specially: `about`, `blob`, `content`, `data`, `facetime`, `facetime-audio`, `file`, `ftp`, `intent`, `javascript`, `jar`, `mailto`, `sms`, `tel`, `vbscript`
344
+
345
+ The value is matched case-insensitively and stored lowercased by the relay.
346
+
347
+ Omit `returnScheme` and nothing changes: the request is created without it and the user stays in the ZKProofport app when the proof is done. There is no auto-switch when proof generation fails either — the error has to be read in the ZKProofport app first.
348
+
286
349
  **OIDC Domain Attestation:**
287
350
 
288
351
  ```typescript
@@ -303,7 +366,6 @@ const relay = await sdk.createRelayRequest('oidc_domain_attestation', {
303
366
  provider: 'google',
304
367
  }, {
305
368
  dappName: 'My DApp',
306
- dappIcon: 'https://myapp.com/icon.png',
307
369
  message: 'Verify your organization membership',
308
370
  });
309
371
 
@@ -314,7 +376,6 @@ const relay = await sdk.createRelayRequest('oidc_domain_attestation', {
314
376
  provider: 'microsoft',
315
377
  }, {
316
378
  dappName: 'My DApp',
317
- dappIcon: 'https://myapp.com/icon.png',
318
379
  message: 'Verify your organization membership',
319
380
  });
320
381
  ```
@@ -327,13 +388,15 @@ Generate a QR code from the relay deep link for the user to scan with the ZKProo
327
388
 
328
389
  ```typescript
329
390
  const qrDataUrl = await sdk.generateQRCode(relay.deepLink, {
330
- width: 400,
391
+ width: 400, // pixels (default: 300)
331
392
  darkColor: '#1a1a1a',
332
393
  margin: 4,
333
394
  });
334
- document.getElementById('qr').src = qrDataUrl;
395
+ (document.getElementById('qr') as HTMLImageElement).src = qrDataUrl;
335
396
  ```
336
397
 
398
+ `QRCodeOptions` accepts `width`, `margin`, `darkColor`, `lightColor` and `errorCorrectionLevel` (`'L' | 'M' | 'Q' | 'H'`, default `'M'`).
399
+
337
400
  **Other QR formats:**
338
401
 
339
402
  ```typescript
@@ -343,7 +406,7 @@ const svg = await sdk.generateQRCodeSVG(relay.deepLink);
343
406
  // Render to canvas
344
407
  await sdk.renderQRCodeToCanvas(canvasElement, relay.deepLink, { width: 400 });
345
408
 
346
- // Check if data fits QR limits
409
+ // Check if data fits QR limits (2953 bytes)
347
410
  const { size, withinLimit } = sdk.checkQRCodeSize(relay.deepLink);
348
411
  ```
349
412
 
@@ -357,7 +420,7 @@ if (ProofportSDK.isMobile()) {
357
420
 
358
421
  ### Step 5: Wait for Proof
359
422
 
360
- **`waitForProof` (recommended)** — Uses WebSocket (Socket.IO) for instant delivery, with automatic HTTP polling fallback if `socket.io-client` is not installed or connection fails.
423
+ **`waitForProof` (recommended)** — Uses WebSocket (Socket.IO) for instant delivery and falls back to HTTP polling automatically if the socket connection fails.
361
424
 
362
425
  ```typescript
363
426
  const result = await sdk.waitForProof(relay.requestId, {
@@ -368,6 +431,8 @@ const result = await sdk.waitForProof(relay.requestId, {
368
431
  });
369
432
  ```
370
433
 
434
+ `result.status` is `'pending'`, `'completed'` or `'failed'`. On `'failed'`, `result.error` explains why.
435
+
371
436
  **Alternative: Subscribe to real-time updates directly:**
372
437
 
373
438
  ```typescript
@@ -387,7 +452,7 @@ const unsubscribe = await sdk.subscribe(relay.requestId, {
387
452
 
388
453
  ```typescript
389
454
  // Single poll
390
- const result = await sdk.pollResult(relay.requestId);
455
+ const once = await sdk.pollResult(relay.requestId);
391
456
 
392
457
  // Poll until terminal state
393
458
  const result = await sdk.waitForResult(relay.requestId, {
@@ -397,17 +462,29 @@ const result = await sdk.waitForResult(relay.requestId, {
397
462
  });
398
463
  ```
399
464
 
465
+ When you are done, `sdk.disconnect()` closes any open socket.
466
+
400
467
  ### Step 6: Verify On-Chain
401
468
 
402
469
  Verify the proof cryptographically by calling the deployed Solidity verifier contract.
403
470
 
471
+ The relay result tells you which verifier contract to call (`verifierAddress`, `chainId`), so verification goes through `verifyResponseOnChain`:
472
+
404
473
  ```typescript
474
+ import type { CircuitType, ProofResponse } from '@zkproofport-app/sdk';
475
+
405
476
  if (result.status === 'completed') {
406
- const verification = await sdk.verifyOnChain(
407
- result.circuit,
408
- result.proof,
409
- result.publicInputs
410
- );
477
+ const response: ProofResponse = {
478
+ requestId: result.requestId,
479
+ circuit: result.circuit as CircuitType,
480
+ status: 'completed',
481
+ proof: result.proof,
482
+ publicInputs: result.publicInputs,
483
+ verifierAddress: result.verifierAddress,
484
+ chainId: result.chainId,
485
+ };
486
+
487
+ const verification = await sdk.verifyResponseOnChain(response);
411
488
 
412
489
  if (verification.valid) {
413
490
  console.log('Proof verified on-chain!');
@@ -417,47 +494,57 @@ if (result.status === 'completed') {
417
494
  }
418
495
  ```
419
496
 
420
- **Or verify from a `ProofResponse` object:**
497
+ The SDK connects to the right network on its own. Pass your own ethers `Provider` or `Signer` as the second argument if you would rather use your existing connection:
421
498
 
422
499
  ```typescript
423
- const verification = await sdk.verifyResponseOnChain(response);
500
+ const verification = await sdk.verifyResponseOnChain(response, myProvider);
424
501
  ```
425
502
 
503
+ `verifyResponseOnChain` never throws for a bad response: an incomplete one (`status` other than `'completed'`, missing `proof` or `publicInputs`) returns `{ valid: false, error: 'Invalid or incomplete response' }`, and a reverting contract call returns `{ valid: false, error }` with the revert message.
504
+
505
+ > `sdk.verifyOnChain(circuit, proof, publicInputs, providerOrSigner?)` takes the same proof in raw pieces, but those pieces carry no verifier address — it returns `{ valid: false, error: 'No verifier address provided...' }` unless the SDK was constructed with a verifier for that circuit. Prefer `verifyResponseOnChain`.
506
+
426
507
  ### Step 7: Extract Scope, Nullifier, and Domain
427
508
 
428
509
  After verification, extract data from the public inputs:
429
510
 
430
511
  ```typescript
431
- if (result.status === 'completed') {
432
- // Extract scope — the keccak256 hash of the scope string you provided
433
- const scope = sdk.extractScope(result.publicInputs, result.circuit);
512
+ import type { CircuitType } from '@zkproofport-app/sdk';
513
+
514
+ if (result.status === 'completed' && result.publicInputs && result.circuit) {
515
+ const circuit = result.circuit as CircuitType;
516
+
517
+ // Extract scope — the bytes32 scope value carried in the public inputs
518
+ const scope = sdk.extractScope(result.publicInputs, circuit);
434
519
 
435
520
  // Extract nullifier — a unique, deterministic hash per user + scope
436
521
  // Same user with the same scope always produces the same nullifier
437
- const nullifier = sdk.extractNullifier(result.publicInputs, result.circuit);
522
+ const nullifier = sdk.extractNullifier(result.publicInputs, circuit);
438
523
 
439
- console.log('Scope:', scope); // '0x7a6b70726f...'
524
+ console.log('Scope:', scope); // '0x7a6b70726f...'
440
525
  console.log('Nullifier:', nullifier); // '0xabc123...'
441
526
 
442
527
  // Extract domain — only for OIDC Domain Attestation
443
- if (result.circuit === 'oidc_domain_attestation') {
444
- const domain = sdk.extractDomain(result.publicInputs, result.circuit);
528
+ if (circuit === 'oidc_domain_attestation') {
529
+ const domain = sdk.extractDomain(result.publicInputs, circuit);
445
530
  console.log('Domain:', domain); // 'example.com'
446
531
  }
447
532
  }
448
533
  ```
449
534
 
535
+ All three return `null` rather than throwing when the public inputs are too short for the circuit's layout.
536
+
450
537
  The **nullifier** serves as a privacy-preserving user identifier:
451
538
  - Deterministic: same user + same scope = same nullifier (enables duplicate detection)
452
- - Privacy-preserving: the wallet address (Coinbase) or email (OIDC) is never revealed
539
+ - Privacy-preserving: the wallet address (Coinbase), the email (OIDC) or the license (mDL) is never revealed
453
540
  - Scope-bound: different scopes produce different nullifiers for the same user
454
541
 
455
542
  > **OIDC Domain:** The nullifier is a hash of the user's email and scope. The same email + scope always produces the same nullifier, enabling Sybil resistance without revealing the email address.
456
543
 
457
544
  The **domain** (OIDC Domain Attestation only) is the email domain the user proved:
458
- - Extracted from the circuit's public inputs
545
+ - Decoded from the circuit's public inputs, up to 64 ASCII characters
459
546
  - Matches the domain parameter provided during proof request
460
- - Available only for `oidc_domain_attestation` circuits
547
+ - `extractDomain` returns `null` for every other circuit
461
548
 
462
549
  **Standalone utility functions** are also available for use outside the SDK class:
463
550
 
@@ -468,7 +555,8 @@ import {
468
555
  extractDomainFromPublicInputs,
469
556
  } from '@zkproofport-app/sdk';
470
557
 
471
- // Works with all circuits: coinbase_attestation, coinbase_country_attestation, oidc_domain_attestation, mdl_kr_ownership, mdl_kr_age, mdl_kr_region
558
+ // Scope and nullifier work for all six circuits — the layout is picked from the
559
+ // circuit id, so always pass it:
472
560
  const scope = extractScopeFromPublicInputs(publicInputs, 'coinbase_attestation');
473
561
  const nullifier = extractNullifierFromPublicInputs(publicInputs, 'coinbase_attestation');
474
562
 
@@ -476,17 +564,23 @@ const nullifier = extractNullifierFromPublicInputs(publicInputs, 'coinbase_attes
476
564
  const oidcScope = extractScopeFromPublicInputs(publicInputs, 'oidc_domain_attestation');
477
565
  const oidcNullifier = extractNullifierFromPublicInputs(publicInputs, 'oidc_domain_attestation');
478
566
 
567
+ // The three mDL circuits share one layout
568
+ const mdlNullifier = extractNullifierFromPublicInputs(publicInputs, 'mdl_kr_age');
569
+
479
570
  // Extract domain from OIDC Domain Attestation
480
571
  const domain = extractDomainFromPublicInputs(publicInputs, 'oidc_domain_attestation');
481
- // domain: 'example.com' or null if circuit doesn't match or inputs insufficient
572
+ // domain: 'example.com', or null if the circuit doesn't match or inputs are insufficient
482
573
  ```
483
574
 
575
+ Omitting the circuit argument falls back to the `coinbase_attestation` layout, which is why the examples above always pass it explicitly.
576
+
484
577
  ## Complete Example
485
578
 
486
579
  End-to-end integration using the relay flow:
487
580
 
488
581
  ```typescript
489
582
  import { ProofportSDK } from '@zkproofport-app/sdk';
583
+ import type { CircuitType, ProofResponse } from '@zkproofport-app/sdk';
490
584
  import { BrowserProvider } from 'ethers';
491
585
 
492
586
  async function verifyUser() {
@@ -508,30 +602,38 @@ async function verifyUser() {
508
602
 
509
603
  // Display QR code
510
604
  const qrDataUrl = await sdk.generateQRCode(relay.deepLink, { width: 400 });
511
- document.getElementById('qr-image').src = qrDataUrl;
512
- document.getElementById('status').textContent = 'Scan the QR code with ZKProofport';
605
+ (document.getElementById('qr-image') as HTMLImageElement).src = qrDataUrl;
606
+ document.getElementById('status')!.textContent = 'Scan the QR code with ZKProofport';
513
607
 
514
608
  // Wait for proof result
515
609
  const result = await sdk.waitForProof(relay.requestId, {
516
610
  onStatusChange: (update) => {
517
- document.getElementById('status').textContent = `Status: ${update.status}`;
611
+ document.getElementById('status')!.textContent = `Status: ${update.status}`;
518
612
  },
519
613
  });
520
614
 
521
615
  if (result.status === 'completed') {
522
616
  // Verify on-chain
523
- const verification = await sdk.verifyOnChain(
524
- result.circuit,
525
- result.proof,
526
- result.publicInputs
527
- );
617
+ const response: ProofResponse = {
618
+ requestId: result.requestId,
619
+ circuit: result.circuit as CircuitType,
620
+ status: 'completed',
621
+ proof: result.proof,
622
+ publicInputs: result.publicInputs,
623
+ verifierAddress: result.verifierAddress,
624
+ chainId: result.chainId,
625
+ };
626
+
627
+ const verification = await sdk.verifyResponseOnChain(response);
528
628
 
529
629
  if (verification.valid) {
530
- document.getElementById('status').textContent = 'Identity verified!';
630
+ document.getElementById('status')!.textContent = 'Identity verified!';
531
631
  // Grant access to your application
632
+ } else {
633
+ document.getElementById('status')!.textContent = `Invalid proof: ${verification.error}`;
532
634
  }
533
635
  } else {
534
- document.getElementById('status').textContent = `Failed: ${result.error}`;
636
+ document.getElementById('status')!.textContent = `Failed: ${result.error}`;
535
637
  }
536
638
 
537
639
  // Cleanup
@@ -541,11 +643,11 @@ async function verifyUser() {
541
643
 
542
644
  ## Configuration
543
645
 
544
- `ProofportSDK.create()` returns a fully configured SDK instance. No manual configuration is needed — relay URLs, verifier contracts, and chain settings are all built-in.
646
+ `ProofportSDK.create()` returns a fully configured SDK instance. No manual configuration is needed — the relay endpoint and the network used for verification are built in.
545
647
 
546
648
  ## Types Reference
547
649
 
548
- All 15 exported types:
650
+ Types you can import:
549
651
 
550
652
  ```typescript
551
653
  import type {
@@ -554,6 +656,9 @@ import type {
554
656
  CoinbaseKycInputs,
555
657
  CoinbaseCountryInputs,
556
658
  OidcDomainInputs,
659
+ MdlKrOwnershipInputs,
660
+ MdlKrAgeInputs,
661
+ MdlKrRegionInputs,
557
662
  CircuitInputs,
558
663
  ProofRequest,
559
664
  ProofResponse,
@@ -569,21 +674,24 @@ import type {
569
674
 
570
675
  | Type | Description |
571
676
  |------|-------------|
572
- | `CircuitType` | `'coinbase_attestation' \| 'coinbase_country_attestation' \| 'oidc_domain_attestation'` |
573
- | `ProofRequestStatus` | `'pending' \| 'completed' \| 'error' \| 'cancelled'` |
574
- | `CoinbaseKycInputs` | Inputs for `coinbase_attestation` (`{ scope, userAddress?, rawTransaction? }`) |
575
- | `CoinbaseCountryInputs` | Inputs for `coinbase_country_attestation` (`{ scope, countryList, isIncluded, ... }`) |
576
- | `OidcDomainInputs` | Inputs for `oidc_domain_attestation` (`{ domain, scope, provider? }`) |
577
- | `CircuitInputs` | Union: `CoinbaseKycInputs \| CoinbaseCountryInputs \| OidcDomainInputs` |
578
- | `ProofRequest` | Proof request object with `requestId`, `circuit`, `inputs`, metadata, and expiry |
579
- | `ProofResponse` | Proof response with `status`, `proof`, `publicInputs`, `verifierAddress`, `chainId` |
677
+ | `CircuitType` | `'coinbase_attestation' \| 'coinbase_country_attestation' \| 'oidc_domain_attestation' \| 'mdl_kr_ownership' \| 'mdl_kr_age' \| 'mdl_kr_region'` |
678
+ | `ProofRequestStatus` | `'pending' \| 'completed' \| 'error' \| 'cancelled'` — the status on a `ProofResponse` |
679
+ | `CoinbaseKycInputs` | Inputs for `coinbase_attestation`: `{ scope, userAddress?, rawTransaction? }` |
680
+ | `CoinbaseCountryInputs` | Inputs for `coinbase_country_attestation`: `{ scope, countryList, isIncluded, userAddress?, rawTransaction? }` |
681
+ | `OidcDomainInputs` | Inputs for `oidc_domain_attestation`: `{ domain, scope, provider? }` |
682
+ | `MdlKrOwnershipInputs` | Inputs for `mdl_kr_ownership`: `{ scope, discloseFlags? }` |
683
+ | `MdlKrAgeInputs` | Inputs for `mdl_kr_age`: `{ scope, ageThreshold }` |
684
+ | `MdlKrRegionInputs` | Inputs for `mdl_kr_region`: `{ scope, targetRegion }` |
685
+ | `CircuitInputs` | Union of every input type above (plus an empty-input form for circuits that need nothing from the dApp) |
686
+ | `ProofRequest` | Request object: `requestId`, `circuit`, `inputs`, `createdAt`, plus optional `message`, `dappName`, `dappIcon`, `returnScheme`, `expiresAt` |
687
+ | `ProofResponse` | Proof response: `requestId`, `circuit`, `status`, and — when completed — `proof`, `publicInputs`, `numPublicInputs`, `verifierAddress`, `chainId`, `timestamp`; `error` when it failed |
580
688
  | `QRCodeOptions` | QR customization: `width`, `margin`, `darkColor`, `lightColor`, `errorCorrectionLevel` |
581
689
  | `VerifierContract` | Verifier contract info: `{ address, chainId, abi }` |
582
- | `ProofportConfig` | SDK configuration (internal use — `ProofportSDK.create()` handles defaults) |
583
- | `ChallengeResponse` | Challenge from relay: `{ challenge, expiresAt }` |
584
- | `WalletSigner` | Signer interface: `{ signMessage(msg), getAddress() }` |
585
- | `RelayProofRequest` | Relay response: `{ requestId, deepLink, status, pollUrl }` |
586
- | `RelayProofResult` | Relay result: `{ requestId, status, proof?, publicInputs?, circuit?, error? }` |
690
+ | `ProofportConfig` | Constructor configuration — `ProofportSDK.create()` fills it in for you |
691
+ | `ChallengeResponse` | Challenge from the relay: `{ requestId, challenge, expiresAt }` |
692
+ | `WalletSigner` | Signer interface: `{ signMessage(message), getAddress() }` |
693
+ | `RelayProofRequest` | Result of `createRelayRequest()`: `{ requestId, deepLink, status, pollUrl }` |
694
+ | `RelayProofResult` | Result of `waitForProof()` / `waitForResult()` / `pollResult()`: `{ requestId, status: 'pending' \| 'completed' \| 'failed', deepLink?, createdAt?, updatedAt?, proof?, publicInputs?, verifierAddress?, chainId?, circuit?, error? }` |
587
695
 
588
696
  The `OidcDomainInputs` interface:
589
697
 
@@ -595,6 +703,8 @@ interface OidcDomainInputs {
595
703
  }
596
704
  ```
597
705
 
706
+ Note that `RelayProofResult` (what you get back from the relay) and `ProofResponse` (what `verifyResponseOnChain` takes) are different types: the relay result's `status` uses `'failed'`, and its `circuit` is a plain `string`. Step 6 shows the conversion.
707
+
598
708
  ## Public Input Layout Constants
599
709
 
600
710
  The SDK exports constants defining the field positions in each circuit's public inputs array. These are useful when working with standalone extraction functions or building custom verification logic.
@@ -604,24 +714,25 @@ import {
604
714
  COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT,
605
715
  COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT,
606
716
  OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT,
717
+ MDL_KR_PUBLIC_INPUT_LAYOUT,
607
718
  } from '@zkproofport-app/sdk';
608
719
  ```
609
720
 
610
- **Coinbase KYC Attestation** (128 fields total):
721
+ **Coinbase KYC Attestation** (128 fields):
611
722
  ```typescript
612
723
  COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT = {
613
- SIGNAL_HASH_START: 0, // RSA modulus limbs (Coinbase signer)
724
+ SIGNAL_HASH_START: 0, // Signal hash
614
725
  SIGNAL_HASH_END: 31,
615
- MERKLE_ROOT_START: 32, // Merkle root of signers
726
+ MERKLE_ROOT_START: 32, // Merkle root of the attestation signer list
616
727
  MERKLE_ROOT_END: 63,
617
- SCOPE_START: 64, // keccak256 hash of scope string
728
+ SCOPE_START: 64, // Scope value
618
729
  SCOPE_END: 95,
619
730
  NULLIFIER_START: 96, // Unique identifier per user+scope
620
731
  NULLIFIER_END: 127,
621
732
  }
622
733
  ```
623
734
 
624
- **Coinbase Country Attestation** (150 fields total):
735
+ **Coinbase Country Attestation** (150 fields):
625
736
  ```typescript
626
737
  COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT = {
627
738
  SIGNAL_HASH_START: 0,
@@ -639,7 +750,7 @@ COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT = {
639
750
  }
640
751
  ```
641
752
 
642
- **OIDC Domain Attestation** (148 fields total):
753
+ **OIDC Domain Attestation** (148 fields):
643
754
  ```typescript
644
755
  OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT = {
645
756
  PUBKEY_MODULUS_START: 0, // RSA modulus limbs (JWT issuer key)
@@ -647,29 +758,69 @@ OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT = {
647
758
  DOMAIN_STORAGE_START: 18, // Domain bytes (up to 64 ASCII characters)
648
759
  DOMAIN_STORAGE_END: 81,
649
760
  DOMAIN_LEN: 82, // Domain string length
650
- SCOPE_START: 83, // keccak256 hash of scope string
761
+ SCOPE_START: 83, // Scope value
651
762
  SCOPE_END: 114,
652
763
  NULLIFIER_START: 115, // Unique identifier per user+scope
653
764
  NULLIFIER_END: 146,
654
765
  PROVIDER: 147, // OIDC provider code (0=none, 1=Google, 2=Microsoft)
655
766
 
656
- // Deprecated aliases (use new names above)
767
+ // Deprecated aliases (use the names above)
657
768
  DOMAIN_START: 18, // @deprecated Use DOMAIN_STORAGE_START
658
769
  DOMAIN_END: 82, // @deprecated Use DOMAIN_LEN
659
770
  }
660
771
  ```
661
772
 
773
+ **Korea Mobile ID (mDL)** — one layout shared by the three mDL circuits. The prefix is identical everywhere; the suffix depends on which predicate the circuit proves (`mdl_kr_ownership` 97 fields, `mdl_kr_age` 66, `mdl_kr_region` 96):
774
+ ```typescript
775
+ MDL_KR_PUBLIC_INPUT_LAYOUT = {
776
+ SCOPE_START: 0, // Scope value
777
+ SCOPE_END: 31,
778
+ NULLIFIER_START: 32, // nullifier_value
779
+ NULLIFIER_END: 63,
780
+
781
+ // mdl_kr_ownership only
782
+ OWNERSHIP_DISCLOSE_FLAGS: 64,
783
+ OWNERSHIP_OWNER_COMMIT_START: 65,
784
+ OWNERSHIP_OWNER_COMMIT_END: 96,
785
+
786
+ // mdl_kr_age only
787
+ AGE_THRESHOLD: 64,
788
+ AGE_CURRENT_YEAR: 65,
789
+
790
+ // mdl_kr_region only
791
+ REGION_CODE_START: 64,
792
+ REGION_CODE_END: 95,
793
+ }
794
+ ```
795
+
662
796
  > **Note on field positions:** Each position in the public inputs array corresponds to a field element in the circuit. For bytes32 values (scope, nullifier, signal hash), 32 consecutive fields are concatenated to form the final value.
663
797
 
664
798
  ## Error Handling
665
799
 
666
- All async SDK methods throw standard `Error` objects:
800
+ Async SDK methods throw standard `Error` objects. The messages are stable enough to log, not to switch on:
667
801
 
668
802
  ```typescript
669
803
  try {
804
+ // Coinbase circuit without a signer
670
805
  await sdk.createRelayRequest('coinbase_attestation', { scope: 'app.com' });
671
806
  } catch (err) {
672
- // "Signer not set. Call setSigner() first."
807
+ // "Signer not set. Call setSigner() first. Wallet signature is required for this circuit."
808
+ }
809
+
810
+ try {
811
+ await sdk.createRelayRequest('mdl_kr_age', { scope: 'app.com', ageThreshold: 0 });
812
+ } catch (err) {
813
+ // "ageThreshold is required and must be an integer between 1 and 150 (circuit: mdl_kr_age)"
814
+ }
815
+
816
+ try {
817
+ await sdk.createRelayRequest('coinbase_attestation', { scope: 'app.com' }, {
818
+ returnScheme: 'https://myapp.com/done',
819
+ });
820
+ } catch (err) {
821
+ // "returnScheme must be a bare custom scheme such as "mydapp://" or an https
822
+ // origin such as "https://myapp.com" — paths, query strings and fragments are
823
+ // not accepted" (thrown before any network call)
673
824
  }
674
825
 
675
826
  try {
@@ -679,6 +830,10 @@ try {
679
830
  }
680
831
  ```
681
832
 
833
+ A relay rejection surfaces as its message, e.g. `Duplicate nonce (replay detected)` for a reused `nonce`, or `Request not found or expired` from `pollResult()`.
834
+
835
+ Verification is the exception: `verifyResponseOnChain` and `verifyOnChain` report failures as `{ valid: false, error }` instead of throwing.
836
+
682
837
  ## Networks
683
838
 
684
839
  Proofs are verified on **Base** (Ethereum L2). The SDK handles network configuration automatically — no manual setup required.