@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.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
@@ -157,6 +167,11 @@ const RPC_ENDPOINTS = {
157
167
  84532: 'https://sepolia.base.org', // Base Sepolia
158
168
  8453: 'https://mainnet.base.org', // Base Mainnet
159
169
  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
160
175
  };
161
176
  /**
162
177
  * Default proof request expiration time in milliseconds.
@@ -318,6 +333,292 @@ const OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT = {
318
333
  DOMAIN_END: 82,
319
334
  };
320
335
 
336
+ /**
337
+ * Canonical ZKProofport circuit identifiers.
338
+ *
339
+ * This module is the single source of truth for circuit IDs. Every other layer
340
+ * — the mobile app, the demo, the relay, server-side provers — reads its list
341
+ * from here instead of keeping a private copy that silently drifts.
342
+ *
343
+ * It has **no imports and no dependencies**, so it can be pulled in on its own:
344
+ *
345
+ * ```typescript
346
+ * import { CIRCUIT_IDS, isCircuitId } from '@zkproofport-app/sdk/circuits';
347
+ * ```
348
+ *
349
+ * The same names are also re-exported from the package root
350
+ * (`@zkproofport-app/sdk`) for callers that already import the SDK.
351
+ *
352
+ * ## Identifiers are canonical and case-sensitive
353
+ *
354
+ * Each ID is the circuit's `name` in its `Nargo.toml`, verbatim. The format is
355
+ * lowercase with underscores. Hyphenated spellings (`coinbase-kyc`,
356
+ * `mdl-kr-age`) are directory or UI route names in other repositories — they
357
+ * are **not** circuit IDs, and passing one produces a nullifier mismatch or a
358
+ * failed on-chain lookup rather than a clear error.
359
+ */
360
+ /**
361
+ * Canonical circuit identifiers, keyed by a stable constant name.
362
+ *
363
+ * Prefer these constants over string literals so a rename is a compile error
364
+ * rather than a runtime surprise.
365
+ *
366
+ * @example
367
+ * ```typescript
368
+ * import { CIRCUIT_IDS } from '@zkproofport-app/sdk/circuits';
369
+ *
370
+ * const relay = await sdk.createRelayRequest(CIRCUIT_IDS.COINBASE_ATTESTATION, {
371
+ * scope: 'myapp.com',
372
+ * });
373
+ * ```
374
+ */
375
+ const CIRCUIT_IDS = Object.freeze({
376
+ /** Coinbase KYC attestation. Officially supported. */
377
+ COINBASE_ATTESTATION: 'coinbase_attestation',
378
+ /** Coinbase country attestation (inclusion / exclusion). Officially supported. */
379
+ COINBASE_COUNTRY_ATTESTATION: 'coinbase_country_attestation',
380
+ /** OIDC email-domain attestation. Officially supported. */
381
+ OIDC_DOMAIN_ATTESTATION: 'oidc_domain_attestation',
382
+ /** Arc eligibility: one EIP-712 action, authorized. Officially supported. */
383
+ ARC_ELIGIBILITY: 'arc_eligibility',
384
+ /** GIWA attestation. Planned — not officially supported yet. */
385
+ GIWA_ATTESTATION: 'giwa_attestation',
386
+ /** Korea Mobile ID ownership. Planned — not officially supported yet. */
387
+ MDL_KR_OWNERSHIP: 'mdl_kr_ownership',
388
+ /** Korea Mobile ID age threshold. Planned — not officially supported yet. */
389
+ MDL_KR_AGE: 'mdl_kr_age',
390
+ /** Korea Mobile ID si/do region. Planned — not officially supported yet. */
391
+ MDL_KR_REGION: 'mdl_kr_region',
392
+ });
393
+ /**
394
+ * Support status for every circuit.
395
+ *
396
+ * The record is exhaustive over {@link CircuitId}: adding an identifier without
397
+ * giving it a status is a compile error, so this list cannot drift from
398
+ * {@link CIRCUIT_IDS}.
399
+ *
400
+ * @example
401
+ * ```typescript
402
+ * import { CIRCUIT_SUPPORT_STATUS } from '@zkproofport-app/sdk/circuits';
403
+ *
404
+ * CIRCUIT_SUPPORT_STATUS.coinbase_attestation; // 'supported'
405
+ * CIRCUIT_SUPPORT_STATUS.mdl_kr_age; // 'planned'
406
+ * ```
407
+ */
408
+ const CIRCUIT_SUPPORT_STATUS = Object.freeze({
409
+ coinbase_attestation: 'supported',
410
+ coinbase_country_attestation: 'supported',
411
+ oidc_domain_attestation: 'supported',
412
+ arc_eligibility: 'experimental',
413
+ giwa_attestation: 'planned',
414
+ mdl_kr_ownership: 'planned',
415
+ mdl_kr_age: 'planned',
416
+ mdl_kr_region: 'planned',
417
+ });
418
+ /**
419
+ * Where each circuit's verification key FILE lives inside the
420
+ * `zkproofport/circuits` repository, relative to its root.
421
+ *
422
+ * Off-chain verification needs the verification key and nothing else — no
423
+ * circuit bytecode, no witness. The key is public by nature: it is what anyone
424
+ * checking a proof must already have, and the Solidity verifiers deployed
425
+ * on-chain embed the same key.
426
+ *
427
+ * NOTE THE TRAILING `/vk` TWICE. In that repository `target/vk` is a DIRECTORY
428
+ * holding `vk`, `vk_hash` and `SHA256SUMS`; the key itself is `target/vk/vk`.
429
+ * These paths were first copied here from the mobile app, which keeps the
430
+ * directory and the filename as two fields and joins them — so taking only the
431
+ * directory produced a URL that answers 404, and the first browser to try
432
+ * off-chain verification said "Could not fetch the verification key (HTTP 404)".
433
+ *
434
+ * Exhaustive over {@link CircuitId} on purpose. A new circuit whose key path is
435
+ * not listed here fails to compile rather than failing at verification time,
436
+ * where the only symptom would be a proof that cannot be checked.
437
+ */
438
+ const CIRCUIT_VK_PATHS = Object.freeze({
439
+ coinbase_attestation: 'coinbase-attestation/target/vk/vk',
440
+ coinbase_country_attestation: 'coinbase-country-attestation/target/vk/vk',
441
+ oidc_domain_attestation: 'oidc-domain-attestation/target/vk/vk',
442
+ arc_eligibility: 'arc-eligibility/target/vk/vk',
443
+ giwa_attestation: 'giwa-attestation/target/vk/vk',
444
+ mdl_kr_ownership: 'mdl/kr-ownership/target/vk/vk',
445
+ mdl_kr_age: 'mdl/kr-age/target/vk/vk',
446
+ mdl_kr_region: 'mdl/kr-region/target/vk/vk',
447
+ });
448
+ /**
449
+ * Every canonical circuit identifier, in declaration order.
450
+ *
451
+ * Includes circuits that are still `planned`. Use {@link SUPPORTED_CIRCUIT_IDS}
452
+ * when you only want the ones that are officially supported today.
453
+ */
454
+ const ALL_CIRCUIT_IDS = Object.freeze(Object.values(CIRCUIT_IDS));
455
+ /**
456
+ * Circuits that are officially supported today: the two Coinbase circuits and
457
+ * OIDC domain attestation.
458
+ *
459
+ * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.
460
+ */
461
+ const SUPPORTED_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'supported'));
462
+ /**
463
+ * Circuits whose identifiers are reserved but which are not officially
464
+ * supported yet: GIWA attestation and the three Korea Mobile ID circuits.
465
+ *
466
+ * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.
467
+ */
468
+ /**
469
+ * Circuits you can prove today, on a testnet, with everything subject to change.
470
+ *
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.
477
+ */
478
+ const EXPERIMENTAL_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter(id => CIRCUIT_SUPPORT_STATUS[id] === 'experimental'));
479
+ const PLANNED_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'planned'));
480
+ /**
481
+ * Narrows an unknown value to a canonical circuit identifier.
482
+ *
483
+ * Returns `false` for hyphenated route names such as `'coinbase-kyc'`, which
484
+ * are not circuit IDs.
485
+ *
486
+ * @example
487
+ * ```typescript
488
+ * isCircuitId('coinbase_attestation'); // true
489
+ * isCircuitId('coinbase-kyc'); // false
490
+ * ```
491
+ */
492
+ function isCircuitId(value) {
493
+ return typeof value === 'string' && ALL_CIRCUIT_IDS.includes(value);
494
+ }
495
+ /**
496
+ * Narrows an unknown value to a circuit identifier that is officially supported
497
+ * today. Planned circuits return `false`.
498
+ *
499
+ * @example
500
+ * ```typescript
501
+ * isSupportedCircuitId('oidc_domain_attestation'); // true
502
+ * isSupportedCircuitId('mdl_kr_age'); // false — planned
503
+ * ```
504
+ */
505
+ function isSupportedCircuitId(value) {
506
+ return isCircuitId(value) && CIRCUIT_SUPPORT_STATUS[value] === 'supported';
507
+ }
508
+ /**
509
+ * Returns the support status of a circuit.
510
+ *
511
+ * @param circuit - A canonical circuit identifier.
512
+ * @throws Error when the value is not a canonical circuit identifier.
513
+ *
514
+ * @example
515
+ * ```typescript
516
+ * getCircuitSupportStatus('giwa_attestation'); // 'planned'
517
+ * ```
518
+ */
519
+ function getCircuitSupportStatus(circuit) {
520
+ if (!isCircuitId(circuit)) {
521
+ throw new Error(`Unknown circuit '${String(circuit)}'. Expected one of: ${ALL_CIRCUIT_IDS.join(', ')}`);
522
+ }
523
+ return CIRCUIT_SUPPORT_STATUS[circuit];
524
+ }
525
+ /**
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.
533
+ *
534
+ * `satisfies` and not an annotation, so a circuit added above is a compile
535
+ * error here until somebody decides which it is.
536
+ */
537
+ const CIRCUIT_NEEDS_WALLET_SIGNATURE = {
538
+ coinbase_attestation: true,
539
+ 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
+ // The signature is inside the OIDC identity token, checked by the prover.
544
+ oidc_domain_attestation: false,
545
+ // Web2 (OmniOne CX) flows with no wallet binding at all.
546
+ mdl_kr_ownership: false,
547
+ mdl_kr_age: false,
548
+ mdl_kr_region: false,
549
+ giwa_attestation: false,
550
+ };
551
+
552
+ const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
553
+ /**
554
+ * The reason the value is not a usable action, or `null` when it is one.
555
+ *
556
+ * A string rather than a boolean because every caller shows it to somebody —
557
+ * a dapp developer reading a validation error, or a person looking at the
558
+ * mobile app's demo screen.
559
+ */
560
+ function validateTypedAction(value) {
561
+ 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.';
563
+ }
564
+ if (typeof value !== 'object' || Array.isArray(value)) {
565
+ return 'action must be an object.';
566
+ }
567
+ const a = value;
568
+ const d = a.domain;
569
+ if (!d || typeof d !== 'object') {
570
+ return 'action.domain is required.';
571
+ }
572
+ for (const k of ['name', 'version', 'verifyingContract']) {
573
+ if (typeof d[k] !== 'string' || !d[k]) {
574
+ return `action.domain.${k} must be a non-empty string.`;
575
+ }
576
+ }
577
+ if (typeof d.chainId !== 'number' || !Number.isInteger(d.chainId)) {
578
+ return 'action.domain.chainId must be an integer.';
579
+ }
580
+ if (!ADDRESS.test(d.verifyingContract)) {
581
+ return 'action.domain.verifyingContract must be a 20-byte hex address.';
582
+ }
583
+ if (!a.types || typeof a.types !== 'object' || Array.isArray(a.types)) {
584
+ return 'action.types is required.';
585
+ }
586
+ if (typeof a.primaryType !== 'string' || !a.primaryType) {
587
+ return 'action.primaryType is required.';
588
+ }
589
+ if (!(a.primaryType in a.types)) {
590
+ return `action.primaryType '${a.primaryType}' is not one of the structs in action.types.`;
591
+ }
592
+ if (!a.message || typeof a.message !== 'object' || Array.isArray(a.message)) {
593
+ return 'action.message is required.';
594
+ }
595
+ // Every field the struct declares must be present, or the wallet signs a
596
+ // different message than the one the caller described.
597
+ const fields = a.types[a.primaryType];
598
+ if (!Array.isArray(fields) || fields.length === 0) {
599
+ return `action.types.${a.primaryType} declares no fields.`;
600
+ }
601
+ const missing = fields
602
+ .map(f => f?.name)
603
+ .filter(n => typeof n === 'string' && !(n in a.message));
604
+ if (missing.length) {
605
+ return `action.message is missing: ${missing.join(', ')}`;
606
+ }
607
+ return null;
608
+ }
609
+ /** The action, or the reason it is not one. For text a person typed. */
610
+ function parseTypedAction(text) {
611
+ let raw;
612
+ try {
613
+ raw = JSON.parse(text);
614
+ }
615
+ catch (e) {
616
+ return { error: `Not valid JSON: ${e.message}` };
617
+ }
618
+ const error = validateTypedAction(raw);
619
+ return error ? { error } : { action: raw };
620
+ }
621
+
321
622
  /**
322
623
  * Deep Link utilities for ZKProofport SDK
323
624
  */
