@zkproofport-app/sdk 0.4.2 → 0.5.0-beta.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.js CHANGED
@@ -2,8 +2,10 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var core = require('@zkproofport-app/core');
6
+ var circuits = require('@zkproofport-app/core/circuits');
7
+ var typedAction = require('@zkproofport-app/core/typedAction');
5
8
  var QRCode = require('qrcode');
6
- var ethers = require('ethers');
7
9
 
8
10
  /**
9
11
  * ZKProofport SDK Constants
@@ -137,50 +139,6 @@ const CIRCUIT_METADATA = {
137
139
  publicInputNames: ['scope', 'nullifier_value', 'region_code'],
138
140
  },
139
141
  };
140
- /**
141
- * Standard verifier contract ABI shared across all Barretenberg-generated verifiers.
142
- * This ABI defines the interface for calling the verify function on deployed verifier contracts.
143
- *
144
- * Uses ethers v6 human-readable ABI format.
145
- *
146
- * @example
147
- * ```typescript
148
- * import { Contract } from 'ethers';
149
- *
150
- * const verifier = new Contract(verifierAddress, VERIFIER_ABI, provider);
151
- * const isValid = await verifier.verify(proofBytes, publicInputs);
152
- * ```
153
- */
154
- const VERIFIER_ABI = [
155
- 'function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool)',
156
- ];
157
- /**
158
- * Public RPC endpoint URLs for supported blockchain networks.
159
- * Used as fallback when no custom provider is supplied.
160
- *
161
- * Supported networks:
162
- * - 84532: Base Sepolia (testnet)
163
- * - 8453: Base Mainnet (production)
164
- * - 91342: GIWA Sepolia (testnet) — where the GIWA account circuit verifies
165
- *
166
- * A chain missing from this map is not a quiet degradation: `getDefaultProvider`
167
- * throws "No RPC endpoint configured for chain N". That is what a caller saw
168
- * when verifying a GIWA proof on chain 91342 — the verifier contract had been
169
- * deployed and the proof was valid, but the SDK had no way to reach the chain
170
- * to ask. Deploying a verifier to a new network means adding it here too.
171
- *
172
- * @example
173
- * ```typescript
174
- * import { JsonRpcProvider } from 'ethers';
175
- *
176
- * const provider = new JsonRpcProvider(RPC_ENDPOINTS[84532]);
177
- * ```
178
- */
179
- const RPC_ENDPOINTS = {
180
- 84532: 'https://sepolia.base.org', // Base Sepolia
181
- 8453: 'https://mainnet.base.org', // Base Mainnet
182
- 91342: 'https://sepolia-rpc.giwa.io', // GIWA Sepolia
183
- };
184
142
  /**
185
143
  * Default proof request expiration time in milliseconds.
186
144
  * Requests older than this are considered expired and should not be processed.
@@ -212,458 +170,6 @@ const DEFAULT_REQUEST_EXPIRY_MS = 10 * 60 * 1000;
212
170
  * ```
213
171
  */
214
172
  const MAX_QR_DATA_SIZE = 2953; // Version 40 with L error correction
