@zkproofport-app/sdk 0.2.16 → 0.3.1

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
@@ -84,6 +84,16 @@ const CIRCUIT_METADATA = {
84
84
  publicInputsCount: 148,
85
85
  publicInputNames: ['pubkey_modulus_limbs', 'domain', 'scope', 'nullifier', 'provider'],
86
86
  },
87
+ arc_eligibility: {
88
+ name: 'Arc Eligibility',
89
+ description: 'Prove eligibility and authorize one EIP-712 action, for an Arc gate',
90
+ // 6 x `pub [u8; 32]` in arc-eligibility/src/main.nr, flattened to bytes.
91
+ // Read off the compiled ABI rather than counted: signal_hash 0-31,
92
+ // domain_separator 32-63, action_hash 64-95, signer_list_merkle_root
93
+ // 96-127, scope 128-159, nullifier 160-191.
94
+ publicInputsCount: 192,
95
+ publicInputNames: ['signal_hash', 'domain_separator', 'action_hash', 'signer_list_merkle_root'],
96
+ },
87
97
  // Planned circuit — the identifier is reserved and the circuit exists, but it
88
98
  // is not officially supported yet. See CIRCUIT_SUPPORT_STATUS in ./circuits.
89
99
  // Forked from coinbase_attestation and shares its four public inputs, so the
@@ -162,6 +172,11 @@ const RPC_ENDPOINTS = {
162
172
  84532: 'https://sepolia.base.org', // Base Sepolia
163
173
  8453: 'https://mainnet.base.org', // Base Mainnet
164
174
  91342: 'https://sepolia-rpc.giwa.io', // GIWA Sepolia
175
+ // Arc Testnet. The arc_eligibility verifier is deployed here and nowhere
176
+ // else, and this line was missing on 2026-09-12 — the circuit shipped, a
177
+ // phone produced a valid proof, and on-chain verification answered "No RPC
178
+ // endpoint configured for chain 5042002", which reads like the proof's fault.
179
+ 5042002: 'https://rpc.testnet.arc.io', // Arc Testnet
165
180
  };
