@zkproofport-app/sdk 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -73,6 +73,26 @@ const CIRCUIT_METADATA = {
73
73
  publicInputsCount: 148,
74
74
  publicInputNames: ['pubkey_modulus_limbs', 'domain', 'scope', 'nullifier', 'provider'],
75
75
  },
76
+ // Korea Mobile ID (mDL) circuits. Counts are byte-flattened field totals
77
+ // from the compiled circuit ABIs: [u8; 32] arrays contribute 32 inputs each.
78
+ mdl_kr_ownership: {
79
+ name: 'Korea Mobile ID — Ownership',
80
+ description: "Prove valid Korean mobile driver's license ownership with selective disclosure",
81
+ publicInputsCount: 97,
82
+ publicInputNames: ['scope', 'nullifier_value', 'disclose_flags', 'owner_commit'],
83
+ },
84
+ mdl_kr_age: {
85
+ name: 'Korea Mobile ID — Age',
86
+ description: "Prove minimum age from Korean mobile driver's license without revealing birth date",
87
+ publicInputsCount: 66,
88
+ publicInputNames: ['scope', 'nullifier_value', 'age_threshold', 'current_year'],
89
+ },
90
+ mdl_kr_region: {
91
+ name: 'Korea Mobile ID — Region',
92
+ description: "Prove si/do residency from Korean mobile driver's license without revealing address",
93
+ publicInputsCount: 96,
94
+ publicInputNames: ['scope', 'nullifier_value', 'region_code'],
95
+ },
76
96
  };
77
97
  /**
78
98
  * Standard verifier contract ABI shared across all Barretenberg-generated verifiers.
@@ -204,6 +224,36 @@ const COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT = {
204
224
  NULLIFIER_START: 118,
205
225
  NULLIFIER_END: 149,
206
226
  };
227
+ /**
228
+ * Korea Mobile ID (mDL) circuit public input layout (field offsets).
229
+ * All three mDL circuits share the same prefix: scope [0..31] and
230
+ * nullifier_value [32..63], each stored one byte per field element.
231
+ * The suffix differs per predicate circuit:
232
+ * - mdl_kr_ownership (97 inputs): disclose_flags [64], owner_commit [65..96]
233
+ * - mdl_kr_age (66 inputs): age_threshold [64], current_year [65]
234
+ * - mdl_kr_region (96 inputs): region_code [64..95]
235
+ *
236
+ * @example
237
+ * ```typescript
238
+ * const nullifier = publicInputs.slice(
239
+ * MDL_KR_PUBLIC_INPUT_LAYOUT.NULLIFIER_START,
240
+ * MDL_KR_PUBLIC_INPUT_LAYOUT.NULLIFIER_END + 1
241
+ * );
242
+ * ```
243
+ */
244
+ const MDL_KR_PUBLIC_INPUT_LAYOUT = {
245
+ SCOPE_START: 0,
246
+ SCOPE_END: 31,
247
+ NULLIFIER_START: 32,
248
+ NULLIFIER_END: 63,
249
+ OWNERSHIP_DISCLOSE_FLAGS: 64,
250
+ OWNERSHIP_OWNER_COMMIT_START: 65,
251
+ OWNERSHIP_OWNER_COMMIT_END: 96,
252
+ AGE_THRESHOLD: 64,
253
+ AGE_CURRENT_YEAR: 65,
254
+ REGION_CODE_START: 64,
255
+ REGION_CODE_END: 95,
256
+ };
207
257
  /**
208
258
  * OIDC Domain Attestation circuit public input layout (field offsets).
209
259
  * Defines the field positions in the flattened public inputs array (148 fields total).
@@ -484,6 +534,142 @@ function isProofportDeepLink(url, scheme = DEFAULT_SCHEME) {
484
534
  * @param request - Proof request to validate
485
535
  * @returns Validation result with `valid` flag and optional `error` message
486
536
  */
