pake-cli 3.15.6 → 3.16.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.
@@ -5,6 +5,10 @@ use crate::util::{
5
5
  };
6
6
  #[cfg(target_os = "macos")]
7
7
  use dispatch::Queue;
8
+ #[cfg(target_os = "macos")]
9
+ use objc2::MainThreadMarker;
10
+ #[cfg(target_os = "macos")]
11
+ use objc2_web_kit::WKUserContentController;
8
12
  #[cfg(target_os = "windows")]
9
13
  use std::{os::windows::ffi::OsStrExt, ptr, sync::OnceLock};
10
14
  use std::{
@@ -28,6 +32,24 @@ use tauri::Theme;
28
32
  #[cfg(target_os = "macos")]
29
33
  use tauri::TitleBarStyle;
30
34
 
35
+ #[cfg(target_os = "macos")]
36
+ fn prepare_macos_new_window_configuration(features: &NewWindowFeatures) -> tauri::Result<()> {
37
+ let mtm = MainThreadMarker::new().ok_or_else(|| {
38
+ std::io::Error::other("macOS new-window configuration must run on the main thread")
39
+ })?;
40
+ // WebKit requires the exact target configuration it supplied to be used
41
+ // for the new page. Replace only the inherited content controller so Wry
42
+ // can register Pake's IPC handler and scripts once on the new webview.
43
+ let controller = unsafe { WKUserContentController::new(mtm) };
44
+ unsafe {
45
+ features
46
+ .opener()
47
+ .target_configuration
48
+ .setUserContentController(&controller);
49
+ }
50
+ Ok(())
51
+ }
52
+
31
53
  #[cfg(target_os = "windows")]
32
54
  fn build_proxy_browser_arg(url: &Url) -> Option<String> {
33
55
  let host = url.host_str()?;
@@ -340,6 +362,11 @@ fn build_window(
340
362
  visible,
341
363
  new_window_features,
342
364
  } = opts;
365
+ #[cfg(target_os = "macos")]
366
+ let use_native_window_tabbing = config.multi_window && new_window_features.is_none();
367
+ #[cfg(target_os = "macos")]
368
+ let prefer_native_window_tabbing = use_native_window_tabbing && label != "pake";
369
+
343
370
  let package_name = tauri_config
344
371
  .product_name
345
372
  .clone()
@@ -353,10 +380,13 @@ fn build_window(
353
380
  ))
354
381
  })?;
355
382
 
383
+ // On macOS both HTTP Basic auth and certificate bypass use the same
384
+ // navigation-delegate proxy. Start on a neutral page so the proxy is in
385
+ // place before the target can issue its first authentication challenge.
356
386
  #[cfg(target_os = "macos")]
357
- let cert_bypass_target = if label == "pake"
358
- && window_config.ignore_certificate_errors
387
+ let auth_target = if label == "pake"
359
388
  && window_config.url_type == "web"
