@zkproofport-app/sdk 0.5.0-beta.5 → 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/README.md CHANGED
@@ -190,13 +190,14 @@ one wallet in one scope has exactly one nullifier.
190
190
 
191
191
  ### `coinbase_country_attestation`
192
192
 
193
- Proves a user's country based on Coinbase verification, supporting inclusion and exclusion checks, without revealing the actual country. Requires a wallet signer.
193
+ Proves a user's country based on Coinbase verification, supporting inclusion and exclusion checks, without revealing the actual country. Requires a wallet signer. Like Coinbase KYC, it can authorize one EIP-712 action, and the attestation is confirmed unrevoked — for the very country being proved — right before proving (`attested_at`, checked against your clock).
194
194
 
195
195
  | Field | Type | Required | Description |
196
196
  |-------|------|----------|-------------|
197
197
  | `scope` | `string` | Yes | Application-specific identifier |
198
- | `countryList` | `string[]` | Yes | ISO 3166-1 alpha-2 country codes (e.g., `['US', 'KR']`) |
198
+ | `countryList` | `string[]` | Yes | 1 to 10 upper-case ISO 3166-1 alpha-2 country codes (e.g., `['US', 'KR']`); anything else is refused when the request is created |
199
199
  | `isIncluded` | `boolean` | Yes | `true` = prove user IS from listed countries; `false` = prove user is NOT |
200
+ | `action` | `TypedAction` | No | An EIP-712 action the wallet signs instead of the request's signal hash |
200
201
  | `userAddress` | `string` | No | Address to prove the attestation for (optional, as above) |
201
202
  | `rawTransaction` | `string` | No | Pre-fetched attestation transaction data (optional, as above) |
202
203
 
@@ -208,6 +209,24 @@ const relay = await sdk.createRelayRequest('coinbase_country_attestation', {
208
209
  });
209
210
  ```
210
211
 
212
+ Its nullifier is `keccak(keccak(wallet ++ keccak("coinbase_country_attestation")) ++ scope)`.
213
+
214
+ The list and the direction are public inputs the prover chooses; the wallet's
215
+ signature does not cover them. `verifyResponseOnChain` and
216
+ `verifyResponseOffChain` therefore compare them with the request this SDK
217
+ instance created (relay or deep link) and refuse a proof made against any other
218
+ list. A response the instance cannot place is refused; pass the request's
219
+ condition explicitly in that case:
220
+
221
+ ```typescript
222
+ await sdk.verifyResponseOnChain(response, provider, {
223
+ countryCondition: { countryList: ['US', 'KR'], isIncluded: true },
224
+ });
225
+ ```
226
+
227
+ `checkCountryListMatches(publicInputs, { countryList, isIncluded })` is the same
228
+ check on its own (codes compared as a set, order does not matter).
229
+
211
230
  ### `oidc_domain_attestation`
212
231
 
213
232
  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.
@@ -333,15 +352,16 @@ circuit names are special:
333
352
 
334
353
  | What the circuit does with an action | Circuits |
335
354
  |---|---|
336
- | Signs it when present, signs the signal hash when not | `coinbase_attestation`, `giwa_attestation`, `dojang_playground` |
337
- | Has no action inputs — sending one is refused, not ignored | Coinbase country, OIDC domain |
355
+ | Signs it when present, signs the signal hash when not | `coinbase_attestation`, `coinbase_country_attestation`, `giwa_attestation`, `dojang_playground` |
356
+ | Has no action inputs — sending one is refused, not ignored | OIDC domain |
338
357
 
339
358
  ```typescript
340
359
  import { circuitActionBinding } from '@zkproofport-app/sdk';
341
360
 
342
361
  circuitActionBinding('giwa_attestation'); // 'optional'
343
362
  circuitActionBinding('coinbase_attestation'); // 'optional'
