@zkproofport-app/sdk 0.3.1 → 0.4.0

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.mjs CHANGED
@@ -79,28 +79,24 @@ const CIRCUIT_METADATA = {
79
79
  publicInputsCount: 148,
80
80
  publicInputNames: ['pubkey_modulus_limbs', 'domain', 'scope', 'nullifier', 'provider'],
81
81
  },
82
- arc_eligibility: {
83
- name: 'Arc Eligibility',
84
- description: 'Prove eligibility and authorize one EIP-712 action, for an Arc gate',
85
- // 6 x `pub [u8; 32]` in arc-eligibility/src/main.nr, flattened to bytes.
86
- // Read off the compiled ABI rather than counted: signal_hash 0-31,
87
- // domain_separator 32-63, action_hash 64-95, signer_list_merkle_root
88
- // 96-127, scope 128-159, nullifier 160-191.
89
- publicInputsCount: 192,
90
- publicInputNames: ['signal_hash', 'domain_separator', 'action_hash', 'signer_list_merkle_root'],
91
- },
92
82
  // Planned circuit — the identifier is reserved and the circuit exists, but it
93
83
  // is not officially supported yet. See CIRCUIT_SUPPORT_STATUS in ./circuits.
94
- // Forked from coinbase_attestation and shares its four public inputs, so the
95
- // count is the byte-flattened total (4 x [u8; 32] = 128), matching the
96
- // COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT indices below. The
97
- // coinbase_attestation entry above reports a legacy logical count of 2 and is
98
- // left as-is for backwards compatibility.
84
+ // Forked from coinbase_attestation, then given the EIP-712 pair and a
85
+ // branch: with an action the wallet signs the typed data and signal_hash is
86
+ // zero; without one it signs signal_hash and the pair is zero. Read off the
87
+ // compiled ABI (6 x [u8; 32] = 192): signal_hash 0-31, domain_separator
88
+ // 32-63, action_hash 64-95, signer_list_merkle_root 96-127, scope 128-159,
89
+ // nullifier 160-191.
90
+ //
91
+ // signal_hash is NOT part of this circuit's nullifier: a nullifier derived
92
+ // from a public input nobody signs and nothing constrains would let one
93
+ // wallet mint as many nullifiers as it likes; here the secret is the address
94
+ // plus a constant compiled into the circuit.
99
95
  giwa_attestation: {
100
96
  name: 'GIWA Attestation',
101
- description: 'Prove GIWA identity verification (planned — not officially supported yet)',
102
- publicInputsCount: 128,
103
- publicInputNames: ['signal_hash', 'signer_list_merkle_root', 'scope', 'nullifier'],
97
+ description: 'Prove GIWA identity verification, optionally authorizing one EIP-712 action (planned — not officially supported yet)',
98
+ publicInputsCount: 192,
99
+ publicInputNames: ['signal_hash', 'domain_separator', 'action_hash', 'signer_list_merkle_root', 'scope', 'nullifier'],
104
100
  },
105
101
  // Korea Mobile ID (mDL) circuits. Planned — not officially supported yet.
106
102
  // Counts are byte-flattened field totals from the compiled circuit ABIs:
@@ -167,11 +163,6 @@ const RPC_ENDPOINTS = {
167
163
  84532: 'https://sepolia.base.org', // Base Sepolia
168
164
  8453: 'https://mainnet.base.org', // Base Mainnet
169
165
  91342: 'https://sepolia-rpc.giwa.io', // GIWA Sepolia
170
- // Arc Testnet. The arc_eligibility verifier is deployed here and nowhere
171
- // else, and this line was missing on 2026-09-12 — the circuit shipped, a
172
- // phone produced a valid proof, and on-chain verification answered "No RPC
173
- // endpoint configured for chain 5042002", which reads like the proof's fault.
174
- 5042002: 'https://rpc.testnet.arc.io', // Arc Testnet
175
166
  };
176
167
  /**
177
168
  * Default proof request expiration time in milliseconds.
@@ -379,8 +370,6 @@ const CIRCUIT_IDS = Object.freeze({
379
370
  COINBASE_COUNTRY_ATTESTATION: 'coinbase_country_attestation',
380
371
  /** OIDC email-domain attestation. Officially supported. */