@@ -496,9 +797,22 @@ function parseProofResponseUrl(url) {
496
797
  if (!requestId || !status) {
497
798
  return null;
498
799
  }
800
+ // A missing or unrecognised circuit is a REJECTION, not a default.
801
+ //
802
+ // This used to fall back to `coinbase_attestation`. A callback carrying a
803
+ // country or OIDC proof, or a circuit this build has never heard of, was
804
+ // then handed to the caller labelled Coinbase KYC -- the parse succeeded,
805
+ // the label was wrong, and nothing downstream could tell. Whoever reads
806
+ // `response.circuit` decides which verifier and which public-input layout
807
+ // to use, so the wrong label is a proof checked against another circuit's
808
+ // bytes.
809
+ const circuit = urlObj.searchParams.get('circuit');
810
+ if (!circuit || !isCircuitId(circuit)) {
811
+ return null;
812
+ }
499
813
  const response = {
500
814
  requestId,
501
- circuit: urlObj.searchParams.get('circuit') || 'coinbase_attestation',
815
+ circuit,
502
816
  status,
503
817
  };
504
818
  if (status === 'completed') {
@@ -800,15 +1114,15 @@ function validateProofRequest(request) {
800
1114
  if (!request.circuit) {
801
1115
  return { valid: false, error: 'Missing circuit type' };
802
1116
  }
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}` };
1117
+ // 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
1120
+ // while every other layer accepted it.
1121
+ if (!isCircuitId(request.circuit)) {
1122
+ return {
1123
+ valid: false,
1124
+ error: `Invalid circuit type: ${request.circuit}. Expected one of: ${ALL_CIRCUIT_IDS.join(', ')}`,
1125
+ };
812
1126
  }
813
1127
  if (!request.callbackUrl) {
814
1128
  return { valid: false, error: 'Missing callbackUrl' };
@@ -857,6 +1171,16 @@ function validateProofRequest(request) {
857
1171
  return { valid: false, error: mdlError };
858
1172
  }
859
1173
  }
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' };
1178
+ }
1179
+ const actionError = validateTypedAction(inputs.action);
1180
+ if (actionError) {
1181
+ return { valid: false, error: actionError };
1182
+ }
1183
+ }
860
1184
  // Check expiry
861
1185
  if (request.expiresAt && Date.now() > request.expiresAt) {
862
1186
  return { valid: false, error: 'Request has expired' };
@@ -4007,7 +4331,7 @@ function getDefaultProvider(chainId) {
4007
4331
  * This function resolves the verifier contract from SDK config or proof response,
4008
4332
  * connects to the blockchain, and calls the verify() method with the proof and public inputs.
4009
4333
  *
4010
- * @param circuit - The canonical circuit identifier (e.g., "coinbase_attestation")
4334
+ * @param circuit - The canonical circuit identifier. REQUIRED.
4011
4335
  * @param parsedProof - Parsed proof object containing proofHex and publicInputsHex
4012
4336
  * @param providerOrSigner - Optional ethers.js Provider or Signer instance. If not provided, uses default RPC for the chain
4013
4337
  * @param customVerifier - Optional custom verifier contract config (takes priority over responseVerifier)
@@ -4105,7 +4429,7 @@ function requireVerifier(circuit, verifier) {
4105
4429
  /**
4106
4430
  * Get verifier contract address for a circuit.
4107
4431
  *
4108
- * @param circuit - The canonical circuit identifier (e.g., "coinbase_attestation")
4432
+ * @param circuit - The canonical circuit identifier. REQUIRED.
4109
4433
  * @param customVerifier - Optional custom verifier contract config
4110
4434
  * @returns Verifier contract address as hex string
4111
4435
  * @throws Error if no verifier is configured for the circuit
@@ -4122,7 +4446,7 @@ function getVerifierAddress(circuit, customVerifier) {
4122
4446
  /**
4123
4447
  * Get chain ID for a circuit's verifier contract.
4124
4448
  *
4125
- * @param circuit - The canonical circuit identifier (e.g., "coinbase_attestation")
4449
+ * @param circuit - The canonical circuit identifier. REQUIRED.
4126
4450
  * @param customVerifier - Optional custom verifier contract config
4127
4451
  * @returns Chain ID number (e.g., 11155111 for Sepolia, 84532 for Base Sepolia)
4128
4452
  * @throws Error if no verifier is configured for the circuit
@@ -4136,6 +4460,55 @@ function getVerifierAddress(circuit, customVerifier) {
4136
4460
  function getVerifierChainId(circuit, customVerifier) {
4137
4461
  return requireVerifier(circuit, customVerifier).chainId;
4138
4462
  }
4463
+ /**
4464
+ * Where `scope` and `nullifier` sit in each circuit's public inputs.
4465
+ *
4466
+ * Keyed by `CircuitId`, which is the point: adding a circuit to the SDK makes
4467
+ * this object a compile error until somebody opens that circuit's `fn main`
4468
+ * and writes its offsets down. Nothing here can be inferred from the proof —
4469
+ * the public inputs are a flat array of bytes with no framing.
4470
+ *
4471
+ * It replaced an `if / else if / else` chain whose final `else` handed every
4472
+ * 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
4474
+ * public inputs are signal_hash, domain_separator, action_hash, merkle root,
4475
+ * scope, nullifier, so the nullifier slot under Coinbase's offsets holds the
4476
+ * MERKLE ROOT, a value identical for every user. Duplicate detection built on
4477
+ * it would report one person.
4478
+ */
4479
+ const PUBLIC_INPUT_OFFSETS = Object.freeze({
4480
+ // signal_hash, signer_list_merkle_root, scope, nullifier
4481
+ 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] },
4484
+ // country_list, country_list_length and is_included sit before scope.
4485
+ coinbase_country_attestation: { scope: [86, 117], nullifier: [118, 149] },
4486
+ // pubkey_modulus_limbs and the bounded domain sit before scope.
4487
+ 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
+ // The Korea mDL circuits open with the pair; signal_hash is commented out.
4491
+ mdl_kr_ownership: { scope: [0, 31], nullifier: [32, 63] },
4492
+ mdl_kr_age: { scope: [0, 31], nullifier: [32, 63] },
4493
+ mdl_kr_region: { scope: [0, 31], nullifier: [32, 63] },
4494
+ });
4495
+ /**
4496
+ * The offsets for a circuit, or an error naming what was asked for.
4497
+ *
4498
+ * Throwing is the whole point. A caller that omits the circuit, or names one
4499
+ * this SDK has never heard of, is asking a question with no answer; returning
4500
+ * a guess produced a scope and a nullifier that belong to different bytes and
4501
+ * looked exactly like real ones.
4502
+ */
4503
+ function offsetsFor(circuit) {
4504
+ if (circuit === undefined) {
4505
+ 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(', ')}`);
4506
+ }
4507
+ if (!isCircuitId(circuit)) {
4508
+ throw new Error(`Unknown circuit '${circuit}'. Expected one of: ${ALL_CIRCUIT_IDS.join(', ')}`);
4509
+ }
4510
+ return PUBLIC_INPUT_OFFSETS[circuit];
4511
+ }
4139
4512
  /**
4140
4513
  * Extract scope value from public inputs array.
4141
4514
  *
@@ -4153,24 +4526,7 @@ function getVerifierChainId(circuit, customVerifier) {
4153
4526
  * ```
4154
4527
  */