215
- /**
216
- * Coinbase Attestation circuit public input layout (byte offsets).
217
- * Defines the byte positions of each field in the flattened public inputs array.
218
- *
219
- * Public inputs are packed as bytes32 values:
220
- * - signal_hash: bytes 0-31
221
- * - merkle_root: bytes 32-63
222
- * - scope: bytes 64-95
223
- *
224
- * @example
225
- * ```typescript
226
- * const publicInputs = response.publicInputs;
227
- * const signalHash = publicInputs.slice(
228
- * COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT.SIGNAL_HASH_START,
229
- * COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT.SIGNAL_HASH_END + 1
230
- * );
231
- * ```
232
- */
233
- const COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT = {
234
- SIGNAL_HASH_START: 0,
235
- SIGNAL_HASH_END: 31,
236
- MERKLE_ROOT_START: 32,
237
- MERKLE_ROOT_END: 63,
238
- SCOPE_START: 64,
239
- SCOPE_END: 95,
240
- NULLIFIER_START: 96,
241
- NULLIFIER_END: 127,
242
- };
243
- /**
244
- * Coinbase Country Attestation circuit public input layout (byte offsets).
245
- * Defines the byte positions of each field in the flattened public inputs array.
246
- *
247
- * Public inputs are packed as bytes32 values:
248
- * - signal_hash: bytes 0-31
249
- * - merkle_root: bytes 32-63
250
- * - country_list: bytes 64-83 (20 bytes for 10 countries)
251
- * - country_list_length: byte 84
252
- * - is_included: byte 85
253
- * - scope: bytes 86-117
254
- *
255
- * @example
256
- * ```typescript
257
- * const publicInputs = response.publicInputs;
258
- * const countryList = publicInputs.slice(
259
- * COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT.COUNTRY_LIST_START,
260
- * COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT.COUNTRY_LIST_END + 1
261
- * );
262
- * ```
263
- */
264
- const COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT = {
265
- SIGNAL_HASH_START: 0,
266
- SIGNAL_HASH_END: 31,
267
- MERKLE_ROOT_START: 32,
268
- MERKLE_ROOT_END: 63,
269
- COUNTRY_LIST_START: 64,
270
- COUNTRY_LIST_END: 83,
271
- COUNTRY_LIST_LENGTH: 84,
272
- IS_INCLUDED: 85,
273
- SCOPE_START: 86,
274
- SCOPE_END: 117,
275
- NULLIFIER_START: 118,
276
- NULLIFIER_END: 149,
277
- };
278
- /**
279
- * Korea Mobile ID (mDL) circuit public input layout (field offsets).
280
- * All three mDL circuits share the same prefix: scope [0..31] and
281
- * nullifier_value [32..63], each stored one byte per field element.
282
- * The suffix differs per predicate circuit:
283
- * - mdl_kr_ownership (97 inputs): disclose_flags [64], owner_commit [65..96]
284
- * - mdl_kr_age (66 inputs): age_threshold [64], current_year [65]
285
- * - mdl_kr_region (96 inputs): region_code [64..95]
286
- *
287
- * @example
288
- * ```typescript
289
- * const nullifier = publicInputs.slice(
290
- * MDL_KR_PUBLIC_INPUT_LAYOUT.NULLIFIER_START,
291
- * MDL_KR_PUBLIC_INPUT_LAYOUT.NULLIFIER_END + 1
292
- * );
293
- * ```
294
- */
295
- const MDL_KR_PUBLIC_INPUT_LAYOUT = {
296
- SCOPE_START: 0,
297
- SCOPE_END: 31,
298
- NULLIFIER_START: 32,
299
- NULLIFIER_END: 63,
300
- OWNERSHIP_DISCLOSE_FLAGS: 64,
301
- OWNERSHIP_OWNER_COMMIT_START: 65,
302
- OWNERSHIP_OWNER_COMMIT_END: 96,
303
- AGE_THRESHOLD: 64,
304
- AGE_CURRENT_YEAR: 65,
305
- REGION_CODE_START: 64,
306
- REGION_CODE_END: 95,
307
- };
308
- /**
309
- * OIDC Domain Attestation circuit public input layout (field offsets).
310
- * Defines the field positions in the flattened public inputs array (148 fields total).
311
- *
312
- * Circuit public inputs (from main.nr):
313
- * - pubkey_modulus_limbs: pub [u128; 18] → 18 fields (0–17)
314
- * - domain: pub BoundedVec<u8, 64> → 1 len + 64 storage = 65 fields (18–82)
315
- * - scope: pub [u8; 32] → 32 fields (83–114)
316
- * - nullifier: pub [u8; 32] → 32 fields (115–146)
317
- * - provider: pub u8 → 1 field (147)
318
- *
319
- * @example
320
- * ```typescript
321
- * const scope = publicInputs.slice(
322
- * OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT.SCOPE_START,
323
- * OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT.SCOPE_END + 1
324
- * );
325
- * ```
326
- */
327
- const OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT = {
328
- PUBKEY_MODULUS_START: 0,
329
- PUBKEY_MODULUS_END: 17,
330
- DOMAIN_STORAGE_START: 18,
331
- DOMAIN_STORAGE_END: 81,
332
- DOMAIN_LEN: 82,
333
- SCOPE_START: 83,
334
- SCOPE_END: 114,
335
- NULLIFIER_START: 115,
336
- NULLIFIER_END: 146,
337
- PROVIDER: 147,
338
- /** @deprecated Use DOMAIN_STORAGE_START */
339
- DOMAIN_START: 18,
340
- /** @deprecated Use DOMAIN_LEN */
341
- DOMAIN_END: 82,
342
- };
343
-
344
- /**
345
- * Canonical ZKProofport circuit identifiers.
346
- *
347
- * This module is the single source of truth for circuit IDs. Every other layer
348
- * — the mobile app, the demo, the relay, server-side provers — reads its list
349
- * from here instead of keeping a private copy that silently drifts.
350
- *
351
- * It has **no imports and no dependencies**, so it can be pulled in on its own:
352
- *
353
- * ```typescript
354
- * import { CIRCUIT_IDS, isCircuitId } from '@zkproofport-app/sdk/circuits';
355
- * ```
356
- *
357
- * The same names are also re-exported from the package root
358
- * (`@zkproofport-app/sdk`) for callers that already import the SDK.
359
- *
360
- * ## Identifiers are canonical and case-sensitive
361
- *
362
- * Each ID is the circuit's `name` in its `Nargo.toml`, verbatim. The format is
363
- * lowercase with underscores. Hyphenated spellings (`coinbase-kyc`,
364
- * `mdl-kr-age`) are directory or UI route names in other repositories — they
365
- * are **not** circuit IDs, and passing one produces a nullifier mismatch or a
366
- * failed on-chain lookup rather than a clear error.
367
- */
368
- /**
369
- * Canonical circuit identifiers, keyed by a stable constant name.
370
- *
371
- * Prefer these constants over string literals so a rename is a compile error
372
- * rather than a runtime surprise.
373
- *
374
- * @example
375
- * ```typescript
376
- * import { CIRCUIT_IDS } from '@zkproofport-app/sdk/circuits';
377
- *
378
- * const relay = await sdk.createRelayRequest(CIRCUIT_IDS.COINBASE_ATTESTATION, {
379
- * scope: 'myapp.com',
380
- * });
381
- * ```
382
- */
383
- const CIRCUIT_IDS = Object.freeze({
384
- /** Coinbase KYC attestation. Officially supported. */
385
- COINBASE_ATTESTATION: 'coinbase_attestation',
386
- /** Coinbase country attestation (inclusion / exclusion). Officially supported. */
387
- COINBASE_COUNTRY_ATTESTATION: 'coinbase_country_attestation',
388
- /** OIDC email-domain attestation. Officially supported. */
389
- OIDC_DOMAIN_ATTESTATION: 'oidc_domain_attestation',
390
- /** GIWA attestation. Planned — not officially supported yet. */
391
- GIWA_ATTESTATION: 'giwa_attestation',
392
- /** Dojang Playground on GIWA Sepolia. Experimental — testnet only. */
393
- DOJANG_PLAYGROUND: 'dojang_playground',
394
- /** Korea Mobile ID ownership. Planned — not officially supported yet. */
395
- MDL_KR_OWNERSHIP: 'mdl_kr_ownership',
396
- /** Korea Mobile ID age threshold. Planned — not officially supported yet. */
397
- MDL_KR_AGE: 'mdl_kr_age',
398
- /** Korea Mobile ID si/do region. Planned — not officially supported yet. */
399
- MDL_KR_REGION: 'mdl_kr_region',
400
- });
401
- /**
402
- * Support status for every circuit.
403
- *
404
- * The record is exhaustive over {@link CircuitId}: adding an identifier without
405
- * giving it a status is a compile error, so this list cannot drift from
406
- * {@link CIRCUIT_IDS}.
407
- *
408
- * @example
409
- * ```typescript
410
- * import { CIRCUIT_SUPPORT_STATUS } from '@zkproofport-app/sdk/circuits';
411
- *
412
- * CIRCUIT_SUPPORT_STATUS.coinbase_attestation; // 'supported'
413
- * CIRCUIT_SUPPORT_STATUS.mdl_kr_age; // 'planned'
414
- * ```
415
- */
416
- const CIRCUIT_SUPPORT_STATUS = Object.freeze({
417
- coinbase_attestation: 'supported',
418
- coinbase_country_attestation: 'supported',
419
- oidc_domain_attestation: 'supported',
420
- // Experimental, not planned: the circuit is built, its verifier is
421
- // deployed on GIWA Sepolia and the demo requests it. What keeps it out of
422
- // 'supported' is that it is testnet-only, so the app offers it behind
423
- // Developer Mode and reads its files from main rather than a release tag.
424
- giwa_attestation: 'experimental',
425
- // Same footing as GIWA: built, verifier deployed on GIWA Sepolia, a real
426
- // proof verifies there. Testnet only.
427
- dojang_playground: 'experimental',
428
- mdl_kr_ownership: 'planned',
429
- mdl_kr_age: 'planned',
430
- mdl_kr_region: 'planned',
431
- });
432
- /**
433
- * Where each circuit's verification key FILE lives inside the
434
- * `zkproofport/circuits` repository, relative to its root.
435
- *
436
- * Off-chain verification needs the verification key and nothing else — no
437
- * circuit bytecode, no witness. The key is public by nature: it is what anyone
438
- * checking a proof must already have, and the Solidity verifiers deployed
439
- * on-chain embed the same key.
440
- *
441
- * NOTE THE TRAILING `/vk` TWICE. In that repository `target/vk` is a DIRECTORY
442
- * holding `vk`, `vk_hash` and `SHA256SUMS`; the key itself is `target/vk/vk`.
443
- * These paths were first copied here from the mobile app, which keeps the
444
- * directory and the filename as two fields and joins them — so taking only the
445
- * directory produced a URL that answers 404, and the first browser to try
446
- * off-chain verification said "Could not fetch the verification key (HTTP 404)".
447
- *
448
- * Exhaustive over {@link CircuitId} on purpose. A new circuit whose key path is
449
- * not listed here fails to compile rather than failing at verification time,
450
- * where the only symptom would be a proof that cannot be checked.
451
- */
452
- const CIRCUIT_VK_PATHS = Object.freeze({
453
- coinbase_attestation: 'coinbase-attestation/target/vk/vk',
454
- coinbase_country_attestation: 'coinbase-country-attestation/target/vk/vk',
455
- oidc_domain_attestation: 'oidc-domain-attestation/target/vk/vk',
456
- giwa_attestation: 'giwa-attestation/target/vk/vk',
457
- dojang_playground: 'dojang-playground/target/vk/vk',
458
- mdl_kr_ownership: 'mdl/kr-ownership/target/vk/vk',
459
- mdl_kr_age: 'mdl/kr-age/target/vk/vk',
460
- mdl_kr_region: 'mdl/kr-region/target/vk/vk',
461
- });
462
- /**
463
- * Every canonical circuit identifier, in declaration order.
464
- *
465
- * Includes circuits that are still `planned`. Use {@link SUPPORTED_CIRCUIT_IDS}
466
- * when you only want the ones that are officially supported today.
467
- */
468
- const ALL_CIRCUIT_IDS = Object.freeze(Object.values(CIRCUIT_IDS));
469
- /**
470
- * Circuits that are officially supported today: the two Coinbase circuits and
471
- * OIDC domain attestation.
472
- *
473
- * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.
474
- */
475
- const SUPPORTED_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'supported'));
476
- /**
477
- * Circuits whose identifiers are reserved but which are not officially
478
- * supported yet: GIWA attestation and the three Korea Mobile ID circuits.
479
- *
480
- * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.
481
- */
482
- /**
483
- * Circuits you can prove today, on a testnet, with everything subject to change.
484
- *
485
- * `giwa_attestation` is in this state. Calling it `planned` would be wrong —
486
- * the circuit compiles, the verifier is deployed and a proof verifies against
487
- * it — and calling it `supported` would be worse, because the only deployment
488
- * is a testnet (GIWA Sepolia). Neither existing word was true, so the honest
489
- * move was a third word rather than picking the closer lie.
490
- */
491
- const EXPERIMENTAL_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter(id => CIRCUIT_SUPPORT_STATUS[id] === 'experimental'));
492
- const PLANNED_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'planned'));
493
- /**
494
- * Narrows an unknown value to a canonical circuit identifier.
495
- *
496
- * Returns `false` for hyphenated route names such as `'coinbase-kyc'`, which
497
- * are not circuit IDs.
498
- *
499
- * @example
500
- * ```typescript
501
- * isCircuitId('coinbase_attestation'); // true
502
- * isCircuitId('coinbase-kyc'); // false
503
- * ```
504
- */
505
- function isCircuitId(value) {
506
- return typeof value === 'string' && ALL_CIRCUIT_IDS.includes(value);
507
- }
508
- /**
509
- * Narrows an unknown value to a circuit identifier that is officially supported
510
- * today. Planned circuits return `false`.
511
- *
512
- * @example
513
- * ```typescript
514
- * isSupportedCircuitId('oidc_domain_attestation'); // true
515
- * isSupportedCircuitId('mdl_kr_age'); // false — planned
516
- * ```
517
- */
518
- function isSupportedCircuitId(value) {
519
- return isCircuitId(value) && CIRCUIT_SUPPORT_STATUS[value] === 'supported';
520
- }
521
- /**
522
- * Returns the support status of a circuit.
523
- *
524
- * @param circuit - A canonical circuit identifier.
525
- * @throws Error when the value is not a canonical circuit identifier.
526
- *
527
- * @example
528
- * ```typescript
529
- * getCircuitSupportStatus('giwa_attestation'); // 'planned'
530
- * ```
531
- */
532
- function getCircuitSupportStatus(circuit) {
533
- if (!isCircuitId(circuit)) {
534
- throw new Error(`Unknown circuit '${String(circuit)}'. Expected one of: ${ALL_CIRCUIT_IDS.join(', ')}`);
535
- }
536
- return CIRCUIT_SUPPORT_STATUS[circuit];
537
- }
538
- const CIRCUIT_ACTION_BINDING = {
539
- coinbase_attestation: 'none',
540
- coinbase_country_attestation: 'none',
541
- // The signature lives inside the identity token; no wallet, no action.
542
- oidc_domain_attestation: 'none',
543
- // Branches in-circuit: with an action the wallet signs the typed data,
544
- // without one it personal_signs the request's signal hash.
545
- giwa_attestation: 'optional',
546
- // Same branch as GIWA; see dojang-playground/src/main.nr.
547
- dojang_playground: 'optional',
548
- mdl_kr_ownership: 'none',
549
- mdl_kr_age: 'none',
550
- mdl_kr_region: 'none',
551
- };
552
- /*
553
- * The union is declared above rather than derived from the table's values.
554
- *
555
- * Deriving it narrowed the type to whatever the table happens to hold today --
556
- * 'none' | 'optional' -- and TypeScript then called every `=== 'required'`
557
- * check impossible. The publish typecheck caught it; `rollup` and `vitest` did
558
- * not. 'required' is a state a circuit can legitimately be in (an EIP-712 path
559
- * and nothing else), so the union says so and the table is checked against it.
560
- */
561
- /**
562
- * How this circuit treats an action, or an error naming the id that is not one.
563
- *
564
- * No default: a circuit nobody has classified must not inherit 'none', which
565
- * would silently drop an action a dapp meant to bind.
566
- */
567
- function circuitActionBinding(circuit) {
568
- const binding = CIRCUIT_ACTION_BINDING[circuit];
569
- if (!binding) {
570
- throw new Error(`Unknown circuit '${circuit}'. Expected: ${ALL_CIRCUIT_IDS.join(', ')}`);
571
- }
572
- return binding;
573
- }
574
- const CIRCUIT_NEEDS_WALLET_SIGNATURE = {
575
- coinbase_attestation: true,
576
- coinbase_country_attestation: true,
577
- // The signature is inside the OIDC identity token, checked by the prover.
578
- oidc_domain_attestation: false,
579
- // Web2 (OmniOne CX) flows with no wallet binding at all.
580
- mdl_kr_ownership: false,
581
- mdl_kr_age: false,
582
- mdl_kr_region: false,
583
- // A GIWA-attested wallet signing an EIP-712 action (or its signal hash).
584
- // This read `false` while the circuit had always verified a wallet signature
585
- // -- it sat under the Web2 comment above and was never revisited. A request
586
- // for it would have reached the relay with no signer.
587
- giwa_attestation: true,
588
- // The wallet signs the request, and the same key must have signed its
589
- // Playground faucet transaction.
590
- dojang_playground: true,
591
- };
592
-
593
- const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
594
- /**
595
- * The reason the value is not a usable action, or `null` when it is one.
596
- *
597
- * Structural validation only, not application authorization policy. Consumers
598
- * (e.g. EligibilityGate) must independently enforce the expected chain, verifying
599
- * contract, actor, action parameters, nonce and deadline as applicable.
600
- * EIP-712 signs only fields declared in types, not extra message properties.
601
- *
602
- * A string rather than a boolean because every caller shows it to somebody —
603
- * a dapp developer reading a validation error, or a person looking at the
604
- * mobile app's demo screen.
605
- */
606
- function validateTypedAction(value) {
607
- if (value === undefined || value === null) {
608
- return 'action is required. The circuit proves that a wallet authorised ONE EIP-712 action; there is nothing to prove without it.';
609
- }
610
- if (typeof value !== 'object' || Array.isArray(value)) {
611
- return 'action must be an object.';
612
- }
613
- const a = value;
614
- const d = a.domain;
615
- if (!d || typeof d !== 'object') {
616
- return 'action.domain is required.';
617
- }
618
- for (const k of ['name', 'version', 'verifyingContract']) {
619
- if (typeof d[k] !== 'string' || !d[k]) {
620
- return `action.domain.${k} must be a non-empty string.`;
621
- }
622
- }
623
- if (typeof d.chainId !== 'number' || !Number.isInteger(d.chainId)) {
624
- return 'action.domain.chainId must be an integer.';
625
- }
626
- if (!ADDRESS.test(d.verifyingContract)) {
627
- return 'action.domain.verifyingContract must be a 20-byte hex address.';
628
- }
629
- if (!a.types || typeof a.types !== 'object' || Array.isArray(a.types)) {
630
- return 'action.types is required.';
631
- }
632
- if (typeof a.primaryType !== 'string' || !a.primaryType) {
633
- return 'action.primaryType is required.';
634
- }
635
- if (!(a.primaryType in a.types)) {
636
- return `action.primaryType '${a.primaryType}' is not one of the structs in action.types.`;
637
- }
638
- if (!a.message || typeof a.message !== 'object' || Array.isArray(a.message)) {
639
- return 'action.message is required.';
640
- }
641
- // Every field the struct declares must be present, or the wallet signs a
642
- // different message than the one the caller described.
643
- const fields = a.types[a.primaryType];
644
- if (!Array.isArray(fields) || fields.length === 0) {
645
- return `action.types.${a.primaryType} declares no fields.`;
646
- }
647
- const missing = fields
648
- .map(f => f?.name)
649
- .filter(n => typeof n === 'string' && !(n in a.message));
650
- if (missing.length) {
651
- return `action.message is missing: ${missing.join(', ')}`;
652
- }
653
- return null;
654
- }
655
- /** The action, or the reason it is not one. For text a person typed. */
656
- function parseTypedAction(text) {
657
- let raw;
658
- try {
659
- raw = JSON.parse(text);
660
- }
661
- catch (e) {
662
- return { error: `Not valid JSON: ${e.message}` };
663
- }
664
- const error = validateTypedAction(raw);
665
- return error ? { error } : { action: raw };
666
- }
667
173
 