166
181
  /**
167
182
  * Default proof request expiration time in milliseconds.
@@ -323,6 +338,292 @@ const OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT = {
323
338
  DOMAIN_END: 82,
324
339
  };
325
340
 
341
+ /**
342
+ * Canonical ZKProofport circuit identifiers.
343
+ *
344
+ * This module is the single source of truth for circuit IDs. Every other layer
345
+ * — the mobile app, the demo, the relay, server-side provers — reads its list
346
+ * from here instead of keeping a private copy that silently drifts.
347
+ *
348
+ * It has **no imports and no dependencies**, so it can be pulled in on its own:
349
+ *
350
+ * ```typescript
351
+ * import { CIRCUIT_IDS, isCircuitId } from '@zkproofport-app/sdk/circuits';
352
+ * ```
353
+ *
354
+ * The same names are also re-exported from the package root
355
+ * (`@zkproofport-app/sdk`) for callers that already import the SDK.
356
+ *
357
+ * ## Identifiers are canonical and case-sensitive
358
+ *
359
+ * Each ID is the circuit's `name` in its `Nargo.toml`, verbatim. The format is
360
+ * lowercase with underscores. Hyphenated spellings (`coinbase-kyc`,
361
+ * `mdl-kr-age`) are directory or UI route names in other repositories — they
362
+ * are **not** circuit IDs, and passing one produces a nullifier mismatch or a
363
+ * failed on-chain lookup rather than a clear error.
364
+ */
365
+ /**
366
+ * Canonical circuit identifiers, keyed by a stable constant name.
367
+ *
368
+ * Prefer these constants over string literals so a rename is a compile error
369
+ * rather than a runtime surprise.
370
+ *
371
+ * @example
372
+ * ```typescript
373
+ * import { CIRCUIT_IDS } from '@zkproofport-app/sdk/circuits';
374
+ *
375
+ * const relay = await sdk.createRelayRequest(CIRCUIT_IDS.COINBASE_ATTESTATION, {
376
+ * scope: 'myapp.com',
377
+ * });
378
+ * ```
379
+ */
380
+ const CIRCUIT_IDS = Object.freeze({
381
+ /** Coinbase KYC attestation. Officially supported. */
382
+ COINBASE_ATTESTATION: 'coinbase_attestation',
383
+ /** Coinbase country attestation (inclusion / exclusion). Officially supported. */
384
+ COINBASE_COUNTRY_ATTESTATION: 'coinbase_country_attestation',
385
+ /** OIDC email-domain attestation. Officially supported. */
386
+ OIDC_DOMAIN_ATTESTATION: 'oidc_domain_attestation',
387
+ /** Arc eligibility: one EIP-712 action, authorized. Officially supported. */
388
+ ARC_ELIGIBILITY: 'arc_eligibility',
389
+ /** GIWA attestation. Planned — not officially supported yet. */
390
+ GIWA_ATTESTATION: 'giwa_attestation',
391
+ /** Korea Mobile ID ownership. Planned — not officially supported yet. */
392
+ MDL_KR_OWNERSHIP: 'mdl_kr_ownership',
393
+ /** Korea Mobile ID age threshold. Planned — not officially supported yet. */
394
+ MDL_KR_AGE: 'mdl_kr_age',
395
+ /** Korea Mobile ID si/do region. Planned — not officially supported yet. */
396
+ MDL_KR_REGION: 'mdl_kr_region',
397
+ });
398
+ /**
399
+ * Support status for every circuit.
400
+ *
401
+ * The record is exhaustive over {@link CircuitId}: adding an identifier without
402
+ * giving it a status is a compile error, so this list cannot drift from
403
+ * {@link CIRCUIT_IDS}.
404
+ *
405
+ * @example
406
+ * ```typescript
407
+ * import { CIRCUIT_SUPPORT_STATUS } from '@zkproofport-app/sdk/circuits';
408
+ *
409
+ * CIRCUIT_SUPPORT_STATUS.coinbase_attestation; // 'supported'
410
+ * CIRCUIT_SUPPORT_STATUS.mdl_kr_age; // 'planned'
411
+ * ```
412
+ */
413
+ const CIRCUIT_SUPPORT_STATUS = Object.freeze({
414
+ coinbase_attestation: 'supported',
415
+ coinbase_country_attestation: 'supported',
416
+ oidc_domain_attestation: 'supported',
417
+ arc_eligibility: 'experimental',
418
+ giwa_attestation: 'planned',
419
+ mdl_kr_ownership: 'planned',
420
+ mdl_kr_age: 'planned',
421
+ mdl_kr_region: 'planned',
422
+ });
423
+ /**
424
+ * Where each circuit's verification key FILE lives inside the
425
+ * `zkproofport/circuits` repository, relative to its root.
426
+ *
427
+ * Off-chain verification needs the verification key and nothing else — no
428
+ * circuit bytecode, no witness. The key is public by nature: it is what anyone
429
+ * checking a proof must already have, and the Solidity verifiers deployed
430
+ * on-chain embed the same key.
431
+ *
432
+ * NOTE THE TRAILING `/vk` TWICE. In that repository `target/vk` is a DIRECTORY
433
+ * holding `vk`, `vk_hash` and `SHA256SUMS`; the key itself is `target/vk/vk`.
434
+ * These paths were first copied here from the mobile app, which keeps the
435
+ * directory and the filename as two fields and joins them — so taking only the
436
+ * directory produced a URL that answers 404, and the first browser to try
437
+ * off-chain verification said "Could not fetch the verification key (HTTP 404)".
438
+ *
439
+ * Exhaustive over {@link CircuitId} on purpose. A new circuit whose key path is
440
+ * not listed here fails to compile rather than failing at verification time,
441
+ * where the only symptom would be a proof that cannot be checked.
442
+ */
443
+ const CIRCUIT_VK_PATHS = Object.freeze({
444
+ coinbase_attestation: 'coinbase-attestation/target/vk/vk',
445
+ coinbase_country_attestation: 'coinbase-country-attestation/target/vk/vk',
446
+ oidc_domain_attestation: 'oidc-domain-attestation/target/vk/vk',
447
+ arc_eligibility: 'arc-eligibility/target/vk/vk',
448
+ giwa_attestation: 'giwa-attestation/target/vk/vk',
449
+ mdl_kr_ownership: 'mdl/kr-ownership/target/vk/vk',
450
+ mdl_kr_age: 'mdl/kr-age/target/vk/vk',
451
+ mdl_kr_region: 'mdl/kr-region/target/vk/vk',
452
+ });
453
+ /**
454
+ * Every canonical circuit identifier, in declaration order.
455
+ *
456
+ * Includes circuits that are still `planned`. Use {@link SUPPORTED_CIRCUIT_IDS}
457
+ * when you only want the ones that are officially supported today.
458
+ */
459
+ const ALL_CIRCUIT_IDS = Object.freeze(Object.values(CIRCUIT_IDS));
460
+ /**
461
+ * Circuits that are officially supported today: the two Coinbase circuits and
462
+ * OIDC domain attestation.
463
+ *
464
+ * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.
465
+ */
466
+ const SUPPORTED_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'supported'));
467
+ /**
468
+ * Circuits whose identifiers are reserved but which are not officially
469
+ * supported yet: GIWA attestation and the three Korea Mobile ID circuits.
470
+ *
471
+ * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.
472
+ */
473
+ /**
474
+ * Circuits you can prove today, on a testnet, with everything subject to change.
475
+ *
476
+ * `arc_eligibility` is the reason this status exists. Calling it `planned`
477
+ * would be wrong — the circuit compiles, the verifier is deployed and a proof
478
+ * verifies against it — and calling it `supported` would be worse, because the
479
+ * only deployment is Arc Testnet, Circle has not published a mainnet chain id,
480
+ * and the public-input layout is still open. Neither existing word was true, so
481
+ * the honest move was a third word rather than picking the closer lie.
482
+ */
483
+ const EXPERIMENTAL_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter(id => CIRCUIT_SUPPORT_STATUS[id] === 'experimental'));
484
+ const PLANNED_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'planned'));
485
+ /**
486
+ * Narrows an unknown value to a canonical circuit identifier.
487
+ *
488
+ * Returns `false` for hyphenated route names such as `'coinbase-kyc'`, which
489
+ * are not circuit IDs.
490
+ *
491
+ * @example
492
+ * ```typescript
493
+ * isCircuitId('coinbase_attestation'); // true
494
+ * isCircuitId('coinbase-kyc'); // false
495
+ * ```
496
+ */
497
+ function isCircuitId(value) {
498
+ return typeof value === 'string' && ALL_CIRCUIT_IDS.includes(value);
499
+ }
500
+ /**
501
+ * Narrows an unknown value to a circuit identifier that is officially supported
502
+ * today. Planned circuits return `false`.
503
+ *
504
+ * @example
505
+ * ```typescript
506
+ * isSupportedCircuitId('oidc_domain_attestation'); // true
507
+ * isSupportedCircuitId('mdl_kr_age'); // false — planned
508
+ * ```
509
+ */
510
+ function isSupportedCircuitId(value) {
511
+ return isCircuitId(value) && CIRCUIT_SUPPORT_STATUS[value] === 'supported';
512
+ }
513
+ /**
514
+ * Returns the support status of a circuit.
515
+ *
516
+ * @param circuit - A canonical circuit identifier.
517
+ * @throws Error when the value is not a canonical circuit identifier.
518
+ *
519
+ * @example
520
+ * ```typescript
521
+ * getCircuitSupportStatus('giwa_attestation'); // 'planned'
522
+ * ```
523
+ */
524
+ function getCircuitSupportStatus(circuit) {
525
+ if (!isCircuitId(circuit)) {
526
+ throw new Error(`Unknown circuit '${String(circuit)}'. Expected one of: ${ALL_CIRCUIT_IDS.join(', ')}`);
527
+ }
528
+ return CIRCUIT_SUPPORT_STATUS[circuit];
529
+ }
530
+ /**
531
+ * Whether a circuit's relay request must carry a wallet signature.
532
+ *
533
+ * The SDK checks this to fail fast — "Signer not set" locally beats a 401 from
534
+ * the relay two round trips later, with nothing saying which circuit needed
535
+ * what. The relay declares its OWN copy on purpose and does not import this
536
+ * one: it is a security boundary, and an auth policy that a published package
537
+ * can change is not a policy. A test in the relay asserts the two agree.
538
+ *
539
+ * `satisfies` and not an annotation, so a circuit added above is a compile
540
+ * error here until somebody decides which it is.
541
+ */
542
+ const CIRCUIT_NEEDS_WALLET_SIGNATURE = {
543
+ coinbase_attestation: true,
544
+ coinbase_country_attestation: true,
545
+ // Same Coinbase-attested wallet as above; what changes is that it signs an
546
+ // EIP-712 action rather than signal_hash.
547
+ arc_eligibility: true,
548
+ // The signature is inside the OIDC identity token, checked by the prover.
549
+ oidc_domain_attestation: false,
550
+ // Web2 (OmniOne CX) flows with no wallet binding at all.
551
+ mdl_kr_ownership: false,
552
+ mdl_kr_age: false,
553
+ mdl_kr_region: false,
554
+ giwa_attestation: false,
555
+ };
556
+
557
+ const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
558
+ /**
559
+ * The reason the value is not a usable action, or `null` when it is one.
560
+ *
561
+ * A string rather than a boolean because every caller shows it to somebody —
562
+ * a dapp developer reading a validation error, or a person looking at the
563
+ * mobile app's demo screen.
564
+ */
565
+ function validateTypedAction(value) {
566
+ if (value === undefined || value === null) {
567
+ 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.';
568
+ }
569
+ if (typeof value !== 'object' || Array.isArray(value)) {
570
+ return 'action must be an object.';
571
+ }
572
+ const a = value;
573
+ const d = a.domain;
574
+ if (!d || typeof d !== 'object') {
575
+ return 'action.domain is required.';
576
+ }
577
+ for (const k of ['name', 'version', 'verifyingContract']) {
578
+ if (typeof d[k] !== 'string' || !d[k]) {
579
+ return `action.domain.${k} must be a non-empty string.`;
580
+ }
581
+ }
582
+ if (typeof d.chainId !== 'number' || !Number.isInteger(d.chainId)) {
583
+ return 'action.domain.chainId must be an integer.';
584
+ }
585
+ if (!ADDRESS.test(d.verifyingContract)) {
586
+ return 'action.domain.verifyingContract must be a 20-byte hex address.';
587
+ }
588
+ if (!a.types || typeof a.types !== 'object' || Array.isArray(a.types)) {
589
+ return 'action.types is required.';
590
+ }
591
+ if (typeof a.primaryType !== 'string' || !a.primaryType) {
592
+ return 'action.primaryType is required.';
593
+ }
594
+ if (!(a.primaryType in a.types)) {
595
+ return `action.primaryType '${a.primaryType}' is not one of the structs in action.types.`;
596
+ }
597
+ if (!a.message || typeof a.message !== 'object' || Array.isArray(a.message)) {
598
+ return 'action.message is required.';
599
+ }
600
+ // Every field the struct declares must be present, or the wallet signs a
601
+ // different message than the one the caller described.
602
+ const fields = a.types[a.primaryType];
603
+ if (!Array.isArray(fields) || fields.length === 0) {
604
+ return `action.types.${a.primaryType} declares no fields.`;
605
+ }
606
+ const missing = fields
607
+ .map(f => f?.name)
608
+ .filter(n => typeof n === 'string' && !(n in a.message));
609
+ if (missing.length) {
610
+ return `action.message is missing: ${missing.join(', ')}`;
611
+ }
612
+ return null;
613
+ }
614
+ /** The action, or the reason it is not one. For text a person typed. */
615
+ function parseTypedAction(text) {
616
+ let raw;
617
+ try {
618
+ raw = JSON.parse(text);
619
+ }
620
+ catch (e) {
621
+ return { error: `Not valid JSON: ${e.message}` };
622
+ }
623
+ const error = validateTypedAction(raw);
624
+ return error ? { error } : { action: raw };
625
+ }
626
+
326
627
  /**
327
628
  * Deep Link utilities for ZKProofport SDK
328
629
  */