537
+ /**
538
+ * Validates a free-text mDL input (scope / targetRegion).
539
+ * Rejects empty or whitespace-only values, control characters (which break
540
+ * deep-link query encoding and downstream logging), and values over 256 chars
541
+ * (server-side length cap mirror). UTF-8 text such as Korean region names is
542
+ * allowed.
543
+ *
544
+ * @returns Error message string, or null when valid.
545
+ */
546
+ function validateMdlTextInput(value, fieldName) {
547
+ if (typeof value !== 'string' || value.trim() === '') {
548
+ return `${fieldName} is required and must be a non-empty string`;
549
+ }
550
+ if (value.length > 256) {
551
+ return `${fieldName} must be 256 characters or fewer`;
552
+ }
553
+ // eslint-disable-next-line no-control-regex
554
+ if (/[\u0000-\u001F\u007F]/.test(value)) {
555
+ return `${fieldName} must not contain control characters`;
556
+ }
557
+ return null;
558
+ }
559
+ /**
560
+ * Validates dapp-supplied inputs for the Korea Mobile ID (mDL) circuits.
561
+ * Shared by deep-link request validation and relay request creation so the
562
+ * same guards apply on every request path.
563
+ *
564
+ * Rules:
565
+ * - `scope` — required non-empty string, ≤256 chars, no control characters
566
+ * - `mdl_kr_ownership.discloseFlags` — optional integer 0–15 (0x0F bitmask)
567
+ * - `mdl_kr_age.ageThreshold` — required integer 1–150
568
+ * - `mdl_kr_region.targetRegion` — required non-empty string (Korean si/do),
569
+ * ≤256 chars, no control characters
570
+ *
571
+ * @param circuit - One of the three mDL circuit identifiers
572
+ * @param inputs - Dapp-supplied circuit inputs
573
+ * @returns Error message string, or null when the inputs are valid
574
+ */
575
+ function validateMdlInputs(circuit, inputs) {
576
+ const scopeError = validateMdlTextInput(inputs.scope, 'scope');
577
+ if (scopeError) {
578
+ return scopeError;
579
+ }
580
+ if (circuit === 'mdl_kr_ownership') {
581
+ const { discloseFlags } = inputs;
582
+ if (discloseFlags !== undefined) {
583
+ if (typeof discloseFlags !== 'number' ||
584
+ !Number.isInteger(discloseFlags) ||
585
+ discloseFlags < 0 ||
586
+ discloseFlags > 0x0f) {
587
+ return 'discloseFlags must be an integer between 0 and 15';
588
+ }
589
+ }
590
+ }
591
+ else if (circuit === 'mdl_kr_age') {
592
+ const { ageThreshold } = inputs;
593
+ if (typeof ageThreshold !== 'number' ||
594
+ !Number.isInteger(ageThreshold) ||
595
+ ageThreshold < 1 ||
596
+ ageThreshold > 150) {
597
+ return 'ageThreshold is required and must be an integer between 1 and 150';
598
+ }
599
+ }
600
+ else {
601
+ const { targetRegion } = inputs;
602
+ const regionError = validateMdlTextInput(targetRegion, 'targetRegion');
603
+ if (regionError) {
604
+ return regionError;
605
+ }
606
+ }
607
+ return null;
608
+ }
609
+ /**
610
+ * Longest accepted `returnScheme` value.
611
+ * Kept in sync with proofport-relay `src/returnScheme.ts`.
612
+ */
613
+ const MAX_RETURN_SCHEME_LENGTH = 128;
614
+ /** RFC 3986 scheme (`ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )`) followed by exactly `://`. */
615
+ const BARE_SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\/$/;
616
+ /** `https://host[:port]` with no userinfo, path, query or fragment. */
617
+ const HTTPS_ORIGIN_RE = /^https:\/\/[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+(:[0-9]{1,5})?$/;
618
+ const DENIED_RETURN_SCHEMES = new Set([
619
+ 'about',
620
+ 'blob',
621
+ 'content',
622
+ 'data',
623
+ 'facetime',
624
+ 'facetime-audio',
625
+ 'file',
626
+ 'ftp',
627
+ 'http',
628
+ 'intent',
629
+ 'javascript',
630
+ 'jar',
631
+ 'mailto',
632
+ 'sms',
633
+ 'tel',
634
+ 'vbscript',
635
+ ]);
636
+ /**
637
+ * Validates a `returnScheme` before it is sent to the relay, so a malformed
638
+ * value fails fast in the integrator's own code instead of coming back as a
639
+ * relay 400. The relay re-validates and is the authority.
640
+ *
641
+ * @param value - Candidate return target
642
+ * @returns `null` when acceptable, otherwise a human-readable error message
643
+ *
644
+ * @internal
645
+ */
646
+ function validateReturnScheme(value) {
647
+ if (typeof value !== 'string') {
648
+ return 'returnScheme must be a string';
649
+ }
650
+ if (value.length === 0) {
651
+ return 'returnScheme must not be empty';
652
+ }
653
+ // Length is checked before any regex so a huge string never reaches the matcher.
654
+ if (value.length > MAX_RETURN_SCHEME_LENGTH) {
655
+ return `returnScheme must be at most ${MAX_RETURN_SCHEME_LENGTH} characters`;
656
+ }
657
+ if (/\s/.test(value)) {
658
+ return 'returnScheme must not contain whitespace';
659
+ }
660
+ const normalized = value.toLowerCase();
661
+ if (HTTPS_ORIGIN_RE.test(normalized)) {
662
+ return null;
663
+ }
664
+ if (!BARE_SCHEME_RE.test(normalized)) {
665
+ return 'returnScheme must be a bare custom scheme such as "mydapp://" or an https origin such as "https://myapp.com" — paths, query strings and fragments are not accepted';
666
+ }
667
+ const schemeName = normalized.slice(0, normalized.indexOf(':'));
668
+ if (DENIED_RETURN_SCHEMES.has(schemeName)) {
669
+ return `returnScheme "${schemeName}" is not allowed`;
670
+ }
671
+ return null;
672
+ }
487
673
  function validateProofRequest(request) {
488
674
  if (!request.requestId) {
489
675
  return { valid: false, error: 'Missing requestId' };
@@ -491,7 +677,14 @@ function validateProofRequest(request) {
491
677
  if (!request.circuit) {
492
678
  return { valid: false, error: 'Missing circuit type' };
493
679
  }
494
- if (!['coinbase_attestation', 'coinbase_country_attestation', 'oidc_domain_attestation'].includes(request.circuit)) {
680
+ if (![
681
+ 'coinbase_attestation',
682
+ 'coinbase_country_attestation',
683
+ 'oidc_domain_attestation',
684
+ 'mdl_kr_ownership',
685
+ 'mdl_kr_age',
686
+ 'mdl_kr_region',
687
+ ].includes(request.circuit)) {
495
688
  return { valid: false, error: `Invalid circuit type: ${request.circuit}` };
496
689
  }
497
690
  if (!request.callbackUrl) {
@@ -533,6 +726,14 @@ function validateProofRequest(request) {
533
726
  return { valid: false, error: 'provider must be a non-empty string when specified' };
534
727
  }
535
728
  }
729
+ else if (request.circuit === 'mdl_kr_ownership' ||
730
+ request.circuit === 'mdl_kr_age' ||
731
+ request.circuit === 'mdl_kr_region') {
732
+ const mdlError = validateMdlInputs(request.circuit, request.inputs);
733
+ if (mdlError) {
734
+ return { valid: false, error: mdlError };
735
+ }
736
+ }
536
737
  // Check expiry
537
738
  if (request.expiresAt && Date.now() > request.expiresAt) {
538
739
  return { valid: false, error: 'Request has expired' };
@@ -3838,6 +4039,11 @@ function extractScopeFromPublicInputs(publicInputsHex, circuit) {
3838
4039
  start = 83;
3839
4040
  end = 114;
3840
4041
  }
4042
+ else if (circuit !== undefined && circuit.startsWith('mdl_kr_')) {
4043
+ // All Korea mDL circuits: scope [0..31], nullifier_value [32..63]
4044
+ start = 0;
4045
+ end = 31;
4046
+ }
3841
4047
  else {
3842
4048
  start = 64;
3843
4049
  end = 95;
@@ -3875,6 +4081,11 @@ function extractNullifierFromPublicInputs(publicInputsHex, circuit) {
3875
4081
  start = 115;
3876
4082
  end = 146;
3877
4083
  }
4084
+ else if (circuit !== undefined && circuit.startsWith('mdl_kr_')) {
4085
+ // All Korea mDL circuits: scope [0..31], nullifier_value [32..63]
4086
+ start = 32;
4087
+ end = 63;
4088
+ }
3878
4089
  else {
3879
4090
  start = 96;
3880
4091
  end = 127;
@@ -4142,10 +4353,48 @@ class ProofportSDK {
4142
4353
  if (circuit === 'coinbase_country_attestation') {
4143
4354
  return this.createCoinbaseCountryRequest(inputs, options);
4144
4355
  }
4356
+ else if (circuit === 'mdl_kr_ownership' ||
4357
+ circuit === 'mdl_kr_age' ||
4358
+ circuit === 'mdl_kr_region') {
4359
+ return this.createMdlKrRequest(circuit, inputs, options);
4360
+ }
4145
4361
  else {
4146
4362
  return this.createCoinbaseKycRequest(inputs, options);
4147
4363
  }
4148
4364
  }
4365
+ /**
4366
+ * @internal
4367
+ * Creates a Korea Mobile ID (mDL) proof request for any of the three
4368
+ * mDL predicate circuits (ownership / age / region).
4369
+ *
4370
+ * The mobile app collects the license data on-device; the dapp only
4371
+ * supplies the predicate parameters validated here. No wallet signature
4372
+ * is required for mDL circuits.
4373
+ *
4374
+ * @param circuit - 'mdl_kr_ownership' | 'mdl_kr_age' | 'mdl_kr_region'
4375
+ * @param inputs - Predicate inputs (see MdlKr*Inputs types)
4376
+ * @param options - Request configuration options
4377
+ *
4378
+ * @throws Error if the predicate inputs fail validation
4379
+ */
4380
+ createMdlKrRequest(circuit, inputs, options = {}) {
4381
+ const mdlError = validateMdlInputs(circuit, inputs);
4382
+ if (mdlError) {
4383
+ throw new Error(`${mdlError} (circuit: ${circuit})`);
4384
+ }
4385
+ const request = {
4386
+ requestId: generateRequestId(),
4387
+ circuit,
4388
+ inputs,
4389
+ message: options.message,
4390
+ dappName: options.dappName,
4391
+ dappIcon: options.dappIcon,
4392
+ createdAt: Date.now(),
4393
+ expiresAt: Date.now() + (options.expiresInMs || DEFAULT_REQUEST_EXPIRY_MS),
4394
+ };
4395
+ this.pendingRequests.set(request.requestId, request);
4396
+ return request;
4397
+ }
4149
4398
  // ============ Deep Link Generation ============
4150
4399
  /**
4151
4400
  * @internal
@@ -4806,6 +5055,22 @@ class ProofportSDK {
4806
5055
  if (needsSignature && !this.signer) {
4807
5056
  throw new Error('Signer not set. Call setSigner() first. Wallet signature is required for this circuit.');
4808
5057
  }
5058
+ // mDL circuits: validate predicate inputs client-side before hitting the
5059
+ // relay, so a bad ageThreshold / targetRegion fails fast with a clear error.
5060
+ if (circuit === 'mdl_kr_ownership' || circuit === 'mdl_kr_age' || circuit === 'mdl_kr_region') {
5061
+ const mdlError = validateMdlInputs(circuit, inputs);
5062
+ if (mdlError) {
5063
+ throw new Error(`${mdlError} (circuit: ${circuit})`);
5064
+ }
5065
+ }
5066
+ // Return target: validated before we burn a challenge, so a typo surfaces
5067
+ // as a local error instead of a relay 400 on the second round trip.
5068
+ if (options.returnScheme !== undefined) {
5069
+ const returnSchemeError = validateReturnScheme(options.returnScheme);
5070
+ if (returnSchemeError) {
5071
+ throw new Error(returnSchemeError);
5072
+ }
5073
+ }
4809
5074
  // Get challenge + requestId from relay
4810
5075
  const { requestId, challenge } = await this.getChallenge();
4811
5076
  const body = {
@@ -4826,6 +5091,8 @@ class ProofportSDK {
4826
5091
  body.dappIcon = options.dappIcon;
4827
5092
  if (options.nonce)
4828
5093
  body.nonce = options.nonce;
5094
+ if (options.returnScheme)
5095
+ body.returnScheme = options.returnScheme;
4829
5096
  const response = await fetch(`${this.relayUrl}/api/v1/proof/request`, {
4830
5097
  method: 'POST',
4831
5098
  headers: {
@@ -5126,9 +5393,29 @@ class ProofportSDK {
5126
5393
  *
5127
5394
  * @param circuit - Circuit type identifier
5128
5395
  * @param inputs - Circuit-specific inputs
5129
- * @param options - Request options (message, dappName, dappIcon, nonce)
5396
+ * @param options - Request options (message, dappName, dappIcon, nonce, returnScheme)
5397
+ * @param options.returnScheme - Which app the ZKProofport app should switch back
5398
+ * to once it has finished. Put **your own** deep link scheme here — the bare
5399
+ * scheme your app registers, e.g. `'mydapp://'`. A web app can instead pass its
5400
+ * own https origin, e.g. `'https://myapp.com'`.
5401
+ *
5402
+ * This is intentionally **not** a URL and not a return address: no path, query
5403
+ * string or fragment is accepted, and the ZKProofport app never sends the proof
5404
+ * there. The proof still comes back to you the same way it always did — over the
5405
+ * relay, via `waitForProof()` / `waitForResult()`. `returnScheme` only answers
5406
+ * "which app should come back to the foreground", exactly like the
5407
+ * `redirect.native` value a wallet uses to hand control back after signing.
5408
+ *
5409
+ * **Omit it and nothing changes** — the user simply stays in the ZKProofport app
5410
+ * after the proof and switches back manually. There is no auto-switch on proof
5411
+ * failure either; only on success and on the user declining the request.
5412
+ *
5413
+ * Rejected with an error before the request is sent: empty or whitespace values,
5414
+ * anything longer than 128 characters, values containing a path/query/fragment,
5415
+ * and the `http:`, `file:`, `data:`, `javascript:`, `intent:`, `tel:`, `sms:`,
5416
+ * `mailto:` families.
5130
5417
  * @returns Promise resolving to RelayProofRequest with requestId, deepLink, pollUrl
5131
- * @throws Error if signer not set or relay request fails
5418
+ * @throws Error if signer not set, returnScheme is malformed, or the relay request fails
5132
5419
  *
5133
5420
  * @example
5134
5421
  * ```typescript
@@ -5137,7 +5424,7 @@ class ProofportSDK {
5137
5424
  *
5138
5425
  * const relay = await sdk.createRelayRequest('coinbase_attestation', {
5139
5426
  * scope: 'myapp.com'
5140
- * }, { dappName: 'My DApp' });
5427
+ * }, { dappName: 'My DApp', returnScheme: 'mydapp://' });
5141
5428
  *
5142
5429
  * // Generate QR code from relay deep link
5143
5430
  * const qr = await sdk.generateQRCode(relay.deepLink);
@@ -5152,5 +5439,5 @@ ProofportSDK.WALLET_SIGNATURE_CIRCUITS = [
5152
5439
  'coinbase_country_attestation',
5153
5440
  ];
5154
5441
 
5155
- export { COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, ProofportSDK, ProofportSDK as default, extractDomainFromPublicInputs, extractNullifierFromPublicInputs, extractScopeFromPublicInputs };
5442
+ export { COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, MDL_KR_PUBLIC_INPUT_LAYOUT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, ProofportSDK, ProofportSDK as default, extractDomainFromPublicInputs, extractNullifierFromPublicInputs, extractScopeFromPublicInputs };
5156
5443
  //# sourceMappingURL=index.mjs.map