668
174
  /**
669
175
  * Deep Link utilities for ZKProofport SDK
@@ -853,7 +359,7 @@ function parseProofResponseUrl(url) {
853
359
  // to use, so the wrong label is a proof checked against another circuit's
854
360
  // bytes.
855
361
  const circuit = urlObj.searchParams.get('circuit');
856
- if (!circuit || !isCircuitId(circuit)) {
362
+ if (!circuit || !circuits.isCircuitId(circuit)) {
857
363
  return null;
858
364
  }
859
365
  const response = {
@@ -1163,10 +669,10 @@ function validateProofRequest(request) {
1163
669
  // The list is the SDK's own, not a copy. The copy that used to sit here
1164
670
  // had missed circuits, so a link for one was refused as an invalid type
1165
671
  // while every other layer accepted it.
1166
- if (!isCircuitId(request.circuit)) {
672
+ if (!circuits.isCircuitId(request.circuit)) {
1167
673
  return {
1168
674
  valid: false,
1169
- error: `Invalid circuit type: ${request.circuit}. Expected one of: ${ALL_CIRCUIT_IDS.join(', ')}`,
675
+ error: `Invalid circuit type: ${request.circuit}. Expected one of: ${circuits.ALL_CIRCUIT_IDS.join(', ')}`,
1170
676
  };
1171
677
  }
1172
678
  if (!request.callbackUrl) {
@@ -1220,7 +726,7 @@ function validateProofRequest(request) {
1220
726
  // circuit here, so every action-capable circuit is checked the same way.
1221
727
  {
1222
728
  const action = request.inputs?.action;
1223
- const binding = circuitActionBinding(request.circuit);
729
+ const binding = circuits.circuitActionBinding(request.circuit);
1224
730
  if (binding === 'none') {
1225
731
  if (action !== undefined) {
1226
732
  return {
@@ -1230,7 +736,7 @@ function validateProofRequest(request) {
1230
736
  }
1231
737
  }
1232
738
  else if (binding === 'required' || action !== undefined) {
1233
- const actionError = validateTypedAction(action);
739
+ const actionError = typedAction.validateTypedAction(action);
1234
740
  if (actionError) {
1235
741
  return { valid: false, error: actionError };
1236
742
  }
@@ -1431,370 +937,6 @@ function estimateQRDataSize(requestOrUrl, scheme = DEFAULT_SCHEME) {
1431
937
  };
1432
938
  }
1433
939
 
1434
- /**
1435
- * On-chain verification utilities for ZKProofport SDK
1436
- *
1437
- * Compatible with both ethers v5 and v6.
1438
- */
1439
- // ethers v5/v6 compatibility shims
1440
- const _ethers = ethers.ethers;
1441
- /** @internal ethers v5/v6 compatibility shim */
1442
- function hexZeroPad(value, length) {
1443
- // v6: ethers.zeroPadValue, v5: ethers.utils.hexZeroPad
1444
- if (typeof _ethers.zeroPadValue === 'function')
1445
- return _ethers.zeroPadValue(value, length);
1446
- if (_ethers.utils?.hexZeroPad)
1447
- return _ethers.utils.hexZeroPad(value, length);
1448
- // manual fallback
1449
- const hex = value.startsWith('0x') ? value.slice(2) : value;
1450
- return '0x' + hex.padStart(length * 2, '0');
1451
- }
1452
- /** @internal ethers v5/v6 compatibility shim */
1453
- function createJsonRpcProvider(url) {
1454
- // v6: ethers.JsonRpcProvider, v5: ethers.providers.JsonRpcProvider
1455
- if (typeof _ethers.JsonRpcProvider === 'function')
1456
- return new _ethers.JsonRpcProvider(url);
1457
- if (_ethers.providers?.JsonRpcProvider)
1458
- return new _ethers.providers.JsonRpcProvider(url);
1459
- throw new Error('No JsonRpcProvider found in ethers');
1460
- }
1461
- /**
1462
- * @internal Resolve verifier from SDK config or proof response.
1463
- * SDK config (customVerifier) takes priority over response-provided verifier.
1464
- */
1465
- function resolveVerifier(customVerifier, responseVerifier) {
1466
- if (customVerifier)
1467
- return customVerifier;
1468
- if (responseVerifier?.verifierAddress) {
1469
- return {
1470
- address: responseVerifier.verifierAddress,
1471
- chainId: responseVerifier.chainId ?? 0,
1472
- abi: VERIFIER_ABI,
1473
- };
1474
- }
1475
- return null;
1476
- }
1477
- /**
1478
- * Get verifier contract instance for interacting with on-chain verifier contracts.
1479
- *
1480
- * @param providerOrSigner - ethers.js Provider or Signer instance (v5 or v6 compatible)
1481
- * @param verifier - Verifier contract configuration containing address and ABI
1482
- * @returns ethers.Contract instance connected to the verifier
1483
- *
1484
- * @example
1485
- * ```typescript
1486
- * const provider = new ethers.JsonRpcProvider(rpcUrl);
1487
- * const contract = getVerifierContract(provider, {
1488
- * address: '0x...',
1489
- * chainId: 11155111,
1490
- * abi: VERIFIER_ABI
1491
- * });
1492
- * ```
1493
- */
1494
- function getVerifierContract(providerOrSigner, verifier) {
1495
- return new ethers.ethers.Contract(verifier.address, verifier.abi, providerOrSigner);
1496
- }
1497
- /**
1498
- * Get default JSON-RPC provider for a chain using pre-configured RPC endpoints.
1499
- *
1500
- * @param chainId - The chain ID (e.g., 11155111 for Sepolia, 84532 for Base Sepolia)
1501
- * @returns ethers.JsonRpcProvider instance for the specified chain
1502
- * @throws Error if no RPC endpoint is configured for the chain
1503
- *
1504
- * @example
1505
- * ```typescript
1506
- * const provider = getDefaultProvider(11155111); // Sepolia
1507
- * ```
1508
- */
1509
- function getDefaultProvider(chainId) {
1510
- const rpcUrl = RPC_ENDPOINTS[chainId];
1511
- if (!rpcUrl) {
1512
- throw new Error(`No RPC endpoint configured for chain ${chainId}. Known chains: ${Object.keys(RPC_ENDPOINTS).join(', ')}`);
1513
- }
1514
- return createJsonRpcProvider(rpcUrl);
1515
- }
1516
- /**
1517
- * Verify a zero-knowledge proof on-chain by calling the verifier smart contract.
1518
- *
1519
- * This function resolves the verifier contract from SDK config or proof response,
1520
- * connects to the blockchain, and calls the verify() method with the proof and public inputs.
1521
- *
1522
- * @param circuit - The canonical circuit identifier. REQUIRED.
1523
- * @param parsedProof - Parsed proof object containing proofHex and publicInputsHex
1524
- * @param providerOrSigner - Optional ethers.js Provider or Signer instance. If not provided, uses default RPC for the chain
1525
- * @param customVerifier - Optional custom verifier contract config (takes priority over responseVerifier)
1526
- * @param responseVerifier - Optional verifier info from proof generation response
1527
- * @returns Promise resolving to verification result with valid flag and optional error message
1528
- *
1529
- * @example
1530
- * ```typescript
1531
- * const parsed = parseProofForOnChain(proof, publicInputs, numPublicInputs);
1532
- * const result = await verifyProofOnChain(
1533
- * 'coinbase_attestation',
1534
- * parsed,
1535
- * provider,
1536
- * { address: '0x...', chainId: 11155111, abi: VERIFIER_ABI }
1537
- * );
1538
- *
1539
- * if (result.valid) {
1540
- * console.log('Proof is valid!');
1541
- * } else {
1542
- * console.error('Verification failed:', result.error);
1543
- * }
1544
- * ```
1545
- */
1546
- async function verifyProofOnChain(circuit, parsedProof, providerOrSigner, customVerifier, responseVerifier) {
1547
- const verifier = resolveVerifier(customVerifier, responseVerifier);
1548
- if (!verifier) {
1549
- return {
1550
- valid: false,
1551
- error: 'No verifier address provided. Configure via SDK or ensure proof response includes verifierAddress.',
1552
- };
1553
- }
1554
- const provider = providerOrSigner || (verifier.chainId > 0 ? getDefaultProvider(verifier.chainId) : null);
1555
- if (!provider) {
1556
- return {
1557
- valid: false,
1558
- error: 'No provider available. Provide a provider or ensure chainId is set for RPC lookup.',
1559
- };
1560
- }
1561
- const contract = getVerifierContract(provider, verifier);
1562
- try {
1563
- const isValid = await contract.verify(parsedProof.proofHex, parsedProof.publicInputsHex);
1564
- return { valid: isValid };
1565
- }
1566
- catch (error) {
1567
- const errorMessage = error instanceof Error ? error.message : String(error);
1568
- return { valid: false, error: errorMessage };
1569
- }
1570
- }
1571
- /** @internal Ensure a hex string has the 0x prefix */
1572
- function ensureHexPrefix(hex) {
1573
- return hex.startsWith('0x') ? hex : `0x${hex}`;
1574
- }
1575
- /**
1576
- * Parse proof response into format suitable for on-chain verification.
1577
- *
1578
- * Converts proof and public inputs from relay response format to the format
1579
- * expected by Solidity verifier contracts. Public inputs are zero-padded to
1580
- * 32 bytes (bytes32) to match Solidity's bytes32[] type.
1581
- *
1582
- * @param proof - Proof bytes as hex string (with or without 0x prefix)
1583
- * @param publicInputs - Array of public input values as hex strings
1584
- * @param numPublicInputs - Number of public inputs (for validation)
1585
- * @returns Parsed proof object ready for on-chain verification
1586
- *
1587
- * @example
1588
- * ```typescript
1589
- * const parsed = parseProofForOnChain(
1590
- * '0x1a2b3c...',
1591
- * ['0x01', '0x02', '0x03'],
1592
- * 3
1593
- * );
1594
- *
1595
- * // parsed.proofHex: '0x1a2b3c...'
1596
- * // parsed.publicInputsHex: ['0x0000...01', '0x0000...02', '0x0000...03']
1597
- * ```
1598
- */
1599
- function parseProofForOnChain(proof, publicInputs, numPublicInputs) {
1600
- const proofHex = ensureHexPrefix(proof);
1601
- const publicInputsHex = publicInputs.map((input) => {
1602
- return hexZeroPad(ensureHexPrefix(input), 32);
1603
- });
1604
- return {
1605
- proofHex,
1606
- publicInputsHex,
1607
- numPublicInputs,
1608
- };
1609
- }
1610
- /** @internal Require a verifier or throw with a helpful message */
1611
- function requireVerifier(circuit, verifier) {
1612
- if (!verifier) {
1613
- throw new Error(`No verifier configured for circuit '${circuit}'. Configure via SDK verifiers option.`);
1614
- }
1615
- return verifier;
1616
- }
1617
- /**
1618
- * Get verifier contract address for a circuit.
1619
- *
1620
- * @param circuit - The canonical circuit identifier. REQUIRED.
1621
- * @param customVerifier - Optional custom verifier contract config
1622
- * @returns Verifier contract address as hex string
1623
- * @throws Error if no verifier is configured for the circuit
1624
- *
1625
- * @example
1626
- * ```typescript
1627
- * const address = getVerifierAddress('coinbase_attestation', verifierConfig);
1628
- * console.log(address); // '0x1234...'
1629
- * ```
1630
- */
1631
- function getVerifierAddress(circuit, customVerifier) {
1632
- return requireVerifier(circuit, customVerifier).address;
1633
- }
1634
- /**
1635
- * Get chain ID for a circuit's verifier contract.
1636
- *
1637
- * @param circuit - The canonical circuit identifier. REQUIRED.
1638
- * @param customVerifier - Optional custom verifier contract config
1639
- * @returns Chain ID number (e.g., 11155111 for Sepolia, 84532 for Base Sepolia)
1640
- * @throws Error if no verifier is configured for the circuit
1641
- *
1642
- * @example
1643
- * ```typescript
1644
- * const chainId = getVerifierChainId('coinbase_attestation', verifierConfig);
1645
- * console.log(chainId); // 11155111
1646
- * ```
1647
- */
1648
- function getVerifierChainId(circuit, customVerifier) {
1649
- return requireVerifier(circuit, customVerifier).chainId;
1650
- }
1651
- /**
1652
- * Where `scope` and `nullifier` sit in each circuit's public inputs.
1653
- *
1654
- * Keyed by `CircuitId`, which is the point: adding a circuit to the SDK makes
1655
- * this object a compile error until somebody opens that circuit's `fn main`
1656
- * and writes its offsets down. Nothing here can be inferred from the proof —
1657
- * the public inputs are a flat array of bytes with no framing.
1658
- *
1659
- * It replaced an `if / else if / else` chain whose final `else` handed every
1660
- * unrecognised circuit — and every call that named no circuit at all —
1661
- * Coinbase's offsets. For `giwa_attestation` that is not a near miss: its
1662
- * public inputs are signal_hash, domain_separator, action_hash, merkle root,
1663
- * scope, nullifier, so the nullifier slot under Coinbase's offsets holds the
1664
- * MERKLE ROOT, a value identical for every user. Duplicate detection built on
1665
- * it would report one person.
1666
- */
1667
- const PUBLIC_INPUT_OFFSETS = Object.freeze({
1668
- // signal_hash, signer_list_merkle_root, scope, nullifier
1669
- coinbase_attestation: { scope: [64, 95], nullifier: [96, 127] },
1670
- // No longer Coinbase's layout. The GIWA circuit gained the EIP-712 pair, so
1671
- // it reads: signal_hash, domain_separator, action_hash, merkle root, then
1672
- // scope and nullifier 64 bytes further along than Coinbase.
1673
- giwa_attestation: { scope: [128, 159], nullifier: [160, 191] },
1674
- // signal_hash, domain_separator, action_hash, scope, nullifier: GIWA's
1675
- // order without the merkle root, since there is no issuer list.
1676
- dojang_playground: { scope: [96, 127], nullifier: [128, 159] },
1677
- // country_list, country_list_length and is_included sit before scope.
1678
- coinbase_country_attestation: { scope: [86, 117], nullifier: [118, 149] },
1679
- // pubkey_modulus_limbs and the bounded domain sit before scope.
1680
- oidc_domain_attestation: { scope: [83, 114], nullifier: [115, 146] },
1681
- // The Korea mDL circuits open with the pair; signal_hash is commented out.
1682
- mdl_kr_ownership: { scope: [0, 31], nullifier: [32, 63] },
1683
- mdl_kr_age: { scope: [0, 31], nullifier: [32, 63] },
1684
- mdl_kr_region: { scope: [0, 31], nullifier: [32, 63] },
1685
- });
1686
- /**
1687
- * The offsets for a circuit, or an error naming what was asked for.
1688
- *
1689
- * Throwing is the whole point. A caller that omits the circuit, or names one
1690
- * this SDK has never heard of, is asking a question with no answer; returning
1691
- * a guess produced a scope and a nullifier that belong to different bytes and
1692
- * looked exactly like real ones.
1693
- */
1694
- function offsetsFor(circuit) {
1695
- if (circuit === undefined) {
1696
- 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(', ')}`);
1697
- }
1698
- if (!isCircuitId(circuit)) {
1699
- throw new Error(`Unknown circuit '${circuit}'. Expected one of: ${ALL_CIRCUIT_IDS.join(', ')}`);
1700
- }
1701
- return PUBLIC_INPUT_OFFSETS[circuit];
1702
- }
1703
- /**
1704
- * Extract scope value from public inputs array.
1705
- *
1706
- * The scope is a bytes32 value encoded across 32 consecutive field elements
1707
- * in the public inputs. The exact position depends on the circuit type.
1708
- *
1709
- * @param publicInputsHex - Array of public input hex strings (zero-padded to 32 bytes)
1710
- * @param circuit - Optional circuit identifier to determine field positions
1711
- * @returns Reconstructed scope as hex string with 0x prefix, or null if inputs are insufficient
1712
- *
1713
- * @example
1714
- * ```typescript
1715
- * const scope = extractScopeFromPublicInputs(publicInputsHex, 'coinbase_attestation');
1716
- * console.log(scope); // '0x7a6b70726f6f66706f72742e636f6d...'
1717
- * ```
1718
- */
1719
- function extractScopeFromPublicInputs(publicInputsHex, circuit) {
1720
- const [start, end] = offsetsFor(circuit).scope;
1721
- if (publicInputsHex.length <= end)
1722
- return null;
1723
- const scopeFields = publicInputsHex.slice(start, end + 1);
1724
- return reconstructBytes32FromFields(scopeFields);
1725
- }
1726
- /**
1727
- * Extracts the nullifier (bytes32) from public inputs based on circuit type.
1728
- *
1729
- * The nullifier is a unique, deterministic hash derived from the user's attestation
1730
- * and scope. It serves as a privacy-preserving user identifier — the same user
1731
- * with the same scope always produces the same nullifier, enabling duplicate
1732
- * detection without revealing the wallet address.
1733
- *
1734
- * @param publicInputsHex - Array of hex-encoded field elements
1735
- * @param circuit - Circuit id. REQUIRED: the layout differs per circuit and
1736
- * cannot be inferred from the proof. It used to default to
1737
- * `coinbase_attestation`, which returned the right-shaped bytes read from
1738
- * the wrong place for every other circuit.
1739
- * @returns Nullifier as hex string (bytes32), or null if publicInputs too short
1740
- *
1741
- * @example
1742
- * ```typescript
1743
- * const nullifier = extractNullifierFromPublicInputs(publicInputs, 'coinbase_attestation');
1744
- * console.log(nullifier); // '0xabc123...'
1745
- * ```
1746
- */
1747
- function extractNullifierFromPublicInputs(publicInputsHex, circuit) {
1748
- const [start, end] = offsetsFor(circuit).nullifier;
1749
- if (publicInputsHex.length <= end)
1750
- return null;
1751
- const nullifierFields = publicInputsHex.slice(start, end + 1);
1752
- return reconstructBytes32FromFields(nullifierFields);
1753
- }
1754
- /**
1755
- * Extract domain string from OIDC Domain Attestation public inputs.
1756
- *
1757
- * The domain is stored as a Noir BoundedVec<u8, 64>, which serializes as
1758
- * [storage[0..64], len]. Each storage element is a u8 value in a field element.
1759
- *
1760
- * @param publicInputsHex - Array of public input hex strings
1761
- * @param circuit - Circuit identifier (must be 'oidc_domain_attestation')
1762
- * @returns Domain as ASCII string, or null if circuit doesn't match or inputs are insufficient
1763
- *
1764
- * @example
1765
- * ```typescript
1766
- * const domain = extractDomainFromPublicInputs(publicInputs, 'oidc_domain_attestation');
1767
- * console.log(domain); // 'example.com'
1768
- * ```
1769
- */
1770
- function extractDomainFromPublicInputs(publicInputsHex, circuit) {
1771
- if (circuit !== 'oidc_domain_attestation')
1772
- return null;
1773
- const layout = OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT;
1774
- if (publicInputsHex.length <= layout.DOMAIN_LEN)
1775
- return null;
1776
- const len = Number(BigInt(publicInputsHex[layout.DOMAIN_LEN]) & 0xffn);
1777
- if (len === 0 || len > 64)
1778
- return null;
1779
- const storageFields = publicInputsHex.slice(layout.DOMAIN_STORAGE_START, layout.DOMAIN_STORAGE_START + len);
1780
- const chars = storageFields.map(f => {
1781
- const byte = Number(BigInt(f) & 0xffn);
1782
- return String.fromCharCode(byte);
1783
- });
1784
- return chars.join('');
1785
- }
1786
- /** @internal Reconstruct a bytes32 value from 32 individual field elements */
1787
- function reconstructBytes32FromFields(fields) {
1788
- if (fields.length !== 32) {
1789
- throw new Error(`Expected 32 fields, got ${fields.length}`);
1790
- }
1791
- const bytes = fields.map(f => {
1792
- const byte = BigInt(f) & 0xffn;
1793
- return byte.toString(16).padStart(2, '0');
1794
- }).join('');
1795
- return '0x' + bytes;
1796
- }
1797
-
1798
940
  /**
1799
941
  * Off-chain proof verification — checking a proof against its verification key,
1800
942
  * with no chain, no RPC, and no gas.
@@ -1808,27 +950,28 @@ function reconstructBytes32FromFields(fields) {
1808
950
  * (through mopro's native Barretenberg); it was only the JavaScript side that
1809
951
  * had no such thing.
1810
952
  *
1811
- * HOW IT WORKS. Proofs here are made with `--oracle_hash keccak` and ZK on, so
1812
- * the matching check is `acirVerifyUltraKeccakZkHonk(proof, vk)`. Two things
1813
- * have to line up:
953
+ * HOW IT WORKS. Proofs here are made for the EVM target (`bb prove -t evm`:
954
+ * keccak transcript, ZK on), so the matching check is bb.js's
955
+ * `UltraHonkVerifierBackend.verifyProof(..., { verifierTarget: 'evm' })`. Two
956
+ * things have to line up:
1814
957
  *
1815
- * The version. `@aztec/bb.js@1.0.0-nightly.20250723` is the WASM build of the
1816
- * same bb the circuits are compiled with. Versions are not interchangeable:
1817
- * measured on the Korea Mobile ID age circuit, the pinned bb writes a 16256-
1818
- * byte proof where bb 1.2.0 writes 16224 for the identical circuit and
1819
- * witness — and the verification key comes out the SAME either way, so the
1820
- * key cannot warn you. A mismatch shows up only as verification failing.
958
+ * The version. `@aztec/bb.js` must be the WASM build of the same bb the
959
+ * circuits are compiled with -- `BB_VERSION` in circuits/toolchain.env,
960
+ * 4.2.0-aztecnr-rc.2 since the beta.19 toolchain. Versions are not
961
+ * interchangeable: on the Korea Mobile ID age circuit the 2025 nightly wrote
962
+ * a 16256-byte proof where bb 1.2.0 wrote 16224 for the identical circuit and
963
+ * witness, with the SAME verification key, so the key cannot warn you. A
964
+ * mismatch shows up only as verification failing.
1821
965
  *
1822
- * The flavour. There are four verify entry points and picking the wrong one
1823
- * is not a polite false. `acirVerifyUltraZKHonk` (poseidon) answers false for
1824
- * a perfectly good proof, and `acirVerifyUltraKeccakHonk` — keccak without ZK
1825
- * — TRAPS the WebAssembly with a bare "unreachable", which looks exactly like
1826
- * a corrupt proof, a mismatched key, or a missing SRS.
966
+ * The target. Verifying an EVM proof as any other target is not a polite
967
+ * false. With the 2025 nightly, keccak-without-ZK trapped the WebAssembly
968
+ * with a bare "unreachable" and the poseidon flavour answered false for a
969
+ * good proof.
1827
970
  *
1828
971
  * THE PROOF ARRIVES IN TWO PIECES. The mobile app splits what Barretenberg
1829
972
  * produced into `proof` and `publicInputs` before sending it, because the
1830
- * Solidity verifier takes them separately. Verifying needs them joined back, in
1831
- * that order, which is what `reconstructHonkProof` does.
973
+ * Solidity verifier takes them separately. bb.js 4.x's verifier takes the same
974
+ * two pieces, so nothing is joined back here.
1832
975
  */
1833
976
  /**
1834
977
  * Where verification keys are published.
@@ -1892,8 +1035,8 @@ function hexToBytes(hex) {
1892
1035
  return out;
1893
1036
  }
1894
1037
  /**
1895
- * @internal Public inputs as the flat byte run Barretenberg expects: each one a
1896
- * 32-byte big-endian field element, concatenated in order.
1038
+ * @internal Public inputs as 32-byte big-endian field elements in hex, the
1039
+ * shape bb.js's verifier takes.
1897
1040
  *
1898
1041
  * The relay carries them as hex strings that are USUALLY already 32 bytes, but
1899
1042
  * a value that happens to have leading zeros can arrive short (`0x01`). Left-
@@ -1901,16 +1044,16 @@ function hexToBytes(hex) {
1901
1044
  * verification that works and one that fails on roughly one proof in a hundred,
1902
1045
  * for no reason the caller can see.
1903
1046
  */
1904
- function packPublicInputs(publicInputs) {
1905
- const out = new Uint8Array(publicInputs.length * 32);
1906
- publicInputs.forEach((input, i) => {
1047
+ function normalisePublicInputs(publicInputs) {
1048
+ return publicInputs.map((input, i) => {
1907
1049
  const bytes = hexToBytes(input);
1908
1050
  if (bytes.length > 32) {
1909
1051
  throw new Error(`Public input ${i} is ${bytes.length} bytes; a field element is at most 32`);
1910
1052
  }
1911
- out.set(bytes, i * 32 + (32 - bytes.length));
1053
+ const padded = new Uint8Array(32);
1054
+ padded.set(bytes, 32 - bytes.length);
1055
+ return '0x' + Array.from(padded, b => b.toString(16).padStart(2, '0')).join('');
1912
1056
  });
1913
- return out;
1914
1057
  }
1915
1058
  /**
1916
1059
  * Fetch a circuit's verification key, with a small cache.
@@ -1921,7 +1064,7 @@ async function fetchVerificationKey(circuit, timeoutMs) {
1921
1064
  const cached = vkCache.get(circuit);
1922
1065
  if (cached && Date.now() - cached.at < VK_CACHE_TTL_MS)
1923
1066
  return cached.vk;
1924
- const vkPath = CIRCUIT_VK_PATHS[circuit];
1067
+ const vkPath = circuits.CIRCUIT_VK_PATHS[circuit];
1925
1068
  if (!vkPath) {
1926
1069
  throw new Error(`No verification key path for circuit '${circuit}'. ` +
1927
1070
  `Add it to CIRCUIT_VK_PATHS, or pass verificationKey directly.`);
@@ -2006,35 +1149,15 @@ async function verifyProofOffChain(circuit, proof, publicInputs, options = {}) {
2006
1149
  if (proofBytes.length === 0) {
2007
1150
  return { valid: false, error: 'Proof is empty' };
2008
1151
  }
2009
- const publicInputBytes = packPublicInputs(publicInputs);
1152
+ // Normalised to 32-byte big-endian fields before handing them over: the
1153
+ // relay can deliver a short value (`0x01`), and the check below must see
1154
+ // exactly what the circuit committed to.
1155
+ const fields = normalisePublicInputs(publicInputs);
2010
1156
  const vk = options.verificationKey ?? (await fetchVerificationKey(circuit, timeoutMs));
2011
- // Public inputs first, then the proof — the order Barretenberg wrote them in
2012
- // before the mobile app split them apart for the Solidity verifier.
2013
- const full = bb.reconstructHonkProof(publicInputBytes, proofBytes);
2014
- api = (await bb.Barretenberg.new({ threads: 1 }));
2015
- // THE VERIFY MUST MATCH HOW THE PROOF WAS MADE, AND THERE ARE FOUR OF THEM.
2016
- //
2017
- // `scripts/build.sh` in the circuits repo proves with `--oracle_hash keccak`
2018
- // and ZK on — so the only entry point that answers correctly is the one that
2019
- // is BOTH. Measured against a real proof of the Korea Mobile ID age circuit,
2020
- // freshly generated against its own key:
2021
- //
2022
- // acirVerifyUltraKeccakZkHonk true ← the right one
2023
- // acirVerifyUltraKeccakHonk TRAPS with a bare "unreachable"
2024
- // acirVerifyUltraZKHonk false (poseidon: wrong oracle hash)
2025
- //
2026
- // The middle row is why this took a while: picking keccak-without-ZK does
2027
- // not return false, it kills the WebAssembly instance with a message that
2028
- // names nothing. It looks identical to a malformed proof, to a mismatched
2029
- // key, and to a missing SRS.
2030
- //
2031
- // Two more things that produce that same trap, both required:
2032
- // initSRSForCircuitSize must be called before the first verify.
2033
- // The key must be a RawBuffer — a plain Uint8Array is marshalled as a
2034
- // length-prefixed vector, so its length is read as key material.
2035
- // Both are what bb.js's own BarretenbergVerifier does before verifying.
2036
- await api.initSRSForCircuitSize(0);
2037
- const valid = await api.acirVerifyUltraKeccakZkHonk(full, new bb.RawBuffer(vk));
1157
+ const barretenberg = await bb.Barretenberg.new({ threads: 1 });
1158
+ api = barretenberg;
1159
+ const verifier = new bb.UltraHonkVerifierBackend(barretenberg);
1160
+ const valid = await verifier.verifyProof({ proof: proofBytes, publicInputs: fields, verificationKey: vk }, { verifierTarget: 'evm' });
2038
1161
  return { valid };
2039
1162
  }
2040
1163
  catch (error) {
@@ -2661,9 +1784,9 @@ class ProofportSDK {
2661
1784
  * ```
2662
1785
  */
2663
1786
  async verifyOnChain(circuit, proof, publicInputs, providerOrSigner) {
2664
- const parsedProof = parseProofForOnChain(proof, publicInputs, publicInputs.length);
1787
+ const parsedProof = core.parseProofForOnChain(proof, publicInputs, publicInputs.length);
2665
1788
  const customVerifier = this.config.verifiers[circuit];
2666
- return verifyProofOnChain(circuit, parsedProof, providerOrSigner, customVerifier);
1789
+ return core.verifyProofOnChain(circuit, parsedProof, providerOrSigner, customVerifier);
2667
1790
  }
2668
1791
  /**
2669
1792
  * Verifies a proof response on-chain using the deployed verifier contract.
@@ -2703,13 +1826,13 @@ class ProofportSDK {
2703
1826
  if (response.status !== 'completed' || !response.proof || !response.publicInputs) {
2704
1827
  return { valid: false, error: 'Invalid or incomplete response' };
2705
1828
  }
2706
- const parsedProof = parseProofForOnChain(response.proof, response.publicInputs, response.publicInputs.length);
1829
+ const parsedProof = core.parseProofForOnChain(response.proof, response.publicInputs, response.publicInputs.length);
2707
1830
  const customVerifier = this.config.verifiers[response.circuit];
2708
1831
  const responseVerifier = {
2709
1832
  verifierAddress: response.verifierAddress,
2710
1833
  chainId: response.chainId,
2711
1834
  };
2712
- return verifyProofOnChain(response.circuit, parsedProof, providerOrSigner, customVerifier, responseVerifier);
1835
+ return core.verifyProofOnChain(response.circuit, parsedProof, providerOrSigner, customVerifier, responseVerifier);
2713
1836
  }
2714
1837
  /**
2715
1838
  * Verifies a proof response off-chain, against the circuit's verification key.
@@ -2761,7 +1884,7 @@ class ProofportSDK {
2761
1884
  */
2762
1885
  getVerifierAddress(circuit) {
2763
1886
  const customVerifier = this.config.verifiers[circuit];
2764
- return getVerifierAddress(circuit, customVerifier);
1887
+ return core.getVerifierAddress(circuit, customVerifier);
2765
1888
  }
2766
1889
  /**
2767
1890
  * Gets the blockchain chain ID where the verifier contract is deployed.
@@ -2782,7 +1905,7 @@ class ProofportSDK {
2782
1905
  */
2783
1906
  getVerifierChainId(circuit) {
2784
1907
  const customVerifier = this.config.verifiers[circuit];
2785
- return getVerifierChainId(circuit, customVerifier);
1908
+ return core.getVerifierChainId(circuit, customVerifier);
2786
1909
  }
2787
1910
  /**
2788
1911
  * Gets metadata for a specific circuit type.
@@ -3073,7 +2196,7 @@ class ProofportSDK {
3073
2196
  // calling createRelayRequest() for it with no signer got no local error and
3074
2197
  // a relay 401 two round trips later.
3075
2198
  static needsWalletSignature(circuit) {
3076
- return CIRCUIT_NEEDS_WALLET_SIGNATURE[circuit] === true;
2199
+ return circuits.CIRCUIT_NEEDS_WALLET_SIGNATURE[circuit] === true;
3077
2200
  }
3078
2201
  async createRelayRequest(circuit, inputs, options = {}) {
3079
2202
  if (!this.relayUrl) {
@@ -3099,13 +2222,13 @@ class ProofportSDK {
3099
2222
  // The circuit id belongs in the table, not in the check.
3100
2223
  {
3101
2224
  const action = inputs.action;
3102
- const binding = circuitActionBinding(circuit);
2225
+ const binding = circuits.circuitActionBinding(circuit);
3103
2226
  if (binding === 'none' && action !== undefined) {
3104
2227
  throw new Error(`${circuit} has no action inputs, so an action sent with it would not be proved. ` +
3105
2228
  'Remove it, or use a circuit that binds one.');
3106
2229
  }
3107
2230
  if (binding === 'required' || (binding === 'optional' && action !== undefined)) {
3108
- const actionError = validateTypedAction(action);
2231
+ const actionError = typedAction.validateTypedAction(action);
3109
2232
  if (actionError) {
3110
2233
  throw new Error(`${actionError} (circuit: ${circuit})`);
3111
2234
  }
@@ -3127,6 +2250,13 @@ class ProofportSDK {
3127
2250
  inputs,
3128
2251
  challenge,
3129
2252
  };
2253
+ // The relay never reads `inputs`, so the scope is also sent on the envelope,
2254
+ // where the relay counts requests per scope. The app proves the scope inside
2255
+ // `inputs` and uses the envelope copy only when `inputs` has none; both are
2256
+ // the same string here.
2257
+ const scope = inputs.scope;
2258
+ if (typeof scope === 'string' && scope.length > 0)
2259
+ body.scope = scope;
3130
2260
  // Sign challenge for circuits that require wallet signature
3131
2261
  if (needsSignature && this.signer) {
3132
2262
  body.signature = await this.signer.signMessage(challenge);
@@ -3392,7 +2522,7 @@ class ProofportSDK {
3392
2522
  * ```
3393
2523
  */
3394
2524
  extractScope(publicInputs, circuit) {
3395
- return extractScopeFromPublicInputs(publicInputs, circuit);
2525
+ return core.extractScopeFromPublicInputs(publicInputs, circuit);
3396
2526
  }
3397
2527
  /**
3398
2528
  * Extracts the nullifier (bytes32) from proof public inputs.
@@ -3416,7 +2546,7 @@ class ProofportSDK {
3416
2546
  * ```
3417
2547
  */
3418
2548
  extractNullifier(publicInputs, circuit) {
3419
- return extractNullifierFromPublicInputs(publicInputs, circuit);
2549
+ return core.extractNullifierFromPublicInputs(publicInputs, circuit);
3420
2550
  }
3421
2551
  /**
3422
2552
  * Extracts the domain string from OIDC Domain Attestation proof public inputs.
@@ -3437,34 +2567,97 @@ class ProofportSDK {
3437
2567
  * ```
3438
2568
  */
3439
2569
  extractDomain(publicInputs, circuit) {
3440
- return extractDomainFromPublicInputs(publicInputs, circuit);
2570
+ return core.extractDomainFromPublicInputs(publicInputs, circuit);
3441
2571
  }
3442
2572
  }
