@zkproofport-app/sdk 0.5.0-beta.2 → 0.5.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@ const RELAY_URLS = {
32
32
  * Mirrors the mobile app's `CIRCUIT_NETWORKS` (proofport-app
33
33
  * src/config/contracts.ts), because a proof is checked where the app said its
34
34
  * verifier is: Base for the Coinbase and OIDC circuits, GIWA Sepolia for GIWA
35
- * and Dojang (testnet-only), Base Sepolia for Korea Mobile ID (testnet-only).
35
+ * and Dojang (testnet-only).
36
36
  * The ADDRESS on that chain always comes from core's pinned table, never from
37
37
  * a proof response.
38
38
  */
@@ -43,9 +43,6 @@ const VERIFICATION_CHAINS = {
43
43
  oidc_domain_attestation: 8453,
44
44
  giwa_attestation: 91342,
45
45
  dojang_playground: 91342,
46
- mdl_kr_ownership: 84532,
47
- mdl_kr_age: 84532,
48
- mdl_kr_region: 84532,
49
46
  },
50
47
  staging: {
51
48
  coinbase_attestation: 84532,
@@ -53,9 +50,6 @@ const VERIFICATION_CHAINS = {
53
50
  oidc_domain_attestation: 84532,
54
51
  giwa_attestation: 91342,
55
52
  dojang_playground: 91342,
56
- mdl_kr_ownership: 84532,
57
- mdl_kr_age: 84532,
58
- mdl_kr_region: 84532,
59
53
  },
60
54
  local: {
61
55
  coinbase_attestation: 84532,
@@ -63,9 +57,6 @@ const VERIFICATION_CHAINS = {
63
57
  oidc_domain_attestation: 84532,
64
58
  giwa_attestation: 91342,
65
59
  dojang_playground: 91342,
66
- mdl_kr_ownership: 84532,
67
- mdl_kr_age: 84532,
68
- mdl_kr_region: 84532,
69
60
  },
70
61
  };
71
62
  /**
@@ -126,8 +117,10 @@ const CIRCUIT_METADATA = {
126
117
  oidc_domain_attestation: {
127
118
  name: 'OIDC Domain',
128
119
  description: 'Prove email domain affiliation via OIDC JWT',
129
- publicInputsCount: 148,
130
- publicInputNames: ['pubkey_modulus_limbs', 'domain', 'scope', 'nullifier', 'provider'],
120
+ // v2: 148 v1 fields, then aud_hash (32), nonce_mode (1), nonce_binding
121
+ // (32), now (1). oidc-domain-attestation/src/main.nr.
122
+ publicInputsCount: 214,
123
+ publicInputNames: ['pubkey_modulus_limbs', 'domain', 'scope', 'nullifier', 'provider', 'aud_hash', 'nonce_mode', 'nonce_binding', 'now'],
131
124
  },
132
125
  // Planned circuit — the identifier is reserved and the circuit exists, but it
133
126
  // is not officially supported yet. See CIRCUIT_SUPPORT_STATUS in ./circuits.
@@ -160,27 +153,6 @@ const CIRCUIT_METADATA = {
160
153
  publicInputsCount: 160,
161
154
  publicInputNames: ['signal_hash', 'domain_separator', 'action_hash', 'scope', 'nullifier'],
162
155
  },
163
- // Korea Mobile ID (mDL) circuits. Planned — not officially supported yet.
164
- // Counts are byte-flattened field totals from the compiled circuit ABIs:
165
- // [u8; 32] arrays contribute 32 inputs each.
166
- mdl_kr_ownership: {
167
- name: 'Korea Mobile ID — Ownership',
168
- description: "Prove valid Korean mobile driver's license ownership with selective disclosure",
169
- publicInputsCount: 97,
170
- publicInputNames: ['scope', 'nullifier_value', 'disclose_flags', 'owner_commit'],
171
- },
172
- mdl_kr_age: {
173
- name: 'Korea Mobile ID — Age',
174
- description: "Prove minimum age from Korean mobile driver's license without revealing birth date",
175
- publicInputsCount: 66,
176
- publicInputNames: ['scope', 'nullifier_value', 'age_threshold', 'current_year'],
177
- },
178
- mdl_kr_region: {
179
- name: 'Korea Mobile ID — Region',
180
- description: "Prove si/do residency from Korean mobile driver's license without revealing address",
181
- publicInputsCount: 96,
182
- publicInputNames: ['scope', 'nullifier_value', 'region_code'],
183
- },
184
156
  };
185
157
  /**
186
158
  * Default proof request expiration time in milliseconds.
@@ -424,6 +396,12 @@ function parseProofResponseUrl(url) {
424
396
  if (timestamp) {
425
397
  response.timestamp = parseInt(timestamp, 10);
426
398
  }
399
+ // Kept as sent, even when unknown: verification refuses it by name,
400
+ // which says more than a callback that fails to parse.
401
+ const signerVariant = urlObj.searchParams.get('signerVariant');
402
+ if (signerVariant !== null) {
403
+ response.signerVariant = signerVariant;
404
+ }
427
405
  }
428
406
  else if (status === 'error') {
429
407
  response.error = urlObj.searchParams.get('error') || undefined;
@@ -455,94 +433,6 @@ function parseProofResponseUrl(url) {
455
433
  function isProofportDeepLink(url, scheme = DEFAULT_SCHEME) {
456
434
  return url.toLowerCase().startsWith(`${scheme.toLowerCase()}://`);
457
435
  }
458
- /**
459
- * @internal
460
- * Validates a proof request for completeness and correctness.
461
- *
462
- * Performs comprehensive validation including:
463
- * - Required fields (requestId, circuit, callbackUrl)
464
- * - Circuit type validity (must be a supported circuit)
465
- * - Circuit-specific input validation (e.g., userAddress format, countryList structure)
466
- * - Expiration check (if expiresAt is provided)
467
- *
468
- * Note: userAddress is optional for both circuits - the mobile app will prompt
469
- * for wallet connection if not provided.
470
- *
471
- * @param request - Proof request to validate
472
- * @returns Validation result with `valid` flag and optional `error` message
473
- */
474
- /**
475
- * Validates a free-text mDL input (scope / targetRegion).
476
- * Rejects empty or whitespace-only values, control characters (which break
477
- * deep-link query encoding and downstream logging), and values over 256 chars
478
- * (server-side length cap mirror). UTF-8 text such as Korean region names is
479
- * allowed.
480
- *
481
- * @returns Error message string, or null when valid.
482
- */
483
- function validateMdlTextInput(value, fieldName) {
484
- if (typeof value !== 'string' || value.trim() === '') {
485
- return `${fieldName} is required and must be a non-empty string`;
486
- }
487
- if (value.length > 256) {
488
- return `${fieldName} must be 256 characters or fewer`;
489
- }
490
- // eslint-disable-next-line no-control-regex
491
- if (/[\u0000-\u001F\u007F]/.test(value)) {
492
- return `${fieldName} must not contain control characters`;
493
- }
494
- return null;
495
- }
496
- /**
497
- * Validates dapp-supplied inputs for the Korea Mobile ID (mDL) circuits.
498
- * Shared by deep-link request validation and relay request creation so the
499
- * same guards apply on every request path.
500
- *
501
- * Rules:
502
- * - `scope` — required non-empty string, ≤256 chars, no control characters
503
- * - `mdl_kr_ownership.discloseFlags` — optional integer 0–15 (0x0F bitmask)
504
- * - `mdl_kr_age.ageThreshold` — required integer 1–150
505
- * - `mdl_kr_region.targetRegion` — required non-empty string (Korean si/do),
506
- * ≤256 chars, no control characters
507
- *
508
- * @param circuit - One of the three mDL circuit identifiers
509
- * @param inputs - Dapp-supplied circuit inputs
510
- * @returns Error message string, or null when the inputs are valid
511
- */
512
- function validateMdlInputs(circuit, inputs) {
513
- const scopeError = validateMdlTextInput(inputs.scope, 'scope');
514
- if (scopeError) {
515
- return scopeError;
516
- }
517
- if (circuit === 'mdl_kr_ownership') {
518
- const { discloseFlags } = inputs;
519
- if (discloseFlags !== undefined) {
520
- if (typeof discloseFlags !== 'number' ||
521
- !Number.isInteger(discloseFlags) ||
522
- discloseFlags < 0 ||
523
- discloseFlags > 0x0f) {
524
- return 'discloseFlags must be an integer between 0 and 15';
525
- }
526
- }
527
- }
528
- else if (circuit === 'mdl_kr_age') {
529
- const { ageThreshold } = inputs;
530
- if (typeof ageThreshold !== 'number' ||
531
- !Number.isInteger(ageThreshold) ||
532
- ageThreshold < 1 ||
533
- ageThreshold > 150) {
534
- return 'ageThreshold is required and must be an integer between 1 and 150';
535
- }
536
- }
537
- else {
538
- const { targetRegion } = inputs;
539
- const regionError = validateMdlTextInput(targetRegion, 'targetRegion');
540
- if (regionError) {
541
- return regionError;
542
- }
543
- }
544
- return null;
545
- }
546
436
  /**
547
437
  * Longest accepted `returnScheme` value.
548
438
  * Kept in sync with proofport-relay `src/returnScheme.ts`.
@@ -702,6 +592,22 @@ function detectReturnScheme(userAgent) {
702
592
  }
703
593
  return undefined;
704
594
  }
595
+ /**
596
+ * @internal
597
+ * Validates a proof request for completeness and correctness.
598
+ *
599
+ * Performs comprehensive validation including:
600
+ * - Required fields (requestId, circuit, callbackUrl)
601
+ * - Circuit type validity (must be a supported circuit)
602
+ * - Circuit-specific input validation (e.g., userAddress format, countryList structure)
603
+ * - Expiration check (if expiresAt is provided)
604
+ *
605
+ * Note: userAddress is optional for both circuits - the mobile app will prompt
606
+ * for wallet connection if not provided.
607
+ *
608
+ * @param request - Proof request to validate
609
+ * @returns Validation result with `valid` flag and optional `error` message
610
+ */
705
611
  function validateProofRequest(request) {
706
612
  if (!request.requestId) {
707
613
  return { valid: false, error: 'Missing requestId' };
@@ -757,14 +663,6 @@ function validateProofRequest(request) {
757
663
  return { valid: false, error: 'provider must be a non-empty string when specified' };
758
664
  }
759
665
  }
760
- else if (request.circuit === 'mdl_kr_ownership' ||
761
- request.circuit === 'mdl_kr_age' ||
762
- request.circuit === 'mdl_kr_region') {
763
- const mdlError = validateMdlInputs(request.circuit, request.inputs);
764
- if (mdlError) {
765
- return { valid: false, error: mdlError };
766
- }
767
- }
768
666
  // The action, decided by the circuit's own entry rather than by naming a
769
667
  // circuit here, so every action-capable circuit is checked the same way.
770
668
  {
@@ -1001,7 +899,7 @@ function estimateQRDataSize(requestOrUrl, scheme = DEFAULT_SCHEME) {
1001
899
  * The version. `@aztec/bb.js` must be the WASM build of the same bb the
1002
900
  * circuits are compiled with -- `BB_VERSION` in circuits/toolchain.env,
1003
901
  * 4.2.0-aztecnr-rc.2 since the beta.19 toolchain. Versions are not
1004
- * interchangeable: on the Korea Mobile ID age circuit the 2025 nightly wrote
902
+ * interchangeable: on one of our circuits the 2025 nightly wrote
1005
903
  * a 16256-byte proof where bb 1.2.0 wrote 16224 for the identical circuit and
1006
904
  * witness, with the SAME verification key, so the key cannot warn you. A
1007
905
  * mismatch shows up only as verification failing.
@@ -1103,15 +1001,12 @@ function normalisePublicInputs(publicInputs) {
1103
1001
  *
1104
1002
  * @internal
1105
1003
  */
1106
- async function fetchVerificationKey(circuit, timeoutMs) {
1107
- const cached = vkCache.get(circuit);
1004
+ async function fetchVerificationKey(circuit, variant, timeoutMs) {
1005
+ const cacheKey = `${circuit}/${variant}`;
1006
+ const cached = vkCache.get(cacheKey);
1108
1007
  if (cached && Date.now() - cached.at < VK_CACHE_TTL_MS)
1109
1008
  return cached.vk;
1110
- const vkPath = circuits.CIRCUIT_VK_PATHS[circuit];
1111
- if (!vkPath) {
1112
- throw new Error(`No verification key path for circuit '${circuit}'. ` +
1113
- `Add it to CIRCUIT_VK_PATHS, or pass verificationKey directly.`);
1114
- }
1009
+ const vkPath = circuits.getCircuitPackage(circuit, variant).vkPath;
1115
1010
  const url = `${CIRCUITS_VK_BASE}/${vkPath}`;
1116
1011
  const controller = new AbortController();
1117
1012
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -1128,7 +1023,7 @@ async function fetchVerificationKey(circuit, timeoutMs) {
1128
1023
  throw new Error(`The verification key at ${url} is ${vk.length} bytes, far too small to be one. ` +
1129
1024
  `The path is probably wrong and something else was served.`);
1130
1025
  }
1131
- vkCache.set(circuit, { vk, at: Date.now() });
1026
+ vkCache.set(cacheKey, { vk, at: Date.now() });
1132
1027
  return vk;
1133
1028
  }
1134
1029
  finally {
@@ -1141,9 +1036,11 @@ async function fetchVerificationKey(circuit, timeoutMs) {
1141
1036
  * Needs no setup from the caller: the verifier ships with this SDK and is
1142
1037
  * loaded on first use.
1143
1038
  *
1144
- * For `oidc_domain_attestation` it first checks the proof's RSA key against the
1145
- * provider's published JWKS (core's `checkOidcProviderKey`); a key the provider
1146
- * does not publish, or a JWKS that cannot be fetched, is `{ valid: false }`.
1039
+ * For `oidc_domain_attestation` it first checks the token's audience, nonce and
1040
+ * time (core's `checkOidcPolicy`, against `options.request` when given) and the
1041
+ * proof's RSA key against the provider's published JWKS (core's
1042
+ * `checkOidcProviderKey`); any refusal, or a JWKS that cannot be fetched, is
1043
+ * `{ valid: false }`.
1147
1044
  *
1148
1045
  * @param circuit - Canonical circuit identifier
1149
1046
  * @param proof - Proof bytes as hex, WITHOUT public inputs (as the relay sends it)
@@ -1166,12 +1063,16 @@ async function verifyProofOffChain(circuit, proof, publicInputs, options = {}) {
1166
1063
  if (!circuits.isCircuitId(circuit)) {
1167
1064
  return { valid: false, error: `Unknown circuit '${circuit}'. Known: ${circuits.ALL_CIRCUIT_IDS.join(', ')}.` };
1168
1065
  }
1066
+ const signer = circuits.resolveSignerVariant(circuit, options.signerVariant, proof);
1067
+ if ('error' in signer) {
1068
+ return { valid: false, error: signer.error };
1069
+ }
1169
1070
  // Before Barretenberg, and for the same reason as on-chain: a proof can be
1170
1071
  // mathematically valid and still mean nothing, because some circuits take
1171
1072
  // their trust anchor as a public input the prover chooses. For OIDC that is
1172
1073
  // the RSA key -- see core's oidcProviderKeys. Checked here so a dApp that
1173
1074
  // verifies off-chain gets the same answer as one that verifies on-chain.
1174
- const issuer = await core.checkProofIssuer(circuit, publicInputs);
1075
+ const issuer = await core.checkProofIssuer(circuit, publicInputs, { request: options.request });
1175
1076
  if (!issuer.ok) {
1176
1077
  return { valid: false, error: issuer.error };
1177
1078
  }
@@ -1212,7 +1113,7 @@ async function verifyProofOffChain(circuit, proof, publicInputs, options = {}) {
1212
1113
  // relay can deliver a short value (`0x01`), and the check below must see
1213
1114
  // exactly what the circuit committed to.
1214
1115
  const fields = normalisePublicInputs(publicInputs);
1215
- const vk = options.verificationKey ?? (await fetchVerificationKey(circuit, timeoutMs));
1116
+ const vk = options.verificationKey ?? (await fetchVerificationKey(circuit, signer.variant, timeoutMs));
1216
1117
  const barretenberg = await bb.Barretenberg.new({ threads: 1 });
1217
1118
  api = barretenberg;
1218
1119
  const verifier = new bb.UltraHonkVerifierBackend(barretenberg);
@@ -1246,6 +1147,17 @@ const RELAY_TERMINAL_STATUSES = ['completed', 'error', 'failed', 'cancelled', 'e
1246
1147
  function isTerminalRelayStatus(status) {
1247
1148
  return RELAY_TERMINAL_STATUSES.includes(status);
1248
1149
  }
1150
+ /**
1151
+ * How long `waitForProof()` / `waitForResult()` wait when the caller passes no
1152
+ * `timeoutMs`: 600 000 ms (10 min).
1153
+ *
1154
+ * The relay answers `expired` itself at 540 s, so with this default a dApp
1155
+ * always gets a terminal status to branch on rather than its own timeout. A
1156
+ * heavy proof on an older phone takes a few minutes after the user confirms;
1157
+ * the earlier 300 000 ms default could fire while such a proof was still being
1158
+ * made.
1159
+ */
1160
+ const DEFAULT_RELAY_WAIT_MS = 600000;
1249
1161
 
1250
1162
  /**
1251
1163
  * Proofport SDK - Main class
@@ -1313,6 +1225,13 @@ class ProofportSDK {
1313
1225
  */
1314
1226
  constructor(config = {}) {
1315
1227
  this.pendingRequests = new Map();
1228
+ /**
1229
+ * requestId -> scope of the relay requests this instance created, so that
1230
+ * verifyResponse* can check an OIDC proof's request-bound nonce without the
1231
+ * dApp passing the request back in. Oldest entries go first past
1232
+ * {@link ProofportSDK.MAX_REMEMBERED_RELAY_REQUESTS}.
1233
+ */
1234
+ this.relayRequests = new Map();
1316
1235
  this.signer = null;
1317
1236
  this.socket = null;
1318
1237
  /**
@@ -1389,7 +1308,7 @@ class ProofportSDK {
1389
1308
  const request = {
1390
1309
  requestId: generateRequestId(),
1391
1310
  // The circuit asked for, always. This used to be the literal
1392
- // 'coinbase_attestation', and every circuit that was not country or mDL
1311
+ // 'coinbase_attestation', and every circuit that was not country
1393
1312
  // was routed here -- a GIWA, Dojang or OIDC request silently became a
1394
1313
  // Coinbase KYC request.
1395
1314
  circuit,
@@ -1496,45 +1415,10 @@ class ProofportSDK {
1496
1415
  switch (builder) {
1497
1416
  case 'country':
1498
1417
  return this.createCoinbaseCountryRequest(inputs, options);
1499
- case 'mdl':
1500
- return this.createMdlKrRequest(circuit, inputs, options);
1501
1418
  case 'scoped':
1502
1419
  return this.createScopedRequest(circuit, inputs, options);
1503
1420
  }
1504
1421
  }
1505
- /**
1506
- * @internal
1507
- * Creates a Korea Mobile ID (mDL) proof request for any of the three
1508
- * mDL predicate circuits (ownership / age / region).
1509
- *
1510
- * The mobile app collects the license data on-device; the dapp only
1511
- * supplies the predicate parameters validated here. No wallet signature
1512
- * is required for mDL circuits.
1513
- *
1514
- * @param circuit - 'mdl_kr_ownership' | 'mdl_kr_age' | 'mdl_kr_region'
1515
- * @param inputs - Predicate inputs (see MdlKr*Inputs types)
1516
- * @param options - Request configuration options
1517
- *
1518
- * @throws Error if the predicate inputs fail validation
1519
- */
1520
- createMdlKrRequest(circuit, inputs, options = {}) {
1521
- const mdlError = validateMdlInputs(circuit, inputs);
1522
- if (mdlError) {
1523
- throw new Error(`${mdlError} (circuit: ${circuit})`);
1524
- }
1525
- const request = {
1526
- requestId: generateRequestId(),
1527
- circuit,
1528
- inputs,
1529
- message: options.message,
1530
- dappName: options.dappName,
1531
- dappIcon: options.dappIcon,
1532
- createdAt: Date.now(),
1533
- expiresAt: Date.now() + (options.expiresInMs || DEFAULT_REQUEST_EXPIRY_MS),
1534
- };
1535
- this.pendingRequests.set(request.requestId, request);
1536
- return request;
1537
- }
1538
1422
  // ============ Deep Link Generation ============
1539
1423
  /**
1540
1424
  * @internal
@@ -1874,9 +1758,50 @@ class ProofportSDK {
1874
1758
  * }
1875
1759
  * ```
1876
1760
  */
1877
- async verifyOnChain(circuit, proof, publicInputs, providerOrSigner) {
1761
+ async verifyOnChain(circuit, proof, publicInputs, providerOrSigner, options = {}) {
1878
1762
  const parsedProof = core.parseProofForOnChain(proof, publicInputs, publicInputs.length);
1879
- return core.verifyProofOnChain(circuit, parsedProof, providerOrSigner, this.verifierTarget(circuit));
1763
+ return core.verifyProofOnChain(circuit, parsedProof, providerOrSigner, this.verifierTarget(circuit), undefined, {
1764
+ request: options.request,
1765
+ signerVariant: options.signerVariant,
1766
+ });
1767
+ }
1768
+ /**
1769
+ * The request an `oidc_domain_attestation` response answers: the one the
1770
+ * caller passed, else the relay request this instance created under the
1771
+ * response's requestId. `{ error }` when there is none, or when the two
1772
+ * disagree -- an OIDC proof made with a request-bound nonce is only
1773
+ * meaningful against the request it was bound to, so a response this SDK
1774
+ * cannot place is refused rather than checked without it.
1775
+ */
1776
+ oidcRequestFor(response, explicit) {
1777
+ if (explicit !== undefined) {
1778
+ if (explicit === null || typeof explicit !== 'object' || explicit.requestId !== response.requestId) {
1779
+ return {
1780
+ error: `The request passed in (${JSON.stringify(explicit?.requestId)}) is not the one this response answers (${JSON.stringify(response.requestId)}).`,
1781
+ };
1782
+ }
1783
+ return { request: explicit };
1784
+ }
1785
+ const remembered = this.relayRequests.get(response.requestId);
1786
+ if (!remembered) {
1787
+ return {
1788
+ error: `No request ${JSON.stringify(response.requestId)} was created by this SDK instance, so the OIDC proof's ` +
1789
+ 'nonce cannot be checked against it. Pass { request: { scope, requestId } } -- the scope and relay ' +
1790
+ 'requestId of the request you created.',
1791
+ };
1792
+ }
1793
+ return { request: remembered };
1794
+ }
1795
+ /** @internal Remember a relay request's scope for verifyResponse*. */
1796
+ rememberRelayRequest(requestId, scope) {
1797
+ if (typeof requestId !== 'string' || typeof scope !== 'string')
1798
+ return;
1799
+ this.relayRequests.delete(requestId);
1800
+ this.relayRequests.set(requestId, { scope, requestId });
1801
+ while (this.relayRequests.size > ProofportSDK.MAX_REMEMBERED_RELAY_REQUESTS) {
1802
+ const oldest = this.relayRequests.keys().next().value;
1803
+ this.relayRequests.delete(oldest);
1804
+ }
1880
1805
  }
1881
1806
  /**
1882
1807
  * The verifier for a circuit: the one the dApp configured, otherwise the
@@ -1903,6 +1828,9 @@ class ProofportSDK {
1903
1828
  *
1904
1829
  * @param response - ProofResponse object from parseResponse()
1905
1830
  * @param providerOrSigner - ethers v6 Provider or Signer (defaults to base mainnet public RPC)
1831
+ * @param options.request - For `oidc_domain_attestation`: the `{ scope, requestId }`
1832
+ * the response answers. Not needed for a relay request this SDK instance
1833
+ * created; an OIDC response this instance cannot place is refused without it.
1906
1834
  *
1907
1835
  * @returns Promise resolving to verification result with valid boolean and optional error message
1908
1836
  *
@@ -1929,10 +1857,17 @@ class ProofportSDK {
1929
1857
  * });
1930
1858
  * ```
1931
1859
  */
1932
- async verifyResponseOnChain(response, providerOrSigner) {
1860
+ async verifyResponseOnChain(response, providerOrSigner, options = {}) {
1933
1861
  if (response.status !== 'completed' || !response.proof || !response.publicInputs) {
1934
1862
  return { valid: false, error: 'Invalid or incomplete response' };
1935
1863
  }
1864
+ let request;
1865
+ if (response.circuit === 'oidc_domain_attestation') {
1866
+ const found = this.oidcRequestFor(response, options.request);
1867
+ if ('error' in found)
1868
+ return { valid: false, error: found.error };
1869
+ request = found.request;
1870
+ }
1936
1871
  const parsedProof = core.parseProofForOnChain(response.proof, response.publicInputs, response.publicInputs.length);
1937
1872
  // The response's verifierAddress and chainId are a claim to check, never
1938
1873
  // the contract to call: whoever wrote the response would otherwise pick
@@ -1941,7 +1876,7 @@ class ProofportSDK {
1941
1876
  return core.verifyProofOnChain(response.circuit, parsedProof, providerOrSigner, this.verifierTarget(response.circuit), {
1942
1877
  verifierAddress: response.verifierAddress,
1943
1878
  chainId: response.chainId,
1944
- });
1879
+ }, { request, signerVariant: response.signerVariant });
1945
1880
  }
1946
1881
  /**
1947
1882
  * Verifies a proof response off-chain, against the circuit's verification key.
@@ -1971,7 +1906,14 @@ class ProofportSDK {
1971
1906
  if (response.status !== 'completed' || !response.proof || !response.publicInputs) {
1972
1907
  return { valid: false, error: 'Invalid or incomplete response' };
1973
1908
  }
1974
- return verifyProofOffChain(response.circuit, response.proof, response.publicInputs, options);
1909
+ let request;
1910
+ if (response.circuit === 'oidc_domain_attestation') {
1911
+ const found = this.oidcRequestFor(response, options?.request);
1912
+ if ('error' in found)
1913
+ return { valid: false, error: found.error };
1914
+ request = found.request;
1915
+ }
1916
+ return verifyProofOffChain(response.circuit, response.proof, response.publicInputs, { ...options, request, signerVariant: response.signerVariant });
1975
1917
  }
1976
1918
  // ============ Utility Methods ============
1977
1919
  /**
@@ -2329,14 +2271,6 @@ class ProofportSDK {
2329
2271
  if (needsSignature && !this.signer) {
2330
2272
  throw new Error('Signer not set. Call setSigner() first. Wallet signature is required for this circuit.');
2331
2273
  }
2332
- // mDL circuits: validate predicate inputs client-side before hitting the
2333
- // relay, so a bad ageThreshold / targetRegion fails fast with a clear error.
2334
- if (circuit === 'mdl_kr_ownership' || circuit === 'mdl_kr_age' || circuit === 'mdl_kr_region') {
2335
- const mdlError = validateMdlInputs(circuit, inputs);
2336
- if (mdlError) {
2337
- throw new Error(`${mdlError} (circuit: ${circuit})`);
2338
- }
2339
- }
2340
2274
  // The action, per the circuit's entry in CIRCUIT_ACTION_BINDING. Checked
2341
2275
  // here, before a challenge is burned, because the alternative is a refusal
2342
2276
  // from inside the mobile app -- a screen the dapp developer never sees,
@@ -2410,7 +2344,9 @@ class ProofportSDK {
2410
2344
  const error = await response.json().catch(() => ({ error: `HTTP ${response.status}` }));
2411
2345
  throw new Error(error.error || `Relay request failed: HTTP ${response.status}`);
2412
2346
  }
2413
- return await response.json();
2347
+ const created = await response.json();
2348
+ this.rememberRelayRequest(created.requestId, scope);
2349
+ return created;
2414
2350
  }
2415
2351
  /**
2416
2352
  * Polls the relay for proof result status.
@@ -2450,14 +2386,14 @@ class ProofportSDK {
2450
2386
  * @param requestId - The relay-issued request ID
2451
2387
  * @param options - Polling options
2452
2388
  * @param options.intervalMs - Polling interval in milliseconds (default: 2000)
2453
- * @param options.timeoutMs - Maximum polling time in milliseconds (default: 300000 = 5 min)
2389
+ * @param options.timeoutMs - Maximum polling time in milliseconds (default: 600000 = 10 min, `DEFAULT_RELAY_WAIT_MS`)
2454
2390
  * @param options.onStatusChange - Callback when status changes
2455
2391
  * @returns Promise resolving to final RelayProofResult
2456
2392
  * @throws Error if timeout or relay error
2457
2393
  */
2458
2394
  async waitForResult(requestId, options = {}) {
2459
2395
  const interval = options.intervalMs || 2000;
2460
- const timeout = options.timeoutMs || 300000;
2396
+ const timeout = options.timeoutMs || DEFAULT_RELAY_WAIT_MS;
2461
2397
  const startTime = Date.now();
2462
2398
  let lastStatus = '';
2463
2399
  while (Date.now() - startTime < timeout) {
@@ -2569,12 +2505,12 @@ class ProofportSDK {
2569
2505
  *
2570
2506
  * @param requestId - The relay-issued request ID
2571
2507
  * @param options - Configuration options
2572
- * @param options.timeoutMs - Maximum wait time in ms (default: 300000 = 5 min)
2508
+ * @param options.timeoutMs - Maximum wait time in ms (default: 600000 = 10 min, `DEFAULT_RELAY_WAIT_MS`)
2573
2509
  * @param options.onStatusChange - Callback for status updates
2574
2510
  * @returns Promise resolving to final RelayProofResult
2575
2511
  */
2576
2512
  async waitForProof(requestId, options = {}) {
2577
- const timeout = options.timeoutMs || 300000;
2513
+ const timeout = options.timeoutMs || DEFAULT_RELAY_WAIT_MS;
2578
2514
  // Try Socket.IO first
2579
2515
  if (this.relayUrl) {
2580
2516
  try {
@@ -2693,6 +2629,8 @@ class ProofportSDK {
2693
2629
  return core.extractDomainFromPublicInputs(publicInputs, circuit);
2694
2630
  }
2695
2631
  }
2632
+ /** Bound on {@link relayRequests}; a dApp that verifies later can pass the request explicitly. */
2633
+ ProofportSDK.MAX_REMEMBERED_RELAY_REQUESTS = 1000;
2696
2634
  /**
2697
2635
  * How each circuit's request is built and checked. Keyed by circuit id and
2698
2636
  * exhaustive over it, so a new circuit is a compile error here instead of
@@ -2704,9 +2642,6 @@ ProofportSDK.REQUEST_BUILDERS = {
2704
2642
  oidc_domain_attestation: 'scoped',
2705
2643
  giwa_attestation: 'scoped',
2706
2644
  dojang_playground: 'scoped',
2707
- mdl_kr_ownership: 'mdl',
2708
- mdl_kr_age: 'mdl',
2709
- mdl_kr_region: 'mdl',
2710
2645
  };
2711
2646
 
2712
2647
  Object.defineProperty(exports, "COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT", {
@@ -2717,18 +2652,50 @@ Object.defineProperty(exports, "COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT", {
2717
2652
  enumerable: true,
2718
2653
  get: function () { return core.COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT; }
2719
2654
  });
2720
- Object.defineProperty(exports, "MDL_KR_PUBLIC_INPUT_LAYOUT", {
2655
+ Object.defineProperty(exports, "OIDC_ALLOWED_AUDIENCES", {
2721
2656
  enumerable: true,
2722
- get: function () { return core.MDL_KR_PUBLIC_INPUT_LAYOUT; }
2657
+ get: function () { return core.OIDC_ALLOWED_AUDIENCES; }
2723
2658
  });
2724
2659
  Object.defineProperty(exports, "OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT", {
2725
2660
  enumerable: true,
2726
2661
  get: function () { return core.OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT; }
2727
2662
  });
2663
+ Object.defineProperty(exports, "OIDC_NONCE_DOMAIN", {
2664
+ enumerable: true,
2665
+ get: function () { return core.OIDC_NONCE_DOMAIN; }
2666
+ });
2667
+ Object.defineProperty(exports, "OIDC_NOW_MAX_AGE_SECONDS", {
2668
+ enumerable: true,
2669
+ get: function () { return core.OIDC_NOW_MAX_AGE_SECONDS; }
2670
+ });
2671
+ Object.defineProperty(exports, "OIDC_NOW_MAX_FUTURE_SECONDS", {
2672
+ enumerable: true,
2673
+ get: function () { return core.OIDC_NOW_MAX_FUTURE_SECONDS; }
2674
+ });
2675
+ Object.defineProperty(exports, "OIDC_V1_PUBLIC_INPUT_COUNT", {
2676
+ enumerable: true,
2677
+ get: function () { return core.OIDC_V1_PUBLIC_INPUT_COUNT; }
2678
+ });
2679
+ Object.defineProperty(exports, "OIDC_V2_PUBLIC_INPUT_COUNT", {
2680
+ enumerable: true,
2681
+ get: function () { return core.OIDC_V2_PUBLIC_INPUT_COUNT; }
2682
+ });
2683
+ Object.defineProperty(exports, "PINNED_VARIANT_VERIFIERS", {
2684
+ enumerable: true,
2685
+ get: function () { return core.PINNED_VARIANT_VERIFIERS; }
2686
+ });
2728
2687
  Object.defineProperty(exports, "PINNED_VERIFIERS", {
2729
2688
  enumerable: true,
2730
2689
  get: function () { return core.PINNED_VERIFIERS; }
2731
2690
  });
2691
+ Object.defineProperty(exports, "checkOidcPolicy", {
2692
+ enumerable: true,
2693
+ get: function () { return core.checkOidcPolicy; }
2694
+ });
2695
+ Object.defineProperty(exports, "deriveOidcNonceBinding", {
2696
+ enumerable: true,
2697
+ get: function () { return core.deriveOidcNonceBinding; }
2698
+ });
2732
2699
  Object.defineProperty(exports, "extractDomainFromPublicInputs", {
2733
2700
  enumerable: true,
2734
2701
  get: function () { return core.extractDomainFromPublicInputs; }
@@ -2741,10 +2708,18 @@ Object.defineProperty(exports, "extractScopeFromPublicInputs", {
2741
2708
  enumerable: true,
2742
2709
  get: function () { return core.extractScopeFromPublicInputs; }
2743
2710
  });
2711
+ Object.defineProperty(exports, "getPinnedVariantVerifier", {
2712
+ enumerable: true,
2713
+ get: function () { return core.getPinnedVariantVerifier; }
2714
+ });
2744
2715
  Object.defineProperty(exports, "getPinnedVerifier", {
2745
2716
  enumerable: true,
2746
2717
  get: function () { return core.getPinnedVerifier; }
2747
2718
  });
2719
+ Object.defineProperty(exports, "oidcAudienceHash", {
2720
+ enumerable: true,
2721
+ get: function () { return core.oidcAudienceHash; }
2722
+ });
2748
2723
  Object.defineProperty(exports, "ALL_CIRCUIT_IDS", {
2749
2724
  enumerable: true,
2750
2725
  get: function () { return circuits.ALL_CIRCUIT_IDS; }
@@ -2757,6 +2732,10 @@ Object.defineProperty(exports, "CIRCUIT_IDS", {
2757
2732
  enumerable: true,
2758
2733
  get: function () { return circuits.CIRCUIT_IDS; }
2759
2734
  });
2735
+ Object.defineProperty(exports, "CIRCUIT_PACKAGES", {
2736
+ enumerable: true,
2737
+ get: function () { return circuits.CIRCUIT_PACKAGES; }
2738
+ });
2760
2739
  Object.defineProperty(exports, "CIRCUIT_SUPPORT_STATUS", {
2761
2740
  enumerable: true,
2762
2741
  get: function () { return circuits.CIRCUIT_SUPPORT_STATUS; }
@@ -2773,6 +2752,10 @@ Object.defineProperty(exports, "PLANNED_CIRCUIT_IDS", {
2773
2752
  enumerable: true,
2774
2753
  get: function () { return circuits.PLANNED_CIRCUIT_IDS; }
2775
2754
  });
2755
+ Object.defineProperty(exports, "SIGNER_VARIANTS", {
2756
+ enumerable: true,
2757
+ get: function () { return circuits.SIGNER_VARIANTS; }
2758
+ });
2776
2759
  Object.defineProperty(exports, "SUPPORTED_CIRCUIT_IDS", {
2777
2760
  enumerable: true,
2778
2761
  get: function () { return circuits.SUPPORTED_CIRCUIT_IDS; }
@@ -2781,6 +2764,14 @@ Object.defineProperty(exports, "circuitActionBinding", {
2781
2764
  enumerable: true,
2782
2765
  get: function () { return circuits.circuitActionBinding; }
2783
2766
  });
2767
+ Object.defineProperty(exports, "evmProofByteLength", {
2768
+ enumerable: true,
2769
+ get: function () { return circuits.evmProofByteLength; }
2770
+ });
2771
+ Object.defineProperty(exports, "getCircuitPackage", {
2772
+ enumerable: true,
2773
+ get: function () { return circuits.getCircuitPackage; }
2774
+ });
2784
2775
  Object.defineProperty(exports, "getCircuitSupportStatus", {
2785
2776
  enumerable: true,
2786
2777
  get: function () { return circuits.getCircuitSupportStatus; }
@@ -2789,10 +2780,22 @@ Object.defineProperty(exports, "isCircuitId", {
2789
2780
  enumerable: true,
2790
2781
  get: function () { return circuits.isCircuitId; }
2791
2782
  });
2783
+ Object.defineProperty(exports, "isSignerVariant", {
2784
+ enumerable: true,
2785
+ get: function () { return circuits.isSignerVariant; }
2786
+ });
2792
2787
  Object.defineProperty(exports, "isSupportedCircuitId", {
2793
2788
  enumerable: true,
2794
2789
  get: function () { return circuits.isSupportedCircuitId; }
2795
2790
  });
2791
+ Object.defineProperty(exports, "resolveSignerVariant", {
2792
+ enumerable: true,
2793
+ get: function () { return circuits.resolveSignerVariant; }
2794
+ });
2795
+ Object.defineProperty(exports, "signerVariantsOf", {
2796
+ enumerable: true,
2797
+ get: function () { return circuits.signerVariantsOf; }
2798
+ });
2796
2799
  Object.defineProperty(exports, "parseTypedAction", {
2797
2800
  enumerable: true,
2798
2801
  get: function () { return typedAction.parseTypedAction; }
@@ -2801,6 +2804,7 @@ Object.defineProperty(exports, "validateTypedAction", {
2801
2804
  enumerable: true,
2802
2805
  get: function () { return typedAction.validateTypedAction; }
2803
2806
  });
2807
+ exports.DEFAULT_RELAY_WAIT_MS = DEFAULT_RELAY_WAIT_MS;
2804
2808
  exports.ProofportSDK = ProofportSDK;
2805
2809
  exports.RELAY_TERMINAL_STATUSES = RELAY_TERMINAL_STATUSES;
2806
2810
  exports.default = ProofportSDK;