344
- circuitActionBinding('coinbase_country_attestation'); // 'none'
363
+ circuitActionBinding('coinbase_country_attestation'); // 'optional'
364
+ circuitActionBinding('oidc_domain_attestation'); // 'none'
345
365
  ```
346
366
 
347
367
  Sending an action to a circuit that cannot prove one is an error rather than a
@@ -1016,7 +1036,7 @@ import type {
1016
1036
  | `CircuitType` | Alias of `CircuitId`. Kept as the name used throughout `ProofRequest`, `ProofResponse` and the SDK methods |
1017
1037
  | `ProofRequestStatus` | `'pending' \| 'completed' \| 'error' \| 'cancelled'` — the status on a `ProofResponse` |
1018
1038
  | `CoinbaseKycInputs` | Inputs for `coinbase_attestation`: `{ scope, userAddress?, rawTransaction? }` |
1019
- | `CoinbaseCountryInputs` | Inputs for `coinbase_country_attestation`: `{ scope, countryList, isIncluded, userAddress?, rawTransaction? }` |
1039
+ | `CoinbaseCountryInputs` | Inputs for `coinbase_country_attestation`: `{ scope, countryList, isIncluded, action?, userAddress?, rawTransaction? }` |
1020
1040
  | `OidcDomainInputs` | Inputs for `oidc_domain_attestation`: `{ domain, scope, provider? }` |
1021
1041
  | `ActionBoundInputs` | Inputs for the circuits that can bind an EIP-712 action: `{ scope, action?, userAddress?, rawTransaction? }`. Whether the action is required is the circuit's property, in `CIRCUIT_ACTION_BINDING` — optional for `giwa_attestation` and `dojang_playground` |
1022
1042
  | `GiwaAttestationInputs` | Alias of `ActionBoundInputs`, for `giwa_attestation` |
@@ -1084,19 +1104,22 @@ const signalHash = toBytes32(publicInputs.slice(L.SIGNAL_HASH_START, L.SIGNAL_HA
1084
1104
  | 160–191 | `NULLIFIER_START` / `_END` | `nullifier` | `bytes32`, unique per wallet and scope |
1085
1105
  | 192 | `ATTESTED_AT` | `attested_at` | Unix seconds at which the attestation was confirmed unrevoked; checked against your clock by every verify method |
1086
1106
 
1087
- ### `coinbase_country_attestation` — 150 entries
1107
+ ### `coinbase_country_attestation` — 215 entries
1088
1108
 
1089
1109
  | Index | Constants | Circuit field | What it is |
1090
1110
  |-------|-----------|---------------|------------|
1091
- | 0–31 | `SIGNAL_HASH_START` / `_END` | `signal_hash` | `bytes32` anti-replay challenge |
1092
- | 32–63 | `MERKLE_ROOT_START` / `_END` | `signer_list_merkle_root` | `bytes32` signer-list root |
1093
- | 64–83 | `COUNTRY_LIST_START` / `_END` | `country_list` | 10 slots of 2 ASCII bytes, one byte per entry — entries 64–65 are the first code, 66–67 the second, and so on, in the order you sent them. Only the first `country_list_length` slots are meaningful; the rest are zero |
1094
- | 84 | `COUNTRY_LIST_LENGTH` | `country_list_length` | How many of those 10 slots count |
1095
- | 85 | `IS_INCLUDED` | `is_included` | `1` = the user **is** in the list, `0` = the user is **not** |
1096
- | 86–117 | `SCOPE_START` / `_END` | `scope` | `bytes32` scope hash |
1097
- | 118–149 | `NULLIFIER_START` / `_END` | `nullifier` | `bytes32`, unique per user and scope |
1098
-
1099
- The country list and the `is_included` flag are public inputs, which means the proof is only as strong as the list it was proved against. Read indices 64–85 back and confirm they are the list and the direction you asked for; a valid proof against a different list is still a valid proof.
1111
+ | 0–31 | `SIGNAL_HASH_START` / `_END` | `signal_hash` | `bytes32` anti-replay challenge; zero when the wallet signed an EIP-712 action |
1112
+ | 32–63 | `DOMAIN_SEPARATOR_START` / `_END` | `domain_separator` | EIP-712 domain separator of the signed action; zero without one |
1113
+ | 64–95 | `ACTION_HASH_START` / `_END` | `action_hash` | EIP-712 struct hash of the signed action; zero without one |
1114
+ | 96–127 | `MERKLE_ROOT_START` / `_END` | `signer_list_merkle_root` | `bytes32` signer-list root |
1115
+ | 128–147 | `COUNTRY_LIST_START` / `_END` | `country_list` | 10 slots of 2 ASCII bytes, one byte per entry — entries 128–129 are the first code, 130–131 the second, and so on, in the order you sent them. Only the first `country_list_length` slots are meaningful; the rest are zero |
1116
+ | 148 | `COUNTRY_LIST_LENGTH` | `country_list_length` | How many of those 10 slots count |
1117
+ | 149 | `IS_INCLUDED` | `is_included` | `1` = the user **is** in the list, `0` = the user is **not** |
1118
+ | 150–181 | `SCOPE_START` / `_END` | `scope` | `bytes32` scope hash |
1119
+ | 182–213 | `NULLIFIER_START` / `_END` | `nullifier` | `bytes32`, unique per wallet and scope |
1120
+ | 214 | `ATTESTED_AT` | `attested_at` | Unix seconds at which the attestation was confirmed unrevoked; checked against your clock by every verify method |
1121
+
1122
+ The country list and the `is_included` flag are public inputs, which means the proof is only as strong as the list it was proved against. The `verifyResponse*` methods compare them with your request (above); if you verify through another path, call `checkCountryListMatches` or read indices 128–149 back yourself — a valid proof against a different list is still a valid proof.
1100
1123
 
1101
1124
  ### `oidc_domain_attestation` — 214 entries
1102
1125
 
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { ProofRequest, ProofResponse, CircuitType, CircuitInputs, ProofportConfig, QRCodeOptions, ChallengeResponse, WalletSigner, RelayProofRequest, RelayProofResult } from './types';
5
5
  import { type OffChainVerifyOptions } from './offChainVerifier';
6
- import { type OidcRequestContext } from './verifier';
6
+ import { type CountryCondition, type OidcRequestContext } from './verifier';
7
7
  import type { SDKEnvironment } from './types';
8
8
  /**
9
9
  * Main SDK class for interacting with the ZKProofport mobile app.
@@ -51,6 +51,12 @@ export declare class ProofportSDK {
51
51
  private relayRequests;
52
52
  /** Bound on {@link relayRequests}; a dApp that verifies later can pass the request explicitly. */
53
53
  static readonly MAX_REMEMBERED_RELAY_REQUESTS = 1000;
54
+ /**
55
+ * requestId -> the country list and direction of the Coinbase Country relay
56
+ * requests this instance created, so verifyResponse* can hold the proof to
57
+ * them. Bounded like {@link relayRequests}.
58
+ */
59
+ private countryRequests;
54
60
  private signer;
55
61
  private relayUrl;
56
62
  private socket;
@@ -494,6 +500,7 @@ export declare class ProofportSDK {
494
500
  verifyOnChain(circuit: CircuitType, proof: string, publicInputs: string[], providerOrSigner?: any, options?: {
495
501
  request?: OidcRequestContext;
496
502
  signerVariant?: string;
503
+ countryCondition?: CountryCondition;
497
504
  }): Promise<{
498
505
  valid: boolean;
499
506
  error?: string;
@@ -509,6 +516,17 @@ export declare class ProofportSDK {
509
516
  private oidcRequestFor;
510
517
  /** @internal Remember a relay request's scope for verifyResponse*. */
511
518
  private rememberRelayRequest;
519
+ /**
520
+ * The country list and direction a `coinbase_country_attestation` response
521
+ * must have been proved against: the one the caller passed, else the relay
522
+ * request this instance created under the response's requestId, else the
523
+ * deep-link request still pending under it. `{ error }` when there is none:
524
+ * the proof's list is the prover's choice, so a response this SDK cannot
525
+ * place is refused rather than accepted for whatever list it carries.
526
+ */
527
+ private countryConditionFor;
528
+ /** @internal Remember a Coinbase Country relay request's list and direction for verifyResponse*. */
529
+ private rememberCountryRequest;
512
530
  /**
513
531
  * The verifier for a circuit: the one the dApp configured, otherwise the
514
532
  * pinned deployment on the chain this SDK's environment verifies that
@@ -527,6 +545,10 @@ export declare class ProofportSDK {
527
545
  * @param options.request - For `oidc_domain_attestation`: the `{ scope, requestId }`
528
546
  * the response answers. Not needed for a relay request this SDK instance
529
547
  * created; an OIDC response this instance cannot place is refused without it.
548
+ * @param options.countryCondition - For `coinbase_country_attestation`: the
549
+ * `{ countryList, isIncluded }` the request asked for. Not needed for a
550
+ * request this SDK instance created (relay or deep link); a Country response
551
+ * this instance cannot place is refused without it.
530
552
  *
531
553
  * @returns Promise resolving to verification result with valid boolean and optional error message
532
554
  *
@@ -555,6 +577,7 @@ export declare class ProofportSDK {
555
577
  */
556
578
  verifyResponseOnChain(response: ProofResponse, providerOrSigner?: any, options?: {
557
579
  request?: OidcRequestContext;
580
+ countryCondition?: CountryCondition;
558
581
  }): Promise<{
559
582
  valid: boolean;
560
583
  error?: string;
@@ -123,4 +123,4 @@ export declare const DEFAULT_REQUEST_EXPIRY_MS: number;
123
123
  * ```