4155
4528
  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
- }
4529
+ const [start, end] = offsetsFor(circuit).scope;
4174
4530
  if (publicInputsHex.length <= end)
4175
4531
  return null;
4176
4532
  const scopeFields = publicInputsHex.slice(start, end + 1);
@@ -4185,7 +4541,10 @@ function extractScopeFromPublicInputs(publicInputsHex, circuit) {
4185
4541
  * detection without revealing the wallet address.
4186
4542
  *
4187
4543
  * @param publicInputsHex - Array of hex-encoded field elements
4188
- * @param circuit - Circuit type (defaults to coinbase_attestation)
4544
+ * @param circuit - Circuit id. REQUIRED: the layout differs per circuit and
4545
+ * cannot be inferred from the proof. It used to default to
4546
+ * `coinbase_attestation`, which returned the right-shaped bytes read from
4547
+ * the wrong place for every other circuit.
4189
4548
  * @returns Nullifier as hex string (bytes32), or null if publicInputs too short
4190
4549
  *
4191
4550
  * @example
@@ -4195,24 +4554,7 @@ function extractScopeFromPublicInputs(publicInputsHex, circuit) {
4195
4554
  * ```
4196
4555
  */
4197
4556
  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
- }
4557
+ const [start, end] = offsetsFor(circuit).nullifier;
4216
4558
  if (publicInputsHex.length <= end)
4217
4559
  return null;
4218
4560
  const nullifierFields = publicInputsHex.slice(start, end + 1);
@@ -4262,181 +4604,6 @@ function reconstructBytes32FromFields(fields) {
4262
4604
  return '0x' + bytes;
4263
4605
  }
4264
4606
 
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 FILE lives inside the
4346
- * `zkproofport/circuits` 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
- * NOTE THE TRAILING `/vk` TWICE. In that repository `target/vk` is a DIRECTORY
4354
- * holding `vk`, `vk_hash` and `SHA256SUMS`; the key itself is `target/vk/vk`.
4355
- * These paths were first copied here from the mobile app, which keeps the
4356
- * directory and the filename as two fields and joins them — so taking only the
4357
- * directory produced a URL that answers 404, and the first browser to try
4358
- * off-chain verification said "Could not fetch the verification key (HTTP 404)".
4359
- *
4360
- * Exhaustive over {@link CircuitId} on purpose. A new circuit whose key path is
4361
- * not listed here fails to compile rather than failing at verification time,
4362
- * where the only symptom would be a proof that cannot be checked.
4363
- */
4364
- const CIRCUIT_VK_PATHS = Object.freeze({
4365
- coinbase_attestation: 'coinbase-attestation/target/vk/vk',
4366
- coinbase_country_attestation: 'coinbase-country-attestation/target/vk/vk',
4367
- oidc_domain_attestation: 'oidc-domain-attestation/target/vk/vk',
4368
- giwa_attestation: 'giwa-attestation/target/vk/vk',
4369
- mdl_kr_ownership: 'mdl/kr-ownership/target/vk/vk',
4370
- mdl_kr_age: 'mdl/kr-age/target/vk/vk',
4371
- mdl_kr_region: 'mdl/kr-region/target/vk/vk',
4372
- });
4373
- /**
4374
- * Every canonical circuit identifier, in declaration order.
4375
- *
4376
- * Includes circuits that are still `planned`. Use {@link SUPPORTED_CIRCUIT_IDS}
4377
- * when you only want the ones that are officially supported today.
4378
- */
4379
- const ALL_CIRCUIT_IDS = Object.freeze(Object.values(CIRCUIT_IDS));
4380
- /**
4381
- * Circuits that are officially supported today: the two Coinbase circuits and
4382
- * OIDC domain attestation.
4383
- *
4384
- * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.
4385
- */
4386
- const SUPPORTED_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'supported'));
4387
- /**
4388
- * Circuits whose identifiers are reserved but which are not officially
4389
- * supported yet: GIWA attestation and the three Korea Mobile ID circuits.
4390
- *
4391
- * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.
4392
- */
4393
- const PLANNED_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'planned'));
4394
- /**
4395
- * Narrows an unknown value to a canonical circuit identifier.
4396
- *
4397
- * Returns `false` for hyphenated route names such as `'coinbase-kyc'`, which
4398
- * are not circuit IDs.
4399
- *
4400
- * @example
4401
- * ```typescript
4402
- * isCircuitId('coinbase_attestation'); // true
4403
- * isCircuitId('coinbase-kyc'); // false
4404
- * ```
4405
- */
4406
- function isCircuitId(value) {
4407
- return typeof value === 'string' && ALL_CIRCUIT_IDS.includes(value);
4408
- }
4409
- /**
4410
- * Narrows an unknown value to a circuit identifier that is officially supported
4411
- * today. Planned circuits return `false`.
4412
- *
4413
- * @example
4414
- * ```typescript
4415
- * isSupportedCircuitId('oidc_domain_attestation'); // true
4416
- * isSupportedCircuitId('mdl_kr_age'); // false — planned
4417
- * ```
4418
- */
4419
- function isSupportedCircuitId(value) {
4420
- return isCircuitId(value) && CIRCUIT_SUPPORT_STATUS[value] === 'supported';
4421
- }
4422
- /**
4423
- * Returns the support status of a circuit.
4424
- *
4425
- * @param circuit - A canonical circuit identifier.
4426
- * @throws Error when the value is not a canonical circuit identifier.
4427
- *
4428
- * @example
4429
- * ```typescript
4430
- * getCircuitSupportStatus('giwa_attestation'); // 'planned'
4431
- * ```
4432
- */
4433
- function getCircuitSupportStatus(circuit) {
4434
- if (!isCircuitId(circuit)) {
4435
- throw new Error(`Unknown circuit '${String(circuit)}'. Expected one of: ${ALL_CIRCUIT_IDS.join(', ')}`);
4436
- }
4437
- return CIRCUIT_SUPPORT_STATUS[circuit];
4438
- }
4439
-
4440
4607
  /**
4441
4608
  * Off-chain proof verification — checking a proof against its verification key,
4442
4609
  * with no chain, no RPC, and no gas.
@@ -5634,11 +5801,83 @@ class ProofportSDK {
5634
5801
  }
5635
5802
  return await response.json();
5636
5803
  }
5804
+ /**
5805
+ * Creates a proof request through the relay server.
5806
+ *
5807
+ * This is the recommended way to create proof requests. The relay server:
5808
+ * - Issues a server-side requestId (validated by the mobile app)
5809
+ * - Tracks request status in Redis
5810
+ * - Builds the deep link with relay callback URL
5811
+ * - Stores inputs hash for deep link integrity verification
5812
+ *
5813
+ * @param circuit - Circuit type identifier
5814
+ * @param inputs - Circuit-specific inputs
5815
+ * @param options - Request options (message, dappName, dappIcon, nonce, returnScheme)
5816
+ * @param options.returnScheme - Which app the ZKProofport app should switch back
5817
+ * to once it has finished. Put **your own** deep link scheme here — the bare
5818
+ * scheme your app registers, e.g. `'mydapp://'`. Only a native app has an
5819
+ * answer to this question; a web app omits the option entirely, because
5820
+ * opening its https origin would launch a new browser tab on a fresh page
5821
+ * rather than return the user to the tab they started from.
5822
+ *
5823
+ * This is intentionally **not** a URL and not a return address: no path, query
5824
+ * string or fragment is accepted, and the ZKProofport app never sends the proof
5825
+ * there. The proof still comes back to you the same way it always did — over the
5826
+ * relay, via `waitForProof()` / `waitForResult()`. `returnScheme` only answers
5827
+ * "which app should come back to the foreground", exactly like the
5828
+ * `redirect.native` value a wallet uses to hand control back after signing.
5829
+ *
5830
+ * **Omit it and the SDK decides for you.** When the request is created from a
5831
+ * page running in Chrome for iOS (`CriOS`) or Firefox for iOS (`FxiOS`), the
5832
+ * SDK fills in `'googlechrome://'` or `'firefox://'` by itself: opening either
5833
+ * scheme bare brings that browser forward without navigating, so the user lands
5834
+ * back on the tab they were already reading. Every other browser sends nothing,
5835
+ * because nothing safe exists to send — Safari has no such scheme, and Brave
5836
+ * and Arc are indistinguishable from Safari on iOS, so guessing would eject the
5837
+ * user into the wrong browser.
5838
+ * On Android nothing is needed at all: the ZKProofport app brings itself to the
5839
+ * back and the browser resumes exactly as it was. When no target is sent and
5840
+ * none can be inferred, the ZKProofport app tells the user the proof was
5841
+ * delivered and lets them switch back themselves.
5842
+ *
5843
+ * There is no auto-switch on proof failure; only on success and on the user
5844
+ * declining the request.
5845
+ *
5846
+ * Rejected with an error before the request is sent: empty or whitespace values,
5847
+ * anything longer than 128 characters, values carrying a host/path/query/fragment
5848
+ * (including https origins such as `'https://myapp.com'`), and the `http:`,
5849
+ * `file:`, `data:`, `javascript:`, `intent:`, `tel:`, `sms:`, `mailto:` families.
5850
+ * @returns Promise resolving to RelayProofRequest with requestId, deepLink, pollUrl
5851
+ * @throws Error if signer not set, returnScheme is malformed, or the relay request fails
5852
+ *
5853
+ * @example
5854
+ * ```typescript
5855
+ * const sdk = ProofportSDK.create();
5856
+ * sdk.setSigner(signer);
5857
+ *
5858
+ * const relay = await sdk.createRelayRequest('coinbase_attestation', {
5859
+ * scope: 'myapp.com'
5860
+ * }, { dappName: 'My DApp', returnScheme: 'mydapp://' });
5861
+ *
5862
+ * // Generate QR code from relay deep link
5863
+ * const qr = await sdk.generateQRCode(relay.deepLink);
5864
+ *
5865
+ * // Wait for proof (WebSocket primary, polling fallback)
5866
+ * const result = await sdk.waitForProof(relay.requestId);
5867
+ * ```
5868
+ */
5869
+ // 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.
5873
+ static needsWalletSignature(circuit) {
5874
+ return CIRCUIT_NEEDS_WALLET_SIGNATURE[circuit] === true;
5875
+ }
5637
5876
  async createRelayRequest(circuit, inputs, options = {}) {
5638
5877
  if (!this.relayUrl) {
5639
5878
  throw new Error('relayUrl is required. Set it in ProofportSDK config.');
5640
5879
  }
5641
- const needsSignature = ProofportSDK.WALLET_SIGNATURE_CIRCUITS.includes(circuit);
5880
+ const needsSignature = ProofportSDK.needsWalletSignature(circuit);
5642
5881
  if (needsSignature && !this.signer) {
5643
5882
  throw new Error('Signer not set. Call setSigner() first. Wallet signature is required for this circuit.');
5644
5883
  }
@@ -5650,6 +5889,17 @@ class ProofportSDK {
5650
5889
  throw new Error(`${mdlError} (circuit: ${circuit})`);
5651
5890
  }
5652
5891
  }
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})`);
5901
+ }
5902
+ }
5653
5903
  // Return target: validated before we burn a challenge, so a typo surfaces