381
372
  OIDC_DOMAIN_ATTESTATION: 'oidc_domain_attestation',
382
- /** Arc eligibility: one EIP-712 action, authorized. Officially supported. */
383
- ARC_ELIGIBILITY: 'arc_eligibility',
384
373
  /** GIWA attestation. Planned — not officially supported yet. */
385
374
  GIWA_ATTESTATION: 'giwa_attestation',
386
375
  /** Korea Mobile ID ownership. Planned — not officially supported yet. */
@@ -409,8 +398,11 @@ const CIRCUIT_SUPPORT_STATUS = Object.freeze({
409
398
  coinbase_attestation: 'supported',
410
399
  coinbase_country_attestation: 'supported',
411
400
  oidc_domain_attestation: 'supported',
412
- arc_eligibility: 'experimental',
413
- giwa_attestation: 'planned',
401
+ // Experimental, not planned: the circuit is built, its verifier is
402
+ // deployed on GIWA Sepolia and the demo requests it. What keeps it out of
403
+ // 'supported' is that it is testnet-only, so the app offers it behind
404
+ // Developer Mode and reads its files from main rather than a release tag.
405
+ giwa_attestation: 'experimental',
414
406
  mdl_kr_ownership: 'planned',
415
407
  mdl_kr_age: 'planned',
416
408
  mdl_kr_region: 'planned',
@@ -439,7 +431,6 @@ const CIRCUIT_VK_PATHS = Object.freeze({
439
431
  coinbase_attestation: 'coinbase-attestation/target/vk/vk',
440
432
  coinbase_country_attestation: 'coinbase-country-attestation/target/vk/vk',
441
433
  oidc_domain_attestation: 'oidc-domain-attestation/target/vk/vk',
442
- arc_eligibility: 'arc-eligibility/target/vk/vk',
443
434
  giwa_attestation: 'giwa-attestation/target/vk/vk',
444
435
  mdl_kr_ownership: 'mdl/kr-ownership/target/vk/vk',
445
436
  mdl_kr_age: 'mdl/kr-age/target/vk/vk',
@@ -468,12 +459,11 @@ const SUPPORTED_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter((id) => CIRCU
468
459
  /**
469
460
  * Circuits you can prove today, on a testnet, with everything subject to change.
470
461
  *
471
- * `arc_eligibility` is the reason this status exists. Calling it `planned`
472
- * would be wrong — the circuit compiles, the verifier is deployed and a proof
473
- * verifies against it — and calling it `supported` would be worse, because the
474
- * only deployment is Arc Testnet, Circle has not published a mainnet chain id,
475
- * and the public-input layout is still open. Neither existing word was true, so
476
- * the honest move was a third word rather than picking the closer lie.
462
+ * `giwa_attestation` is in this state. Calling it `planned` would be wrong —
463
+ * the circuit compiles, the verifier is deployed and a proof verifies against
464
+ * it — and calling it `supported` would be worse, because the only deployment
465
+ * is a testnet (GIWA Sepolia). Neither existing word was true, so the honest
466
+ * move was a third word rather than picking the closer lie.
477
467
  */
478
468
  const EXPERIMENTAL_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter(id => CIRCUIT_SUPPORT_STATUS[id] === 'experimental'));
479
469
  const PLANNED_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'planned'));
@@ -522,44 +512,72 @@ function getCircuitSupportStatus(circuit) {
522
512
  }
523
513
  return CIRCUIT_SUPPORT_STATUS[circuit];
524
514
  }
515
+ const CIRCUIT_ACTION_BINDING = {
516
+ coinbase_attestation: 'none',
517
+ coinbase_country_attestation: 'none',
518
+ // The signature lives inside the identity token; no wallet, no action.
519
+ oidc_domain_attestation: 'none',
520
+ // Branches in-circuit: with an action the wallet signs the typed data,
521
+ // without one it personal_signs the request's signal hash.
522
+ giwa_attestation: 'optional',
523
+ mdl_kr_ownership: 'none',
524
+ mdl_kr_age: 'none',
525
+ mdl_kr_region: 'none',
526
+ };
527
+ /*
528
+ * The union is declared above rather than derived from the table's values.
529
+ *
530
+ * Deriving it narrowed the type to whatever the table happens to hold today --
531
+ * 'none' | 'optional' -- and TypeScript then called every `=== 'required'`
532
+ * check impossible. The publish typecheck caught it; `rollup` and `vitest` did
533
+ * not. 'required' is a state a circuit can legitimately be in (an EIP-712 path
534
+ * and nothing else), so the union says so and the table is checked against it.
535
+ */
525
536
  /**
526
- * Whether a circuit's relay request must carry a wallet signature.
527
- *
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.
537
+ * How this circuit treats an action, or an error naming the id that is not one.
533
538
  *
534
- * `satisfies` and not an annotation, so a circuit added above is a compile
535
- * error here until somebody decides which it is.
539
+ * No default: a circuit nobody has classified must not inherit 'none', which
540
+ * would silently drop an action a dapp meant to bind.
536
541
  */
542
+ function circuitActionBinding(circuit) {
543
+ const binding = CIRCUIT_ACTION_BINDING[circuit];
544
+ if (!binding) {
545
+ throw new Error(`Unknown circuit '${circuit}'. Expected: ${ALL_CIRCUIT_IDS.join(', ')}`);
546
+ }
547
+ return binding;
548
+ }
537
549
  const CIRCUIT_NEEDS_WALLET_SIGNATURE = {
538
550
  coinbase_attestation: true,
539
551
  coinbase_country_attestation: true,
540
- // Same Coinbase-attested wallet as above; what changes is that it signs an
541
- // EIP-712 action rather than signal_hash.
542
- arc_eligibility: true,
543
552
  // The signature is inside the OIDC identity token, checked by the prover.
544
553
  oidc_domain_attestation: false,
545
554
  // Web2 (OmniOne CX) flows with no wallet binding at all.
546
555
  mdl_kr_ownership: false,
547
556
  mdl_kr_age: false,
548
557
  mdl_kr_region: false,
549
- giwa_attestation: false,
558
+ // A GIWA-attested wallet signing an EIP-712 action (or its signal hash).
559
+ // This read `false` while the circuit had always verified a wallet signature
560
+ // -- it sat under the Web2 comment above and was never revisited. A request
561
+ // for it would have reached the relay with no signer.
562
+ giwa_attestation: true,
550
563
  };
551
564
 
552
565
  const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
553
566
  /**
554
567
  * The reason the value is not a usable action, or `null` when it is one.
555
568
  *
569
+ * Structural validation only, not application authorization policy. Consumers
570
+ * (e.g. EligibilityGate) must independently enforce the expected chain, verifying
571
+ * contract, actor, action parameters, nonce and deadline as applicable.
572
+ * EIP-712 signs only fields declared in types, not extra message properties.
573
+ *
556
574
  * A string rather than a boolean because every caller shows it to somebody —
557
575
  * a dapp developer reading a validation error, or a person looking at the
558
576
  * mobile app's demo screen.
559
577
  */
560
578
  function validateTypedAction(value) {
561
579
  if (value === undefined || value === null) {
562
- return 'action is required for arc_eligibility. The circuit proves that a wallet authorised ONE EIP-712 action; there is nothing to prove without it.';
580
+ return 'action is required. The circuit proves that a wallet authorised ONE EIP-712 action; there is nothing to prove without it.';
563
581
  }
564
582
  if (typeof value !== 'object' || Array.isArray(value)) {
565
583
  return 'action must be an object.';
@@ -1115,8 +1133,7 @@ function validateProofRequest(request) {
1115
1133
  return { valid: false, error: 'Missing circuit type' };
1116
1134
  }
1117
1135
  // The list is the SDK's own, not a copy. The copy that used to sit here
1118
- // named six circuits and had missed two — `giwa_attestation` and
1119
- // `arc_eligibility` — so a link for either was refused as an invalid type
1136
+ // had missed circuits, so a link for one was refused as an invalid type
1120
1137
  // while every other layer accepted it.
1121
1138
  if (!isCircuitId(request.circuit)) {
1122
1139
  return {
@@ -1171,14 +1188,24 @@ function validateProofRequest(request) {
1171
1188
  return { valid: false, error: mdlError };
1172
1189
  }
1173
1190
  }
1174
- else if (request.circuit === 'arc_eligibility') {
1175
- const inputs = request.inputs;
1176
- if (inputs.userAddress && !/^0x[a-fA-F0-9]{40}$/.test(inputs.userAddress)) {
1177
- return { valid: false, error: 'Invalid userAddress format' };
1191
+ // The action, decided by the circuit's own entry rather than by naming a
1192
+ // circuit here, so every action-capable circuit is checked the same way.
1193
+ {
1194
+ const action = request.inputs?.action;
1195
+ const binding = circuitActionBinding(request.circuit);
1196
+ if (binding === 'none') {
1197
+ if (action !== undefined) {
1198
+ return {
1199
+ valid: false,
1200
+ error: `${request.circuit} has no action inputs, so an action sent with it would not be proved. Refusing rather than dropping it.`,
1201
+ };
1202
+ }
1178
1203
  }
1179
- const actionError = validateTypedAction(inputs.action);
1180
- if (actionError) {
1181
- return { valid: false, error: actionError };
1204
+ else if (binding === 'required' || action !== undefined) {
1205
+ const actionError = validateTypedAction(action);
1206
+ if (actionError) {
1207
+ return { valid: false, error: actionError };
1208
+ }
1182
1209
  }
1183
1210
  }
1184
1211
  // Check expiry
@@ -4321,7 +4348,7 @@ function getVerifierContract(providerOrSigner, verifier) {
4321
4348
  function getDefaultProvider(chainId) {
4322
4349
  const rpcUrl = RPC_ENDPOINTS[chainId];
4323
4350
  if (!rpcUrl) {
4324
- throw new Error(`No RPC endpoint configured for chain ${chainId}`);
4351
+ throw new Error(`No RPC endpoint configured for chain ${chainId}. Known chains: ${Object.keys(RPC_ENDPOINTS).join(', ')}`);
4325
4352
  }
4326
4353
  return createJsonRpcProvider(rpcUrl);
4327
4354
  }
@@ -4470,7 +4497,7 @@ function getVerifierChainId(circuit, customVerifier) {
4470
4497
  *
4471
4498
  * It replaced an `if / else if / else` chain whose final `else` handed every
4472
4499
  * unrecognised circuit — and every call that named no circuit at all —
4473
- * Coinbase's offsets. For `arc_eligibility` that is not a near miss: its
4500
+ * Coinbase's offsets. For `giwa_attestation` that is not a near miss: its
4474
4501
  * public inputs are signal_hash, domain_separator, action_hash, merkle root,
4475
4502
  * scope, nullifier, so the nullifier slot under Coinbase's offsets holds the
4476
4503
  * MERKLE ROOT, a value identical for every user. Duplicate detection built on
@@ -4479,14 +4506,14 @@ function getVerifierChainId(circuit, customVerifier) {
4479
4506
  const PUBLIC_INPUT_OFFSETS = Object.freeze({
4480
4507
  // signal_hash, signer_list_merkle_root, scope, nullifier
4481
4508
  coinbase_attestation: { scope: [64, 95], nullifier: [96, 127] },
4482
- // Same four fields in the same order as Coinbase.
4483
- giwa_attestation: { scope: [64, 95], nullifier: [96, 127] },
4509
+ // No longer Coinbase's layout. The GIWA circuit gained the EIP-712 pair, so
4510
+ // it reads: signal_hash, domain_separator, action_hash, merkle root, then
4511
+ // scope and nullifier 64 bytes further along than Coinbase.
4512
+ giwa_attestation: { scope: [128, 159], nullifier: [160, 191] },
4484
4513
  // country_list, country_list_length and is_included sit before scope.
4485
4514
  coinbase_country_attestation: { scope: [86, 117], nullifier: [118, 149] },
4486
4515
  // pubkey_modulus_limbs and the bounded domain sit before scope.
4487
4516
  oidc_domain_attestation: { scope: [83, 114], nullifier: [115, 146] },
4488
- // signal_hash, domain_separator, action_hash, merkle root, then the pair.
4489
- arc_eligibility: { scope: [128, 159], nullifier: [160, 191] },
4490
4517
  // The Korea mDL circuits open with the pair; signal_hash is commented out.
4491
4518
  mdl_kr_ownership: { scope: [0, 31], nullifier: [32, 63] },
4492
4519
  mdl_kr_age: { scope: [0, 31], nullifier: [32, 63] },
@@ -5867,9 +5894,9 @@ class ProofportSDK {
5867
5894
  * ```
5868
5895
  */
5869
5896
  // Which circuits need a wallet signature, from the one record in
5870
- // src/circuits.ts. The typed-out pair that used to sit here had missed
5871
- // arc_eligibility, so a dapp calling createRelayRequest() for it with no
5872
- // signer got no local error and a relay 401 two round trips later.
5897
+ // src/circuits.ts. A typed-out list here once missed a circuit, so a dapp
5898
+ // calling createRelayRequest() for it with no signer got no local error and
5899
+ // a relay 401 two round trips later.
5873
5900
  static needsWalletSignature(circuit) {
5874
5901
  return CIRCUIT_NEEDS_WALLET_SIGNATURE[circuit] === true;
5875
5902
  }
@@ -5889,15 +5916,24 @@ class ProofportSDK {
5889
5916
  throw new Error(`${mdlError} (circuit: ${circuit})`);
5890
5917
  }
5891
5918
  }
5892
- // arc_eligibility: the action is what the proof binds to, so a request
5893
- // without a usable one cannot be honoured. Checked here, before a
5894
- // challenge is burned, because the alternative is a refusal from inside
5895
- // the mobile app -- a screen the dapp developer never sees, reached after
5896
- // the person has already scanned a QR code.
5897
- if (circuit === 'arc_eligibility') {
5898
- const actionError = validateTypedAction(inputs.action);
5899
- if (actionError) {
5900
- throw new Error(`${actionError} (circuit: ${circuit})`);
5919
+ // The action, per the circuit's entry in CIRCUIT_ACTION_BINDING. Checked
5920
+ // here, before a challenge is burned, because the alternative is a refusal
5921
+ // from inside the mobile app -- a screen the dapp developer never sees,
5922
+ // reached after the person has already scanned a QR code.
5923
+ //
5924
+ // The circuit id belongs in the table, not in the check.
5925
+ {
5926
+ const action = inputs.action;
5927
+ const binding = circuitActionBinding(circuit);
5928
+ if (binding === 'none' && action !== undefined) {
5929
+ throw new Error(`${circuit} has no action inputs, so an action sent with it would not be proved. ` +
5930
+ 'Remove it, or use a circuit that binds one.');
5931
+ }
5932
+ if (binding === 'required' || (binding === 'optional' && action !== undefined)) {
5933
+ const actionError = validateTypedAction(action);
5934
+ if (actionError) {
5935
+ throw new Error(`${actionError} (circuit: ${circuit})`);
5936
+ }
5901
5937
  }
5902
5938
  }
5903
5939
  // Return target: validated before we burn a challenge, so a typo surfaces
@@ -6225,5 +6261,5 @@ class ProofportSDK {
6225
6261
  }
6226
6262
  }
6227
6263
 
6228
- export { ALL_CIRCUIT_IDS, CIRCUIT_IDS, CIRCUIT_SUPPORT_STATUS, CIRCUIT_VK_PATHS, COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, EXPERIMENTAL_CIRCUIT_IDS, MDL_KR_PUBLIC_INPUT_LAYOUT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, PLANNED_CIRCUIT_IDS, ProofportSDK, SUPPORTED_CIRCUIT_IDS, ProofportSDK as default, extractDomainFromPublicInputs, extractNullifierFromPublicInputs, extractScopeFromPublicInputs, getCircuitSupportStatus, isCircuitId, isSupportedCircuitId, parseTypedAction, validateTypedAction, verifyProofOffChain };
6264
+ export { ALL_CIRCUIT_IDS, CIRCUIT_ACTION_BINDING, CIRCUIT_IDS, CIRCUIT_SUPPORT_STATUS, CIRCUIT_VK_PATHS, COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, EXPERIMENTAL_CIRCUIT_IDS, MDL_KR_PUBLIC_INPUT_LAYOUT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, PLANNED_CIRCUIT_IDS, ProofportSDK, SUPPORTED_CIRCUIT_IDS, circuitActionBinding, ProofportSDK as default, extractDomainFromPublicInputs, extractNullifierFromPublicInputs, extractScopeFromPublicInputs, getCircuitSupportStatus, isCircuitId, isSupportedCircuitId, parseTypedAction, validateTypedAction, verifyProofOffChain };
6229
6265
  //# sourceMappingURL=index.mjs.map