124
124
  */
125
125
  export declare const MAX_QR_DATA_SIZE = 2953;
126
- export { VERIFIER_ABI, RPC_ENDPOINTS, COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, } from '@zkproofport-app/core';
126
+ export { VERIFIER_ABI, RPC_ENDPOINTS, COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_COUNT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, } from '@zkproofport-app/core';
package/dist/index.d.ts CHANGED
@@ -34,7 +34,7 @@ export { PINNED_VERIFIERS, PINNED_VARIANT_VERIFIERS, getPinnedVerifier, getPinne
34
34
  export type { PinnedVerifier } from './verifier';
35
35
  export { checkOidcPolicy, deriveOidcNonceBinding, oidcAudienceHash, OIDC_ALLOWED_AUDIENCES, OIDC_NONCE_DOMAIN, OIDC_NOW_MAX_AGE_SECONDS, OIDC_NOW_MAX_FUTURE_SECONDS, OIDC_V1_PUBLIC_INPUT_COUNT, OIDC_V2_PUBLIC_INPUT_COUNT, } from './verifier';
36
36
  export type { OidcRequestContext, OidcAudience, OidcNonceMode } from './verifier';
37
- export { COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, } from './constants';
37
+ export { COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_COUNT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, } from './constants';
38
38
  export type { CircuitId, CircuitSupportStatus, SignerVariant, CircuitPackage } from './circuits';
39
39
  export { CIRCUIT_ACTION_BINDING, circuitActionBinding, } from './circuits';
40
40
  export type { CircuitActionBinding } from './circuits';
package/dist/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
- import { checkProofIssuer, parseProofForOnChain, verifyProofOnChain, getPinnedVerifier, extractScopeFromPublicInputs, extractNullifierFromPublicInputs, extractDomainFromPublicInputs } from '@zkproofport-app/core';
2
- export { COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, OIDC_ALLOWED_AUDIENCES, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, OIDC_NONCE_DOMAIN, OIDC_NOW_MAX_AGE_SECONDS, OIDC_NOW_MAX_FUTURE_SECONDS, OIDC_V1_PUBLIC_INPUT_COUNT, OIDC_V2_PUBLIC_INPUT_COUNT, PINNED_VARIANT_VERIFIERS, PINNED_VERIFIERS, checkOidcPolicy, deriveOidcNonceBinding, extractDomainFromPublicInputs, extractNullifierFromPublicInputs, extractScopeFromPublicInputs, getPinnedVariantVerifier, getPinnedVerifier, oidcAudienceHash } from '@zkproofport-app/core';
1
+ import { countryListError, checkProofIssuer, parseProofForOnChain, verifyProofOnChain, getPinnedVerifier, extractScopeFromPublicInputs, extractNullifierFromPublicInputs, extractDomainFromPublicInputs } from '@zkproofport-app/core';
2
+ export { COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_COUNT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, OIDC_ALLOWED_AUDIENCES, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, OIDC_NONCE_DOMAIN, OIDC_NOW_MAX_AGE_SECONDS, OIDC_NOW_MAX_FUTURE_SECONDS, OIDC_V1_PUBLIC_INPUT_COUNT, OIDC_V2_PUBLIC_INPUT_COUNT, PINNED_VARIANT_VERIFIERS, PINNED_VERIFIERS, checkOidcPolicy, deriveOidcNonceBinding, extractDomainFromPublicInputs, extractNullifierFromPublicInputs, extractScopeFromPublicInputs, getPinnedVariantVerifier, getPinnedVerifier, oidcAudienceHash } from '@zkproofport-app/core';
3
3
  import { isCircuitId, ALL_CIRCUIT_IDS, circuitActionBinding, resolveSignerVariant, getCircuitPackage, CIRCUIT_NEEDS_WALLET_SIGNATURE } from '@zkproofport-app/core/circuits';
4
4
  export { ALL_CIRCUIT_IDS, CIRCUIT_ACTION_BINDING, CIRCUIT_IDS, CIRCUIT_PACKAGES, CIRCUIT_SUPPORT_STATUS, CIRCUIT_VK_PATHS, EXPERIMENTAL_CIRCUIT_IDS, PLANNED_CIRCUIT_IDS, SIGNER_VARIANTS, SUPPORTED_CIRCUIT_IDS, circuitActionBinding, evmProofByteLength, getCircuitPackage, getCircuitSupportStatus, isCircuitId, isSignerVariant, isSupportedCircuitId, resolveSignerVariant, signerVariantsOf } from '@zkproofport-app/core/circuits';
5
5
  import { validateTypedAction } from '@zkproofport-app/core/typedAction';
@@ -107,11 +107,15 @@ const CIRCUIT_METADATA = {
107
107
  },
108
108
  coinbase_country_attestation: {
109
109
  name: 'Coinbase Country',
110
- description: 'Prove Coinbase country verification',
111
- // 32 + 32 + (10 x 2) + 1 + 1 + 32 + 32 = 150, counted off
112
- // coinbase-country-attestation/src/main.nr. Read 14 until 2026-09-04.
113
- publicInputsCount: 150,
114
- publicInputNames: ['signal_hash', 'signer_list_merkle_root', 'country_list', 'country_list_length', 'is_included'],
110
+ description: 'Prove Coinbase country verification, optionally authorizing one EIP-712 action',
111
+ // Coinbase Country v2 (coinbase-country-attestation-v2/src/main.nr):
112
+ // signal_hash 0-31, domain_separator 32-63, action_hash 64-95,
113
+ // signer_list_merkle_root 96-127, country_list 128-147 (10 x 2 bytes),
114
+ // country_list_length 148, is_included 149, scope 150-181,
115
+ // nullifier 182-213, attested_at 214 = 215 fields. v1's 150 fields are
116
+ // refused on every verify path.
117
+ publicInputsCount: 215,
118
+ publicInputNames: ['signal_hash', 'domain_separator', 'action_hash', 'signer_list_merkle_root', 'country_list', 'country_list_length', 'is_included', 'scope', 'nullifier', 'attested_at'],
115
119
  },
116
120
  oidc_domain_attestation: {
117
121
  name: 'OIDC Domain',
@@ -640,11 +644,9 @@ function validateProofRequest(request) {
640
644
  if (inputs.userAddress && !/^0x[a-fA-F0-9]{40}$/.test(inputs.userAddress)) {
641
645
  return { valid: false, error: 'Invalid userAddress format' };
642
646
  }
643
- if (!inputs.countryList || !Array.isArray(inputs.countryList) || inputs.countryList.length === 0) {
644
- return { valid: false, error: 'countryList is required and must be a non-empty array' };
645
- }
646
- if (!inputs.countryList.every(c => typeof c === 'string')) {
647
- return { valid: false, error: 'countryList must contain only strings' };
647
+ const listError = countryListError(inputs.countryList);
648
+ if (listError) {
649
+ return { valid: false, error: listError };
648
650
  }
649
651
  if (typeof inputs.isIncluded !== 'boolean') {
650
652
  return { valid: false, error: 'isIncluded is required and must be a boolean' };
@@ -3938,7 +3940,7 @@ async function verifyProofOffChain(circuit, proof, publicInputs, options = {}) {
3938
3940
  // their trust anchor as a public input the prover chooses. For OIDC that is
3939
3941
  // the RSA key -- see core's oidcProviderKeys. Checked here so a dApp that
3940
3942
  // verifies off-chain gets the same answer as one that verifies on-chain.
3941
- const issuer = await checkProofIssuer(circuit, publicInputs, { request: options.request });
3943
+ const issuer = await checkProofIssuer(circuit, publicInputs, { request: options.request, countryCondition: options.countryCondition });
3942
3944
  if (!issuer.ok) {
3943
3945
  return { valid: false, error: issuer.error };
3944
3946
  }
@@ -4098,6 +4100,12 @@ class ProofportSDK {
4098
4100
  * {@link ProofportSDK.MAX_REMEMBERED_RELAY_REQUESTS}.
4099
4101
  */
4100
4102
  this.relayRequests = new Map();
4103
+ /**
4104
+ * requestId -> the country list and direction of the Coinbase Country relay
4105
+ * requests this instance created, so verifyResponse* can hold the proof to
4106
+ * them. Bounded like {@link relayRequests}.
4107
+ */
4108
+ this.countryRequests = new Map();
4101
4109
  this.signer = null;
4102
4110
  this.socket = null;
4103
4111
  /**
@@ -4229,6 +4237,15 @@ class ProofportSDK {
4229
4237
  if (typeof inputs.isIncluded !== 'boolean') {
4230
4238
  throw new Error('isIncluded is required for coinbase_country_attestation circuit');
4231
4239
  }
4240
+ const listError = countryListError(inputs.countryList);
4241
+ if (listError)
4242
+ throw new Error(`${listError} (circuit: coinbase_country_attestation)`);
4243
+ // Coinbase Country v2 binds an action or none, like Coinbase KYC v2.
4244
+ if (inputs.action !== undefined) {
4245
+ const actionError = validateTypedAction(inputs.action);
4246
+ if (actionError)
4247
+ throw new Error(`${actionError} (circuit: coinbase_country_attestation)`);
4248
+ }
4232
4249
  const request = {
4233
4250
  requestId: generateRequestId(),
4234
4251
  circuit: 'coinbase_country_attestation',
@@ -4629,6 +4646,7 @@ class ProofportSDK {
4629
4646
  return verifyProofOnChain(circuit, parsedProof, providerOrSigner, this.verifierTarget(circuit), undefined, {
4630
4647
  request: options.request,
4631
4648
  signerVariant: options.signerVariant,
4649
+ countryCondition: options.countryCondition,
4632
4650
  });
4633
4651
  }
4634
4652
  /**
@@ -4669,6 +4687,43 @@ class ProofportSDK {
4669
4687
  this.relayRequests.delete(oldest);
4670
4688
  }
4671
4689
  }
4690
+ /**
4691
+ * The country list and direction a `coinbase_country_attestation` response
4692
+ * must have been proved against: the one the caller passed, else the relay
4693
+ * request this instance created under the response's requestId, else the
4694
+ * deep-link request still pending under it. `{ error }` when there is none:
4695
+ * the proof's list is the prover's choice, so a response this SDK cannot
4696
+ * place is refused rather than accepted for whatever list it carries.
4697
+ */
4698
+ countryConditionFor(response, explicit) {
4699
+ if (explicit !== undefined)
4700
+ return { condition: explicit };
4701
+ const remembered = this.countryRequests.get(response.requestId);
4702
+ if (remembered)
4703
+ return { condition: remembered };
4704
+ const pending = this.pendingRequests.get(response.requestId);
4705
+ const inputs = pending?.circuit === 'coinbase_country_attestation' ? pending.inputs : undefined;
4706
+ if (inputs && Array.isArray(inputs.countryList) && typeof inputs.isIncluded === 'boolean') {
4707
+ return { condition: { countryList: [...inputs.countryList], isIncluded: inputs.isIncluded } };
4708
+ }
4709
+ return {
4710
+ error: `No Coinbase Country request ${JSON.stringify(response.requestId)} was created by this SDK instance, so the ` +
4711
+ "proof's country list cannot be checked against what was asked. Pass { countryCondition: { countryList, " +
4712
+ 'isIncluded } } -- the list and direction of the request you created.',
4713
+ };
4714
+ }
4715
+ /** @internal Remember a Coinbase Country relay request's list and direction for verifyResponse*. */
4716
+ rememberCountryRequest(requestId, inputs) {
4717
+ const i = inputs;
4718
+ if (typeof requestId !== 'string' || !i || !Array.isArray(i.countryList) || typeof i.isIncluded !== 'boolean')
4719
+ return;
4720
+ this.countryRequests.delete(requestId);
4721
+ this.countryRequests.set(requestId, { countryList: [...i.countryList], isIncluded: i.isIncluded });
4722
+ while (this.countryRequests.size > ProofportSDK.MAX_REMEMBERED_RELAY_REQUESTS) {
4723
+ const oldest = this.countryRequests.keys().next().value;
4724
+ this.countryRequests.delete(oldest);
4725
+ }
4726
+ }
4672
4727
  /**
4673
4728
  * The verifier for a circuit: the one the dApp configured, otherwise the
4674
4729
  * pinned deployment on the chain this SDK's environment verifies that
@@ -4697,6 +4752,10 @@ class ProofportSDK {
4697
4752
  * @param options.request - For `oidc_domain_attestation`: the `{ scope, requestId }`
4698
4753
  * the response answers. Not needed for a relay request this SDK instance
4699
4754
  * created; an OIDC response this instance cannot place is refused without it.
4755
+ * @param options.countryCondition - For `coinbase_country_attestation`: the
4756
+ * `{ countryList, isIncluded }` the request asked for. Not needed for a
4757
+ * request this SDK instance created (relay or deep link); a Country response
4758
+ * this instance cannot place is refused without it.
4700
4759
  *
4701
4760
  * @returns Promise resolving to verification result with valid boolean and optional error message
4702
4761
  *
@@ -4734,6 +4793,13 @@ class ProofportSDK {
4734
4793
  return { valid: false, error: found.error };
4735
4794
  request = found.request;
4736
4795
  }
4796
+ let countryCondition;
4797
+ if (response.circuit === 'coinbase_country_attestation') {
4798
+ const found = this.countryConditionFor(response, options.countryCondition);
4799
+ if ('error' in found)
4800
+ return { valid: false, error: found.error };
4801
+ countryCondition = found.condition;
4802
+ }
4737
4803
  const parsedProof = parseProofForOnChain(response.proof, response.publicInputs, response.publicInputs.length);
4738
4804
  // The response's verifierAddress and chainId are a claim to check, never
4739
4805
  // the contract to call: whoever wrote the response would otherwise pick
@@ -4742,7 +4808,7 @@ class ProofportSDK {
4742
4808
  return verifyProofOnChain(response.circuit, parsedProof, providerOrSigner, this.verifierTarget(response.circuit), {
4743
4809
  verifierAddress: response.verifierAddress,
4744
4810
  chainId: response.chainId,
4745
- }, { request, signerVariant: response.signerVariant });
4811
+ }, { request, signerVariant: response.signerVariant, countryCondition });
4746
4812
  }
4747
4813
  /**
4748
4814
  * Verifies a proof response off-chain, against the circuit's verification key.
@@ -4779,7 +4845,14 @@ class ProofportSDK {
4779
4845
  return { valid: false, error: found.error };
4780
4846
  request = found.request;
4781
4847
  }
4782
- return verifyProofOffChain(response.circuit, response.proof, response.publicInputs, { ...options, request, signerVariant: response.signerVariant });
4848
+ let countryCondition;
4849
+ if (response.circuit === 'coinbase_country_attestation') {
4850
+ const found = this.countryConditionFor(response, options?.countryCondition);
4851
+ if ('error' in found)
4852
+ return { valid: false, error: found.error };
4853
+ countryCondition = found.condition;
4854
+ }
4855
+ return verifyProofOffChain(response.circuit, response.proof, response.publicInputs, { ...options, request, countryCondition, signerVariant: response.signerVariant });
4783
4856
  }
4784
4857
  // ============ Utility Methods ============
4785
4858
  /**
@@ -5157,6 +5230,16 @@ class ProofportSDK {
5157
5230
  }
5158
5231
  }
5159
5232
  }
5233
+ // A Coinbase Country list the circuit would refuse is refused here, before a
5234
+ // challenge is burned, as createProofRequest does.
5235
+ if (circuit === 'coinbase_country_attestation') {
5236
+ const c = inputs;
5237
+ const listError = countryListError(c.countryList);
5238
+ if (listError)
5239
+ throw new Error(`${listError} (circuit: ${circuit})`);
5240
+ if (typeof c.isIncluded !== 'boolean')
5241
+ throw new Error('isIncluded is required for coinbase_country_attestation circuit');
5242
+ }
5160
5243
  // Return target: validated before we burn a challenge, so a typo surfaces
5161
5244
  // as a local error instead of a relay 400 on the second round trip.
5162
5245
  if (options.returnScheme !== undefined) {
@@ -5212,6 +5295,8 @@ class ProofportSDK {
5212
5295
  }
5213
5296
  const created = await response.json();
5214
5297
  this.rememberRelayRequest(created.requestId, scope);
5298
+ if (circuit === 'coinbase_country_attestation')
5299
+ this.rememberCountryRequest(created.requestId, inputs);
5215
5300
  return created;
5216
5301
  }
5217
5302
  /**