@zkproofport-app/sdk 0.3.0 → 0.3.3

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
@@ -96,16 +96,22 @@ const CIRCUIT_METADATA = {
96
96
  },
97
97
  // Planned circuit — the identifier is reserved and the circuit exists, but it
98
98
  // is not officially supported yet. See CIRCUIT_SUPPORT_STATUS in ./circuits.
99
- // Forked from coinbase_attestation and shares its four public inputs, so the
100
- // count is the byte-flattened total (4 x [u8; 32] = 128), matching the
101
- // COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT indices below. The
102
- // coinbase_attestation entry above reports a legacy logical count of 2 and is
103
- // left as-is for backwards compatibility.
99
+ // Forked from coinbase_attestation, then given the EIP-712 pair and a
100
+ // branch: with an action the wallet signs the typed data and signal_hash is
101
+ // zero; without one it signs signal_hash and the pair is zero. Read off the
102
+ // compiled ABI (6 x [u8; 32] = 192): signal_hash 0-31, domain_separator
103
+ // 32-63, action_hash 64-95, signer_list_merkle_root 96-127, scope 128-159,
104
+ // nullifier 160-191 -- the same order as arc_eligibility.
105
+ //
106
+ // Unlike Arc, signal_hash is NOT part of this circuit's nullifier. That one
107
+ // derives its nullifier from a public input nobody signs and nothing
108
+ // constrains, so one wallet can mint as many nullifiers as it likes; here
109
+ // the secret is the address plus a constant compiled into the circuit.
104
110
  giwa_attestation: {
105
111
  name: 'GIWA Attestation',
106
- description: 'Prove GIWA identity verification (planned — not officially supported yet)',
107
- publicInputsCount: 128,
108
- publicInputNames: ['signal_hash', 'signer_list_merkle_root', 'scope', 'nullifier'],
112
+ description: 'Prove GIWA identity verification, optionally authorizing one EIP-712 action (planned — not officially supported yet)',
113
+ publicInputsCount: 192,
114
+ publicInputNames: ['signal_hash', 'domain_separator', 'action_hash', 'signer_list_merkle_root', 'scope', 'nullifier'],
109
115
  },
110
116
  // Korea Mobile ID (mDL) circuits. Planned — not officially supported yet.
111
117
  // Counts are byte-flattened field totals from the compiled circuit ABIs:
@@ -172,6 +178,11 @@ const RPC_ENDPOINTS = {
172
178
  84532: 'https://sepolia.base.org', // Base Sepolia
173
179
  8453: 'https://mainnet.base.org', // Base Mainnet
174
180
  91342: 'https://sepolia-rpc.giwa.io', // GIWA Sepolia
181
+ // Arc Testnet. The arc_eligibility verifier is deployed here and nowhere
182
+ // else, and this line was missing on 2026-09-12 — the circuit shipped, a
183
+ // phone produced a valid proof, and on-chain verification answered "No RPC
184
+ // endpoint configured for chain 5042002", which reads like the proof's fault.
185
+ 5042002: 'https://rpc.testnet.arc.io', // Arc Testnet
175
186
  };
176
187
  /**
177
188
  * Default proof request expiration time in milliseconds.
@@ -379,7 +390,7 @@ const CIRCUIT_IDS = Object.freeze({
379
390
  COINBASE_COUNTRY_ATTESTATION: 'coinbase_country_attestation',
380
391
  /** OIDC email-domain attestation. Officially supported. */
381
392
  OIDC_DOMAIN_ATTESTATION: 'oidc_domain_attestation',
382
- /** Arc eligibility: one EIP-712 action, authorized. Officially supported. */
393
+ /** Arc eligibility: exact EIP-712 action authorization. Experimental on Arc Testnet. */
383
394
  ARC_ELIGIBILITY: 'arc_eligibility',
384
395
  /** GIWA attestation. Planned — not officially supported yet. */
385
396
  GIWA_ATTESTATION: 'giwa_attestation',
