@zkproofport-app/sdk 0.2.9 → 0.2.11

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
@@ -618,8 +618,16 @@ function validateMdlInputs(circuit, inputs) {
618
618
  const MAX_RETURN_SCHEME_LENGTH = 128;
619
619
  /** RFC 3986 scheme (`ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )`) followed by exactly `://`. */
620
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})?$/;
621
+ /**
622
+ * Schemes we refuse even in the bare `scheme://` shape.
623
+ *
624
+ * `http` and `https` are the back door the shape rule alone leaves open: with
625
+ * no host, `https://` is shaped exactly like a bare custom scheme and would
626
+ * otherwise pass. A browser pointed at nowhere is not a return target, and
627
+ * accepting it would re-open the URL-shaped door this field just closed.
628
+ * `googlechrome://` and `firefox://` are deliberately NOT denied — bare, they
629
+ * foreground that browser without navigating, which is the whole point.
630
+ */
623
631
  const DENIED_RETURN_SCHEMES = new Set([
624
632
  'about',
625
633
  'blob',
@@ -630,6 +638,7 @@ const DENIED_RETURN_SCHEMES = new Set([
630
638
  'file',
631
639
  'ftp',
632
640
  'http',
641
+ 'https',
633
642
  'intent',
634
643
  'javascript',
635
644
  'jar',
@@ -643,6 +652,11 @@ const DENIED_RETURN_SCHEMES = new Set([
643
652
  * value fails fast in the integrator's own code instead of coming back as a
644
653
  * relay 400. The relay re-validates and is the authority.
645
654
  *
655
+ * One shape is accepted: a bare custom scheme such as `mydapp://`. It names an
656
+ * app to switch back to, never a page to visit — so hosts, paths and query
657
+ * strings are rejected, and an https URL is not a valid value. A web page has
658
+ * no app to return to and simply omits the field.
659
+ *
646
660
  * @param value - Candidate return target
647
661
  * @returns `null` when acceptable, otherwise a human-readable error message
648
662
  *
@@ -663,11 +677,8 @@ function validateReturnScheme(value) {
663
677
  return 'returnScheme must not contain whitespace';
664
678
  }
665
679
  const normalized = value.toLowerCase();
666
- if (HTTPS_ORIGIN_RE.test(normalized)) {
667
- return null;
668
- }
669
680
  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';
681
+ return 'returnScheme must be a bare custom scheme such as "mydapp://" — hosts, paths, query strings and fragments are not accepted, and an https URL is not a return target';
671
682
  }
672
683
  const schemeName = normalized.slice(0, normalized.indexOf(':'));
673
684
  if (DENIED_RETURN_SCHEMES.has(schemeName)) {
@@ -675,6 +686,90 @@ function validateReturnScheme(value) {
675
686
  }
676
687
  return null;
677
688
  }
689
+ /**
690
+ * The return target to use when the integrator named none.
691
+ *
692
+ * Only the requesting PAGE can answer "which browser is this?" — iOS gives the
693
+ * ZKProofport app nothing to work with (`sourceApplication` is nil across team
694
+ * identifiers, and there is no public API to foreground an app by bundle id).
695
+ * So the detection has to happen here, in the SDK, and travel with the request.
696
+ *
697
+ * Two cases are detected, each from a POSITIVE token, and each verified in the
698
+ * browser's own source to foreground WITHOUT navigating anywhere:
699
+ *
700
+ * Chrome for iOS (`CriOS/`) -> `googlechrome://`
701
+ * Firefox for iOS (`FxiOS/`) -> `firefox://`
702
+ *
703
+ * The BARE scheme is the point in both cases: it carries no URL, so the browser
704
+ * comes forward with nothing to open and the user lands on the tab they were
705
+ * already reading — no new tab, no navigation, no reload, JS state intact.
706
+ *
707
+ * Chrome: `ios/chrome/app/startup/chrome_app_startup_parameters.mm` replaces
708
+ * the scheme with `http`, producing `http://`, which fails GURL validation
709
+ * (`if (!externalURL.is_valid()) return nil;`), so the startup parameters are
710
+ * nil and there is nothing to open.
711
+ *
712
+ * Firefox: `firefox://` has an empty host, and
713
+ * `Coordinators/Router/RouteBuilder.swift` resolves the host through
714
+ * `DeeplinkInput.Host(rawValue: "")`, which is nil because no case has an empty
715
+ * raw value (`Coordinators/Router/DeeplinkInput.swift`). The scheme is not http
716
+ * either, so `makeRoute(url:)` falls to its final `else { return nil }`, and
717
+ * `SceneDelegate.handleOpenURL` then hits `guard let route = ... else
718
+ * { return }` and does nothing at all. Verified on the warm path — the browser
719
+ * is already running, which is always true here, because the user came from a
720
+ * page in it.
721
+ *
722
+ * Everything else deliberately returns undefined, and that is not a gap:
723
+ *
724
+ * - Safari has no equivalent. No scheme means "just come forward";
725
+ * `https://<url>` navigates and opens a new tab, `x-web-search://` opens a
726
+ * search page. Both lose the user's page.
727
+ * - Brave and Arc for iOS are UNDETECTABLE. Brave ships no UA token and
728
+ * identifies itself only via Client Hints, which WebKit does not implement,
729
+ * so it looks exactly like Safari. Inferring "Safari" by elimination and
730
+ * firing a Safari-ish scheme would throw Brave and Arc users into a browser
731
+ * they were not using — strictly worse than doing nothing.
732
+ * - In-app webviews (KakaoTalk, Naver, Line, Instagram) are not tabs at all.
733
+ * Any scheme destroys the page; the system "< Back to X" breadcrumb is the
734
+ * only thing that preserves it.
735
+ * - Edge (`EdgiOS`) and Opera (`OPiOS` / `OPT`) for iOS are NOT included.
736
+ * Both are closed source, so `microsoft-edge://` and `opera-http://` cannot
737
+ * be checked the way Chrome's and Firefox's were. Their bare schemes are
738
+ * plausible analogues, and that is exactly why they stay out: an unverified
739
+ * guess that opens a new tab is the bug this field exists to avoid. Add one
740
+ * only with evidence that it foregrounds WITHOUT navigating.
741
+ * - Android needs nothing: the ZKProofport app brings itself to the back
742
+ * (`moveTaskToBack`), which resumes the browser exactly as it was.
743
+ *
744
+ * When this returns undefined the field is omitted and the ZKProofport app
745
+ * tells the user the proof was delivered and to switch back themselves.
746
+ *
747
+ * @param userAgent - override, for tests. Defaults to `navigator.userAgent`.
748
+ * Returns undefined when there is no navigator at all (Node, SSR).
749
+ * @returns a scheme that is safe to open, or undefined to send nothing
750
+ *
751
+ * @internal
752
+ */
753
+ function detectReturnScheme(userAgent) {
754
+ const ua = (typeof navigator === 'undefined' ? undefined : navigator.userAgent);
755
+ if (typeof ua !== 'string' || ua.length === 0) {
756
+ return undefined;
757
+ }
758
+ // A device token is required alongside the browser token. `CriOS` and `FxiOS`
759
+ // are already iOS-only and unambiguous on their own, but pairing them with the
760
+ // device token costs nothing and stops a synthetic or spoofed desktop user
761
+ // agent from producing a mobile-only scheme.
762
+ if (!/(iPhone|iPad|iPod)/.test(ua)) {
763
+ return undefined;
764
+ }
765
+ if (/CriOS\//.test(ua)) {
766
+ return 'googlechrome://';
767
+ }
768
+ if (/FxiOS\//.test(ua)) {
769
+ return 'firefox://';
770
+ }
771
+ return undefined;
772
+ }
678
773
  function validateProofRequest(request) {
679
774
  if (!request.requestId) {
680
775
  return { valid: false, error: 'Missing requestId' };
@@ -2229,8 +2324,13 @@ class ProofportSDK {
2229
2324
  body.dappIcon = options.dappIcon;
2230
2325
  if (options.nonce)
2231
2326
  body.nonce = options.nonce;
2232
- if (options.returnScheme)
2233
- body.returnScheme = options.returnScheme;
2327
+ // An explicit value always wins. Otherwise ask the SDK to work one out from
2328
+ // the page's own user agent — the requesting page is the only side that can
2329
+ // (iOS tells the app nothing about who launched it). Both paths can end in
2330
+ // "send nothing", which is a legitimate outcome, not a failure.
2331
+ const returnScheme = options.returnScheme ?? detectReturnScheme();
2332
+ if (returnScheme)
2333
+ body.returnScheme = returnScheme;
2234
2334
  const response = await fetch(`${this.relayUrl}/api/v1/proof/request`, {
2235
2335
  method: 'POST',
2236
2336
  headers: {
@@ -2534,8 +2634,10 @@ class ProofportSDK {
2534
2634
  * @param options - Request options (message, dappName, dappIcon, nonce, returnScheme)
2535
2635
  * @param options.returnScheme - Which app the ZKProofport app should switch back
2536
2636
  * 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'`.
2637
+ * scheme your app registers, e.g. `'mydapp://'`. Only a native app has an
2638
+ * answer to this question; a web app omits the option entirely, because
2639
+ * opening its https origin would launch a new browser tab on a fresh page
2640
+ * rather than return the user to the tab they started from.
2539
2641
  *
2540
2642
  * This is intentionally **not** a URL and not a return address: no path, query
2541
2643
  * string or fragment is accepted, and the ZKProofport app never sends the proof
@@ -2544,14 +2646,26 @@ class ProofportSDK {
2544
2646
  * "which app should come back to the foreground", exactly like the
2545
2647
  * `redirect.native` value a wallet uses to hand control back after signing.
2546
2648
  *
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.
2649
+ * **Omit it and the SDK decides for you.** When the request is created from a
2650
+ * page running in Chrome for iOS (`CriOS`) or Firefox for iOS (`FxiOS`), the
2651
+ * SDK fills in `'googlechrome://'` or `'firefox://'` by itself: opening either
2652
+ * scheme bare brings that browser forward without navigating, so the user lands
2653
+ * back on the tab they were already reading. Every other browser sends nothing,
2654
+ * because nothing safe exists to send — Safari has no such scheme, and Brave
2655
+ * and Arc are indistinguishable from Safari on iOS, so guessing would eject the
2656
+ * user into the wrong browser.
2657
+ * On Android nothing is needed at all: the ZKProofport app brings itself to the
2658
+ * back and the browser resumes exactly as it was. When no target is sent and
2659
+ * none can be inferred, the ZKProofport app tells the user the proof was
2660
+ * delivered and lets them switch back themselves.
2661
+ *
2662
+ * There is no auto-switch on proof failure; only on success and on the user
2663
+ * declining the request.
2550
2664
  *
2551
2665
  * 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.
2666
+ * anything longer than 128 characters, values carrying a host/path/query/fragment
2667
+ * (including https origins such as `'https://myapp.com'`), and the `http:`,
2668
+ * `file:`, `data:`, `javascript:`, `intent:`, `tel:`, `sms:`, `mailto:` families.
2555
2669
  * @returns Promise resolving to RelayProofRequest with requestId, deepLink, pollUrl
2556
2670
  * @throws Error if signer not set, returnScheme is malformed, or the relay request fails
2557
2671
  *