@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.js CHANGED
@@ -78,6 +78,26 @@ const CIRCUIT_METADATA = {
78
78
  publicInputsCount: 148,
79
79
  publicInputNames: ['pubkey_modulus_limbs', 'domain', 'scope', 'nullifier', 'provider'],
80
80
  },
81
+ // Korea Mobile ID (mDL) circuits. Counts are byte-flattened field totals
82
+ // from the compiled circuit ABIs: [u8; 32] arrays contribute 32 inputs each.
83
+ mdl_kr_ownership: {
84
+ name: 'Korea Mobile ID — Ownership',
85
+ description: "Prove valid Korean mobile driver's license ownership with selective disclosure",
86
+ publicInputsCount: 97,
87
+ publicInputNames: ['scope', 'nullifier_value', 'disclose_flags', 'owner_commit'],
88
+ },
89
+ mdl_kr_age: {
90
+ name: 'Korea Mobile ID — Age',
91
+ description: "Prove minimum age from Korean mobile driver's license without revealing birth date",
92
+ publicInputsCount: 66,
93
+ publicInputNames: ['scope', 'nullifier_value', 'age_threshold', 'current_year'],
94
+ },
95
+ mdl_kr_region: {
96
+ name: 'Korea Mobile ID — Region',
97
+ description: "Prove si/do residency from Korean mobile driver's license without revealing address",
98
+ publicInputsCount: 96,
99
+ publicInputNames: ['scope', 'nullifier_value', 'region_code'],
100
+ },
81
101
  };
82
102
  /**
83
103
  * Standard verifier contract ABI shared across all Barretenberg-generated verifiers.
@@ -209,6 +229,36 @@ const COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT = {
209
229
  NULLIFIER_START: 118,
210
230
  NULLIFIER_END: 149,
211
231
  };
232
+ /**
233
+ * Korea Mobile ID (mDL) circuit public input layout (field offsets).
234
+ * All three mDL circuits share the same prefix: scope [0..31] and
235
+ * nullifier_value [32..63], each stored one byte per field element.
236
+ * The suffix differs per predicate circuit:
237
+ * - mdl_kr_ownership (97 inputs): disclose_flags [64], owner_commit [65..96]
238
+ * - mdl_kr_age (66 inputs): age_threshold [64], current_year [65]
239
+ * - mdl_kr_region (96 inputs): region_code [64..95]
240
+ *
241
+ * @example
242
+ * ```typescript
243
+ * const nullifier = publicInputs.slice(
244
+ * MDL_KR_PUBLIC_INPUT_LAYOUT.NULLIFIER_START,
245
+ * MDL_KR_PUBLIC_INPUT_LAYOUT.NULLIFIER_END + 1
246
+ * );
247
+ * ```
248
+ */
249
+ const MDL_KR_PUBLIC_INPUT_LAYOUT = {
250
+ SCOPE_START: 0,
251
+ SCOPE_END: 31,
252
+ NULLIFIER_START: 32,
253
+ NULLIFIER_END: 63,
254
+ OWNERSHIP_DISCLOSE_FLAGS: 64,
255
+ OWNERSHIP_OWNER_COMMIT_START: 65,
256
+ OWNERSHIP_OWNER_COMMIT_END: 96,
257
+ AGE_THRESHOLD: 64,
258
+ AGE_CURRENT_YEAR: 65,
259
+ REGION_CODE_START: 64,
260
+ REGION_CODE_END: 95,
261
+ };
212
262
  /**
213
263
  * OIDC Domain Attestation circuit public input layout (field offsets).
214
264
  * Defines the field positions in the flattened public inputs array (148 fields total).
@@ -489,6 +539,142 @@ function isProofportDeepLink(url, scheme = DEFAULT_SCHEME) {
489
539
  * @param request - Proof request to validate
490
540
  * @returns Validation result with `valid` flag and optional `error` message
491
541
  */
