@zkproofport-app/sdk 0.2.15 → 0.3.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.esm.js CHANGED
@@ -79,6 +79,16 @@ 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
+ },
82
92
  // Planned circuit — the identifier is reserved and the circuit exists, but it
83
93
  // is not officially supported yet. See CIRCUIT_SUPPORT_STATUS in ./circuits.
84
94
  // Forked from coinbase_attestation and shares its four public inputs, so the
@@ -318,6 +328,292 @@ const OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT = {
318
328
  DOMAIN_END: 82,
319
329
  };
320
330
 
331
+ /**
332
+ * Canonical ZKProofport circuit identifiers.
333
+ *
334
+ * This module is the single source of truth for circuit IDs. Every other layer
335
+ * — the mobile app, the demo, the relay, server-side provers — reads its list
336
+ * from here instead of keeping a private copy that silently drifts.
337
+ *
338
+ * It has **no imports and no dependencies**, so it can be pulled in on its own:
339
+ *
340
+ * ```typescript
341
+ * import { CIRCUIT_IDS, isCircuitId } from '@zkproofport-app/sdk/circuits';
342
+ * ```
343
+ *
344
+ * The same names are also re-exported from the package root
345
+ * (`@zkproofport-app/sdk`) for callers that already import the SDK.
346
+ *
347
+ * ## Identifiers are canonical and case-sensitive
348
+ *
349
+ * Each ID is the circuit's `name` in its `Nargo.toml`, verbatim. The format is
350
+ * lowercase with underscores. Hyphenated spellings (`coinbase-kyc`,
351
+ * `mdl-kr-age`) are directory or UI route names in other repositories — they
352
+ * are **not** circuit IDs, and passing one produces a nullifier mismatch or a
353
+ * failed on-chain lookup rather than a clear error.
354
+ */
355
+ /**
356
+ * Canonical circuit identifiers, keyed by a stable constant name.
357
+ *
358
+ * Prefer these constants over string literals so a rename is a compile error
359
+ * rather than a runtime surprise.
360
+ *
361
+ * @example
362
+ * ```typescript
363
+ * import { CIRCUIT_IDS } from '@zkproofport-app/sdk/circuits';
364
+ *
365
+ * const relay = await sdk.createRelayRequest(CIRCUIT_IDS.COINBASE_ATTESTATION, {
366
+ * scope: 'myapp.com',
367
+ * });
368
+ * ```
369
+ */
370
+ const CIRCUIT_IDS = Object.freeze({
371
+ /** Coinbase KYC attestation. Officially supported. */
372
+ COINBASE_ATTESTATION: 'coinbase_attestation',
373
+ /** Coinbase country attestation (inclusion / exclusion). Officially supported. */
374
+ COINBASE_COUNTRY_ATTESTATION: 'coinbase_country_attestation',
375
+ /** OIDC email-domain attestation. Officially supported. */
376
+ OIDC_DOMAIN_ATTESTATION: 'oidc_domain_attestation',
377
+ /** Arc eligibility: one EIP-712 action, authorized. Officially supported. */
378
+ ARC_ELIGIBILITY: 'arc_eligibility',
379
+ /** GIWA attestation. Planned — not officially supported yet. */
380
+ GIWA_ATTESTATION: 'giwa_attestation',
381
+ /** Korea Mobile ID ownership. Planned — not officially supported yet. */
382
+ MDL_KR_OWNERSHIP: 'mdl_kr_ownership',
383
+ /** Korea Mobile ID age threshold. Planned — not officially supported yet. */
384
+ MDL_KR_AGE: 'mdl_kr_age',
385
+ /** Korea Mobile ID si/do region. Planned — not officially supported yet. */
386
+ MDL_KR_REGION: 'mdl_kr_region',
387
+ });
388
+ /**
389
+ * Support status for every circuit.
390
+ *
391
+ * The record is exhaustive over {@link CircuitId}: adding an identifier without
392
+ * giving it a status is a compile error, so this list cannot drift from
393
+ * {@link CIRCUIT_IDS}.
394
+ *
395
+ * @example
396
+ * ```typescript
397
+ * import { CIRCUIT_SUPPORT_STATUS } from '@zkproofport-app/sdk/circuits';
398
+ *
399
+ * CIRCUIT_SUPPORT_STATUS.coinbase_attestation; // 'supported'
400
+ * CIRCUIT_SUPPORT_STATUS.mdl_kr_age; // 'planned'
401
+ * ```
402
+ */
403
+ const CIRCUIT_SUPPORT_STATUS = Object.freeze({
404
+ coinbase_attestation: 'supported',
405
+ coinbase_country_attestation: 'supported',
406
+ oidc_domain_attestation: 'supported',
407
+ arc_eligibility: 'experimental',
408
+ giwa_attestation: 'planned',
409
+ mdl_kr_ownership: 'planned',
410
+ mdl_kr_age: 'planned',
411
+ mdl_kr_region: 'planned',
412
+ });
413
+ /**
414
+ * Where each circuit's verification key FILE lives inside the
415
+ * `zkproofport/circuits` repository, relative to its root.
416
+ *
417
+ * Off-chain verification needs the verification key and nothing else — no
418
+ * circuit bytecode, no witness. The key is public by nature: it is what anyone
419
+ * checking a proof must already have, and the Solidity verifiers deployed
420
+ * on-chain embed the same key.
421
+ *
422
+ * NOTE THE TRAILING `/vk` TWICE. In that repository `target/vk` is a DIRECTORY
423
+ * holding `vk`, `vk_hash` and `SHA256SUMS`; the key itself is `target/vk/vk`.
424
+ * These paths were first copied here from the mobile app, which keeps the
425
+ * directory and the filename as two fields and joins them — so taking only the
426
+ * directory produced a URL that answers 404, and the first browser to try
427
+ * off-chain verification said "Could not fetch the verification key (HTTP 404)".
428
+ *
429
+ * Exhaustive over {@link CircuitId} on purpose. A new circuit whose key path is
430
+ * not listed here fails to compile rather than failing at verification time,
431
+ * where the only symptom would be a proof that cannot be checked.
432
+ */
433
+ const CIRCUIT_VK_PATHS = Object.freeze({
434
+ coinbase_attestation: 'coinbase-attestation/target/vk/vk',
435
+ coinbase_country_attestation: 'coinbase-country-attestation/target/vk/vk',
436
+ oidc_domain_attestation: 'oidc-domain-attestation/target/vk/vk',
437
+ arc_eligibility: 'arc-eligibility/target/vk/vk',
438
+ giwa_attestation: 'giwa-attestation/target/vk/vk',
439
+ mdl_kr_ownership: 'mdl/kr-ownership/target/vk/vk',
440
+ mdl_kr_age: 'mdl/kr-age/target/vk/vk',
441
+ mdl_kr_region: 'mdl/kr-region/target/vk/vk',
442
+ });
443
+ /**
444
+ * Every canonical circuit identifier, in declaration order.
445
+ *
446
+ * Includes circuits that are still `planned`. Use {@link SUPPORTED_CIRCUIT_IDS}
447
+ * when you only want the ones that are officially supported today.
448
+ */
449
+ const ALL_CIRCUIT_IDS = Object.freeze(Object.values(CIRCUIT_IDS));
450
+ /**
451
+ * Circuits that are officially supported today: the two Coinbase circuits and
452
+ * OIDC domain attestation.
453
+ *
454
+ * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.
455
+ */
456
+ const SUPPORTED_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'supported'));
457
+ /**
458
+ * Circuits whose identifiers are reserved but which are not officially
459
+ * supported yet: GIWA attestation and the three Korea Mobile ID circuits.
460
+ *
461
+ * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.
462
+ */
463
+ /**
464
+ * Circuits you can prove today, on a testnet, with everything subject to change.
465
+ *
466
+ * `arc_eligibility` is the reason this status exists. Calling it `planned`
467
+ * would be wrong — the circuit compiles, the verifier is deployed and a proof
468
+ * verifies against it — and calling it `supported` would be worse, because the
469
+ * only deployment is Arc Testnet, Circle has not published a mainnet chain id,
470
+ * and the public-input layout is still open. Neither existing word was true, so
471
+ * the honest move was a third word rather than picking the closer lie.
472
+ */
473
+ const EXPERIMENTAL_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter(id => CIRCUIT_SUPPORT_STATUS[id] === 'experimental'));
474
+ const PLANNED_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'planned'));
475
+ /**
476
+ * Narrows an unknown value to a canonical circuit identifier.
477
+ *
478
+ * Returns `false` for hyphenated route names such as `'coinbase-kyc'`, which
479
+ * are not circuit IDs.
480
+ *
481
+ * @example
482
+ * ```typescript
483
+ * isCircuitId('coinbase_attestation'); // true
484
+ * isCircuitId('coinbase-kyc'); // false
485
+ * ```
486
+ */
487
+ function isCircuitId(value) {
488
+ return typeof value === 'string' && ALL_CIRCUIT_IDS.includes(value);
489
+ }
490
+ /**
491
+ * Narrows an unknown value to a circuit identifier that is officially supported
492
+ * today. Planned circuits return `false`.
493
+ *
494
+ * @example
495
+ * ```typescript
496
+ * isSupportedCircuitId('oidc_domain_attestation'); // true
497
+ * isSupportedCircuitId('mdl_kr_age'); // false — planned
498
+ * ```
499
+ */
500
+ function isSupportedCircuitId(value) {
501
+ return isCircuitId(value) && CIRCUIT_SUPPORT_STATUS[value] === 'supported';
502
+ }
503
+ /**
504
+ * Returns the support status of a circuit.
505
+ *
506
+ * @param circuit - A canonical circuit identifier.
507
+ * @throws Error when the value is not a canonical circuit identifier.
508
+ *
509
+ * @example
510
+ * ```typescript
511
+ * getCircuitSupportStatus('giwa_attestation'); // 'planned'
512
+ * ```
513
+ */
514
+ function getCircuitSupportStatus(circuit) {
515
+ if (!isCircuitId(circuit)) {
516
+ throw new Error(`Unknown circuit '${String(circuit)}'. Expected one of: ${ALL_CIRCUIT_IDS.join(', ')}`);
517
+ }
518
+ return CIRCUIT_SUPPORT_STATUS[circuit];
519
+ }
520
+ /**
521
+ * Whether a circuit's relay request must carry a wallet signature.
522
+ *
523
+ * The SDK checks this to fail fast — "Signer not set" locally beats a 401 from
524
+ * the relay two round trips later, with nothing saying which circuit needed
525
+ * what. The relay declares its OWN copy on purpose and does not import this
526
+ * one: it is a security boundary, and an auth policy that a published package
527
+ * can change is not a policy. A test in the relay asserts the two agree.
528
+ *
529
+ * `satisfies` and not an annotation, so a circuit added above is a compile
530
+ * error here until somebody decides which it is.
531
+ */
532
+ const CIRCUIT_NEEDS_WALLET_SIGNATURE = {
533
+ coinbase_attestation: true,
534
+ coinbase_country_attestation: true,
535
+ // Same Coinbase-attested wallet as above; what changes is that it signs an
536
+ // EIP-712 action rather than signal_hash.
537
+ arc_eligibility: true,
538
+ // The signature is inside the OIDC identity token, checked by the prover.
539
+ oidc_domain_attestation: false,
540
+ // Web2 (OmniOne CX) flows with no wallet binding at all.
541
+ mdl_kr_ownership: false,
542
+ mdl_kr_age: false,
543
+ mdl_kr_region: false,
544
+ giwa_attestation: false,
545
+ };
546
+
547
+ const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
548
+ /**
549
+ * The reason the value is not a usable action, or `null` when it is one.
550
+ *
551
+ * A string rather than a boolean because every caller shows it to somebody —
552
+ * a dapp developer reading a validation error, or a person looking at the
553
+ * mobile app's demo screen.
554
+ */
555
+ function validateTypedAction(value) {
556
+ if (value === undefined || value === null) {
557
+ 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.';
558
+ }
559
+ if (typeof value !== 'object' || Array.isArray(value)) {
560
+ return 'action must be an object.';
561
+ }
562
+ const a = value;
563
+ const d = a.domain;
564
+ if (!d || typeof d !== 'object') {
565
+ return 'action.domain is required.';
566
+ }
567
+ for (const k of ['name', 'version', 'verifyingContract']) {
568
+ if (typeof d[k] !== 'string' || !d[k]) {
569
+ return `action.domain.${k} must be a non-empty string.`;
570
+ }
571
+ }
572
+ if (typeof d.chainId !== 'number' || !Number.isInteger(d.chainId)) {
573
+ return 'action.domain.chainId must be an integer.';
574
+ }
575
+ if (!ADDRESS.test(d.verifyingContract)) {
576
+ return 'action.domain.verifyingContract must be a 20-byte hex address.';
577
+ }
578
+ if (!a.types || typeof a.types !== 'object' || Array.isArray(a.types)) {
579
+ return 'action.types is required.';
580
+ }
581
+ if (typeof a.primaryType !== 'string' || !a.primaryType) {
582
+ return 'action.primaryType is required.';
583
+ }
584
+ if (!(a.primaryType in a.types)) {
585
+ return `action.primaryType '${a.primaryType}' is not one of the structs in action.types.`;
586
+ }
587
+ if (!a.message || typeof a.message !== 'object' || Array.isArray(a.message)) {
588
+ return 'action.message is required.';
589
+ }
590
+ // Every field the struct declares must be present, or the wallet signs a
591
+ // different message than the one the caller described.
592
+ const fields = a.types[a.primaryType];
593
+ if (!Array.isArray(fields) || fields.length === 0) {
594
+ return `action.types.${a.primaryType} declares no fields.`;
595
+ }
596
+ const missing = fields
597
+ .map(f => f?.name)
598
+ .filter(n => typeof n === 'string' && !(n in a.message));
599
+ if (missing.length) {
600
+ return `action.message is missing: ${missing.join(', ')}`;
601
+ }
602
+ return null;
603
+ }
604
+ /** The action, or the reason it is not one. For text a person typed. */
605
+ function parseTypedAction(text) {
606
+ let raw;
607
+ try {
608
+ raw = JSON.parse(text);
609
+ }
610
+ catch (e) {
611
+ return { error: `Not valid JSON: ${e.message}` };
612
+ }
613
+ const error = validateTypedAction(raw);
614
+ return error ? { error } : { action: raw };
615
+ }
616
+
321
617
  /**
322
618
  * Deep Link utilities for ZKProofport SDK
323
619
  */