3443
2573
 
3444
- exports.ALL_CIRCUIT_IDS = ALL_CIRCUIT_IDS;
3445
- exports.CIRCUIT_ACTION_BINDING = CIRCUIT_ACTION_BINDING;
3446
- exports.CIRCUIT_IDS = CIRCUIT_IDS;
3447
- exports.CIRCUIT_SUPPORT_STATUS = CIRCUIT_SUPPORT_STATUS;
3448
- exports.CIRCUIT_VK_PATHS = CIRCUIT_VK_PATHS;
3449
- exports.COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT = COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT;
3450
- exports.COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT = COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT;
3451
- exports.EXPERIMENTAL_CIRCUIT_IDS = EXPERIMENTAL_CIRCUIT_IDS;
3452
- exports.MDL_KR_PUBLIC_INPUT_LAYOUT = MDL_KR_PUBLIC_INPUT_LAYOUT;
3453
- exports.OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT = OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT;
3454
- exports.PLANNED_CIRCUIT_IDS = PLANNED_CIRCUIT_IDS;
2574
+ Object.defineProperty(exports, "COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT", {
2575
+ enumerable: true,
2576
+ get: function () { return core.COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT; }
2577
+ });
2578
+ Object.defineProperty(exports, "COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT", {
2579
+ enumerable: true,
2580
+ get: function () { return core.COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT; }
2581
+ });
2582
+ Object.defineProperty(exports, "MDL_KR_PUBLIC_INPUT_LAYOUT", {
2583
+ enumerable: true,
2584
+ get: function () { return core.MDL_KR_PUBLIC_INPUT_LAYOUT; }
2585
+ });
2586
+ Object.defineProperty(exports, "OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT", {
2587
+ enumerable: true,
2588
+ get: function () { return core.OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT; }
2589
+ });
2590
+ Object.defineProperty(exports, "extractDomainFromPublicInputs", {
2591
+ enumerable: true,
2592
+ get: function () { return core.extractDomainFromPublicInputs; }
2593
+ });
2594
+ Object.defineProperty(exports, "extractNullifierFromPublicInputs", {
2595
+ enumerable: true,
2596
+ get: function () { return core.extractNullifierFromPublicInputs; }
2597
+ });
2598
+ Object.defineProperty(exports, "extractScopeFromPublicInputs", {
2599
+ enumerable: true,
2600
+ get: function () { return core.extractScopeFromPublicInputs; }
2601
+ });
2602
+ Object.defineProperty(exports, "ALL_CIRCUIT_IDS", {
2603
+ enumerable: true,
2604
+ get: function () { return circuits.ALL_CIRCUIT_IDS; }
2605
+ });
2606
+ Object.defineProperty(exports, "CIRCUIT_ACTION_BINDING", {
2607
+ enumerable: true,
2608
+ get: function () { return circuits.CIRCUIT_ACTION_BINDING; }
2609
+ });
2610
+ Object.defineProperty(exports, "CIRCUIT_IDS", {
2611
+ enumerable: true,
2612
+ get: function () { return circuits.CIRCUIT_IDS; }
2613
+ });
2614
+ Object.defineProperty(exports, "CIRCUIT_SUPPORT_STATUS", {
2615
+ enumerable: true,
2616
+ get: function () { return circuits.CIRCUIT_SUPPORT_STATUS; }
2617
+ });
2618
+ Object.defineProperty(exports, "CIRCUIT_VK_PATHS", {
2619
+ enumerable: true,
2620
+ get: function () { return circuits.CIRCUIT_VK_PATHS; }
2621
+ });
2622
+ Object.defineProperty(exports, "EXPERIMENTAL_CIRCUIT_IDS", {
2623
+ enumerable: true,
2624
+ get: function () { return circuits.EXPERIMENTAL_CIRCUIT_IDS; }
2625
+ });
2626
+ Object.defineProperty(exports, "PLANNED_CIRCUIT_IDS", {
2627
+ enumerable: true,
2628
+ get: function () { return circuits.PLANNED_CIRCUIT_IDS; }
2629
+ });
2630
+ Object.defineProperty(exports, "SUPPORTED_CIRCUIT_IDS", {
2631
+ enumerable: true,
2632
+ get: function () { return circuits.SUPPORTED_CIRCUIT_IDS; }
2633
+ });
2634
+ Object.defineProperty(exports, "circuitActionBinding", {
2635
+ enumerable: true,
2636
+ get: function () { return circuits.circuitActionBinding; }
2637
+ });
2638
+ Object.defineProperty(exports, "getCircuitSupportStatus", {
2639
+ enumerable: true,
2640
+ get: function () { return circuits.getCircuitSupportStatus; }
2641
+ });
2642
+ Object.defineProperty(exports, "isCircuitId", {
2643
+ enumerable: true,
2644
+ get: function () { return circuits.isCircuitId; }
2645
+ });
2646
+ Object.defineProperty(exports, "isSupportedCircuitId", {
2647
+ enumerable: true,
2648
+ get: function () { return circuits.isSupportedCircuitId; }
2649
+ });
2650
+ Object.defineProperty(exports, "parseTypedAction", {
2651
+ enumerable: true,
2652
+ get: function () { return typedAction.parseTypedAction; }
2653
+ });
2654
+ Object.defineProperty(exports, "validateTypedAction", {
2655
+ enumerable: true,
2656
+ get: function () { return typedAction.validateTypedAction; }
2657
+ });
3455
2658
  exports.ProofportSDK = ProofportSDK;
3456
2659
  exports.RELAY_TERMINAL_STATUSES = RELAY_TERMINAL_STATUSES;
3457
- exports.SUPPORTED_CIRCUIT_IDS = SUPPORTED_CIRCUIT_IDS;
3458
- exports.circuitActionBinding = circuitActionBinding;
3459
2660
  exports.default = ProofportSDK;
3460
- exports.extractDomainFromPublicInputs = extractDomainFromPublicInputs;
3461
- exports.extractNullifierFromPublicInputs = extractNullifierFromPublicInputs;
3462
- exports.extractScopeFromPublicInputs = extractScopeFromPublicInputs;
3463
- exports.getCircuitSupportStatus = getCircuitSupportStatus;
3464
- exports.isCircuitId = isCircuitId;
3465
- exports.isSupportedCircuitId = isSupportedCircuitId;
3466
2661
  exports.isTerminalRelayStatus = isTerminalRelayStatus;
3467
- exports.parseTypedAction = parseTypedAction;
3468
- exports.validateTypedAction = validateTypedAction;
3469
2662
  exports.verifyProofOffChain = verifyProofOffChain;
3470
2663
  //# sourceMappingURL=index.js.map