542
+ /**
543
+ * Validates a free-text mDL input (scope / targetRegion).
544
+ * Rejects empty or whitespace-only values, control characters (which break
545
+ * deep-link query encoding and downstream logging), and values over 256 chars
546
+ * (server-side length cap mirror). UTF-8 text such as Korean region names is
547
+ * allowed.
548
+ *
549
+ * @returns Error message string, or null when valid.
550
+ */
551
+ function validateMdlTextInput(value, fieldName) {
552
+ if (typeof value !== 'string' || value.trim() === '') {
553
+ return `${fieldName} is required and must be a non-empty string`;
554
+ }
555
+ if (value.length > 256) {
556
+ return `${fieldName} must be 256 characters or fewer`;
557
+ }
558
+ // eslint-disable-next-line no-control-regex
559
+ if (/[\u0000-\u001F\u007F]/.test(value)) {
560
+ return `${fieldName} must not contain control characters`;
561
+ }
562
+ return null;
563
+ }
564
+ /**
565
+ * Validates dapp-supplied inputs for the Korea Mobile ID (mDL) circuits.
566
+ * Shared by deep-link request validation and relay request creation so the
567
+ * same guards apply on every request path.
568
+ *
569
+ * Rules:
570
+ * - `scope` — required non-empty string, ≤256 chars, no control characters
571
+ * - `mdl_kr_ownership.discloseFlags` — optional integer 0–15 (0x0F bitmask)
572
+ * - `mdl_kr_age.ageThreshold` — required integer 1–150
573
+ * - `mdl_kr_region.targetRegion` — required non-empty string (Korean si/do),
574
+ * ≤256 chars, no control characters
575
+ *
576
+ * @param circuit - One of the three mDL circuit identifiers
577
+ * @param inputs - Dapp-supplied circuit inputs
578
+ * @returns Error message string, or null when the inputs are valid
579
+ */
580
+ function validateMdlInputs(circuit, inputs) {
581
+ const scopeError = validateMdlTextInput(inputs.scope, 'scope');
582
+ if (scopeError) {
583
+ return scopeError;
584
+ }
585
+ if (circuit === 'mdl_kr_ownership') {
586
+ const { discloseFlags } = inputs;
587
+ if (discloseFlags !== undefined) {
588
+ if (typeof discloseFlags !== 'number' ||
589
+ !Number.isInteger(discloseFlags) ||
590
+ discloseFlags < 0 ||
591
+ discloseFlags > 0x0f) {
592
+ return 'discloseFlags must be an integer between 0 and 15';
593
+ }
594
+ }
595
+ }
596
+ else if (circuit === 'mdl_kr_age') {
597
+ const { ageThreshold } = inputs;
598
+ if (typeof ageThreshold !== 'number' ||
599
+ !Number.isInteger(ageThreshold) ||
600
+ ageThreshold < 1 ||
601
+ ageThreshold > 150) {
602
+ return 'ageThreshold is required and must be an integer between 1 and 150';
603
+ }
604
+ }
605
+ else {
606
+ const { targetRegion } = inputs;
607
+ const regionError = validateMdlTextInput(targetRegion, 'targetRegion');
608
+ if (regionError) {
609
+ return regionError;
610
+ }
611
+ }
612
+ return null;
613
+ }
614
+ /**
615
+ * Longest accepted `returnScheme` value.
616
+ * Kept in sync with proofport-relay `src/returnScheme.ts`.
617
+ */
618
+ const MAX_RETURN_SCHEME_LENGTH = 128;
619
+ /** RFC 3986 scheme (`ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )`) followed by exactly `://`. */
620
+ const BARE_SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\/$/;
621
+ /** `https://host[:port]` with no userinfo, path, query or fragment. */
622
+ 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})?$/;
623
+ const DENIED_RETURN_SCHEMES = new Set([
624
+ 'about',
625
+ 'blob',
626
+ 'content',
627
+ 'data',
628
+ 'facetime',
629
+ 'facetime-audio',
630
+ 'file',
631
+ 'ftp',
632
+ 'http',
633
+ 'intent',
634
+ 'javascript',
635
+ 'jar',
636
+ 'mailto',
637
+ 'sms',
638
+ 'tel',
639
+ 'vbscript',
640
+ ]);
641
+ /**
642
+ * Validates a `returnScheme` before it is sent to the relay, so a malformed
643
+ * value fails fast in the integrator's own code instead of coming back as a
644
+ * relay 400. The relay re-validates and is the authority.
645
+ *
646
+ * @param value - Candidate return target
647
+ * @returns `null` when acceptable, otherwise a human-readable error message
648
+ *
649
+ * @internal
650
+ */
651
+ function validateReturnScheme(value) {
652
+ if (typeof value !== 'string') {
653
+ return 'returnScheme must be a string';
654
+ }
655
+ if (value.length === 0) {
656
+ return 'returnScheme must not be empty';
657
+ }
658
+ // Length is checked before any regex so a huge string never reaches the matcher.
659
+ if (value.length > MAX_RETURN_SCHEME_LENGTH) {
660
+ return `returnScheme must be at most ${MAX_RETURN_SCHEME_LENGTH} characters`;
661
+ }
662
+ if (/\s/.test(value)) {
663
+ return 'returnScheme must not contain whitespace';
664
+ }
665
+ const normalized = value.toLowerCase();
666
+ if (HTTPS_ORIGIN_RE.test(normalized)) {
667
+ return null;
668
+ }
669
+ if (!BARE_SCHEME_RE.test(normalized)) {
670
+ 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';
671
+ }
672
+ const schemeName = normalized.slice(0, normalized.indexOf(':'));
673
+ if (DENIED_RETURN_SCHEMES.has(schemeName)) {
674
+ return `returnScheme "${schemeName}" is not allowed`;
675
+ }
676
+ return null;
677
+ }
492
678
  function validateProofRequest(request) {
493
679
  if (!request.requestId) {
494
680
  return { valid: false, error: 'Missing requestId' };
@@ -496,7 +682,14 @@ function validateProofRequest(request) {
496
682
  if (!request.circuit) {
497
683
  return { valid: false, error: 'Missing circuit type' };
498
684
  }
499
- if (!['coinbase_attestation', 'coinbase_country_attestation', 'oidc_domain_attestation'].includes(request.circuit)) {
685
+ if (![
686
+ 'coinbase_attestation',
687
+ 'coinbase_country_attestation',
688
+ 'oidc_domain_attestation',
689
+ 'mdl_kr_ownership',
690
+ 'mdl_kr_age',
691
+ 'mdl_kr_region',
692
+ ].includes(request.circuit)) {
500
693
  return { valid: false, error: `Invalid circuit type: ${request.circuit}` };
501
694
  }
502
695
  if (!request.callbackUrl) {
@@ -538,6 +731,14 @@ function validateProofRequest(request) {
538
731
  return { valid: false, error: 'provider must be a non-empty string when specified' };
539
732
  }
540
733
  }
734
+ else if (request.circuit === 'mdl_kr_ownership' ||
735
+ request.circuit === 'mdl_kr_age' ||
736
+ request.circuit === 'mdl_kr_region') {
737
+ const mdlError = validateMdlInputs(request.circuit, request.inputs);
738
+ if (mdlError) {
739
+ return { valid: false, error: mdlError };
740
+ }
741
+ }
541
742
  // Check expiry
542
743
  if (request.expiresAt && Date.now() > request.expiresAt) {
543
744
  return { valid: false, error: 'Request has expired' };
@@ -976,6 +1177,11 @@ function extractScopeFromPublicInputs(publicInputsHex, circuit) {
976
1177
  start = 83;
977
1178
  end = 114;
978
1179
  }
1180
+ else if (circuit !== undefined && circuit.startsWith('mdl_kr_')) {
1181
+ // All Korea mDL circuits: scope [0..31], nullifier_value [32..63]
1182
+ start = 0;
1183
+ end = 31;
1184
+ }
979
1185
  else {
980
1186
  start = 64;
981
1187
  end = 95;
@@ -1013,6 +1219,11 @@ function extractNullifierFromPublicInputs(publicInputsHex, circuit) {
1013
1219
  start = 115;
1014
1220
  end = 146;
1015
1221
  }
1222
+ else if (circuit !== undefined && circuit.startsWith('mdl_kr_')) {
1223
+ // All Korea mDL circuits: scope [0..31], nullifier_value [32..63]
1224
+ start = 32;
1225
+ end = 63;
1226
+ }
1016
1227
  else {
1017
1228
  start = 96;
1018
1229
  end = 127;
@@ -1280,10 +1491,48 @@ class ProofportSDK {
1280
1491
  if (circuit === 'coinbase_country_attestation') {
1281
1492
  return this.createCoinbaseCountryRequest(inputs, options);
1282
1493
  }
1494
+ else if (circuit === 'mdl_kr_ownership' ||
1495
+ circuit === 'mdl_kr_age' ||
1496
+ circuit === 'mdl_kr_region') {
1497
+ return this.createMdlKrRequest(circuit, inputs, options);
1498
+ }
1283
1499
  else {
1284
1500
  return this.createCoinbaseKycRequest(inputs, options);
1285
1501
  }
1286
1502
  }
1503
+ /**
1504
+ * @internal
1505
+ * Creates a Korea Mobile ID (mDL) proof request for any of the three
1506
+ * mDL predicate circuits (ownership / age / region).
1507
+ *
1508
+ * The mobile app collects the license data on-device; the dapp only
1509
+ * supplies the predicate parameters validated here. No wallet signature
1510
+ * is required for mDL circuits.
1511
+ *
1512
+ * @param circuit - 'mdl_kr_ownership' | 'mdl_kr_age' | 'mdl_kr_region'
1513
+ * @param inputs - Predicate inputs (see MdlKr*Inputs types)
1514
+ * @param options - Request configuration options
1515
+ *
1516
+ * @throws Error if the predicate inputs fail validation
1517
+ */
1518
+ createMdlKrRequest(circuit, inputs, options = {}) {
1519
+ const mdlError = validateMdlInputs(circuit, inputs);
1520
+ if (mdlError) {
1521
+ throw new Error(`${mdlError} (circuit: ${circuit})`);
1522
+ }
1523
+ const request = {
1524
+ requestId: generateRequestId(),
1525
+ circuit,
1526
+ inputs,
1527
+ message: options.message,
1528
+ dappName: options.dappName,
1529
+ dappIcon: options.dappIcon,
1530
+ createdAt: Date.now(),
1531
+ expiresAt: Date.now() + (options.expiresInMs || DEFAULT_REQUEST_EXPIRY_MS),
1532
+ };
1533
+ this.pendingRequests.set(request.requestId, request);
1534
+ return request;
1535
+ }
1287
1536
  // ============ Deep Link Generation ============
1288
1537
  /**
1289
1538
  * @internal
@@ -1944,6 +2193,22 @@ class ProofportSDK {
1944
2193
  if (needsSignature && !this.signer) {
1945
2194
  throw new Error('Signer not set. Call setSigner() first. Wallet signature is required for this circuit.');
1946
2195
  }
2196
+ // mDL circuits: validate predicate inputs client-side before hitting the
2197
+ // relay, so a bad ageThreshold / targetRegion fails fast with a clear error.
2198
+ if (circuit === 'mdl_kr_ownership' || circuit === 'mdl_kr_age' || circuit === 'mdl_kr_region') {
2199
+ const mdlError = validateMdlInputs(circuit, inputs);
2200
+ if (mdlError) {
2201
+ throw new Error(`${mdlError} (circuit: ${circuit})`);
2202
+ }
2203
+ }
2204
+ // Return target: validated before we burn a challenge, so a typo surfaces
2205
+ // as a local error instead of a relay 400 on the second round trip.
2206
+ if (options.returnScheme !== undefined) {
2207
+ const returnSchemeError = validateReturnScheme(options.returnScheme);
2208
+ if (returnSchemeError) {
2209
+ throw new Error(returnSchemeError);
2210
+ }
2211
+ }
1947
2212
  // Get challenge + requestId from relay
1948
2213
  const { requestId, challenge } = await this.getChallenge();
1949
2214
  const body = {
@@ -1964,6 +2229,8 @@ class ProofportSDK {
1964
2229
  body.dappIcon = options.dappIcon;
1965
2230
  if (options.nonce)
1966
2231
  body.nonce = options.nonce;
2232
+ if (options.returnScheme)
2233
+ body.returnScheme = options.returnScheme;
1967
2234
  const response = await fetch(`${this.relayUrl}/api/v1/proof/request`, {
1968
2235
  method: 'POST',
1969
2236
  headers: {
@@ -2264,9 +2531,29 @@ class ProofportSDK {
2264
2531
  *
2265
2532
  * @param circuit - Circuit type identifier
2266
2533
  * @param inputs - Circuit-specific inputs
2267
- * @param options - Request options (message, dappName, dappIcon, nonce)
2534
+ * @param options - Request options (message, dappName, dappIcon, nonce, returnScheme)
2535
+ * @param options.returnScheme - Which app the ZKProofport app should switch back
2536
+ * to once it has finished. Put **your own** deep link scheme here — the bare
2537
+ * scheme your app registers, e.g. `'mydapp://'`. A web app can instead pass its
2538
+ * own https origin, e.g. `'https://myapp.com'`.
2539
+ *
2540
+ * This is intentionally **not** a URL and not a return address: no path, query
2541
+ * string or fragment is accepted, and the ZKProofport app never sends the proof
2542
+ * there. The proof still comes back to you the same way it always did — over the
2543
+ * relay, via `waitForProof()` / `waitForResult()`. `returnScheme` only answers
2544
+ * "which app should come back to the foreground", exactly like the
2545
+ * `redirect.native` value a wallet uses to hand control back after signing.
2546
+ *
2547
+ * **Omit it and nothing changes** — the user simply stays in the ZKProofport app
2548
+ * after the proof and switches back manually. There is no auto-switch on proof
2549
+ * failure either; only on success and on the user declining the request.
2550
+ *
2551
+ * Rejected with an error before the request is sent: empty or whitespace values,
2552
+ * anything longer than 128 characters, values containing a path/query/fragment,
2553
+ * and the `http:`, `file:`, `data:`, `javascript:`, `intent:`, `tel:`, `sms:`,
2554
+ * `mailto:` families.
2268
2555
  * @returns Promise resolving to RelayProofRequest with requestId, deepLink, pollUrl
2269
- * @throws Error if signer not set or relay request fails
2556
+ * @throws Error if signer not set, returnScheme is malformed, or the relay request fails
2270
2557
  *
2271
2558
  * @example
2272
2559
  * ```typescript
@@ -2275,7 +2562,7 @@ class ProofportSDK {
2275
2562
  *
2276
2563
  * const relay = await sdk.createRelayRequest('coinbase_attestation', {
2277
2564
  * scope: 'myapp.com'
2278
- * }, { dappName: 'My DApp' });
2565
+ * }, { dappName: 'My DApp', returnScheme: 'mydapp://' });
2279
2566
  *
2280
2567
  * // Generate QR code from relay deep link
2281
2568
  * const qr = await sdk.generateQRCode(relay.deepLink);
@@ -2292,6 +2579,7 @@ ProofportSDK.WALLET_SIGNATURE_CIRCUITS = [
2292
2579
 
2293
2580
  exports.COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT = COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT;
2294
2581
  exports.COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT = COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT;
2582
+ exports.MDL_KR_PUBLIC_INPUT_LAYOUT = MDL_KR_PUBLIC_INPUT_LAYOUT;
2295
2583
  exports.OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT = OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT;
2296
2584
  exports.ProofportSDK = ProofportSDK;
2297
2585
  exports.default = ProofportSDK;