@@ -496,9 +792,22 @@ function parseProofResponseUrl(url) {
496
792
  if (!requestId || !status) {
497
793
  return null;
498
794
  }
795
+ // A missing or unrecognised circuit is a REJECTION, not a default.
796
+ //
797
+ // This used to fall back to `coinbase_attestation`. A callback carrying a
798
+ // country or OIDC proof, or a circuit this build has never heard of, was
799
+ // then handed to the caller labelled Coinbase KYC -- the parse succeeded,
800
+ // the label was wrong, and nothing downstream could tell. Whoever reads
801
+ // `response.circuit` decides which verifier and which public-input layout
802
+ // to use, so the wrong label is a proof checked against another circuit's
803
+ // bytes.
804
+ const circuit = urlObj.searchParams.get('circuit');
805
+ if (!circuit || !isCircuitId(circuit)) {
806
+ return null;
807
+ }
499
808
  const response = {
500
809
  requestId,
501
- circuit: urlObj.searchParams.get('circuit') || 'coinbase_attestation',
810
+ circuit,
502
811
  status,
503
812
  };
504
813
  if (status === 'completed') {
@@ -800,15 +1109,15 @@ function validateProofRequest(request) {
800
1109
  if (!request.circuit) {
801
1110
  return { valid: false, error: 'Missing circuit type' };
802
1111
  }
803
- if (![
804
- 'coinbase_attestation',
805
- 'coinbase_country_attestation',
806
- 'oidc_domain_attestation',
807
- 'mdl_kr_ownership',
808
- 'mdl_kr_age',
809
- 'mdl_kr_region',
810
- ].includes(request.circuit)) {
811
- return { valid: false, error: `Invalid circuit type: ${request.circuit}` };
1112
+ // The list is the SDK's own, not a copy. The copy that used to sit here
1113
+ // named six circuits and had missed two — `giwa_attestation` and
1114
+ // `arc_eligibility` — so a link for either was refused as an invalid type
1115
+ // while every other layer accepted it.
1116
+ if (!isCircuitId(request.circuit)) {
1117
+ return {
1118
+ valid: false,
1119
+ error: `Invalid circuit type: ${request.circuit}. Expected one of: ${ALL_CIRCUIT_IDS.join(', ')}`,
1120
+ };
812
1121
  }
813
1122
  if (!request.callbackUrl) {
814
1123
  return { valid: false, error: 'Missing callbackUrl' };
@@ -857,6 +1166,16 @@ function validateProofRequest(request) {
857
1166
  return { valid: false, error: mdlError };
858
1167
  }
859
1168
  }
1169
+ else if (request.circuit === 'arc_eligibility') {
1170
+ const inputs = request.inputs;
1171
+ if (inputs.userAddress && !/^0x[a-fA-F0-9]{40}$/.test(inputs.userAddress)) {
1172
+ return { valid: false, error: 'Invalid userAddress format' };
1173
+ }
1174
+ const actionError = validateTypedAction(inputs.action);
1175
+ if (actionError) {
1176
+ return { valid: false, error: actionError };
1177
+ }
1178
+ }
860
1179
  // Check expiry
861
1180
  if (request.expiresAt && Date.now() > request.expiresAt) {
862
1181
  return { valid: false, error: 'Request has expired' };
@@ -4007,7 +4326,7 @@ function getDefaultProvider(chainId) {
4007
4326
  * This function resolves the verifier contract from SDK config or proof response,
4008
4327
  * connects to the blockchain, and calls the verify() method with the proof and public inputs.
4009
4328
  *
4010
- * @param circuit - The canonical circuit identifier (e.g., "coinbase_attestation")
4329
+ * @param circuit - The canonical circuit identifier. REQUIRED.
4011
4330
  * @param parsedProof - Parsed proof object containing proofHex and publicInputsHex
4012
4331
  * @param providerOrSigner - Optional ethers.js Provider or Signer instance. If not provided, uses default RPC for the chain
4013
4332
  * @param customVerifier - Optional custom verifier contract config (takes priority over responseVerifier)
@@ -4105,7 +4424,7 @@ function requireVerifier(circuit, verifier) {
4105
4424
  /**
4106
4425
  * Get verifier contract address for a circuit.
4107
4426
  *
4108
- * @param circuit - The canonical circuit identifier (e.g., "coinbase_attestation")
4427
+ * @param circuit - The canonical circuit identifier. REQUIRED.
4109
4428
  * @param customVerifier - Optional custom verifier contract config
4110
4429
  * @returns Verifier contract address as hex string
4111
4430
  * @throws Error if no verifier is configured for the circuit
@@ -4122,7 +4441,7 @@ function getVerifierAddress(circuit, customVerifier) {
4122
4441
  /**
4123
4442
  * Get chain ID for a circuit's verifier contract.
4124
4443
  *
4125
- * @param circuit - The canonical circuit identifier (e.g., "coinbase_attestation")
4444
+ * @param circuit - The canonical circuit identifier. REQUIRED.
4126
4445
  * @param customVerifier - Optional custom verifier contract config
4127
4446
  * @returns Chain ID number (e.g., 11155111 for Sepolia, 84532 for Base Sepolia)
4128
4447
  * @throws Error if no verifier is configured for the circuit
@@ -4136,6 +4455,55 @@ function getVerifierAddress(circuit, customVerifier) {
4136
4455
  function getVerifierChainId(circuit, customVerifier) {
4137
4456
  return requireVerifier(circuit, customVerifier).chainId;
4138
4457
  }
4458
+ /**
4459
+ * Where `scope` and `nullifier` sit in each circuit's public inputs.
4460
+ *
4461
+ * Keyed by `CircuitId`, which is the point: adding a circuit to the SDK makes
4462
+ * this object a compile error until somebody opens that circuit's `fn main`
4463
+ * and writes its offsets down. Nothing here can be inferred from the proof —
4464
+ * the public inputs are a flat array of bytes with no framing.
4465
+ *
4466
+ * It replaced an `if / else if / else` chain whose final `else` handed every
4467
+ * unrecognised circuit — and every call that named no circuit at all —
4468
+ * Coinbase's offsets. For `arc_eligibility` that is not a near miss: its
4469
+ * public inputs are signal_hash, domain_separator, action_hash, merkle root,
4470
+ * scope, nullifier, so the nullifier slot under Coinbase's offsets holds the
4471
+ * MERKLE ROOT, a value identical for every user. Duplicate detection built on
4472
+ * it would report one person.
4473
+ */
4474
+ const PUBLIC_INPUT_OFFSETS = Object.freeze({
4475
+ // signal_hash, signer_list_merkle_root, scope, nullifier
4476
+ coinbase_attestation: { scope: [64, 95], nullifier: [96, 127] },
4477
+ // Same four fields in the same order as Coinbase.
4478
+ giwa_attestation: { scope: [64, 95], nullifier: [96, 127] },
4479
+ // country_list, country_list_length and is_included sit before scope.
4480
+ coinbase_country_attestation: { scope: [86, 117], nullifier: [118, 149] },
4481
+ // pubkey_modulus_limbs and the bounded domain sit before scope.
4482
+ oidc_domain_attestation: { scope: [83, 114], nullifier: [115, 146] },
4483
+ // signal_hash, domain_separator, action_hash, merkle root, then the pair.
4484
+ arc_eligibility: { scope: [128, 159], nullifier: [160, 191] },
4485
+ // The Korea mDL circuits open with the pair; signal_hash is commented out.
4486
+ mdl_kr_ownership: { scope: [0, 31], nullifier: [32, 63] },
4487
+ mdl_kr_age: { scope: [0, 31], nullifier: [32, 63] },
4488
+ mdl_kr_region: { scope: [0, 31], nullifier: [32, 63] },
4489
+ });
4490
+ /**
4491
+ * The offsets for a circuit, or an error naming what was asked for.
4492
+ *
4493
+ * Throwing is the whole point. A caller that omits the circuit, or names one
4494
+ * this SDK has never heard of, is asking a question with no answer; returning
4495
+ * a guess produced a scope and a nullifier that belong to different bytes and
4496
+ * looked exactly like real ones.
4497
+ */
4498
+ function offsetsFor(circuit) {
4499
+ if (circuit === undefined) {
4500
+ throw new Error(`A circuit id is required to read public inputs: the layout differs per circuit and cannot be inferred from the proof. Pass one of: ${ALL_CIRCUIT_IDS.join(', ')}`);
4501
+ }
4502
+ if (!isCircuitId(circuit)) {
4503
+ throw new Error(`Unknown circuit '${circuit}'. Expected one of: ${ALL_CIRCUIT_IDS.join(', ')}`);
4504
+ }
4505
+ return PUBLIC_INPUT_OFFSETS[circuit];
4506
+ }
4139
4507
  /**
4140
4508
  * Extract scope value from public inputs array.
4141
4509
  *
@@ -4153,24 +4521,7 @@ function getVerifierChainId(circuit, customVerifier) {
4153
4521
  * ```
4154
4522
  */
4155
4523
  function extractScopeFromPublicInputs(publicInputsHex, circuit) {
4156
- let start, end;
4157
- if (circuit === 'coinbase_country_attestation') {
4158
- start = 86;
4159
- end = 117;
4160
- }
4161
- else if (circuit === 'oidc_domain_attestation') {
4162
- start = 83;
4163
- end = 114;
4164
- }
4165
- else if (circuit !== undefined && circuit.startsWith('mdl_kr_')) {
4166
- // All Korea mDL circuits: scope [0..31], nullifier_value [32..63]
4167
- start = 0;
4168
- end = 31;
4169
- }
4170
- else {
4171
- start = 64;
4172
- end = 95;
4173
- }
4524
+ const [start, end] = offsetsFor(circuit).scope;
4174
4525
  if (publicInputsHex.length <= end)
4175
4526
  return null;
4176
4527
  const scopeFields = publicInputsHex.slice(start, end + 1);
@@ -4185,7 +4536,10 @@ function extractScopeFromPublicInputs(publicInputsHex, circuit) {
4185
4536
  * detection without revealing the wallet address.
4186
4537
  *
4187
4538
  * @param publicInputsHex - Array of hex-encoded field elements
4188
- * @param circuit - Circuit type (defaults to coinbase_attestation)
4539
+ * @param circuit - Circuit id. REQUIRED: the layout differs per circuit and
4540
+ * cannot be inferred from the proof. It used to default to
4541
+ * `coinbase_attestation`, which returned the right-shaped bytes read from
4542
+ * the wrong place for every other circuit.
4189
4543
  * @returns Nullifier as hex string (bytes32), or null if publicInputs too short
4190
4544
  *
4191
4545
  * @example
@@ -4195,24 +4549,7 @@ function extractScopeFromPublicInputs(publicInputsHex, circuit) {
4195
4549
  * ```
4196
4550
  */
4197
4551
  function extractNullifierFromPublicInputs(publicInputsHex, circuit) {
4198
- let start, end;
4199
- if (circuit === 'coinbase_country_attestation') {
4200
- start = 118;
4201
- end = 149;
4202
- }
4203
- else if (circuit === 'oidc_domain_attestation') {
4204
- start = 115;
4205
- end = 146;
4206
- }
4207
- else if (circuit !== undefined && circuit.startsWith('mdl_kr_')) {
4208
- // All Korea mDL circuits: scope [0..31], nullifier_value [32..63]
4209
- start = 32;
4210
- end = 63;
4211
- }
4212
- else {
4213
- start = 96;
4214
- end = 127;
4215
- }
4552
+ const [start, end] = offsetsFor(circuit).nullifier;
4216
4553
  if (publicInputsHex.length <= end)
4217
4554
  return null;
4218
4555
  const nullifierFields = publicInputsHex.slice(start, end + 1);
@@ -4262,174 +4599,6 @@ function reconstructBytes32FromFields(fields) {
4262
4599
  return '0x' + bytes;
4263
4600
  }
4264
4601
 
4265
- /**
4266
- * Canonical ZKProofport circuit identifiers.
4267
- *
4268
- * This module is the single source of truth for circuit IDs. Every other layer
4269
- * — the mobile app, the demo, the relay, server-side provers — reads its list
4270
- * from here instead of keeping a private copy that silently drifts.
4271
- *
4272
- * It has **no imports and no dependencies**, so it can be pulled in on its own:
4273
- *
4274
- * ```typescript
4275
- * import { CIRCUIT_IDS, isCircuitId } from '@zkproofport-app/sdk/circuits';
4276
- * ```
4277
- *
4278
- * The same names are also re-exported from the package root
4279
- * (`@zkproofport-app/sdk`) for callers that already import the SDK.
4280
- *
4281
- * ## Identifiers are canonical and case-sensitive
4282
- *
4283
- * Each ID is the circuit's `name` in its `Nargo.toml`, verbatim. The format is
4284
- * lowercase with underscores. Hyphenated spellings (`coinbase-kyc`,
4285
- * `mdl-kr-age`) are directory or UI route names in other repositories — they
4286
- * are **not** circuit IDs, and passing one produces a nullifier mismatch or a
4287
- * failed on-chain lookup rather than a clear error.
4288
- */
4289
- /**
4290
- * Canonical circuit identifiers, keyed by a stable constant name.
4291
- *
4292
- * Prefer these constants over string literals so a rename is a compile error
4293
- * rather than a runtime surprise.
4294
- *
4295
- * @example
4296
- * ```typescript
4297
- * import { CIRCUIT_IDS } from '@zkproofport-app/sdk/circuits';
4298
- *
4299
- * const relay = await sdk.createRelayRequest(CIRCUIT_IDS.COINBASE_ATTESTATION, {
4300
- * scope: 'myapp.com',
4301
- * });
4302
- * ```
4303
- */
4304
- const CIRCUIT_IDS = Object.freeze({
4305
- /** Coinbase KYC attestation. Officially supported. */
4306
- COINBASE_ATTESTATION: 'coinbase_attestation',
4307
- /** Coinbase country attestation (inclusion / exclusion). Officially supported. */
4308
- COINBASE_COUNTRY_ATTESTATION: 'coinbase_country_attestation',
4309
- /** OIDC email-domain attestation. Officially supported. */
4310
- OIDC_DOMAIN_ATTESTATION: 'oidc_domain_attestation',
4311
- /** GIWA attestation. Planned — not officially supported yet. */
4312
- GIWA_ATTESTATION: 'giwa_attestation',
4313
- /** Korea Mobile ID ownership. Planned — not officially supported yet. */
4314
- MDL_KR_OWNERSHIP: 'mdl_kr_ownership',
4315
- /** Korea Mobile ID age threshold. Planned — not officially supported yet. */
4316
- MDL_KR_AGE: 'mdl_kr_age',
4317
- /** Korea Mobile ID si/do region. Planned — not officially supported yet. */
4318
- MDL_KR_REGION: 'mdl_kr_region',
4319
- });
4320
- /**
4321
- * Support status for every circuit.
4322
- *
4323
- * The record is exhaustive over {@link CircuitId}: adding an identifier without
4324
- * giving it a status is a compile error, so this list cannot drift from
4325
- * {@link CIRCUIT_IDS}.
4326
- *
4327
- * @example
4328
- * ```typescript
4329
- * import { CIRCUIT_SUPPORT_STATUS } from '@zkproofport-app/sdk/circuits';
4330
- *
4331
- * CIRCUIT_SUPPORT_STATUS.coinbase_attestation; // 'supported'
4332
- * CIRCUIT_SUPPORT_STATUS.mdl_kr_age; // 'planned'
4333
- * ```
4334
- */
4335
- const CIRCUIT_SUPPORT_STATUS = Object.freeze({
4336
- coinbase_attestation: 'supported',
4337
- coinbase_country_attestation: 'supported',
4338
- oidc_domain_attestation: 'supported',
4339
- giwa_attestation: 'planned',
4340
- mdl_kr_ownership: 'planned',
4341
- mdl_kr_age: 'planned',
4342
- mdl_kr_region: 'planned',
4343
- });
4344
- /**
4345
- * Where each circuit's verification key lives inside the `zkproofport/circuits`
4346
- * repository, relative to its root.
4347
- *
4348
- * Off-chain verification needs the verification key and nothing else — no
4349
- * circuit bytecode, no witness. The key is public by nature: it is what anyone
4350
- * checking a proof must already have, and the Solidity verifiers deployed
4351
- * on-chain embed the same key.
4352
- *
4353
- * Exhaustive over {@link CircuitId} on purpose. A new circuit whose key path is
4354
- * not listed here fails to compile rather than failing at verification time,
4355
- * where the only symptom would be a proof that cannot be checked.
4356
- */
4357
- const CIRCUIT_VK_PATHS = Object.freeze({
4358
- coinbase_attestation: 'coinbase-attestation/target/vk',
4359
- coinbase_country_attestation: 'coinbase-country-attestation/target/vk',
4360
- oidc_domain_attestation: 'oidc-domain-attestation/target/vk',
4361
- giwa_attestation: 'giwa-attestation/target/vk',
4362
- mdl_kr_ownership: 'mdl/kr-ownership/target/vk',
4363
- mdl_kr_age: 'mdl/kr-age/target/vk',
4364
- mdl_kr_region: 'mdl/kr-region/target/vk',
4365
- });
4366
- /**
4367
- * Every canonical circuit identifier, in declaration order.
4368
- *
4369
- * Includes circuits that are still `planned`. Use {@link SUPPORTED_CIRCUIT_IDS}
4370
- * when you only want the ones that are officially supported today.
4371
- */
4372
- const ALL_CIRCUIT_IDS = Object.freeze(Object.values(CIRCUIT_IDS));
4373
- /**
4374
- * Circuits that are officially supported today: the two Coinbase circuits and
4375
- * OIDC domain attestation.
4376
- *
4377
- * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.
4378
- */
4379
- const SUPPORTED_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'supported'));
4380
- /**
4381
- * Circuits whose identifiers are reserved but which are not officially
4382
- * supported yet: GIWA attestation and the three Korea Mobile ID circuits.
4383
- *
4384
- * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.
4385
- */
4386
- const PLANNED_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'planned'));
4387
- /**
4388
- * Narrows an unknown value to a canonical circuit identifier.
4389
- *
4390
- * Returns `false` for hyphenated route names such as `'coinbase-kyc'`, which
4391
- * are not circuit IDs.
4392
- *
4393
- * @example
4394
- * ```typescript
4395
- * isCircuitId('coinbase_attestation'); // true
4396
- * isCircuitId('coinbase-kyc'); // false
4397
- * ```
4398
- */
4399
- function isCircuitId(value) {
4400
- return typeof value === 'string' && ALL_CIRCUIT_IDS.includes(value);
4401
- }
4402
- /**
4403
- * Narrows an unknown value to a circuit identifier that is officially supported
4404
- * today. Planned circuits return `false`.
4405
- *
4406
- * @example
4407
- * ```typescript
4408
- * isSupportedCircuitId('oidc_domain_attestation'); // true
4409
- * isSupportedCircuitId('mdl_kr_age'); // false — planned
4410
- * ```
4411
- */
4412
- function isSupportedCircuitId(value) {
4413
- return isCircuitId(value) && CIRCUIT_SUPPORT_STATUS[value] === 'supported';
4414
- }
4415
- /**
4416
- * Returns the support status of a circuit.
4417
- *
4418
- * @param circuit - A canonical circuit identifier.
4419
- * @throws Error when the value is not a canonical circuit identifier.
4420
- *
4421
- * @example
4422
- * ```typescript
4423
- * getCircuitSupportStatus('giwa_attestation'); // 'planned'
4424
- * ```
4425
- */
4426
- function getCircuitSupportStatus(circuit) {
4427
- if (!isCircuitId(circuit)) {
4428
- throw new Error(`Unknown circuit '${String(circuit)}'. Expected one of: ${ALL_CIRCUIT_IDS.join(', ')}`);
4429
- }
4430
- return CIRCUIT_SUPPORT_STATUS[circuit];
4431
- }
4432
-
4433
4602
  /**
4434
4603
  * Off-chain proof verification — checking a proof against its verification key,
4435
4604
  * with no chain, no RPC, and no gas.
@@ -5627,11 +5796,83 @@ class ProofportSDK {
5627
5796
  }
5628
5797
  return await response.json();
5629
5798
  }
5799
+ /**
5800
+ * Creates a proof request through the relay server.
5801
+ *
5802
+ * This is the recommended way to create proof requests. The relay server:
5803
+ * - Issues a server-side requestId (validated by the mobile app)
5804
+ * - Tracks request status in Redis
5805
+ * - Builds the deep link with relay callback URL
5806
+ * - Stores inputs hash for deep link integrity verification
5807
+ *
5808
+ * @param circuit - Circuit type identifier
5809
+ * @param inputs - Circuit-specific inputs
5810
+ * @param options - Request options (message, dappName, dappIcon, nonce, returnScheme)
5811
+ * @param options.returnScheme - Which app the ZKProofport app should switch back
5812
+ * to once it has finished. Put **your own** deep link scheme here — the bare
5813
+ * scheme your app registers, e.g. `'mydapp://'`. Only a native app has an
5814
+ * answer to this question; a web app omits the option entirely, because
5815
+ * opening its https origin would launch a new browser tab on a fresh page
5816
+ * rather than return the user to the tab they started from.
5817
+ *
5818
+ * This is intentionally **not** a URL and not a return address: no path, query
5819
+ * string or fragment is accepted, and the ZKProofport app never sends the proof
5820
+ * there. The proof still comes back to you the same way it always did — over the
5821
+ * relay, via `waitForProof()` / `waitForResult()`. `returnScheme` only answers
5822
+ * "which app should come back to the foreground", exactly like the
5823
+ * `redirect.native` value a wallet uses to hand control back after signing.
5824
+ *
5825
+ * **Omit it and the SDK decides for you.** When the request is created from a
5826
+ * page running in Chrome for iOS (`CriOS`) or Firefox for iOS (`FxiOS`), the
5827
+ * SDK fills in `'googlechrome://'` or `'firefox://'` by itself: opening either
5828
+ * scheme bare brings that browser forward without navigating, so the user lands
5829
+ * back on the tab they were already reading. Every other browser sends nothing,
5830
+ * because nothing safe exists to send — Safari has no such scheme, and Brave
5831
+ * and Arc are indistinguishable from Safari on iOS, so guessing would eject the
5832
+ * user into the wrong browser.
5833
+ * On Android nothing is needed at all: the ZKProofport app brings itself to the
5834
+ * back and the browser resumes exactly as it was. When no target is sent and
5835
+ * none can be inferred, the ZKProofport app tells the user the proof was
5836
+ * delivered and lets them switch back themselves.
5837
+ *
5838
+ * There is no auto-switch on proof failure; only on success and on the user
5839
+ * declining the request.
5840
+ *
5841
+ * Rejected with an error before the request is sent: empty or whitespace values,
5842
+ * anything longer than 128 characters, values carrying a host/path/query/fragment
5843
+ * (including https origins such as `'https://myapp.com'`), and the `http:`,
5844
+ * `file:`, `data:`, `javascript:`, `intent:`, `tel:`, `sms:`, `mailto:` families.
5845
+ * @returns Promise resolving to RelayProofRequest with requestId, deepLink, pollUrl
5846
+ * @throws Error if signer not set, returnScheme is malformed, or the relay request fails
5847
+ *
5848
+ * @example
5849
+ * ```typescript
5850
+ * const sdk = ProofportSDK.create();
5851
+ * sdk.setSigner(signer);
5852
+ *
5853
+ * const relay = await sdk.createRelayRequest('coinbase_attestation', {
5854
+ * scope: 'myapp.com'
5855
+ * }, { dappName: 'My DApp', returnScheme: 'mydapp://' });
5856
+ *
5857
+ * // Generate QR code from relay deep link
5858
+ * const qr = await sdk.generateQRCode(relay.deepLink);
5859
+ *
5860
+ * // Wait for proof (WebSocket primary, polling fallback)
5861
+ * const result = await sdk.waitForProof(relay.requestId);
5862
+ * ```
5863
+ */
5864
+ // Which circuits need a wallet signature, from the one record in
5865
+ // src/circuits.ts. The typed-out pair that used to sit here had missed
5866
+ // arc_eligibility, so a dapp calling createRelayRequest() for it with no
5867
+ // signer got no local error and a relay 401 two round trips later.
5868
+ static needsWalletSignature(circuit) {
5869
+ return CIRCUIT_NEEDS_WALLET_SIGNATURE[circuit] === true;
5870
+ }
5630
5871
  async createRelayRequest(circuit, inputs, options = {}) {
5631
5872
  if (!this.relayUrl) {
5632
5873
  throw new Error('relayUrl is required. Set it in ProofportSDK config.');
5633
5874
  }
5634
- const needsSignature = ProofportSDK.WALLET_SIGNATURE_CIRCUITS.includes(circuit);
5875
+ const needsSignature = ProofportSDK.needsWalletSignature(circuit);
5635
5876
  if (needsSignature && !this.signer) {
5636
5877
  throw new Error('Signer not set. Call setSigner() first. Wallet signature is required for this circuit.');
5637
5878
  }
@@ -5643,6 +5884,17 @@ class ProofportSDK {
5643
5884
  throw new Error(`${mdlError} (circuit: ${circuit})`);
5644
5885
  }
5645
5886
  }
5887
+ // arc_eligibility: the action is what the proof binds to, so a request
5888
+ // without a usable one cannot be honoured. Checked here, before a
5889
+ // challenge is burned, because the alternative is a refusal from inside
5890
+ // the mobile app -- a screen the dapp developer never sees, reached after
5891
+ // the person has already scanned a QR code.
5892
+ if (circuit === 'arc_eligibility') {
5893
+ const actionError = validateTypedAction(inputs.action);
5894
+ if (actionError) {
5895
+ throw new Error(`${actionError} (circuit: ${circuit})`);
5896
+ }
5897
+ }
5646
5898
  // Return target: validated before we burn a challenge, so a typo surfaces
5647
5899
  // as a local error instead of a relay 400 on the second round trip.
5648
5900
  if (options.returnScheme !== undefined) {
@@ -5967,76 +6219,6 @@ class ProofportSDK {
5967
6219
  return extractDomainFromPublicInputs(publicInputs, circuit);
5968
6220
  }
5969
6221
  }
5970
- /**
5971
- * Creates a proof request through the relay server.
5972
- *
5973
- * This is the recommended way to create proof requests. The relay server:
5974
- * - Issues a server-side requestId (validated by the mobile app)
5975
- * - Tracks request status in Redis
5976
- * - Builds the deep link with relay callback URL
5977
- * - Stores inputs hash for deep link integrity verification
5978
- *
5979
- * @param circuit - Circuit type identifier
5980
- * @param inputs - Circuit-specific inputs
5981
- * @param options - Request options (message, dappName, dappIcon, nonce, returnScheme)
5982
- * @param options.returnScheme - Which app the ZKProofport app should switch back
5983
- * to once it has finished. Put **your own** deep link scheme here — the bare
5984
- * scheme your app registers, e.g. `'mydapp://'`. Only a native app has an
5985
- * answer to this question; a web app omits the option entirely, because
5986
- * opening its https origin would launch a new browser tab on a fresh page
5987
- * rather than return the user to the tab they started from.
5988
- *
5989
- * This is intentionally **not** a URL and not a return address: no path, query
5990
- * string or fragment is accepted, and the ZKProofport app never sends the proof
5991
- * there. The proof still comes back to you the same way it always did — over the
5992
- * relay, via `waitForProof()` / `waitForResult()`. `returnScheme` only answers
5993
- * "which app should come back to the foreground", exactly like the
5994
- * `redirect.native` value a wallet uses to hand control back after signing.
5995
- *
5996
- * **Omit it and the SDK decides for you.** When the request is created from a
5997
- * page running in Chrome for iOS (`CriOS`) or Firefox for iOS (`FxiOS`), the
5998
- * SDK fills in `'googlechrome://'` or `'firefox://'` by itself: opening either
5999
- * scheme bare brings that browser forward without navigating, so the user lands
6000
- * back on the tab they were already reading. Every other browser sends nothing,
6001
- * because nothing safe exists to send — Safari has no such scheme, and Brave
6002
- * and Arc are indistinguishable from Safari on iOS, so guessing would eject the
6003
- * user into the wrong browser.
6004
- * On Android nothing is needed at all: the ZKProofport app brings itself to the
6005
- * back and the browser resumes exactly as it was. When no target is sent and
6006
- * none can be inferred, the ZKProofport app tells the user the proof was
6007
- * delivered and lets them switch back themselves.
6008
- *
6009
- * There is no auto-switch on proof failure; only on success and on the user
6010
- * declining the request.
6011
- *
6012
- * Rejected with an error before the request is sent: empty or whitespace values,
6013
- * anything longer than 128 characters, values carrying a host/path/query/fragment
6014
- * (including https origins such as `'https://myapp.com'`), and the `http:`,
6015
- * `file:`, `data:`, `javascript:`, `intent:`, `tel:`, `sms:`, `mailto:` families.
6016
- * @returns Promise resolving to RelayProofRequest with requestId, deepLink, pollUrl
6017
- * @throws Error if signer not set, returnScheme is malformed, or the relay request fails
6018
- *
6019
- * @example
6020
- * ```typescript
6021
- * const sdk = ProofportSDK.create();
6022
- * sdk.setSigner(signer);
6023
- *
6024
- * const relay = await sdk.createRelayRequest('coinbase_attestation', {
6025
- * scope: 'myapp.com'
6026
- * }, { dappName: 'My DApp', returnScheme: 'mydapp://' });
6027
- *
6028
- * // Generate QR code from relay deep link
6029
- * const qr = await sdk.generateQRCode(relay.deepLink);
6030
- *
6031
- * // Wait for proof (WebSocket primary, polling fallback)
6032
- * const result = await sdk.waitForProof(relay.requestId);
6033
- * ```
6034
- */
6035
- // Circuits that require wallet signature (used as circuit input)
6036
- ProofportSDK.WALLET_SIGNATURE_CIRCUITS = [
6037
- 'coinbase_attestation',
6038
- 'coinbase_country_attestation',
6039
- ];
6040
6222
 
6041
- export { ALL_CIRCUIT_IDS, CIRCUIT_IDS, CIRCUIT_SUPPORT_STATUS, CIRCUIT_VK_PATHS, COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, 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, verifyProofOffChain };
6223
+ 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 };
6042
6224
  //# sourceMappingURL=index.esm.js.map