@@ -410,7 +421,11 @@ const CIRCUIT_SUPPORT_STATUS = Object.freeze({
410
421
  coinbase_country_attestation: 'supported',
411
422
  oidc_domain_attestation: 'supported',
412
423
  arc_eligibility: 'experimental',
413
- giwa_attestation: 'planned',
424
+ // Experimental, not planned: the circuit is built, its verifier is
425
+ // deployed on GIWA Sepolia and the demo requests it. What keeps it out of
426
+ // 'supported' is that it is testnet-only, so the app offers it behind
427
+ // Developer Mode and reads its files from main rather than a release tag.
428
+ giwa_attestation: 'experimental',
414
429
  mdl_kr_ownership: 'planned',
415
430
  mdl_kr_age: 'planned',
416
431
  mdl_kr_region: 'planned',
@@ -522,18 +537,44 @@ function getCircuitSupportStatus(circuit) {
522
537
  }
523
538
  return CIRCUIT_SUPPORT_STATUS[circuit];
524
539
  }
525
- /**
526
- * Whether a circuit's relay request must carry a wallet signature.
540
+ const CIRCUIT_ACTION_BINDING = {
541
+ coinbase_attestation: 'none',
542
+ coinbase_country_attestation: 'none',
543
+ // The signature lives inside the identity token; no wallet, no action.
544
+ oidc_domain_attestation: 'none',
545
+ // Both branch in-circuit: with an action the wallet signs the typed data,
546
+ // without one it personal_signs the request's signal hash. Arc read
547
+ // 'required' until 2026-09-22 because its circuit had no second path -- a
548
+ // request without an action built a vector 64 bytes short and died inside
549
+ // the prover. The circuit gained the branch; the table follows it.
550
+ arc_eligibility: 'optional',
551
+ giwa_attestation: 'optional',
552
+ mdl_kr_ownership: 'none',
553
+ mdl_kr_age: 'none',
554
+ mdl_kr_region: 'none',
555
+ };
556
+ /*
557
+ * The union is declared above rather than derived from the table's values.
527
558
  *
528
- * The SDK checks this to fail fast — "Signer not set" locally beats a 401 from
529
- * the relay two round trips later, with nothing saying which circuit needed
530
- * what. The relay declares its OWN copy on purpose and does not import this
531
- * one: it is a security boundary, and an auth policy that a published package
532
- * can change is not a policy. A test in the relay asserts the two agree.
559
+ * Deriving it narrowed the type to whatever the table happens to hold today --
560
+ * 'none' | 'optional' -- and TypeScript then called every `=== 'required'`
561
+ * check impossible. The publish typecheck caught it; `rollup` and `vitest` did
562
+ * not. 'required' is a state a circuit can legitimately be in (an EIP-712 path
563
+ * and nothing else), so the union says so and the table is checked against it.
564
+ */
565
+ /**
566
+ * How this circuit treats an action, or an error naming the id that is not one.
533
567
  *
534
- * `satisfies` and not an annotation, so a circuit added above is a compile
535
- * error here until somebody decides which it is.
568
+ * No default: a circuit nobody has classified must not inherit 'none', which
569
+ * would silently drop an action a dapp meant to bind.
536
570
  */
571
+ function circuitActionBinding(circuit) {
572
+ const binding = CIRCUIT_ACTION_BINDING[circuit];
573
+ if (!binding) {
574
+ throw new Error(`Unknown circuit '${circuit}'. Expected: ${ALL_CIRCUIT_IDS.join(', ')}`);
575
+ }
576
+ return binding;
577
+ }
537
578
  const CIRCUIT_NEEDS_WALLET_SIGNATURE = {
538
579
  coinbase_attestation: true,
539
580
  coinbase_country_attestation: true,
@@ -546,13 +587,22 @@ const CIRCUIT_NEEDS_WALLET_SIGNATURE = {
546
587
  mdl_kr_ownership: false,
547
588
  mdl_kr_age: false,
548
589
  mdl_kr_region: false,
549
- giwa_attestation: false,
590
+ // A GIWA-attested wallet signing an EIP-712 action, the same shape as Arc.
591
+ // This read `false` while the circuit had always verified a wallet signature
592
+ // -- it sat under the Web2 comment above and was never revisited. A request
593
+ // for it would have reached the relay with no signer.
594
+ giwa_attestation: true,
550
595
  };
551
596
 
552
597
  const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
553
598
  /**
554
599
  * The reason the value is not a usable action, or `null` when it is one.
555
600
  *
601
+ * Structural validation only, not application authorization policy. Consumers
602
+ * (e.g. EligibilityGate) must independently enforce the expected chain, verifying
603
+ * contract, actor, action parameters, nonce and deadline as applicable.
604
+ * EIP-712 signs only fields declared in types, not extra message properties.
605
+ *
556
606
  * A string rather than a boolean because every caller shows it to somebody —
557
607
  * a dapp developer reading a validation error, or a person looking at the
558
608
  * mobile app's demo screen.
@@ -1176,9 +1226,26 @@ function validateProofRequest(request) {
1176
1226
  if (inputs.userAddress && !/^0x[a-fA-F0-9]{40}$/.test(inputs.userAddress)) {
1177
1227
  return { valid: false, error: 'Invalid userAddress format' };
1178
1228
  }
1179
- const actionError = validateTypedAction(inputs.action);
1180
- if (actionError) {
1181
- return { valid: false, error: actionError };
1229
+ }
1230
+ // The action, decided by the circuit's own entry rather than by naming a
1231
+ // circuit here. This used to be part of the arc_eligibility branch above,
1232
+ // which is why a second action-capable circuit had nowhere to be checked.
1233
+ {
1234
+ const action = request.inputs?.action;
1235
+ const binding = circuitActionBinding(request.circuit);
1236
+ if (binding === 'none') {
1237
+ if (action !== undefined) {
1238
+ return {
1239
+ valid: false,
1240
+ error: `${request.circuit} has no action inputs, so an action sent with it would not be proved. Refusing rather than dropping it.`,
1241
+ };
1242
+ }
1243
+ }
1244
+ else if (binding === 'required' || action !== undefined) {
1245
+ const actionError = validateTypedAction(action);
1246
+ if (actionError) {
1247
+ return { valid: false, error: actionError };
1248
+ }
1182
1249
  }
1183
1250
  }
1184
1251
  // Check expiry
@@ -1612,8 +1679,10 @@ function getVerifierChainId(circuit, customVerifier) {
1612
1679
  const PUBLIC_INPUT_OFFSETS = Object.freeze({
1613
1680
  // signal_hash, signer_list_merkle_root, scope, nullifier
1614
1681
  coinbase_attestation: { scope: [64, 95], nullifier: [96, 127] },
1615
- // Same four fields in the same order as Coinbase.
1616
- giwa_attestation: { scope: [64, 95], nullifier: [96, 127] },
1682
+ // No longer Coinbase's layout. The GIWA circuit gained the EIP-712 pair, so
1683
+ // it reads like Arc: signal_hash, domain_separator, action_hash, merkle
1684
+ // root, then scope and nullifier 64 bytes further along than Coinbase.
1685
+ giwa_attestation: { scope: [128, 159], nullifier: [160, 191] },
1617
1686
  // country_list, country_list_length and is_included sit before scope.
1618
1687
  coinbase_country_attestation: { scope: [86, 117], nullifier: [118, 149] },
1619
1688
  // pubkey_modulus_limbs and the bounded domain sit before scope.
@@ -3022,15 +3091,26 @@ class ProofportSDK {
3022
3091
  throw new Error(`${mdlError} (circuit: ${circuit})`);
3023
3092
  }
3024
3093
  }
3025
- // arc_eligibility: the action is what the proof binds to, so a request
3026
- // without a usable one cannot be honoured. Checked here, before a
3027
- // challenge is burned, because the alternative is a refusal from inside
3028
- // the mobile app -- a screen the dapp developer never sees, reached after
3029
- // the person has already scanned a QR code.
3030
- if (circuit === 'arc_eligibility') {
3031
- const actionError = validateTypedAction(inputs.action);
3032
- if (actionError) {
3033
- throw new Error(`${actionError} (circuit: ${circuit})`);
3094
+ // The action, per the circuit's entry in CIRCUIT_ACTION_BINDING. Checked
3095
+ // here, before a challenge is burned, because the alternative is a refusal
3096
+ // from inside the mobile app -- a screen the dapp developer never sees,
3097
+ // reached after the person has already scanned a QR code.
3098
+ //
3099
+ // This named arc_eligibility directly until giwa_attestation became the
3100
+ // second circuit that can carry an action; the id belongs in the table,
3101
+ // not in the check.
3102
+ {
3103
+ const action = inputs.action;
3104
+ const binding = circuitActionBinding(circuit);
3105
+ if (binding === 'none' && action !== undefined) {
3106
+ throw new Error(`${circuit} has no action inputs, so an action sent with it would not be proved. ` +
3107
+ 'Remove it, or use a circuit that binds one.');
3108
+ }
3109
+ if (binding === 'required' || (binding === 'optional' && action !== undefined)) {
3110
+ const actionError = validateTypedAction(action);
3111
+ if (actionError) {
3112
+ throw new Error(`${actionError} (circuit: ${circuit})`);
3113
+ }
3034
3114
  }
3035
3115
  }
3036
3116
  // Return target: validated before we burn a challenge, so a typo surfaces
@@ -3359,6 +3439,7 @@ class ProofportSDK {
3359
3439
  }
3360
3440
 
3361
3441
  exports.ALL_CIRCUIT_IDS = ALL_CIRCUIT_IDS;
3442
+ exports.CIRCUIT_ACTION_BINDING = CIRCUIT_ACTION_BINDING;
3362
3443
  exports.CIRCUIT_IDS = CIRCUIT_IDS;
3363
3444
  exports.CIRCUIT_SUPPORT_STATUS = CIRCUIT_SUPPORT_STATUS;
3364
3445
  exports.CIRCUIT_VK_PATHS = CIRCUIT_VK_PATHS;
@@ -3370,6 +3451,7 @@ exports.OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT = OIDC_DOMAIN_ATTESTATION_PU
3370
3451
  exports.PLANNED_CIRCUIT_IDS = PLANNED_CIRCUIT_IDS;
3371
3452
  exports.ProofportSDK = ProofportSDK;
3372
3453
  exports.SUPPORTED_CIRCUIT_IDS = SUPPORTED_CIRCUIT_IDS;
3454
+ exports.circuitActionBinding = circuitActionBinding;
3373
3455
  exports.default = ProofportSDK;
3374
3456
  exports.extractDomainFromPublicInputs = extractDomainFromPublicInputs;
3375
3457
  exports.extractNullifierFromPublicInputs = extractNullifierFromPublicInputs;