@@ -501,9 +802,22 @@ function parseProofResponseUrl(url) {
501
802
  if (!requestId || !status) {
502
803
  return null;
503
804
  }
805
+ // A missing or unrecognised circuit is a REJECTION, not a default.
806
+ //
807
+ // This used to fall back to `coinbase_attestation`. A callback carrying a
808
+ // country or OIDC proof, or a circuit this build has never heard of, was
809
+ // then handed to the caller labelled Coinbase KYC -- the parse succeeded,
810
+ // the label was wrong, and nothing downstream could tell. Whoever reads
811
+ // `response.circuit` decides which verifier and which public-input layout
812
+ // to use, so the wrong label is a proof checked against another circuit's
813
+ // bytes.
814
+ const circuit = urlObj.searchParams.get('circuit');
815
+ if (!circuit || !isCircuitId(circuit)) {
816
+ return null;
817
+ }
504
818
  const response = {
505
819
  requestId,
506
- circuit: urlObj.searchParams.get('circuit') || 'coinbase_attestation',
820
+ circuit,
507
821
  status,
508
822
  };
509
823
  if (status === 'completed') {
@@ -805,15 +1119,15 @@ function validateProofRequest(request) {
805
1119
  if (!request.circuit) {
806
1120
  return { valid: false, error: 'Missing circuit type' };
807
1121
  }
808
- if (![
809
- 'coinbase_attestation',
810
- 'coinbase_country_attestation',
811
- 'oidc_domain_attestation',
812
- 'mdl_kr_ownership',
813
- 'mdl_kr_age',
814
- 'mdl_kr_region',
815
- ].includes(request.circuit)) {
816
- return { valid: false, error: `Invalid circuit type: ${request.circuit}` };
1122
+ // The list is the SDK's own, not a copy. The copy that used to sit here
1123
+ // named six circuits and had missed two — `giwa_attestation` and
1124
+ // `arc_eligibility` — so a link for either was refused as an invalid type
1125
+ // while every other layer accepted it.
1126
+ if (!isCircuitId(request.circuit)) {
1127
+ return {
1128
+ valid: false,
1129
+ error: `Invalid circuit type: ${request.circuit}. Expected one of: ${ALL_CIRCUIT_IDS.join(', ')}`,
1130
+ };
817
1131
  }
818
1132
  if (!request.callbackUrl) {
819
1133
  return { valid: false, error: 'Missing callbackUrl' };
@@ -862,6 +1176,16 @@ function validateProofRequest(request) {
862
1176
  return { valid: false, error: mdlError };
863
1177
  }
864
1178
  }
1179
+ else if (request.circuit === 'arc_eligibility') {
1180
+ const inputs = request.inputs;
1181
+ if (inputs.userAddress && !/^0x[a-fA-F0-9]{40}$/.test(inputs.userAddress)) {
1182
+ return { valid: false, error: 'Invalid userAddress format' };
1183
+ }
1184
+ const actionError = validateTypedAction(inputs.action);
1185
+ if (actionError) {
1186
+ return { valid: false, error: actionError };
1187
+ }
1188
+ }
865
1189
  // Check expiry
866
1190
  if (request.expiresAt && Date.now() > request.expiresAt) {
867
1191
  return { valid: false, error: 'Request has expired' };
@@ -1145,7 +1469,7 @@ function getDefaultProvider(chainId) {
1145
1469
  * This function resolves the verifier contract from SDK config or proof response,
1146
1470
  * connects to the blockchain, and calls the verify() method with the proof and public inputs.
1147
1471
  *
1148
- * @param circuit - The canonical circuit identifier (e.g., "coinbase_attestation")
1472
+ * @param circuit - The canonical circuit identifier. REQUIRED.
1149
1473
  * @param parsedProof - Parsed proof object containing proofHex and publicInputsHex
1150
1474
  * @param providerOrSigner - Optional ethers.js Provider or Signer instance. If not provided, uses default RPC for the chain
1151
1475
  * @param customVerifier - Optional custom verifier contract config (takes priority over responseVerifier)
@@ -1243,7 +1567,7 @@ function requireVerifier(circuit, verifier) {
1243
1567
  /**
1244
1568
  * Get verifier contract address for a circuit.
1245
1569
  *
1246
- * @param circuit - The canonical circuit identifier (e.g., "coinbase_attestation")
1570
+ * @param circuit - The canonical circuit identifier. REQUIRED.
1247
1571
  * @param customVerifier - Optional custom verifier contract config
1248
1572
  * @returns Verifier contract address as hex string
1249
1573
  * @throws Error if no verifier is configured for the circuit
@@ -1260,7 +1584,7 @@ function getVerifierAddress(circuit, customVerifier) {
1260
1584
  /**
1261
1585
  * Get chain ID for a circuit's verifier contract.
1262
1586
  *
1263
- * @param circuit - The canonical circuit identifier (e.g., "coinbase_attestation")
1587
+ * @param circuit - The canonical circuit identifier. REQUIRED.
1264
1588
  * @param customVerifier - Optional custom verifier contract config
1265
1589
  * @returns Chain ID number (e.g., 11155111 for Sepolia, 84532 for Base Sepolia)
1266
1590
  * @throws Error if no verifier is configured for the circuit
@@ -1274,6 +1598,55 @@ function getVerifierAddress(circuit, customVerifier) {
1274
1598
  function getVerifierChainId(circuit, customVerifier) {
1275
1599
  return requireVerifier(circuit, customVerifier).chainId;
1276
1600
  }
1601
+ /**
1602
+ * Where `scope` and `nullifier` sit in each circuit's public inputs.
1603
+ *
1604
+ * Keyed by `CircuitId`, which is the point: adding a circuit to the SDK makes
1605
+ * this object a compile error until somebody opens that circuit's `fn main`
1606
+ * and writes its offsets down. Nothing here can be inferred from the proof —
1607
+ * the public inputs are a flat array of bytes with no framing.
1608
+ *
1609
+ * It replaced an `if / else if / else` chain whose final `else` handed every
1610
+ * unrecognised circuit — and every call that named no circuit at all —
1611
+ * Coinbase's offsets. For `arc_eligibility` that is not a near miss: its
1612
+ * public inputs are signal_hash, domain_separator, action_hash, merkle root,
1613
+ * scope, nullifier, so the nullifier slot under Coinbase's offsets holds the
1614
+ * MERKLE ROOT, a value identical for every user. Duplicate detection built on
1615
+ * it would report one person.
1616
+ */
1617
+ const PUBLIC_INPUT_OFFSETS = Object.freeze({
1618
+ // signal_hash, signer_list_merkle_root, scope, nullifier
1619
+ coinbase_attestation: { scope: [64, 95], nullifier: [96, 127] },
1620
+ // Same four fields in the same order as Coinbase.
1621
+ giwa_attestation: { scope: [64, 95], nullifier: [96, 127] },
1622
+ // country_list, country_list_length and is_included sit before scope.
1623
+ coinbase_country_attestation: { scope: [86, 117], nullifier: [118, 149] },
1624
+ // pubkey_modulus_limbs and the bounded domain sit before scope.
1625
+ oidc_domain_attestation: { scope: [83, 114], nullifier: [115, 146] },
1626
+ // signal_hash, domain_separator, action_hash, merkle root, then the pair.
1627
+ arc_eligibility: { scope: [128, 159], nullifier: [160, 191] },
1628
+ // The Korea mDL circuits open with the pair; signal_hash is commented out.
1629
+ mdl_kr_ownership: { scope: [0, 31], nullifier: [32, 63] },
1630
+ mdl_kr_age: { scope: [0, 31], nullifier: [32, 63] },
1631
+ mdl_kr_region: { scope: [0, 31], nullifier: [32, 63] },
1632
+ });
1633
+ /**
1634
+ * The offsets for a circuit, or an error naming what was asked for.
1635
+ *
1636
+ * Throwing is the whole point. A caller that omits the circuit, or names one
1637
+ * this SDK has never heard of, is asking a question with no answer; returning
1638
+ * a guess produced a scope and a nullifier that belong to different bytes and
1639
+ * looked exactly like real ones.
1640
+ */
1641
+ function offsetsFor(circuit) {
1642
+ if (circuit === undefined) {
1643
+ 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(', ')}`);
1644
+ }
1645
+ if (!isCircuitId(circuit)) {
1646
+ throw new Error(`Unknown circuit '${circuit}'. Expected one of: ${ALL_CIRCUIT_IDS.join(', ')}`);
1647
+ }
1648
+ return PUBLIC_INPUT_OFFSETS[circuit];
1649
+ }
1277
1650
  /**
1278
1651
  * Extract scope value from public inputs array.
1279
1652
  *
@@ -1291,24 +1664,7 @@ function getVerifierChainId(circuit, customVerifier) {
1291
1664
  * ```
1292
1665
  */
1293
1666
  function extractScopeFromPublicInputs(publicInputsHex, circuit) {
1294
- let start, end;
1295
- if (circuit === 'coinbase_country_attestation') {
1296
- start = 86;
1297
- end = 117;
1298
- }
1299
- else if (circuit === 'oidc_domain_attestation') {
1300
- start = 83;
1301
- end = 114;
1302
- }
1303
- else if (circuit !== undefined && circuit.startsWith('mdl_kr_')) {
1304
- // All Korea mDL circuits: scope [0..31], nullifier_value [32..63]
1305
- start = 0;
1306
- end = 31;
1307
- }
1308
- else {
1309
- start = 64;
1310
- end = 95;
1311
- }
1667
+ const [start, end] = offsetsFor(circuit).scope;
1312
1668
  if (publicInputsHex.length <= end)
1313
1669
  return null;
1314
1670
  const scopeFields = publicInputsHex.slice(start, end + 1);
@@ -1323,7 +1679,10 @@ function extractScopeFromPublicInputs(publicInputsHex, circuit) {
1323
1679
  * detection without revealing the wallet address.
1324
1680
  *
1325
1681
  * @param publicInputsHex - Array of hex-encoded field elements
1326
- * @param circuit - Circuit type (defaults to coinbase_attestation)
1682
+ * @param circuit - Circuit id. REQUIRED: the layout differs per circuit and
1683
+ * cannot be inferred from the proof. It used to default to
1684
+ * `coinbase_attestation`, which returned the right-shaped bytes read from
1685
+ * the wrong place for every other circuit.
1327
1686
  * @returns Nullifier as hex string (bytes32), or null if publicInputs too short
1328
1687
  *
1329
1688
  * @example
@@ -1333,24 +1692,7 @@ function extractScopeFromPublicInputs(publicInputsHex, circuit) {
1333
1692
  * ```
1334
1693
  */
1335
1694
  function extractNullifierFromPublicInputs(publicInputsHex, circuit) {
1336
- let start, end;
1337
- if (circuit === 'coinbase_country_attestation') {
1338
- start = 118;
1339
- end = 149;
1340
- }
1341
- else if (circuit === 'oidc_domain_attestation') {
1342
- start = 115;
1343
- end = 146;
1344
- }
1345
- else if (circuit !== undefined && circuit.startsWith('mdl_kr_')) {
1346
- // All Korea mDL circuits: scope [0..31], nullifier_value [32..63]
1347
- start = 32;
1348
- end = 63;
1349
- }
1350
- else {
1351
- start = 96;
1352
- end = 127;
1353
- }
1695
+ const [start, end] = offsetsFor(circuit).nullifier;
1354
1696
  if (publicInputsHex.length <= end)
1355
1697
  return null;
1356
1698
  const nullifierFields = publicInputsHex.slice(start, end + 1);
@@ -1400,181 +1742,6 @@ function reconstructBytes32FromFields(fields) {
1400
1742
  return '0x' + bytes;
1401
1743
  }
1402
1744
 
1403
- /**
1404
- * Canonical ZKProofport circuit identifiers.
1405
- *
1406
- * This module is the single source of truth for circuit IDs. Every other layer
1407
- * — the mobile app, the demo, the relay, server-side provers — reads its list
1408
- * from here instead of keeping a private copy that silently drifts.
1409
- *
1410
- * It has **no imports and no dependencies**, so it can be pulled in on its own:
1411
- *
1412
- * ```typescript
1413
- * import { CIRCUIT_IDS, isCircuitId } from '@zkproofport-app/sdk/circuits';
1414
- * ```
1415
- *
1416
- * The same names are also re-exported from the package root
1417
- * (`@zkproofport-app/sdk`) for callers that already import the SDK.
1418
- *
1419
- * ## Identifiers are canonical and case-sensitive
1420
- *
1421
- * Each ID is the circuit's `name` in its `Nargo.toml`, verbatim. The format is
1422
- * lowercase with underscores. Hyphenated spellings (`coinbase-kyc`,
1423
- * `mdl-kr-age`) are directory or UI route names in other repositories — they
1424
- * are **not** circuit IDs, and passing one produces a nullifier mismatch or a
1425
- * failed on-chain lookup rather than a clear error.
1426
- */
1427
- /**
1428
- * Canonical circuit identifiers, keyed by a stable constant name.
1429
- *
1430
- * Prefer these constants over string literals so a rename is a compile error
1431
- * rather than a runtime surprise.
1432
- *
1433
- * @example
1434
- * ```typescript
1435
- * import { CIRCUIT_IDS } from '@zkproofport-app/sdk/circuits';
1436
- *
1437
- * const relay = await sdk.createRelayRequest(CIRCUIT_IDS.COINBASE_ATTESTATION, {
1438
- * scope: 'myapp.com',
1439
- * });
1440
- * ```
1441
- */
1442
- const CIRCUIT_IDS = Object.freeze({
1443
- /** Coinbase KYC attestation. Officially supported. */
1444
- COINBASE_ATTESTATION: 'coinbase_attestation',
1445
- /** Coinbase country attestation (inclusion / exclusion). Officially supported. */
1446
- COINBASE_COUNTRY_ATTESTATION: 'coinbase_country_attestation',
1447
- /** OIDC email-domain attestation. Officially supported. */
1448
- OIDC_DOMAIN_ATTESTATION: 'oidc_domain_attestation',
1449
- /** GIWA attestation. Planned — not officially supported yet. */
1450
- GIWA_ATTESTATION: 'giwa_attestation',
1451
- /** Korea Mobile ID ownership. Planned — not officially supported yet. */
1452
- MDL_KR_OWNERSHIP: 'mdl_kr_ownership',
1453
- /** Korea Mobile ID age threshold. Planned — not officially supported yet. */
1454
- MDL_KR_AGE: 'mdl_kr_age',
1455
- /** Korea Mobile ID si/do region. Planned — not officially supported yet. */
1456
- MDL_KR_REGION: 'mdl_kr_region',
1457
- });
1458
- /**
1459
- * Support status for every circuit.
1460
- *
1461
- * The record is exhaustive over {@link CircuitId}: adding an identifier without
1462
- * giving it a status is a compile error, so this list cannot drift from
1463
- * {@link CIRCUIT_IDS}.
1464
- *
1465
- * @example
1466
- * ```typescript
1467
- * import { CIRCUIT_SUPPORT_STATUS } from '@zkproofport-app/sdk/circuits';
1468
- *
1469
- * CIRCUIT_SUPPORT_STATUS.coinbase_attestation; // 'supported'
1470
- * CIRCUIT_SUPPORT_STATUS.mdl_kr_age; // 'planned'
1471
- * ```
1472
- */
1473
- const CIRCUIT_SUPPORT_STATUS = Object.freeze({
1474
- coinbase_attestation: 'supported',
1475
- coinbase_country_attestation: 'supported',
1476
- oidc_domain_attestation: 'supported',
1477
- giwa_attestation: 'planned',
1478
- mdl_kr_ownership: 'planned',
1479
- mdl_kr_age: 'planned',
1480
- mdl_kr_region: 'planned',
1481
- });
1482
- /**
1483
- * Where each circuit's verification key FILE lives inside the
1484
- * `zkproofport/circuits` repository, relative to its root.
1485
- *
1486
- * Off-chain verification needs the verification key and nothing else — no
1487
- * circuit bytecode, no witness. The key is public by nature: it is what anyone
1488
- * checking a proof must already have, and the Solidity verifiers deployed
1489
- * on-chain embed the same key.
1490
- *
1491
- * NOTE THE TRAILING `/vk` TWICE. In that repository `target/vk` is a DIRECTORY
1492
- * holding `vk`, `vk_hash` and `SHA256SUMS`; the key itself is `target/vk/vk`.
1493
- * These paths were first copied here from the mobile app, which keeps the
1494
- * directory and the filename as two fields and joins them — so taking only the
1495
- * directory produced a URL that answers 404, and the first browser to try
1496
- * off-chain verification said "Could not fetch the verification key (HTTP 404)".
1497
- *
1498
- * Exhaustive over {@link CircuitId} on purpose. A new circuit whose key path is
1499
- * not listed here fails to compile rather than failing at verification time,
1500
- * where the only symptom would be a proof that cannot be checked.
1501
- */
1502
- const CIRCUIT_VK_PATHS = Object.freeze({
1503
- coinbase_attestation: 'coinbase-attestation/target/vk/vk',
1504
- coinbase_country_attestation: 'coinbase-country-attestation/target/vk/vk',
1505
- oidc_domain_attestation: 'oidc-domain-attestation/target/vk/vk',
1506
- giwa_attestation: 'giwa-attestation/target/vk/vk',
1507
- mdl_kr_ownership: 'mdl/kr-ownership/target/vk/vk',
1508
- mdl_kr_age: 'mdl/kr-age/target/vk/vk',
1509
- mdl_kr_region: 'mdl/kr-region/target/vk/vk',
1510
- });
1511
- /**
1512
- * Every canonical circuit identifier, in declaration order.
1513
- *
1514
- * Includes circuits that are still `planned`. Use {@link SUPPORTED_CIRCUIT_IDS}
1515
- * when you only want the ones that are officially supported today.
1516
- */
1517
- const ALL_CIRCUIT_IDS = Object.freeze(Object.values(CIRCUIT_IDS));
1518
- /**
1519
- * Circuits that are officially supported today: the two Coinbase circuits and
1520
- * OIDC domain attestation.
1521
- *
1522
- * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.
1523
- */
1524
- const SUPPORTED_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'supported'));
1525
- /**
1526
- * Circuits whose identifiers are reserved but which are not officially
1527
- * supported yet: GIWA attestation and the three Korea Mobile ID circuits.
1528
- *
1529
- * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.
1530
- */
1531
- const PLANNED_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'planned'));
1532
- /**
1533
- * Narrows an unknown value to a canonical circuit identifier.
1534
- *
1535
- * Returns `false` for hyphenated route names such as `'coinbase-kyc'`, which
1536
- * are not circuit IDs.
1537
- *
1538
- * @example
1539
- * ```typescript
1540
- * isCircuitId('coinbase_attestation'); // true
1541
- * isCircuitId('coinbase-kyc'); // false
1542
- * ```
1543
- */
1544
- function isCircuitId(value) {
1545
- return typeof value === 'string' && ALL_CIRCUIT_IDS.includes(value);
1546
- }
1547
- /**
1548
- * Narrows an unknown value to a circuit identifier that is officially supported
1549
- * today. Planned circuits return `false`.
1550
- *
1551
- * @example
1552
- * ```typescript
1553
- * isSupportedCircuitId('oidc_domain_attestation'); // true
1554
- * isSupportedCircuitId('mdl_kr_age'); // false — planned
1555
- * ```
1556
- */
1557
- function isSupportedCircuitId(value) {
1558
- return isCircuitId(value) && CIRCUIT_SUPPORT_STATUS[value] === 'supported';
1559
- }
1560
- /**
1561
- * Returns the support status of a circuit.
1562
- *
1563
- * @param circuit - A canonical circuit identifier.
1564
- * @throws Error when the value is not a canonical circuit identifier.
1565
- *
1566
- * @example
1567
- * ```typescript
1568
- * getCircuitSupportStatus('giwa_attestation'); // 'planned'
1569
- * ```
1570
- */
1571
- function getCircuitSupportStatus(circuit) {
1572
- if (!isCircuitId(circuit)) {
1573
- throw new Error(`Unknown circuit '${String(circuit)}'. Expected one of: ${ALL_CIRCUIT_IDS.join(', ')}`);
1574
- }
1575
- return CIRCUIT_SUPPORT_STATUS[circuit];
1576
- }
1577
-
1578
1745
  /**
1579
1746
  * Off-chain proof verification — checking a proof against its verification key,
1580
1747
  * with no chain, no RPC, and no gas.
@@ -2772,11 +2939,83 @@ class ProofportSDK {
2772
2939
  }
2773
2940
  return await response.json();
2774
2941
  }
2942
+ /**
2943
+ * Creates a proof request through the relay server.
2944
+ *
2945
+ * This is the recommended way to create proof requests. The relay server:
2946
+ * - Issues a server-side requestId (validated by the mobile app)
2947
+ * - Tracks request status in Redis
2948
+ * - Builds the deep link with relay callback URL
2949
+ * - Stores inputs hash for deep link integrity verification
2950
+ *
2951
+ * @param circuit - Circuit type identifier
2952
+ * @param inputs - Circuit-specific inputs
2953
+ * @param options - Request options (message, dappName, dappIcon, nonce, returnScheme)
2954
+ * @param options.returnScheme - Which app the ZKProofport app should switch back
2955
+ * to once it has finished. Put **your own** deep link scheme here — the bare
2956
+ * scheme your app registers, e.g. `'mydapp://'`. Only a native app has an
2957
+ * answer to this question; a web app omits the option entirely, because
2958
+ * opening its https origin would launch a new browser tab on a fresh page
2959
+ * rather than return the user to the tab they started from.
2960
+ *
2961
+ * This is intentionally **not** a URL and not a return address: no path, query
2962
+ * string or fragment is accepted, and the ZKProofport app never sends the proof
2963
+ * there. The proof still comes back to you the same way it always did — over the
2964
+ * relay, via `waitForProof()` / `waitForResult()`. `returnScheme` only answers
2965
+ * "which app should come back to the foreground", exactly like the
2966
+ * `redirect.native` value a wallet uses to hand control back after signing.
2967
+ *
2968
+ * **Omit it and the SDK decides for you.** When the request is created from a
2969
+ * page running in Chrome for iOS (`CriOS`) or Firefox for iOS (`FxiOS`), the
2970
+ * SDK fills in `'googlechrome://'` or `'firefox://'` by itself: opening either
2971
+ * scheme bare brings that browser forward without navigating, so the user lands
2972
+ * back on the tab they were already reading. Every other browser sends nothing,
2973
+ * because nothing safe exists to send — Safari has no such scheme, and Brave
2974
+ * and Arc are indistinguishable from Safari on iOS, so guessing would eject the
2975
+ * user into the wrong browser.
2976
+ * On Android nothing is needed at all: the ZKProofport app brings itself to the
2977
+ * back and the browser resumes exactly as it was. When no target is sent and
2978
+ * none can be inferred, the ZKProofport app tells the user the proof was
2979
+ * delivered and lets them switch back themselves.
2980
+ *
2981
+ * There is no auto-switch on proof failure; only on success and on the user
2982
+ * declining the request.
2983
+ *
2984
+ * Rejected with an error before the request is sent: empty or whitespace values,
2985
+ * anything longer than 128 characters, values carrying a host/path/query/fragment
2986
+ * (including https origins such as `'https://myapp.com'`), and the `http:`,
2987
+ * `file:`, `data:`, `javascript:`, `intent:`, `tel:`, `sms:`, `mailto:` families.
2988
+ * @returns Promise resolving to RelayProofRequest with requestId, deepLink, pollUrl
2989
+ * @throws Error if signer not set, returnScheme is malformed, or the relay request fails
2990
+ *
2991
+ * @example
2992
+ * ```typescript
2993
+ * const sdk = ProofportSDK.create();
2994
+ * sdk.setSigner(signer);
2995
+ *
2996
+ * const relay = await sdk.createRelayRequest('coinbase_attestation', {
2997
+ * scope: 'myapp.com'
2998
+ * }, { dappName: 'My DApp', returnScheme: 'mydapp://' });
2999
+ *
3000
+ * // Generate QR code from relay deep link
3001
+ * const qr = await sdk.generateQRCode(relay.deepLink);
3002
+ *
3003
+ * // Wait for proof (WebSocket primary, polling fallback)
3004
+ * const result = await sdk.waitForProof(relay.requestId);
3005
+ * ```
3006
+ */
3007
+ // Which circuits need a wallet signature, from the one record in
3008
+ // src/circuits.ts. The typed-out pair that used to sit here had missed
3009
+ // arc_eligibility, so a dapp calling createRelayRequest() for it with no
3010
+ // signer got no local error and a relay 401 two round trips later.
3011
+ static needsWalletSignature(circuit) {
3012
+ return CIRCUIT_NEEDS_WALLET_SIGNATURE[circuit] === true;
3013
+ }
2775
3014
  async createRelayRequest(circuit, inputs, options = {}) {
2776
3015
  if (!this.relayUrl) {
2777
3016
  throw new Error('relayUrl is required. Set it in ProofportSDK config.');
2778
3017
  }
2779
- const needsSignature = ProofportSDK.WALLET_SIGNATURE_CIRCUITS.includes(circuit);
3018
+ const needsSignature = ProofportSDK.needsWalletSignature(circuit);
2780
3019
  if (needsSignature && !this.signer) {
2781
3020
  throw new Error('Signer not set. Call setSigner() first. Wallet signature is required for this circuit.');
2782
3021
  }
@@ -2788,6 +3027,17 @@ class ProofportSDK {
2788
3027
  throw new Error(`${mdlError} (circuit: ${circuit})`);
2789
3028
  }
2790
3029
  }
3030
+ // arc_eligibility: the action is what the proof binds to, so a request
3031
+ // without a usable one cannot be honoured. Checked here, before a
3032
+ // challenge is burned, because the alternative is a refusal from inside
3033
+ // the mobile app -- a screen the dapp developer never sees, reached after
3034
+ // the person has already scanned a QR code.
3035
+ if (circuit === 'arc_eligibility') {
3036
+ const actionError = validateTypedAction(inputs.action);
3037
+ if (actionError) {
3038
+ throw new Error(`${actionError} (circuit: ${circuit})`);
3039
+ }
3040
+ }
2791
3041
  // Return target: validated before we burn a challenge, so a typo surfaces
2792
3042
  // as a local error instead of a relay 400 on the second round trip.
2793
3043
  if (options.returnScheme !== undefined) {
@@ -3112,76 +3362,6 @@ class ProofportSDK {
3112
3362
  return extractDomainFromPublicInputs(publicInputs, circuit);
3113
3363
  }
3114
3364
  }
3115
- /**
3116
- * Creates a proof request through the relay server.
3117
- *
3118
- * This is the recommended way to create proof requests. The relay server:
3119
- * - Issues a server-side requestId (validated by the mobile app)
3120
- * - Tracks request status in Redis
3121
- * - Builds the deep link with relay callback URL
3122
- * - Stores inputs hash for deep link integrity verification
3123
- *
3124
- * @param circuit - Circuit type identifier
3125
- * @param inputs - Circuit-specific inputs
3126
- * @param options - Request options (message, dappName, dappIcon, nonce, returnScheme)
3127
- * @param options.returnScheme - Which app the ZKProofport app should switch back
3128
- * to once it has finished. Put **your own** deep link scheme here — the bare
3129
- * scheme your app registers, e.g. `'mydapp://'`. Only a native app has an
3130
- * answer to this question; a web app omits the option entirely, because
3131
- * opening its https origin would launch a new browser tab on a fresh page
3132
- * rather than return the user to the tab they started from.
3133
- *
3134
- * This is intentionally **not** a URL and not a return address: no path, query
3135
- * string or fragment is accepted, and the ZKProofport app never sends the proof
3136
- * there. The proof still comes back to you the same way it always did — over the
3137
- * relay, via `waitForProof()` / `waitForResult()`. `returnScheme` only answers
3138
- * "which app should come back to the foreground", exactly like the
3139
- * `redirect.native` value a wallet uses to hand control back after signing.
3140
- *
3141
- * **Omit it and the SDK decides for you.** When the request is created from a
3142
- * page running in Chrome for iOS (`CriOS`) or Firefox for iOS (`FxiOS`), the
3143
- * SDK fills in `'googlechrome://'` or `'firefox://'` by itself: opening either
3144
- * scheme bare brings that browser forward without navigating, so the user lands
3145
- * back on the tab they were already reading. Every other browser sends nothing,
3146
- * because nothing safe exists to send — Safari has no such scheme, and Brave
3147
- * and Arc are indistinguishable from Safari on iOS, so guessing would eject the
3148
- * user into the wrong browser.
3149
- * On Android nothing is needed at all: the ZKProofport app brings itself to the
3150
- * back and the browser resumes exactly as it was. When no target is sent and
3151
- * none can be inferred, the ZKProofport app tells the user the proof was
3152
- * delivered and lets them switch back themselves.
3153
- *
3154
- * There is no auto-switch on proof failure; only on success and on the user
3155
- * declining the request.
3156
- *
3157
- * Rejected with an error before the request is sent: empty or whitespace values,
3158
- * anything longer than 128 characters, values carrying a host/path/query/fragment
3159
- * (including https origins such as `'https://myapp.com'`), and the `http:`,
3160
- * `file:`, `data:`, `javascript:`, `intent:`, `tel:`, `sms:`, `mailto:` families.
3161
- * @returns Promise resolving to RelayProofRequest with requestId, deepLink, pollUrl
3162
- * @throws Error if signer not set, returnScheme is malformed, or the relay request fails
3163
- *
3164
- * @example
3165
- * ```typescript
3166
- * const sdk = ProofportSDK.create();
3167
- * sdk.setSigner(signer);
3168
- *
3169
- * const relay = await sdk.createRelayRequest('coinbase_attestation', {
3170
- * scope: 'myapp.com'
3171
- * }, { dappName: 'My DApp', returnScheme: 'mydapp://' });
3172
- *
3173
- * // Generate QR code from relay deep link
3174
- * const qr = await sdk.generateQRCode(relay.deepLink);
3175
- *
3176
- * // Wait for proof (WebSocket primary, polling fallback)
3177
- * const result = await sdk.waitForProof(relay.requestId);
3178
- * ```
3179
- */
3180
- // Circuits that require wallet signature (used as circuit input)
3181
- ProofportSDK.WALLET_SIGNATURE_CIRCUITS = [
3182
- 'coinbase_attestation',
3183
- 'coinbase_country_attestation',
3184
- ];
3185
3365
 
3186
3366
  exports.ALL_CIRCUIT_IDS = ALL_CIRCUIT_IDS;
3187
3367
  exports.CIRCUIT_IDS = CIRCUIT_IDS;
@@ -3189,6 +3369,7 @@ exports.CIRCUIT_SUPPORT_STATUS = CIRCUIT_SUPPORT_STATUS;
3189
3369
  exports.CIRCUIT_VK_PATHS = CIRCUIT_VK_PATHS;
3190
3370
  exports.COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT = COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT;
3191
3371
  exports.COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT = COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT;
3372
+ exports.EXPERIMENTAL_CIRCUIT_IDS = EXPERIMENTAL_CIRCUIT_IDS;
3192
3373
  exports.MDL_KR_PUBLIC_INPUT_LAYOUT = MDL_KR_PUBLIC_INPUT_LAYOUT;
3193
3374
  exports.OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT = OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT;
3194
3375
  exports.PLANNED_CIRCUIT_IDS = PLANNED_CIRCUIT_IDS;
@@ -3201,5 +3382,7 @@ exports.extractScopeFromPublicInputs = extractScopeFromPublicInputs;
3201
3382
  exports.getCircuitSupportStatus = getCircuitSupportStatus;
3202
3383
  exports.isCircuitId = isCircuitId;
3203
3384
  exports.isSupportedCircuitId = isSupportedCircuitId;
3385
+ exports.parseTypedAction = parseTypedAction;
3386
+ exports.validateTypedAction = validateTypedAction;
3204
3387
  exports.verifyProofOffChain = verifyProofOffChain;
3205
3388
  //# sourceMappingURL=index.js.map