5654
5904
  // as a local error instead of a relay 400 on the second round trip.
5655
5905
  if (options.returnScheme !== undefined) {
@@ -5974,76 +6224,6 @@ class ProofportSDK {
5974
6224
  return extractDomainFromPublicInputs(publicInputs, circuit);
5975
6225
  }
5976
6226
  }
5977
- /**
5978
- * Creates a proof request through the relay server.
5979
- *
5980
- * This is the recommended way to create proof requests. The relay server:
5981
- * - Issues a server-side requestId (validated by the mobile app)
5982
- * - Tracks request status in Redis
5983
- * - Builds the deep link with relay callback URL
5984
- * - Stores inputs hash for deep link integrity verification
5985
- *
5986
- * @param circuit - Circuit type identifier
5987
- * @param inputs - Circuit-specific inputs
5988
- * @param options - Request options (message, dappName, dappIcon, nonce, returnScheme)
5989
- * @param options.returnScheme - Which app the ZKProofport app should switch back
5990
- * to once it has finished. Put **your own** deep link scheme here — the bare
5991
- * scheme your app registers, e.g. `'mydapp://'`. Only a native app has an
5992
- * answer to this question; a web app omits the option entirely, because
5993
- * opening its https origin would launch a new browser tab on a fresh page
5994
- * rather than return the user to the tab they started from.
5995
- *
5996
- * This is intentionally **not** a URL and not a return address: no path, query
5997
- * string or fragment is accepted, and the ZKProofport app never sends the proof
5998
- * there. The proof still comes back to you the same way it always did — over the
5999
- * relay, via `waitForProof()` / `waitForResult()`. `returnScheme` only answers
6000
- * "which app should come back to the foreground", exactly like the
6001
- * `redirect.native` value a wallet uses to hand control back after signing.
6002
- *
6003
- * **Omit it and the SDK decides for you.** When the request is created from a
6004
- * page running in Chrome for iOS (`CriOS`) or Firefox for iOS (`FxiOS`), the
6005
- * SDK fills in `'googlechrome://'` or `'firefox://'` by itself: opening either
6006
- * scheme bare brings that browser forward without navigating, so the user lands
6007
- * back on the tab they were already reading. Every other browser sends nothing,
6008
- * because nothing safe exists to send — Safari has no such scheme, and Brave
6009
- * and Arc are indistinguishable from Safari on iOS, so guessing would eject the
6010
- * user into the wrong browser.
6011
- * On Android nothing is needed at all: the ZKProofport app brings itself to the
6012
- * back and the browser resumes exactly as it was. When no target is sent and
6013
- * none can be inferred, the ZKProofport app tells the user the proof was
6014
- * delivered and lets them switch back themselves.
6015
- *
6016
- * There is no auto-switch on proof failure; only on success and on the user
6017
- * declining the request.
6018
- *
6019
- * Rejected with an error before the request is sent: empty or whitespace values,
6020
- * anything longer than 128 characters, values carrying a host/path/query/fragment
6021
- * (including https origins such as `'https://myapp.com'`), and the `http:`,
6022
- * `file:`, `data:`, `javascript:`, `intent:`, `tel:`, `sms:`, `mailto:` families.
6023
- * @returns Promise resolving to RelayProofRequest with requestId, deepLink, pollUrl
6024
- * @throws Error if signer not set, returnScheme is malformed, or the relay request fails
6025
- *
6026
- * @example
6027
- * ```typescript
6028
- * const sdk = ProofportSDK.create();
6029
- * sdk.setSigner(signer);
6030
- *
6031
- * const relay = await sdk.createRelayRequest('coinbase_attestation', {
6032
- * scope: 'myapp.com'
6033
- * }, { dappName: 'My DApp', returnScheme: 'mydapp://' });
6034
- *
6035
- * // Generate QR code from relay deep link
6036
- * const qr = await sdk.generateQRCode(relay.deepLink);
6037
- *
6038
- * // Wait for proof (WebSocket primary, polling fallback)
6039
- * const result = await sdk.waitForProof(relay.requestId);
6040
- * ```
6041
- */
6042
- // Circuits that require wallet signature (used as circuit input)
6043
- ProofportSDK.WALLET_SIGNATURE_CIRCUITS = [
6044
- 'coinbase_attestation',
6045
- 'coinbase_country_attestation',
6046
- ];
6047
6227
 
6048
- 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 };
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 };
6049
6229
  //# sourceMappingURL=index.esm.js.map