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

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
@@ -92,27 +92,32 @@ const DEEP_LINK_HOSTS = {
92
92
  * ```typescript
93
93
  * const metadata = CIRCUIT_METADATA['coinbase_attestation'];
94
94
  * console.log(metadata.name); // "Coinbase KYC"
95
- * console.log(metadata.publicInputsCount); // 128
95
+ * console.log(metadata.publicInputsCount); // 193
96
96
  * ```
97
97
  */
98
98
  const CIRCUIT_METADATA = {
99
99
  coinbase_attestation: {
100
100
  name: 'Coinbase KYC',
101
- description: 'Prove Coinbase identity verification',
102
- // 4 x `pub [u8; 32]` in coinbase-attestation/src/main.nr, flattened to
103
- // bytes: signal_hash, signer_list_merkle_root, scope, nullifier.
104
- // Read 2 here until 2026-09-04, which was wrong under any reading —
105
- // the circuit has never had two public inputs, logical or flattened.
106
- publicInputsCount: 128,
107
- publicInputNames: ['signal_hash', 'signer_list_merkle_root'],
101
+ description: 'Prove Coinbase identity verification, optionally authorizing one EIP-712 action',
102
+ // Coinbase KYC v2 (coinbase-attestation-v2/src/main.nr): GIWA's six
103
+ // `pub [u8; 32]` in its order, then `attested_at: pub u64` = 193 fields.
104
+ // signal_hash 0-31, domain_separator 32-63, action_hash 64-95,
105
+ // signer_list_merkle_root 96-127, scope 128-159, nullifier 160-191,
106
+ // attested_at 192. v1's 128 fields are refused on every verify path.
107
+ publicInputsCount: 193,
108
+ publicInputNames: ['signal_hash', 'domain_separator', 'action_hash', 'signer_list_merkle_root', 'scope', 'nullifier', 'attested_at'],
108
109
  },
109
110
  coinbase_country_attestation: {
110
111
  name: 'Coinbase Country',
111
- description: 'Prove Coinbase country verification',
112
- // 32 + 32 + (10 x 2) + 1 + 1 + 32 + 32 = 150, counted off
113
- // coinbase-country-attestation/src/main.nr. Read 14 until 2026-09-04.
114
- publicInputsCount: 150,
115
- publicInputNames: ['signal_hash', 'signer_list_merkle_root', 'country_list', 'country_list_length', 'is_included'],
112
+ description: 'Prove Coinbase country verification, optionally authorizing one EIP-712 action',
113
+ // Coinbase Country v2 (coinbase-country-attestation-v2/src/main.nr):
114
+ // signal_hash 0-31, domain_separator 32-63, action_hash 64-95,
115
+ // signer_list_merkle_root 96-127, country_list 128-147 (10 x 2 bytes),
116
+ // country_list_length 148, is_included 149, scope 150-181,
117
+ // nullifier 182-213, attested_at 214 = 215 fields. v1's 150 fields are
118
+ // refused on every verify path.
119
+ publicInputsCount: 215,
120
+ publicInputNames: ['signal_hash', 'domain_separator', 'action_hash', 'signer_list_merkle_root', 'country_list', 'country_list_length', 'is_included', 'scope', 'nullifier', 'attested_at'],
116
121
  },
117
122
  oidc_domain_attestation: {
118
123
  name: 'OIDC Domain',
@@ -641,11 +646,9 @@ function validateProofRequest(request) {
641
646
  if (inputs.userAddress && !/^0x[a-fA-F0-9]{40}$/.test(inputs.userAddress)) {
642
647
  return { valid: false, error: 'Invalid userAddress format' };
643
648
  }
644
- if (!inputs.countryList || !Array.isArray(inputs.countryList) || inputs.countryList.length === 0) {
645
- return { valid: false, error: 'countryList is required and must be a non-empty array' };
646
- }
647
- if (!inputs.countryList.every(c => typeof c === 'string')) {
648
- return { valid: false, error: 'countryList must contain only strings' };
649
+ const listError = core.countryListError(inputs.countryList);
650
+ if (listError) {
651
+ return { valid: false, error: listError };
649
652
  }
650
653
  if (typeof inputs.isIncluded !== 'boolean') {
651
654
  return { valid: false, error: 'isIncluded is required and must be a boolean' };
@@ -1072,7 +1075,7 @@ async function verifyProofOffChain(circuit, proof, publicInputs, options = {}) {
1072
1075
  // their trust anchor as a public input the prover chooses. For OIDC that is
1073
1076
  // the RSA key -- see core's oidcProviderKeys. Checked here so a dApp that
1074
1077
  // verifies off-chain gets the same answer as one that verifies on-chain.
1075
- const issuer = await core.checkProofIssuer(circuit, publicInputs, { request: options.request });
1078
+ const issuer = await core.checkProofIssuer(circuit, publicInputs, { request: options.request, countryCondition: options.countryCondition });
1076
1079
  if (!issuer.ok) {
1077
1080
  return { valid: false, error: issuer.error };
1078
1081
  }
@@ -1232,6 +1235,12 @@ class ProofportSDK {
1232
1235
  * {@link ProofportSDK.MAX_REMEMBERED_RELAY_REQUESTS}.
1233
1236
  */
1234
1237
  this.relayRequests = new Map();
1238
+ /**
1239
+ * requestId -> the country list and direction of the Coinbase Country relay
1240
+ * requests this instance created, so verifyResponse* can hold the proof to
1241
+ * them. Bounded like {@link relayRequests}.
1242
+ */
1243
+ this.countryRequests = new Map();
1235
1244
  this.signer = null;
1236
1245
  this.socket = null;
1237
1246
  /**
@@ -1363,6 +1372,15 @@ class ProofportSDK {
1363
1372
  if (typeof inputs.isIncluded !== 'boolean') {
1364
1373
  throw new Error('isIncluded is required for coinbase_country_attestation circuit');
1365
1374
  }
1375
+ const listError = core.countryListError(inputs.countryList);
1376
+ if (listError)
1377
+ throw new Error(`${listError} (circuit: coinbase_country_attestation)`);
1378
+ // Coinbase Country v2 binds an action or none, like Coinbase KYC v2.
1379
+ if (inputs.action !== undefined) {
1380
+ const actionError = typedAction.validateTypedAction(inputs.action);
1381
+ if (actionError)
1382
+ throw new Error(`${actionError} (circuit: coinbase_country_attestation)`);
1383
+ }
1366
1384
  const request = {
1367
1385
  requestId: generateRequestId(),
1368
1386
  circuit: 'coinbase_country_attestation',
@@ -1763,6 +1781,7 @@ class ProofportSDK {
1763
1781
  return core.verifyProofOnChain(circuit, parsedProof, providerOrSigner, this.verifierTarget(circuit), undefined, {
1764
1782
  request: options.request,
1765
1783
  signerVariant: options.signerVariant,
1784
+ countryCondition: options.countryCondition,
1766
1785
  });
1767
1786
  }
1768
1787
  /**
@@ -1803,6 +1822,43 @@ class ProofportSDK {
1803
1822
  this.relayRequests.delete(oldest);
1804
1823
  }
1805
1824
  }
1825
+ /**
1826
+ * The country list and direction a `coinbase_country_attestation` response
1827
+ * must have been proved against: the one the caller passed, else the relay
1828
+ * request this instance created under the response's requestId, else the
1829
+ * deep-link request still pending under it. `{ error }` when there is none:
1830
+ * the proof's list is the prover's choice, so a response this SDK cannot
1831
+ * place is refused rather than accepted for whatever list it carries.
1832
+ */
1833
+ countryConditionFor(response, explicit) {
1834
+ if (explicit !== undefined)
1835
+ return { condition: explicit };
1836
+ const remembered = this.countryRequests.get(response.requestId);
1837
+ if (remembered)
1838
+ return { condition: remembered };
1839
+ const pending = this.pendingRequests.get(response.requestId);
1840
+ const inputs = pending?.circuit === 'coinbase_country_attestation' ? pending.inputs : undefined;
1841
+ if (inputs && Array.isArray(inputs.countryList) && typeof inputs.isIncluded === 'boolean') {
1842
+ return { condition: { countryList: [...inputs.countryList], isIncluded: inputs.isIncluded } };
1843
+ }
1844
+ return {
1845
+ error: `No Coinbase Country request ${JSON.stringify(response.requestId)} was created by this SDK instance, so the ` +
1846
+ "proof's country list cannot be checked against what was asked. Pass { countryCondition: { countryList, " +
1847
+ 'isIncluded } } -- the list and direction of the request you created.',
1848
+ };
1849
+ }
1850
+ /** @internal Remember a Coinbase Country relay request's list and direction for verifyResponse*. */
1851
+ rememberCountryRequest(requestId, inputs) {
1852
+ const i = inputs;
1853
+ if (typeof requestId !== 'string' || !i || !Array.isArray(i.countryList) || typeof i.isIncluded !== 'boolean')
1854
+ return;
1855
+ this.countryRequests.delete(requestId);
1856
+ this.countryRequests.set(requestId, { countryList: [...i.countryList], isIncluded: i.isIncluded });
1857
+ while (this.countryRequests.size > ProofportSDK.MAX_REMEMBERED_RELAY_REQUESTS) {
1858
+ const oldest = this.countryRequests.keys().next().value;
1859
+ this.countryRequests.delete(oldest);
1860
+ }
1861
+ }
1806
1862
  /**
1807
1863
  * The verifier for a circuit: the one the dApp configured, otherwise the
1808
1864
  * pinned deployment on the chain this SDK's environment verifies that
@@ -1831,6 +1887,10 @@ class ProofportSDK {
1831
1887
  * @param options.request - For `oidc_domain_attestation`: the `{ scope, requestId }`
1832
1888
  * the response answers. Not needed for a relay request this SDK instance
1833
1889
  * created; an OIDC response this instance cannot place is refused without it.
1890
+ * @param options.countryCondition - For `coinbase_country_attestation`: the
1891
+ * `{ countryList, isIncluded }` the request asked for. Not needed for a
1892
+ * request this SDK instance created (relay or deep link); a Country response
1893
+ * this instance cannot place is refused without it.
1834
1894
  *
1835
1895
  * @returns Promise resolving to verification result with valid boolean and optional error message
1836
1896
  *
@@ -1868,6 +1928,13 @@ class ProofportSDK {
1868
1928
  return { valid: false, error: found.error };
1869
1929
  request = found.request;
1870
1930
  }
1931
+ let countryCondition;
1932
+ if (response.circuit === 'coinbase_country_attestation') {
1933
+ const found = this.countryConditionFor(response, options.countryCondition);
1934
+ if ('error' in found)
1935
+ return { valid: false, error: found.error };
1936
+ countryCondition = found.condition;
1937
+ }
1871
1938
  const parsedProof = core.parseProofForOnChain(response.proof, response.publicInputs, response.publicInputs.length);
1872
1939
  // The response's verifierAddress and chainId are a claim to check, never
1873
1940
  // the contract to call: whoever wrote the response would otherwise pick
@@ -1876,7 +1943,7 @@ class ProofportSDK {
1876
1943
  return core.verifyProofOnChain(response.circuit, parsedProof, providerOrSigner, this.verifierTarget(response.circuit), {
1877
1944
  verifierAddress: response.verifierAddress,
1878
1945
  chainId: response.chainId,
1879
- }, { request, signerVariant: response.signerVariant });
1946
+ }, { request, signerVariant: response.signerVariant, countryCondition });
1880
1947
  }
1881
1948
  /**
1882
1949
  * Verifies a proof response off-chain, against the circuit's verification key.
@@ -1913,7 +1980,14 @@ class ProofportSDK {
1913
1980
  return { valid: false, error: found.error };
1914
1981
  request = found.request;
1915
1982
  }
1916
- return verifyProofOffChain(response.circuit, response.proof, response.publicInputs, { ...options, request, signerVariant: response.signerVariant });
1983
+ let countryCondition;
1984
+ if (response.circuit === 'coinbase_country_attestation') {
1985
+ const found = this.countryConditionFor(response, options?.countryCondition);
1986
+ if ('error' in found)
1987
+ return { valid: false, error: found.error };
1988
+ countryCondition = found.condition;
1989
+ }
1990
+ return verifyProofOffChain(response.circuit, response.proof, response.publicInputs, { ...options, request, countryCondition, signerVariant: response.signerVariant });
1917
1991
  }
1918
1992
  // ============ Utility Methods ============
1919
1993
  /**
@@ -2291,6 +2365,16 @@ class ProofportSDK {
2291
2365
  }
2292
2366
  }
2293
2367
  }
2368
+ // A Coinbase Country list the circuit would refuse is refused here, before a
2369
+ // challenge is burned, as createProofRequest does.
2370
+ if (circuit === 'coinbase_country_attestation') {
2371
+ const c = inputs;
2372
+ const listError = core.countryListError(c.countryList);
2373
+ if (listError)
2374
+ throw new Error(`${listError} (circuit: ${circuit})`);
2375
+ if (typeof c.isIncluded !== 'boolean')
2376
+ throw new Error('isIncluded is required for coinbase_country_attestation circuit');
2377
+ }
2294
2378
  // Return target: validated before we burn a challenge, so a typo surfaces
2295
2379
  // as a local error instead of a relay 400 on the second round trip.
2296
2380
  if (options.returnScheme !== undefined) {
@@ -2346,6 +2430,8 @@ class ProofportSDK {
2346
2430
  }
2347
2431
  const created = await response.json();
2348
2432
  this.rememberRelayRequest(created.requestId, scope);
2433
+ if (circuit === 'coinbase_country_attestation')
2434
+ this.rememberCountryRequest(created.requestId, inputs);
2349
2435
  return created;
2350
2436
  }
2351
2437
  /**
@@ -2648,6 +2734,10 @@ Object.defineProperty(exports, "COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT", {
2648
2734
  enumerable: true,
2649
2735
  get: function () { return core.COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT; }
2650
2736
  });
2737
+ Object.defineProperty(exports, "COINBASE_COUNTRY_PUBLIC_INPUT_COUNT", {
2738
+ enumerable: true,
2739
+ get: function () { return core.COINBASE_COUNTRY_PUBLIC_INPUT_COUNT; }
2740
+ });
2651
2741
  Object.defineProperty(exports, "COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT", {
2652
2742
  enumerable: true,
2653
2743
  get: function () { return core.COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT; }