389
+ && (config.basic_auth || window_config.ignore_certificate_errors)
360
390
  {
361
391
  Url::parse(&window_config.url).ok()
362
392
  } else {
@@ -366,7 +396,7 @@ fn build_window(
366
396
  // The delegate must be installed before the first TLS challenge. Start on
367
397
  // a neutral page, then navigate from the with_webview callback below.
368
398
  #[cfg(target_os = "macos")]
369
- let url = if cert_bypass_target.is_some() {
399
+ let url = if auth_target.is_some() {
370
400
  WebviewUrl::CustomProtocol(
371
401
  Url::parse("about:blank").expect("about:blank must be a valid URL"),
372
402
  )
@@ -467,12 +497,18 @@ fn build_window(
467
497
  // any script that reads it (e.g. fullscreen polyfill checks for an opt-out
468
498
  // flag), and toast must register `window.pakeToast` before Rust code
469
499
  // calls show_toast().
470
- window_builder = window_builder.initialization_script(&config_script);
500
+ window_builder = window_builder
501
+ .initialization_script_for_all_frames(&config_script)
502
+ .initialization_script_for_all_frames(include_str!("../inject/link_policy.js"))
503
+ .initialization_script_for_all_frames(include_str!("../inject/auth.js"))
504
+ .initialization_script_for_all_frames(include_str!("../inject/frame_links.js"));
471
505
 
472
506
  // find.js is opt-in via --enable-find and no-ops at runtime when disabled,
473
507
  // so only inject its ~700 lines when the feature is on. Avoids parsing the
474
508
  // find UI on every page load in the common (find-off) case. Matches the
475
509
  // enable_find gating already applied to the Find menu item.
510
+ window_builder = window_builder.initialization_script(include_str!("../inject/styles.js"));
511
+
476
512
  if window_config.enable_find {
477
513
  window_builder = window_builder.initialization_script(include_str!("../inject/find.js"));
478
514
  }
@@ -483,7 +519,6 @@ fn build_window(
483
519
  .initialization_script(include_str!("../inject/event.js"))
484
520
  .initialization_script(include_str!("../inject/style.js"))
485
521
  .initialization_script(include_str!("../inject/theme_refresh.js"))
486
- .initialization_script(include_str!("../inject/auth.js"))
487
522
  .initialization_script(include_str!("../inject/custom.js"));
488
523
 
489
524
  #[cfg(target_os = "windows")]
@@ -545,6 +580,22 @@ fn build_window(
545
580
  };
546
581
  window_builder = window_builder.title_bar_style(title_bar_style);
547
582
  window_builder = window_builder.theme(theme);
583
+
584
+ // Tauri disables automatic tabbing unless an identifier is provided.
585
+ // Existing multi-window apps already have a stable bundle identifier,
586
+ // so use it without exposing another CLI/config option. Web-created
587
+ // popups keep their own native window.
588
+ if use_native_window_tabbing {
589
+ window_builder = window_builder
590
+ .tabbing_identifier(&tauri_config.identifier)
591
+ .on_document_title_changed(|window, title| {
592
+ if !title.trim().is_empty() {
593
+ if let Err(error) = window.set_title(&title) {
594
+ eprintln!("[Pake] Failed to update the macOS tab title: {error}");
595
+ }
596
+ }
597
+ });
598
+ }
548
599
  }
549
600
 
550
601
  // Windows and Linux: set data_directory before proxy_url
@@ -594,26 +645,10 @@ fn build_window(
594
645
  }
595
646
 
596
647
  if let Some(features) = new_window_features {
597
- // Reuse only opener-provided position/size on macOS; sharing the opener
598
- // WKWebViewConfiguration triggers duplicate WKScriptMessageHandler
599
- // registrations on macOS 26+ and crashes the app (issue #1194).
600
648
  #[cfg(target_os = "macos")]
601
- {
602
- if let Some(position) = features.position() {
603
- window_builder = window_builder.position(position.x, position.y);
604
- }
605
-
606
- if let Some(size) = features.size() {
607
- window_builder = window_builder.inner_size(size.width, size.height);
608
- }
609
-
610
- window_builder = window_builder.focused(true);
611
- }
649
+ prepare_macos_new_window_configuration(&features)?;
612
650
 
613
- #[cfg(not(target_os = "macos"))]
614
- {
615
- window_builder = window_builder.window_features(features).focused(true);
616
- }
651
+ window_builder = window_builder.window_features(features).focused(true);
617
652
  }
618
653
 
619
654
  // Capture webview-initiated downloads (blob:, data:, Content-Disposition,
@@ -680,25 +715,51 @@ fn build_window(
680
715
 
681
716
  let window = window_builder.build()?;
682
717
 
683
- // macOS WKWebView ignores the Chromium --ignore-certificate-errors flag, so
684
- // install a host-scoped delegate on the process-lifetime main window only.
685
- // Queue setup after construction so wry cannot replace the proxy while it
686
- // finishes initializing its own navigation delegate.
718
+ // A shared identifier alone leaves each NSWindow in automatic mode.
719
+ // Prefer tabs only for Cmd+N clones so they join the main window's tab
720
+ // group. The main window stays bar-less until another tab exists, while
721
+ // web-auth and window.open popups remain separate native windows.
722
+ #[cfg(target_os = "macos")]
723
+ if prefer_native_window_tabbing {
724
+ let tabbing_window = window.clone();
725
+ Queue::main().exec_async(move || match tabbing_window.ns_window() {
726
+ Ok(ns_window_ptr) => unsafe {
727
+ let Some(ns_window) =
728
+ objc2::rc::Retained::retain(ns_window_ptr as *mut objc2_app_kit::NSWindow)
729
+ else {
730
+ eprintln!("[Pake] Failed to retain the macOS window for tabbing.");
731
+ return;
732
+ };
733
+ ns_window.setTabbingMode(objc2_app_kit::NSWindowTabbingMode::Preferred);
734
+ },
735
+ Err(error) => {
736
+ eprintln!("[Pake] Failed to access the macOS window for tabbing: {error}");
737
+ }
738
+ });
739
+ }
740
+
741
+ // WKWebView does not show an HTTP Basic login dialog and ignores Chromium's
742
+ // certificate-error flag. Install one host-scoped delegate for both flows
743
+ // on the process-lifetime main window, then navigate to the real target.
687
744
  #[cfg(target_os = "macos")]
688
- if let Some(target_url) = cert_bypass_target {
745
+ if let Some(target_url) = auth_target {
689
746
  let allowed_host = target_url
690
747
  .host_str()
691
748
  .expect("web URLs must have a host")
692
749
  .to_owned();
693
- let cert_window = window.clone();
750
+ let prompt_for_basic_auth = config.basic_auth;
751
+ let allow_invalid_certificates = window_config.ignore_certificate_errors;
752
+ let auth_window = window.clone();
694
753
  Queue::main().exec_async(move || {
695
- if let Err(error) = cert_window.with_webview(move |webview| {
696
- if !crate::app::cert::install_cert_bypass_and_navigate(
754
+ if let Err(error) = auth_window.with_webview(move |webview| {
755
+ if !crate::app::auth::install_auth_delegate_and_navigate(
697
756
  webview.inner(),
698
757
  allowed_host,
699
758
  target_url.to_string(),
759
+ prompt_for_basic_auth,
760
+ allow_invalid_certificates,
700
761
  ) {
701
- eprintln!("[Pake] Failed to configure macOS certificate bypass.");
762
+ eprintln!("[Pake] Failed to configure macOS authentication handling.");
702
763
  }
703
764
  }) {
704
765
  eprintln!("[Pake] Failed to access the macOS webview: {error}");
@@ -529,52 +529,6 @@ function isDownloadableFile(url) {
529
529
  }
530
530
  }
531
531
 
532
- // Public suffixes where the registrable domain needs more than two labels.
533
- // Not exhaustive; covers common packaging targets that last-two-label
534
- // matching would collapse incorrectly (e.g. amazon.co.uk vs evil.co.uk,
535
- // or every *.github.io site into one "domain").
536
- const MULTI_PART_PUBLIC_SUFFIXES = [
537
- "co.uk",
538
- "org.uk",
539
- "ac.uk",
540
- "gov.uk",
541
- "com.au",
542
- "net.au",
543
- "org.au",
544
- "co.jp",
545
- "ne.jp",
546
- "or.jp",
547
- "co.kr",
548
- "co.in",
549
- "com.br",
550
- "com.cn",
551
- "com.tw",
552
- "com.hk",
553
- "com.sg",
554
- "github.io",
555
- "gitlab.io",
556
- "pages.dev",
557
- ];
558
-
559
- function getRootDomain(hostname) {
560
- const normalized = String(hostname || "").toLowerCase();
561
- if (!normalized) {
562
- return "";
563
- }
564
-
565
- const parts = normalized.split(".").filter(Boolean);
566
- if (parts.length <= 1) {
567
- return normalized;
568
- }
569
-
570
- const lastTwo = parts.slice(-2).join(".");
571
- if (MULTI_PART_PUBLIC_SUFFIXES.includes(lastTwo) && parts.length >= 3) {
572
- return parts.slice(-3).join(".");
573
- }
574
-
575
- return lastTwo;
576
- }
577
-
578
532
  function normalizeAnchorHref(rawHref) {
579
533
  return typeof rawHref === "string" ? rawHref.trim() : "";
580
534
  }
@@ -591,7 +545,10 @@ function shouldBypassPakeLinkHandling(rawHref) {
591
545
  }
592
546
 
593
547
  function shouldNavigateAuthInCurrentWindow() {
594
- return /macintosh|mac os x/i.test(navigator.userAgent);
548
+ // WKWebView can abort on auth popups, while WebKitGTK may return a truthy
549
+ // proxy even when the native side denies the window. Keep those platforms
550
+ // in-place without changing the working WebView2 popup path on Windows.
551
+ return /mac|linux/i.test(getDesktopPlatform());
595
552
  }
596
553
 
597
554
  function canNavigateAuthUrl(url) {
@@ -639,21 +596,55 @@ function openAuthNavigation(originalWindowOpen, url, name, specs) {
639
596
  return authWindow;
640
597
  }
641
598
 
599
+ // Install the receiver at document start: a subframe can open a link before
600
+ // the main page's DOMContentLoaded routing setup has run.
601
+ let openFrameLink = null;
602
+ const pendingFrameLinks = [];
603
+ function isDescendantFrame(source, parent = window) {
604
+ for (let index = 0; index < parent.frames.length; index++) {
605
+ const frame = parent.frames[index];
606
+ if (frame === source || isDescendantFrame(source, frame)) return true;
607
+ }
608
+ return false;
609
+ }
610
+ function routeFrameLink(source, href) {
611
+ try {
612
+ // Recheck on delivery because the frame may have been removed meanwhile.
613
+ if (!isDescendantFrame(source)) return;
614
+ const url = new URL(href);
615
+ if (!["http:", "https:", "mailto:", "tel:"].includes(url.protocol)) return;
616
+ if (openFrameLink) {
617
+ openFrameLink.call(window, url.href, "_blank");
618
+ } else {
619
+ pendingFrameLinks.push({ source, href: url.href });
620
+ }
621
+ } catch (error) {
622
+ console.error("[Pake] Failed to route frame link:", error);
623
+ }
624
+ }
625
+ window.addEventListener("message", (event) => {
626
+ if (
627
+ event.data?.type !== "pake:frame-external-link" ||
628
+ typeof event.data.url !== "string" ||
629
+ !event.source ||
630
+ event.source === window
631
+ )
632
+ return;
633
+ routeFrameLink(event.source, event.data.url);
634
+ });
635
+ window.addEventListener("pagehide", () => {
636
+ pendingFrameLinks.length = 0;
637
+ });
638
+
642
639
  document.addEventListener("DOMContentLoaded", () => {
643
640
  const tauri = window.__TAURI__;
644
641
  const appWindow = tauri.window.getCurrentWindow();
645
642
  const invoke = tauri.core.invoke;
646
643
  const pakeConfig = window["pakeConfig"] || {};
647
644
  const forceInternalNavigation = pakeConfig.force_internal_navigation === true;
648
- const internalUrlRegex = pakeConfig.internal_url_regex || "";
649
- let internalUrlPattern = null;
650
- if (internalUrlRegex) {
651
- try {
652
- internalUrlPattern = new RegExp(internalUrlRegex);
653
- } catch (e) {
654
- console.error("[Pake] Invalid internal_url_regex pattern:", e);
655
- }
656
- }
645
+ const matchesInternalUrl = createInternalUrlMatcher(
646
+ pakeConfig.internal_url_regex,
647
+ );
657
648
 
658
649
  if (!document.getElementById("pake-top-dom") && hasImmersiveHeader()) {
659
650
  const topDom = document.createElement("div");
@@ -728,39 +719,7 @@ document.addEventListener("DOMContentLoaded", () => {
728
719
  });
729
720
  };
730
721
 
731
- // Check if URL belongs to the same domain (including subdomains)
732
- const isSameDomain = (url) => {
733
- try {
734
- const linkUrl = new URL(url);
735
- const currentUrl = new URL(window.location.href);
736
-
737
- if (linkUrl.hostname === currentUrl.hostname) return true;
738
-
739
- // e.g. www.bilibili.com and m.bilibili.com share bilibili.com;
740
- // amazon.co.uk must not share a root with evil.co.uk.
741
- return (
742
- getRootDomain(currentUrl.hostname) === getRootDomain(linkUrl.hostname)
743
- );
744
- } catch (e) {
745
- return false;
746
- }
747
- };
748
-
749
- // Check if URL should be treated as internal based on regex pattern or domain
750
- const isInternalUrl = (url) => {
751
- // If regex pattern is configured, use it as the primary check
752
- if (internalUrlPattern) {
753
- try {
754
- return internalUrlPattern.test(url);
755
- } catch (e) {
756
- console.error("[Pake] Error testing internal_url_regex:", e);
757
- // Fall back to domain check on error
758
- return isSameDomain(url);
759
- }
760
- }
761
- // Default to domain-based check
762
- return isSameDomain(url);
763
- };
722
+ const isInternalUrl = (url) => matchesInternalUrl(url, window.location.href);
764
723
 
765
724
  const detectAnchorElementClick = (e) => {
766
725
  // Safety check: ensure e.target exists and is an Element with closest method
@@ -944,6 +903,23 @@ document.addEventListener("DOMContentLoaded", () => {
944
903
  }
945
904
  };
946
905
 
906
+ // The sender is untrusted even when it belongs to this webview. This bridge
907
+ // may only open external links, never navigate the top page or create auth
908
+ // windows on a sandboxed frame's behalf.
909
+ openFrameLink = (url) => {
910
+ if (
911
+ forceInternalNavigation ||
912
+ isInternalUrl(url) ||
913
+ window.isAuthLink(url)
914
+ ) {
915
+ return;
916
+ }
917
+ handleExternalLink(url);
918
+ };
919
+ for (const { source, href } of pendingFrameLinks.splice(0)) {
920
+ routeFrameLink(source, href);
921
+ }
922
+
947
923
  // Set the default zoom, There are problems with Loop without using try-catch.
948
924
  try {
949
925
  setDefaultZoom();
@@ -199,9 +199,7 @@
199
199
  return;
200
200
  }
201
201
 
202
- const style = document.createElement("style");
203
- style.id = STYLE_ID;
204
- style.textContent = `
202
+ const css = `
205
203
  #${PANEL_ID} {
206
204
  position: fixed;
207
205
  top: 14px;
@@ -312,9 +310,16 @@
312
310
  }
313
311
  `;
314
312
 
315
- (document.head || document.body || document.documentElement)?.appendChild(
316
- style,
317
- );
313
+ if (typeof window.__PAKE_INJECT_STYLE__ === "function") {
314
+ window.__PAKE_INJECT_STYLE__(css, STYLE_ID);
315
+ } else {
316
+ const style = document.createElement("style");
317
+ style.id = STYLE_ID;
318
+ style.textContent = css;
319
+ (document.head || document.body || document.documentElement)?.appendChild(
320
+ style,
321
+ );
322
+ }
318
323
  }
319
324
 
320
325
  function createButton(label, title, onClick) {
@@ -522,29 +527,39 @@
522
527
  }
523
528
 
524
529
  function runSearch(query = state.query) {
530
+ clearTimeout(state.searchTimer);
525
531
  state.query = query;
526
- clearHighlights();
532
+ // A full scan includes any pending page edits. Disconnect while replacing
533
+ // DOM marks so our own mutations cannot schedule another search.
534
+ stopObservingDocumentChanges();
535
+ try {
536
+ clearHighlights();
537
+
538
+ if (!query) {
539
+ state.matches = [];
540
+ state.activeIndex = -1;
541
+ state.truncated = false;
542
+ updateCounter();
543
+ return getState();
544
+ }
527
545
 
528
- if (!query) {
529
- state.matches = [];
530
- state.activeIndex = -1;
531
- state.truncated = false;
532
- updateCounter();
533
- return getState();
534
- }
546
+ const result = collectMatches(query);
547
+ state.matches = result.matches;
548
+ state.truncated = result.truncated;
549
+ state.activeIndex = state.matches.length > 0 ? 0 : -1;
535
550
 
536
- const result = collectMatches(query);
537
- state.matches = result.matches;
538
- state.truncated = result.truncated;
539
- state.activeIndex = state.matches.length > 0 ? 0 : -1;
551
+ if (!applyCustomHighlights()) {
552
+ applyDomHighlights();
553
+ }
540
554
 
541
- if (!applyCustomHighlights()) {
542
- applyDomHighlights();
555
+ updateCounter();
556
+ scrollActiveIntoView();
557
+ return getState();
558
+ } finally {
559
+ if (state.isOpen) {
560
+ observeDocumentChanges();
561
+ }
543
562
  }
544
-
545
- updateCounter();
546
- scrollActiveIntoView();
547
- return getState();
548
563
  }
549
564
 
550
565
  function debounceSearch(query) {
@@ -0,0 +1,62 @@
1
+ // Subframes do not receive the main page's Tauri/event injection on WebKit.
2
+ // Forward only known external destinations; native internal/auth/blank popup
3
+ // behavior, including its WindowProxy return value, stays with the frame.
4
+ (function () {
5
+ if (window === window.top) return;
6
+ const config = window.pakeConfig || {};
7
+ if (config.force_internal_navigation === true) return;
8
+ const isInternalUrl = createInternalUrlMatcher(config.internal_url_regex);
9
+ const originalOpen = window.open;
10
+
11
+ function externalDestination(rawUrl, name) {
12
+ if (
13
+ typeof rawUrl !== "string" ||
14
+ !rawUrl.trim() ||
15
+ rawUrl.trim().startsWith("#")
16
+ ) {
17
+ return null;
18
+ }
19
+ try {
20
+ const url = new URL(rawUrl, document.baseURI);
21
+ if (!["http:", "https:", "mailto:", "tel:"].includes(url.protocol))
22
+ return null;
23
+ if (window.isAuthPopup(url.href, name)) return null;
24
+ if (isInternalUrl(url.href, config.url)) return null;
25
+ return url.href;
26
+ } catch (error) {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function forward(url) {
32
+ window.top.postMessage({ type: "pake:frame-external-link", url }, "*");
33
+ }
34
+
35
+ window.open = function (url, name, specs) {
36
+ // Named targets can navigate an existing frame and must retain its proxy.
37
+ if (name && String(name).toLowerCase() !== "_blank") {
38
+ return originalOpen.call(window, url, name, specs);
39
+ }
40
+ const external = externalDestination(url, name);
41
+ if (!external) return originalOpen.call(window, url, name, specs);
42
+ forward(external);
43
+ return null;
44
+ };
45
+
46
+ document.addEventListener(
47
+ "click",
48
+ (event) => {
49
+ const anchor = event.target?.closest?.("a[href]");
50
+ if (!anchor || anchor.hasAttribute("download")) return;
51
+ // Named browsing contexts may be part of the page's own frame layout.
52
+ if (anchor.target && !["_blank", "_new", "_self"].includes(anchor.target))
53
+ return;
54
+ const external = externalDestination(anchor.getAttribute("href"), "");
55
+ if (!external) return;
56
+ event.preventDefault();
57
+ event.stopImmediatePropagation();
58
+ forward(external);
59
+ },
60
+ true,
61
+ );
62
+ })();
@@ -25,9 +25,7 @@
25
25
  let monitorId = null;
26
26
 
27
27
  if (!document.getElementById("pake-fullscreen-style")) {
28
- const styleEl = document.createElement("style");
29
- styleEl.id = "pake-fullscreen-style";
30
- styleEl.textContent = `
28
+ const css = `
31
29
  body.pake-fullscreen-active {
32
30
  overflow: hidden !important;
33
31
  }
@@ -51,7 +49,14 @@
51
49
  object-fit: contain !important;
52
50
  }
53
51
  `;
54
- document.head.appendChild(styleEl);
52
+ if (typeof window.__PAKE_INJECT_STYLE__ === "function") {
53
+ window.__PAKE_INJECT_STYLE__(css, "pake-fullscreen-style");
54
+ } else {
55
+ const styleEl = document.createElement("style");
56
+ styleEl.id = "pake-fullscreen-style";
57
+ styleEl.textContent = css;
58
+ document.head.appendChild(styleEl);
59
+ }
55
60
  }
56
61
 
57
62
  function startFullscreenMonitor() {
@@ -0,0 +1,60 @@
1
+ // Shared by the main page and the lightweight subframe bridge.
2
+ // This list intentionally preserves Pake's existing domain routing policy.
3
+ const MULTI_PART_PUBLIC_SUFFIXES = [
4
+ "co.uk",
5
+ "org.uk",
6
+ "ac.uk",
7
+ "gov.uk",
8
+ "com.au",
9
+ "net.au",
10
+ "org.au",
11
+ "co.jp",
12
+ "ne.jp",
13
+ "or.jp",
14
+ "co.kr",
15
+ "co.in",
16
+ "com.br",
17
+ "com.cn",
18
+ "com.tw",
19
+ "com.hk",
20
+ "com.sg",
21
+ "github.io",
22
+ "gitlab.io",
23
+ "pages.dev",
24
+ ];
25
+
26
+ function getRootDomain(hostname) {
27
+ const normalized = String(hostname || "").toLowerCase();
28
+ if (!normalized) return "";
29
+ const parts = normalized.split(".").filter(Boolean);
30
+ if (parts.length <= 1) return normalized;
31
+ const lastTwo = parts.slice(-2).join(".");
32
+ if (MULTI_PART_PUBLIC_SUFFIXES.includes(lastTwo) && parts.length >= 3) {
33
+ return parts.slice(-3).join(".");
34
+ }
35
+ return lastTwo;
36
+ }
37
+
38
+ function createInternalUrlMatcher(pattern) {
39
+ let regex = null;
40
+ if (pattern) {
41
+ try {
42
+ regex = new RegExp(pattern);
43
+ } catch (error) {
44
+ console.error("[Pake] Invalid internal_url_regex pattern:", error);
45
+ }
46
+ }
47
+ return (url, baseUrl) => {
48
+ if (regex) return regex.test(url);
49
+ try {
50
+ const target = new URL(url);
51
+ const current = new URL(baseUrl);
52
+ return (
53
+ target.hostname === current.hostname ||
54
+ getRootDomain(target.hostname) === getRootDomain(current.hostname)
55
+ );
56
+ } catch (error) {
57
+ return false;
58
+ }
59
+ };
60
+ }