brave-core icon indicating copy to clipboard operation
brave-core copied to clipboard

[PSST] Feature refactoring part

Open vadimstruts opened this issue 11 months ago • 3 comments

Resolves https://github.com/brave/brave-browser/issues/46045

Refactored an existing PSST code, to make it ready for the implementation of the state part. Covered the PSST feature code by the feature flag and the build flag. Moved psst tab helper to the tab feature as tab helper has been deprecated

vadimstruts avatar May 13 '25 21:05 vadimstruts

Chromium major version is behind target branch (136.0.7103.93 vs 137.0.7151.16). Please rebase.

github-actions[bot] avatar May 14 '25 00:05 github-actions[bot]

Chromium major version is behind target branch (137.0.7151.61 vs 138.0.7204.25). Please rebase.

github-actions[bot] avatar Jun 11 '25 23:06 github-actions[bot]

Chromium major version is behind target branch (138.0.7204.101 vs 139.0.7258.32). Please rebase.

github-actions[bot] avatar Jul 16 '25 20:07 github-actions[bot]

[puLL-Merge] - brave/brave-core@29044

Diff

diff --git browser/DEPS browser/DEPS
index a6729feb09d6..fdfb56866adc 100644
--- browser/DEPS
+++ browser/DEPS
@@ -61,6 +61,7 @@ include_rules += [
   "+brave/components/playlist/common",
   "+brave/components/psst/browser/content",
   "+brave/components/psst/common",
+  "+brave/components/psst/buildflags",
   "+brave/components/skus/browser",
   "+brave/components/skus/common",
   "+brave/components/speedreader",
diff --git browser/about_flags.cc browser/about_flags.cc
index da51d07cf037..940a5d1d4c58 100644
--- browser/about_flags.cc
+++ browser/about_flags.cc
@@ -31,7 +31,7 @@
 #include "brave/components/google_sign_in_permission/features.h"
 #include "brave/components/ntp_background_images/browser/features.h"
 #include "brave/components/playlist/common/buildflags/buildflags.h"
-#include "brave/components/psst/common/features.h"
+#include "brave/components/psst/buildflags/buildflags.h"
 #include "brave/components/request_otr/common/buildflags/buildflags.h"
 #include "brave/components/skus/common/features.h"
 #include "brave/components/speedreader/common/buildflags/buildflags.h"
@@ -99,6 +99,10 @@
 #include "brave/browser/updater/features.h"
 #endif
 
+#if BUILDFLAG(ENABLE_PSST)
+#include "brave/components/psst/common/features.h"
+#endif
+
 #define EXPAND_FEATURE_ENTRIES(...) __VA_ARGS__,
 
 const flags_ui::FeatureEntry::FeatureParam
@@ -270,6 +274,16 @@ const char* const kBraveSyncImplLink[1] = {"https://github.com/brave/go-sync"};
               FEATURE_VALUE_TYPE(playlist::features::kPlaylistFakeUA), \
           }))
 
+#define PSST_FEATURE_ENTRIES                                           \
+  IF_BUILDFLAG(ENABLE_PSST,                                            \
+               EXPAND_FEATURE_ENTRIES({                                \
+                   "enable-psst",                                      \
+                   "Enable PSST (Privacy Site Settings Tool) feature", \
+                   "Enable PSST feature",                              \
+                   kOsAll,                                             \
+                   FEATURE_VALUE_TYPE(psst::features::kEnablePsst),    \
+               }))
+
 #if !BUILDFLAG(IS_ANDROID)
 #define BRAVE_COMMANDS_FEATURE_ENTRIES                                      \
   EXPAND_FEATURE_ENTRIES(                                                   \
@@ -772,13 +786,6 @@ constexpr flags_ui::FeatureEntry::Choice kVerticalTabCollapseDelayChoices[] = {
           FEATURE_VALUE_TYPE(                                                  \
               brave_shields::features::kBraveLocalhostAccessPermission),       \
       },                                                                       \
-      {                                                                        \
-          "brave-psst",                                                        \
-          "Enable PSST (Privacy Site Settings Tool) feature",                  \
-          "Enable PSST feature",                                               \
-          kOsAll,                                                              \
-          FEATURE_VALUE_TYPE(psst::features::kBravePsst),                      \
-      },                                                                       \
       {                                                                        \
           "brave-extension-network-blocking",                                  \
           "Enable extension network blocking",                                 \
@@ -1108,6 +1115,7 @@ constexpr flags_ui::FeatureEntry::Choice kVerticalTabCollapseDelayChoices[] = {
   BRAVE_ADBLOCK_CUSTOM_SCRIPTLETS                                              \
   BRAVE_EDUCATION_FEATURE_ENTRIES                                              \
   BRAVE_UPDATER_FEATURE_ENTRIES                                                \
+  PSST_FEATURE_ENTRIES                                                         \
   LAST_BRAVE_FEATURE_ENTRIES_ITEM  // Keep it as the last item.
 namespace flags_ui {
 namespace {
diff --git browser/brave_profile_prefs.cc browser/brave_profile_prefs.cc
index 00df191040cf..64584d686c37 100644
--- browser/brave_profile_prefs.cc
+++ browser/brave_profile_prefs.cc
@@ -47,6 +47,7 @@
 #include "brave/components/ntp_background_images/buildflags/buildflags.h"
 #include "brave/components/ntp_background_images/common/view_counter_pref_registry.h"
 #include "brave/components/omnibox/browser/brave_omnibox_prefs.h"
+#include "brave/components/psst/buildflags/buildflags.h"
 #include "brave/components/request_otr/common/buildflags/buildflags.h"
 #include "brave/components/search_engines/brave_prepopulated_engines.h"
 #include "brave/components/speedreader/common/buildflags/buildflags.h"
@@ -127,6 +128,10 @@ using extensions::FeatureSwitch;
 #include "brave/browser/ntp_background/ntp_background_prefs.h"
 #endif
 
+#if BUILDFLAG(ENABLE_PSST)
+#include "brave/components/psst/common/pref_names.h"
+#endif
+
 #if BUILDFLAG(ENABLE_CONTAINERS)
 #include "brave/components/containers/core/browser/prefs.h"
 #endif
@@ -376,6 +381,10 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) {
   // Brave Wallet
   brave_wallet::RegisterProfilePrefs(registry);
 
+#if BUILDFLAG(ENABLE_PSST)
+  psst::RegisterProfilePrefs(registry);
+#endif
+
   // Brave Search
   if (brave_search::IsDefaultAPIEnabled()) {
     brave_search::BraveSearchDefaultHost::RegisterProfilePrefs(registry);
diff --git browser/brave_tab_helpers.cc browser/brave_tab_helpers.cc
index 3a40f1d61a79..0fdf7b57ddae 100644
--- browser/brave_tab_helpers.cc
+++ browser/brave_tab_helpers.cc
@@ -30,7 +30,6 @@
 #include "brave/components/brave_perf_predictor/browser/perf_predictor_tab_helper.h"
 #include "brave/components/brave_wayback_machine/buildflags/buildflags.h"
 #include "brave/components/playlist/common/buildflags/buildflags.h"
-#include "brave/components/psst/browser/content/psst_tab_helper.h"
 #include "brave/components/request_otr/common/buildflags/buildflags.h"
 #include "brave/components/speedreader/common/buildflags/buildflags.h"
 #include "brave/components/tor/buildflags/buildflags.h"
@@ -176,8 +175,6 @@ void AttachTabHelpers(content::WebContents* web_contents) {
   brave_ads::AdsTabHelper::CreateForWebContents(web_contents);
   brave_ads::CreativeSearchResultAdTabHelper::MaybeCreateForWebContents(
       web_contents);
-  psst::PsstTabHelper::MaybeCreateForWebContents(
-      web_contents, ISOLATED_WORLD_ID_BRAVE_INTERNAL);
 #if BUILDFLAG(ENABLE_EXTENSIONS) || BUILDFLAG(ENABLE_WEB_DISCOVERY_NATIVE)
   web_discovery::WebDiscoveryTabHelper::MaybeCreateForWebContents(web_contents);
 #endif
diff --git browser/psst/BUILD.gn browser/psst/BUILD.gn
index 2e231b515e37..a097cdb98195 100644
--- browser/psst/BUILD.gn
+++ browser/psst/BUILD.gn
@@ -7,15 +7,18 @@ source_set("browser_tests") {
   testonly = true
   defines = [ "HAS_OUT_OF_PROC_TEST_RUNNER" ]
 
-  sources = [ "psst_tab_helper_browsertest.cc" ]
+  sources = [ "psst_tab_web_contents_observer_browsertest.cc" ]
 
   deps = [
     "//base",
     "//brave/components/brave_shields/content/browser",
     "//brave/components/cosmetic_filters/browser",
+    "//brave/components/psst/browser/content",
     "//brave/components/psst/browser/core",
+    "//brave/components/psst/buildflags",
     "//brave/components/psst/common",
     "//chrome/test:test_support",
+    "//components/component_updater:test_support",
     "//content/test:test_support",
     "//net:test_support",
     "//url",
diff --git browser/psst/psst_tab_helper_browsertest.cc browser/psst/psst_tab_helper_browsertest.cc
deleted file mode 100644
index 5b5942f1a018..000000000000
--- browser/psst/psst_tab_helper_browsertest.cc
+++ /dev/null
@@ -1,173 +0,0 @@
-// Copyright (c) 2023 The Brave Authors. All rights reserved.
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-#include <string>
-
-#include "base/path_service.h"
-#include "base/test/scoped_feature_list.h"
-#include "brave/components/constants/brave_paths.h"
-#include "brave/components/psst/browser/core/psst_rule_registry.h"
-#include "brave/components/psst/common/features.h"
-#include "chrome/test/base/chrome_test_utils.h"
-#include "chrome/test/base/platform_browser_test.h"
-#include "chrome/test/base/testing_browser_process.h"
-#include "content/public/test/browser_test.h"
-#include "content/public/test/browser_test_utils.h"
-#include "content/public/test/content_mock_cert_verifier.h"
-#include "net/dns/mock_host_resolver.h"
-#include "net/test/embedded_test_server/embedded_test_server.h"
-
-namespace psst {
-
-class PsstTabHelperBrowserTest : public PlatformBrowserTest {
- public:
-  PsstTabHelperBrowserTest()
-      : https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {
-    feature_list_.InitAndEnableFeature(psst::features::kBravePsst);
-  }
-
-  void SetUpOnMainThread() override {
-    PlatformBrowserTest::SetUpOnMainThread();
-    base::FilePath test_data_dir;
-    base::PathService::Get(brave::DIR_TEST_DATA, &test_data_dir);
-
-    // Also called in Disabled test.
-    if (psst::PsstRuleRegistry::GetInstance()) {
-      psst::PsstRuleRegistry::GetInstance()->SetComponentPath(
-          test_data_dir.AppendASCII("psst-component-data"));
-    }
-    https_server_.ServeFilesFromDirectory(test_data_dir);
-
-    mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
-    host_resolver()->AddRule("*", "127.0.0.1");
-    ASSERT_TRUE(https_server_.Start());
-  }
-
-  void SetUpCommandLine(base::CommandLine* command_line) override {
-    PlatformBrowserTest::SetUpCommandLine(command_line);
-    mock_cert_verifier_.SetUpCommandLine(command_line);
-  }
-
-  void SetUpInProcessBrowserTestFixture() override {
-    PlatformBrowserTest::SetUpInProcessBrowserTestFixture();
-    mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
-  }
-
-  void TearDownInProcessBrowserTestFixture() override {
-    mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
-    PlatformBrowserTest::TearDownInProcessBrowserTestFixture();
-  }
-
-  content::WebContents* web_contents() {
-    return chrome_test_utils::GetActiveWebContents(this);
-  }
-
- protected:
-  net::EmbeddedTestServer https_server_;
-  base::test::ScopedFeatureList feature_list_;
-
- private:
-  content::ContentMockCertVerifier mock_cert_verifier_;
-};
-
-// TESTS
-
-IN_PROC_BROWSER_TEST_F(PsstTabHelperBrowserTest, RuleMatchTestScriptTrue) {
-  const GURL url = https_server_.GetURL("a.com", "/simple.html");
-
-  const char rules[] =
-      R"(
-      [
-        {
-            "include": [
-                "https://a.com/*"
-            ],
-            "exclude": [
-            ],
-            "version": 1,
-            "test_script": "a/test.js",
-            "policy_script": "a/policy.js"
-        }
-      ]
-      )";
-  psst::PsstRuleRegistry::GetInstance()->OnLoadRules(rules);
-
-  std::u16string expected_title(u"testpolicy");
-  content::TitleWatcher watcher(web_contents(), expected_title);
-  ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
-  EXPECT_EQ(expected_title, watcher.WaitAndGetTitle());
-}
-
-IN_PROC_BROWSER_TEST_F(PsstTabHelperBrowserTest, RuleMatchTestScriptFalse) {
-  const GURL url = https_server_.GetURL("b.com", "/simple.html");
-
-  const char rules[] =
-      R"(
-      [
-        {
-            "include": [
-                "https://b.com/*"
-            ],
-            "exclude": [
-            ],
-            "version": 1,
-            "test_script": "b/test.js",
-            "policy_script": "b/policy.js"
-        }
-      ]
-      )";
-  psst::PsstRuleRegistry::GetInstance()->OnLoadRules(rules);
-
-  std::u16string expected_title(u"test");
-  content::TitleWatcher watcher(web_contents(), expected_title);
-  ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
-  EXPECT_EQ(expected_title, watcher.WaitAndGetTitle());
-}
-
-IN_PROC_BROWSER_TEST_F(PsstTabHelperBrowserTest, NoMatch) {
-  const GURL url = https_server_.GetURL("a.com", "/simple.html");
-
-  const char rules[] =
-      R"(
-      [
-        {
-            "include": [
-                "https://c.com/*"
-            ],
-            "exclude": [
-            ],
-            "version": 1,
-            "test_script": "a/test.js",
-            "policy_script": "a/policy.js"
-        }
-      ]
-      )";
-  psst::PsstRuleRegistry::GetInstance()->OnLoadRules(rules);
-
-  std::u16string expected_title(u"OK");
-  content::TitleWatcher watcher(web_contents(), expected_title);
-  ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
-  EXPECT_EQ(expected_title, watcher.WaitAndGetTitle());
-}
-
-class PsstTabHelperBrowserTestDisabled : public PsstTabHelperBrowserTest {
- public:
-  PsstTabHelperBrowserTestDisabled() {
-    feature_list_.Reset();
-    feature_list_.InitAndDisableFeature(psst::features::kBravePsst);
-  }
-};
-
-IN_PROC_BROWSER_TEST_F(PsstTabHelperBrowserTestDisabled, DoesNotInjectScript) {
-  const GURL url = https_server_.GetURL("a.com", "/simple.html");
-  ASSERT_FALSE(psst::PsstRuleRegistry::GetInstance());
-
-  std::u16string expected_title(u"OK");
-  content::TitleWatcher watcher(web_contents(), expected_title);
-  ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
-  EXPECT_EQ(expected_title, watcher.WaitAndGetTitle());
-}
-
-}  // namespace psst
diff --git a/browser/psst/psst_tab_web_contents_observer_browsertest.cc b/browser/psst/psst_tab_web_contents_observer_browsertest.cc
new file mode 100644
index 000000000000..a7b978839751
--- /dev/null
+++ browser/psst/psst_tab_web_contents_observer_browsertest.cc
@@ -0,0 +1,90 @@
+// Copyright (c) 2025 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#include "brave/components/psst/browser/content/psst_tab_web_contents_observer.h"
+
+#include <string>
+#include <vector>
+
+#include "base/path_service.h"
+#include "base/run_loop.h"
+#include "base/test/bind.h"
+#include "base/test/scoped_feature_list.h"
+#include "brave/components/psst/browser/core/psst_rule.h"
+#include "brave/components/psst/browser/core/psst_rule_registry.h"
+#include "brave/components/psst/buildflags/buildflags.h"
+#include "brave/components/psst/common/features.h"
+#include "brave/components/psst/common/pref_names.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/test/base/chrome_test_utils.h"
+#include "chrome/test/base/platform_browser_test.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "net/dns/mock_host_resolver.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace psst {
+
+class PsstTabWebContentsObserverBrowserTest : public PlatformBrowserTest {
+ public:
+  PsstTabWebContentsObserverBrowserTest()
+      : https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {
+    feature_list_.InitAndEnableFeature(psst::features::kEnablePsst);
+  }
+
+  void SetUpOnMainThread() override {
+    PlatformBrowserTest::SetUpOnMainThread();
+    base::FilePath test_data_dir =
+        base::PathService::CheckedGet(base::DIR_SRC_TEST_DATA_ROOT);
+
+    base::RunLoop run_loop;
+    PsstRuleRegistry::GetInstance()->LoadRules(
+        test_data_dir.AppendASCII("brave/components/test/data/psst"),
+        base::BindLambdaForTesting(
+            [&run_loop](const std::string& contents,
+                        const std::vector<PsstRule>& rules) {
+              run_loop.Quit();
+            }));
+    run_loop.Run();
+
+    https_server_.ServeFilesFromDirectory(
+        test_data_dir.AppendASCII("brave/test/data"));
+    https_server_.AddDefaultHandlers(GetChromeTestDataDir());
+    https_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
+
+    host_resolver()->AddRule("*", "127.0.0.1");
+    ASSERT_TRUE(https_server_.Start());
+  }
+
+  PrefService* GetPrefs() {
+    return chrome_test_utils::GetProfile(this)->GetPrefs();
+  }
+
+  net::EmbeddedTestServer& GetEmbeddedTestServer() { return https_server_; }
+
+  content::WebContents* web_contents() {
+    return chrome_test_utils::GetActiveWebContents(this);
+  }
+
+ protected:
+  base::ScopedTempDir component_dir_;
+  net::EmbeddedTestServer https_server_;
+  base::test::ScopedFeatureList feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(PsstTabWebContentsObserverBrowserTest,
+                       StartScriptHandlerBothScriptsExecuted) {
+  EXPECT_EQ(GetPrefs()->GetBoolean(prefs::kPsstEnabled), true);
+  const GURL url = GetEmbeddedTestServer().GetURL("a.test", "/simple.html");
+
+  std::u16string expected_title(u"a_user-a_policy");
+  content::TitleWatcher watcher(web_contents(), expected_title);
+  ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
+  EXPECT_EQ(expected_title, watcher.WaitAndGetTitle());
+}
+
+}  // namespace psst
diff --git browser/sources.gni browser/sources.gni
index a90ddab7c2e7..516e3f9c9f04 100644
--- browser/sources.gni
+++ browser/sources.gni
@@ -48,6 +48,7 @@ import("//brave/components/brave_vpn/common/buildflags/buildflags.gni")
 import("//brave/components/brave_wayback_machine/buildflags/buildflags.gni")
 import("//brave/components/commander/common/buildflags/buildflags.gni")
 import("//brave/components/containers/buildflags/buildflags.gni")
+import("//brave/components/psst/buildflags/buildflags.gni")
 import("//brave/components/tor/buildflags/buildflags.gni")
 import("//brave/components/web_discovery/buildflags/buildflags.gni")
 import("//chrome/browser/buildflags.gni")
@@ -216,9 +217,7 @@ brave_chrome_browser_deps = [
   "//brave/components/password_strength_meter:mojom",
   "//brave/components/playlist/common/buildflags",
   "//brave/components/privacy_sandbox",
-  "//brave/components/psst/browser/content",
-  "//brave/components/psst/browser/core",
-  "//brave/components/psst/common",
+  "//brave/components/psst/buildflags",
   "//brave/components/request_otr/common/buildflags",
   "//brave/components/resources",
   "//brave/components/skus/browser",
@@ -280,6 +279,14 @@ brave_chrome_browser_deps = [
   "//url",
 ]
 
+if (enable_psst) {
+  brave_chrome_browser_deps += [
+    "//brave/components/psst/browser/content",
+    "//brave/components/psst/browser/core",
+    "//brave/components/psst/common",
+  ]
+}
+
 if (!is_android) {
   # Already included on android by upstream
 
diff --git browser/ui/tabs/BUILD.gn browser/ui/tabs/BUILD.gn
index 9ebb12ceed81..97a1da070022 100644
--- browser/ui/tabs/BUILD.gn
+++ browser/ui/tabs/BUILD.gn
@@ -10,7 +10,10 @@ assert(is_win || is_mac || is_linux || is_chromeos || is_android)
 source_set("tabs_public") {
   sources = [ "public/brave_tab_features.h" ]
 
-  public_deps = [ "//chrome/browser/ui/tabs:tabs_public" ]
+  public_deps = [
+    "//brave/components/psst/buildflags:buildflags",
+    "//chrome/browser/ui/tabs:tabs_public",
+  ]
 }
 
 if (!is_android) {
@@ -23,6 +26,7 @@ if (!is_android) {
       ":tabs_public",
       "//brave/browser/ai_chat",
       "//brave/browser/ui/side_panel",
+      "//brave/components/psst/browser/content",
       "//chrome/browser/profiles",
       "//chrome/browser/ui/tabs:impl",
     ]
diff --git browser/ui/tabs/brave_tab_features.cc browser/ui/tabs/brave_tab_features.cc
index e36039182c6f..f5cf2f158ca1 100644
--- browser/ui/tabs/brave_tab_features.cc
+++ browser/ui/tabs/brave_tab_features.cc
@@ -16,8 +16,13 @@
 #include "brave/browser/ui/side_panel/brave_side_panel_utils.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/tabs/public/tab_features.h"
+#include "chrome/common/chrome_isolated_world_ids.h"
 #include "components/tabs/public/tab_interface.h"
 
+#if BUILDFLAG(ENABLE_PSST)
+#include "brave/components/psst/browser/content/psst_tab_web_contents_observer.h"
+#endif
+
 namespace tabs {
 namespace {
 TabFeatures::TabFeaturesFactory& GetFactory() {
@@ -61,6 +66,13 @@ void BraveTabFeatures::Init(TabInterface& tab, Profile* profile) {
     tab_data_observer_ = std::make_unique<ai_chat::TabDataWebContentsObserver>(
         tab.GetHandle().raw_value(), tab.GetContents());
   }
+
+#if BUILDFLAG(ENABLE_PSST)
+  psst_web_contents_observer_ =
+      psst::PsstTabWebContentsObserver::MaybeCreateForWebContents(
+          tab.GetContents(), profile, profile->GetPrefs(),
+          ISOLATED_WORLD_ID_BRAVE_INTERNAL);
+#endif
 }
 
 }  // namespace tabs
diff --git browser/ui/tabs/public/brave_tab_features.h browser/ui/tabs/public/brave_tab_features.h
index 3aa526da9495..e17d0afeb03c 100644
--- browser/ui/tabs/public/brave_tab_features.h
+++ browser/ui/tabs/public/brave_tab_features.h
@@ -8,6 +8,7 @@
 
 #include <memory>
 
+#include "brave/components/psst/buildflags/buildflags.h"
 #include "chrome/browser/ui/tabs/public/tab_features.h"
 
 class Profile;
@@ -16,6 +17,12 @@ namespace ai_chat {
 class TabDataWebContentsObserver;
 }
 
+#if BUILDFLAG(ENABLE_PSST)
+namespace psst {
+class PsstTabWebContentsObserver;
+}
+#endif
+
 namespace tabs {
 
 class TabInterface;
@@ -27,12 +34,21 @@ class BraveTabFeatures : public TabFeatures {
 
   void Init(TabInterface& tab, Profile* profile) override;
 
+#if BUILDFLAG(ENABLE_PSST)
+  psst::PsstTabWebContentsObserver* psst_web_contents_observer() {
+    return psst_web_contents_observer_.get();
+  }
+#endif
+
  protected:
   friend TabFeatures;
   BraveTabFeatures();
 
  private:
   std::unique_ptr<ai_chat::TabDataWebContentsObserver> tab_data_observer_;
+#if BUILDFLAG(ENABLE_PSST)
+  std::unique_ptr<psst::PsstTabWebContentsObserver> psst_web_contents_observer_;
+#endif
 };
 
 }  // namespace tabs
diff --git chromium_src/chrome/browser/component_updater/DEPS chromium_src/chrome/browser/component_updater/DEPS
index 906b63ad2f1c..55fec70b1700 100644
--- chromium_src/chrome/browser/component_updater/DEPS
+++ chromium_src/chrome/browser/component_updater/DEPS
@@ -3,6 +3,7 @@ include_rules = [
   "+brave/components/ai_chat/core/browser",
   "+brave/components/brave_user_agent/browser",
   "+brave/components/brave_wallet/browser",
+  "+brave/components/psst/buildflags",
   "+brave/components/p3a",
   "+brave/components/psst/browser/core",
 ]
diff --git chromium_src/chrome/browser/component_updater/registration.cc chromium_src/chrome/browser/component_updater/registration.cc
index be54957592d6..1e84d9f40bfb 100644
--- chromium_src/chrome/browser/component_updater/registration.cc
+++ chromium_src/chrome/browser/component_updater/registration.cc
@@ -18,10 +18,14 @@
 #include "brave/components/brave_wallet/browser/wallet_data_files_installer.h"
 #include "brave/components/p3a/component_installer.h"
 #include "brave/components/p3a/p3a_service.h"
-#include "brave/components/psst/browser/core/psst_component_installer.h"
+#include "brave/components/psst/buildflags/buildflags.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/component_updater/component_updater_utils.h"
 
+#if BUILDFLAG(ENABLE_PSST)
+#include "brave/components/psst/browser/core/psst_component_installer.h"
+#endif
+
 #if BUILDFLAG(IS_ANDROID)
 #include "chrome/browser/component_updater/zxcvbn_data_component_installer.h"
 #endif  // BUILDFLAG(IS_ANDROID)
@@ -34,12 +38,14 @@ void RegisterComponentsForUpdate() {
   brave_wallet::WalletDataFilesInstaller::GetInstance()
       .MaybeRegisterWalletDataFilesComponent(cus,
                                              g_browser_process->local_state());
-  psst::RegisterPsstComponent(cus);
   auto* p3a_service = g_brave_browser_process->p3a_service();
   if (p3a_service) {
     p3a::RegisterP3AComponent(
         cus, p3a_service->remote_config_manager()->GetWeakPtr());
   }
+#if BUILDFLAG(ENABLE_PSST)
+  psst::RegisterPsstComponent(cus);
+#endif
 #if BUILDFLAG(IS_ANDROID)
   // Currently behind !BUILDFLAG(IS_ANDROID) in upstream.
   RegisterZxcvbnDataComponent(cus);
diff --git components/BUILD.gn components/BUILD.gn
index eb0e56744c8a..3c3b0dc72912 100644
--- components/BUILD.gn
+++ components/BUILD.gn
@@ -4,6 +4,7 @@
 # You can obtain one at https://mozilla.org/MPL/2.0/.
 
 import("//brave/components/brave_wayback_machine/buildflags/buildflags.gni")
+import("//brave/components/psst/buildflags/buildflags.gni")
 import("//build/config/features.gni")
 import("//build/config/ui.gni")
 import("//testing/test.gni")
@@ -25,6 +26,13 @@ test("brave_components_unittests") {
     "//components/test:run_all_unittests",
   ]
 
+  if (enable_psst) {
+    deps += [
+      "//brave/components/psst/browser/content:unit_tests",
+      "//brave/components/psst/browser/core:unit_tests",
+    ]
+  }
+
   if (enable_brave_wayback_machine) {
     deps += [ "//brave/components/brave_wayback_machine:unit_tests" ]
   }
diff --git components/psst/browser/content/BUILD.gn components/psst/browser/content/BUILD.gn
index 890352135a8b..bbe694caf74b 100644
--- components/psst/browser/content/BUILD.gn
+++ components/psst/browser/content/BUILD.gn
@@ -3,23 +3,49 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at https://mozilla.org/MPL/2.0/.
 
+import("//brave/components/psst/buildflags/buildflags.gni")
+
+assert(enable_psst)
+
 component("content") {
   output_name = "psst_browser_content"
 
   defines = [ "IS_PSST_BROWSER_CONTENT_IMPL" ]
 
   sources = [
-    "psst_tab_helper.cc",
-    "psst_tab_helper.h",
+    "psst_scripts_handler_impl.cc",
+    "psst_scripts_handler_impl.h",
+    "psst_tab_web_contents_observer.cc",
+    "psst_tab_web_contents_observer.h",
   ]
 
-  deps = [
+  public_deps = [
     "//base",
+    "//brave/components/script_injector/common/mojom",
+    "//content/public/browser",
+    "//content/public/common",
+  ]
+
+  deps = [
     "//brave/components/psst/browser/core",
     "//brave/components/psst/common",
-    "//brave/components/script_injector/common/mojom",
-    "//components/sessions",
+    "//components/prefs",
+  ]
+}
+
+source_set("unit_tests") {
+  testonly = true
+
+  sources = [ "psst_tab_web_contents_observer_unittest.cc" ]
+
+  deps = [
+    "//brave/components/psst/browser/content",
+    "//brave/components/psst/browser/core",
+    "//brave/components/psst/common",
+    "//components/sync_preferences:test_support",
     "//content/public/browser",
-    "//url",
+    "//content/test:test_support",
+    "//testing/gtest",
+    "//url:url",
   ]
 }
diff --git components/psst/browser/content/DEPS components/psst/browser/content/DEPS
index c41fa0055da4..a4580e143397 100644
--- components/psst/browser/content/DEPS
+++ components/psst/browser/content/DEPS
@@ -1,5 +1,8 @@
 include_rules = [
   "+components/sessions",
   "+content/public/browser",
-  "+third_party/blink/public"
+  "+content/public/test",
+  "+components/prefs",
+  "+components/sync_preferences/testing_pref_service_syncable.h",
+  "+third_party/blink/public",
 ]
diff --git a/components/psst/browser/content/psst_scripts_handler_impl.cc b/components/psst/browser/content/psst_scripts_handler_impl.cc
new file mode 100644
index 000000000000..de008a10d16c
--- /dev/null
+++ components/psst/browser/content/psst_scripts_handler_impl.cc
@@ -0,0 +1,34 @@
+// Copyright (c) 2025 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#include "brave/components/psst/browser/content/psst_scripts_handler_impl.h"
+
+#include <string>
+#include <utility>
+
+#include "base/strings/utf_string_conversions.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/web_contents.h"
+
+namespace psst {
+
+PsstScriptsHandlerImpl::PsstScriptsHandlerImpl(
+    content::WebContents* web_contents,
+    const int32_t world_id)
+    : web_contents_(web_contents), world_id_(world_id) {
+  CHECK(world_id_ > content::ISOLATED_WORLD_ID_CONTENT_END);
+  CHECK(web_contents_);
+}
+
+PsstScriptsHandlerImpl::~PsstScriptsHandlerImpl() = default;
+
+void PsstScriptsHandlerImpl::InsertScriptInPage(
+    const std::string& script,
+    PsstTabWebContentsObserver::InsertScriptInPageCallback cb) {
+  web_contents_->GetPrimaryMainFrame()->ExecuteJavaScriptInIsolatedWorld(
+      base::UTF8ToUTF16(script), std::move(cb), world_id_);
+}
+
+}  // namespace psst
diff --git a/components/psst/browser/content/psst_scripts_handler_impl.h b/components/psst/browser/content/psst_scripts_handler_impl.h
new file mode 100644
index 000000000000..dfab4197b4bb
--- /dev/null
+++ components/psst/browser/content/psst_scripts_handler_impl.h
@@ -0,0 +1,40 @@
+// Copyright (c) 2025 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#ifndef BRAVE_COMPONENTS_PSST_BROWSER_CONTENT_PSST_SCRIPTS_HANDLER_IMPL_H_
+#define BRAVE_COMPONENTS_PSST_BROWSER_CONTENT_PSST_SCRIPTS_HANDLER_IMPL_H_
+
+#include <string>
+
+#include "brave/components/psst/browser/content/psst_tab_web_contents_observer.h"
+#include "brave/components/script_injector/common/mojom/script_injector.mojom.h"
+#include "content/public/browser/web_contents.h"
+#include "mojo/public/cpp/bindings/associated_remote.h"
+
+namespace psst {
+
+class PsstScriptsHandlerImpl
+    : public PsstTabWebContentsObserver::ScriptsHandler {
+ public:
+  explicit PsstScriptsHandlerImpl(content::WebContents* web_contents,
+                                  const int32_t world_id);
+  ~PsstScriptsHandlerImpl() override;
+
+  // PsstScriptsHandler overrides
+  void InsertScriptInPage(
+      const std::string& script,
+      PsstTabWebContentsObserver::InsertScriptInPageCallback cb) override;
+
+ private:
+  raw_ptr<content::WebContents> web_contents_;
+  const int32_t world_id_;
+
+  mojo::AssociatedRemote<script_injector::mojom::ScriptInjector>
+      script_injector_remote_;
+};
+
+}  // namespace psst
+
+#endif  // BRAVE_COMPONENTS_PSST_BROWSER_CONTENT_PSST_SCRIPTS_HANDLER_IMPL_H_
diff --git components/psst/browser/content/psst_tab_helper.cc components/psst/browser/content/psst_tab_helper.cc
deleted file mode 100644
index 4b83b97a4600..000000000000
--- components/psst/browser/content/psst_tab_helper.cc
+++ /dev/null
@@ -1,133 +0,0 @@
-// Copyright (c) 2023 The Brave Authors. All rights reserved.
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-#include "brave/components/psst/browser/content/psst_tab_helper.h"
-
-#include <string>
-#include <utility>
-
-#include "base/check.h"
-#include "base/functional/bind.h"
-#include "base/logging.h"
-#include "base/strings/utf_string_conversions.h"
-#include "base/task/thread_pool.h"
-#include "base/values.h"
-#include "brave/components/psst/browser/core/psst_rule.h"
-#include "brave/components/psst/browser/core/psst_rule_registry.h"
-#include "brave/components/psst/common/features.h"
-#include "components/sessions/content/session_tab_helper.h"
-#include "content/public/browser/browser_context.h"
-#include "content/public/browser/navigation_entry.h"
-#include "content/public/browser/navigation_handle.h"
-#include "content/public/browser/web_contents.h"
-#include "mojo/public/cpp/bindings/associated_remote.h"
-#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
-
-namespace psst {
-
-// static
-void PsstTabHelper::MaybeCreateForWebContents(content::WebContents* contents,
-                                              const int32_t world_id) {
-  if (contents->GetBrowserContext()->IsOffTheRecord() ||
-      !base::FeatureList::IsEnabled(psst::features::kBravePsst)) {
-    return;
-  }
-
-  psst::PsstTabHelper::CreateForWebContents(contents, world_id);
-}
-
-PsstTabHelper::PsstTabHelper(content::WebContents* web_contents,
-                             const int32_t world_id)
-    : WebContentsObserver(web_contents),
-      content::WebContentsUserData<PsstTabHelper>(*web_contents),
-      world_id_(world_id),
-      psst_rule_registry_(PsstRuleRegistry::GetInstance()) {
-  DCHECK(psst_rule_registry_);
-}
-
-PsstTabHelper::~PsstTabHelper() = default;
-
-void PsstTabHelper::OnTestScriptResult(
-    const std::string& policy_script,
-    const content::GlobalRenderFrameHostId& render_frame_host_id,
-    base::Value value) {
-  if (value.GetIfBool().value_or(false)) {
-    InsertScriptInPage(render_frame_host_id, policy_script, base::DoNothing());
-  }
-}
-
-void PsstTabHelper::InsertTestScript(
-    const content::GlobalRenderFrameHostId& render_frame_host_id,
-    MatchedRule rule) {
-  InsertScriptInPage(render_frame_host_id, rule.test_script,
-                     base::BindOnce(&PsstTabHelper::OnTestScriptResult,
-                                    weak_factory_.GetWeakPtr(),
-                                    rule.policy_script, render_frame_host_id));
-}
-
-void PsstTabHelper::InsertScriptInPage(
-    const content::GlobalRenderFrameHostId& render_frame_host_id,
-    const std::string& script,
-    content::RenderFrameHost::JavaScriptResultCallback cb) {
-  content::RenderFrameHost* render_frame_host =
-      content::RenderFrameHost::FromID(render_frame_host_id);
-
-  // Check if render_frame_host is still valid and if starting rfh is the same.
-  if (render_frame_host &&
-      render_frame_host_id ==
-          web_contents()->GetPrimaryMainFrame()->GetGlobalId()) {
-    GetRemote(render_frame_host)
-        ->RequestAsyncExecuteScript(
-            world_id_, base::UTF8ToUTF16(script),
-            blink::mojom::UserActivationOption::kDoNotActivate,
-            blink::mojom::PromiseResultOption::kAwait, std::move(cb));
-  } else {
-    VLOG(2) << "render_frame_host is invalid.";
-    return;
-  }
-}
-
-mojo::AssociatedRemote<script_injector::mojom::ScriptInjector>&
-PsstTabHelper::GetRemote(content::RenderFrameHost* rfh) {
-  if (!script_injector_remote_.is_bound()) {
-    rfh->GetRemoteAssociatedInterfaces()->GetInterface(
-        &script_injector_remote_);
-  }
-  return script_injector_remote_;
-}
-
-void PsstTabHelper::DidFinishNavigation(
-    content::NavigationHandle* navigation_handle) {
-  if (!navigation_handle->IsInPrimaryMainFrame() ||
-      !navigation_handle->HasCommitted() ||
-      navigation_handle->IsSameDocument()) {
-    return;
-  }
-
-  should_process_ =
-      navigation_handle->GetRestoreType() == content::RestoreType::kNotRestored;
-}
-
-void PsstTabHelper::DocumentOnLoadCompletedInPrimaryMainFrame() {
-  DCHECK(psst_rule_registry_);
-  // Make sure it gets reset.
-  bool should_process = should_process_;
-  should_process_ = false;
-  if (!should_process) {
-    return;
-  }
-  auto url = web_contents()->GetLastCommittedURL();
-
-  content::GlobalRenderFrameHostId render_frame_host_id =
-      web_contents()->GetPrimaryMainFrame()->GetGlobalId();
-
-  psst_rule_registry_->CheckIfMatch(
-      url, base::BindOnce(&PsstTabHelper::InsertTestScript,
-                          weak_factory_.GetWeakPtr(), render_frame_host_id));
-}
-
-WEB_CONTENTS_USER_DATA_KEY_IMPL(PsstTabHelper);
-
-}  // namespace psst
diff --git components/psst/browser/content/psst_tab_helper.h components/psst/browser/content/psst_tab_helper.h
deleted file mode 100644
index 821b0805b885..000000000000
--- components/psst/browser/content/psst_tab_helper.h
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright (c) 2023 The Brave Authors. All rights reserved.
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-#ifndef BRAVE_COMPONENTS_PSST_BROWSER_CONTENT_PSST_TAB_HELPER_H_
-#define BRAVE_COMPONENTS_PSST_BROWSER_CONTENT_PSST_TAB_HELPER_H_
-
-#include <string>
-
-#include "base/component_export.h"
-#include "base/memory/raw_ptr.h"
-#include "base/memory/weak_ptr.h"
-#include "brave/components/psst/browser/core/psst_rule.h"
-#include "brave/components/script_injector/common/mojom/script_injector.mojom.h"
-#include "build/build_config.h"
-#include "components/sessions/core/session_id.h"
-#include "content/public/browser/media_player_id.h"
-#include "content/public/browser/web_contents_observer.h"
-#include "content/public/browser/web_contents_user_data.h"
-#include "mojo/public/cpp/bindings/associated_remote.h"
-
-namespace psst {
-
-class PsstRuleRegistry;
-
-// Used to inject PSST scripts into the page, based on PSST rules.
-class COMPONENT_EXPORT(PSST_BROWSER_CONTENT) PsstTabHelper
-    : public content::WebContentsObserver,
-      public content::WebContentsUserData<PsstTabHelper> {
- public:
-  static void MaybeCreateForWebContents(content::WebContents* contents,
-                                        const int32_t world_id);
-  ~PsstTabHelper() override;
-  PsstTabHelper(const PsstTabHelper&) = delete;
-  PsstTabHelper& operator=(const PsstTabHelper&) = delete;
-
- private:
-  PsstTabHelper(content::WebContents*, const int32_t world_id);
-  // Called to insert both test script and policy script.
-  void InsertScriptInPage(
-      const content::GlobalRenderFrameHostId& render_frame_host_id,
-      const std::string& script,
-      content::RenderFrameHost::JavaScriptResultCallback cb);
-  // Used to insert a PSST test script into the page, which is contained in the
-  // Matched Rule. The result is used to determine whether to insert the policy
-  // script in |OnTestScriptResult|.
-  void InsertTestScript(
-      const content::GlobalRenderFrameHostId& render_frame_host_id,
-      MatchedRule rule);
-  void OnTestScriptResult(
-      const std::string& policy_script,
-      const content::GlobalRenderFrameHostId& render_frame_host_id,
-      base::Value value);
-  mojo::AssociatedRemote<script_injector::mojom::ScriptInjector>& GetRemote(
-      content::RenderFrameHost* rfh);
-  friend class content::WebContentsUserData<PsstTabHelper>;
-
-  // content::WebContentsObserver overrides
-  void DidFinishNavigation(
-      content::NavigationHandle* navigation_handle) override;
-  void DocumentOnLoadCompletedInPrimaryMainFrame() override;
-
-  const int32_t world_id_;
-  const raw_ptr<PsstRuleRegistry> psst_rule_registry_;  // NOT OWNED
-  bool should_process_ = false;
-  // The remote used to send the script to the renderer.
-  mojo::AssociatedRemote<script_injector::mojom::ScriptInjector>
-      script_injector_remote_;
-  base::WeakPtrFactory<PsstTabHelper> weak_factory_{this};
-
-  WEB_CONTENTS_USER_DATA_KEY_DECL();
-};
-
-}  // namespace psst
-
-#endif  // BRAVE_COMPONENTS_PSST_BROWSER_CONTENT_PSST_TAB_HELPER_H_
diff --git a/components/psst/browser/content/psst_tab_web_contents_observer.cc b/components/psst/browser/content/psst_tab_web_contents_observer.cc
new file mode 100644
index 000000000000..4ba8343ea959
--- /dev/null
+++ components/psst/browser/content/psst_tab_web_contents_observer.cc
@@ -0,0 +1,128 @@
+// Copyright (c) 2025 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#include "brave/components/psst/browser/content/psst_tab_web_contents_observer.h"
+
+#include <memory>
+#include <utility>
+
+#include "brave/components/psst/browser/content/psst_scripts_handler_impl.h"
+#include "brave/components/psst/browser/core/psst_rule.h"
+#include "brave/components/psst/browser/core/psst_rule_registry.h"
+#include "brave/components/psst/common/features.h"
+#include "brave/components/psst/common/pref_names.h"
+#include "components/prefs/pref_service.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/navigation_entry.h"
+#include "content/public/browser/navigation_handle.h"
+
+namespace psst {
+
+const char kShouldProcessKey[] = "should_process_key";
+
+struct PsstNavigationData : public base::SupportsUserData::Data {
+ public:
+  explicit PsstNavigationData(int id) : id(id) {}
+
+  int id;
+};
+
+// static
+std::unique_ptr<PsstTabWebContentsObserver>
+PsstTabWebContentsObserver::MaybeCreateForWebContents(
+    content::WebContents* contents,
+    content::BrowserContext* browser_context,
+    PrefService* prefs,
+    const int32_t world_id) {
+  CHECK(contents);
+  CHECK(browser_context);
+  CHECK(prefs);
+
+  if (browser_context->IsOffTheRecord() ||
+      !base::FeatureList::IsEnabled(psst::features::kEnablePsst)) {
+    return nullptr;
+  }
+
+  return base::WrapUnique<PsstTabWebContentsObserver>(
+      new PsstTabWebContentsObserver(
+          contents, PsstRuleRegistry::GetInstance(), prefs,
+          std::make_unique<PsstScriptsHandlerImpl>(contents, world_id)));
+}
+
+PsstTabWebContentsObserver::PsstTabWebContentsObserver(
+    content::WebContents* web_contents,
+    PsstRuleRegistry* registry,
+    PrefService* prefs,
+    std::unique_ptr<ScriptsHandler> script_handler)
+    : WebContentsObserver(web_contents),
+      registry_(registry),
+      prefs_(prefs),
+      script_handler_(std::move(script_handler)) {}
+
+PsstTabWebContentsObserver::~PsstTabWebContentsObserver() = default;
+
+void PsstTabWebContentsObserver::DidFinishNavigation(
+    content::NavigationHandle* handle) {
+  if (!handle->IsInPrimaryMainFrame() || !handle->HasCommitted() ||
+      !handle->GetURL().SchemeIsHTTPOrHTTPS()) {
+    return;
+  }
+
+  if (handle->IsSameDocument() ||
+      handle->GetRestoreType() == content::RestoreType::kRestored ||
+      !prefs_->GetBoolean(prefs::kPsstEnabled)) {
+    return;
+  } else {
+    auto* entry = handle->GetNavigationEntry();
+    entry->SetUserData(kShouldProcessKey, std::make_unique<PsstNavigationData>(
+                                              entry->GetUniqueID()));
+  }
+}
+
+void PsstTabWebContentsObserver::DocumentOnLoadCompletedInPrimaryMainFrame() {
+  int id =
+      web_contents()->GetController().GetLastCommittedEntry()->GetUniqueID();
+  if (!ShouldInsertScriptForPage(id)) {
+    return;
+  }
+
+  registry_->CheckIfMatch(
+      web_contents()->GetLastCommittedURL(),
+      base::BindOnce(&PsstTabWebContentsObserver::InsertUserScript,
+                     weak_factory_.GetWeakPtr(), id));
+}
+
+bool PsstTabWebContentsObserver::ShouldInsertScriptForPage(int id) {
+  auto* entry = web_contents()->GetController().GetLastCommittedEntry();
+  auto* data = entry->GetUserData(kShouldProcessKey);
+  return script_handler_ && data &&
+         static_cast<PsstNavigationData*>(data)->id == id;
+}
+
+void PsstTabWebContentsObserver::InsertUserScript(
+    int id,
+    std::unique_ptr<MatchedRule> rule) {
+  if (!rule || !ShouldInsertScriptForPage(id)) {
+    return;
+  }
+
+  script_handler_->InsertScriptInPage(
+      rule->user_script(),
+      base::BindOnce(&PsstTabWebContentsObserver::OnUserScriptResult,
+                     weak_factory_.GetWeakPtr(), id, rule->policy_script()));
+}
+
+void PsstTabWebContentsObserver::OnUserScriptResult(
+    int id,
+    const std::string& policy_script,
+    base::Value user_script_result) {
+  if (!ShouldInsertScriptForPage(id) || policy_script.empty() ||
+      !user_script_result.is_dict()) {
+    return;
+  }
+  script_handler_->InsertScriptInPage(policy_script, base::DoNothing());
+}
+
+}  // namespace psst
diff --git a/components/psst/browser/content/psst_tab_web_contents_observer.h b/components/psst/browser/content/psst_tab_web_contents_observer.h
new file mode 100644
index 000000000000..78dd3a024f66
--- /dev/null
+++ components/psst/browser/content/psst_tab_web_contents_observer.h
@@ -0,0 +1,75 @@
+// Copyright (c) 2025 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#ifndef BRAVE_COMPONENTS_PSST_BROWSER_CONTENT_PSST_TAB_WEB_CONTENTS_OBSERVER_H_
+#define BRAVE_COMPONENTS_PSST_BROWSER_CONTENT_PSST_TAB_WEB_CONTENTS_OBSERVER_H_
+
+#include <memory>
+#include <optional>
+#include <string>
+
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/values.h"
+#include "content/public/browser/web_contents_observer.h"
+
+class PrefService;
+
+namespace psst {
+
+class MatchedRule;
+class PsstRuleRegistry;
+
+class COMPONENT_EXPORT(PSST_BROWSER_CONTENT) PsstTabWebContentsObserver
+    : public content::WebContentsObserver {
+ public:
+  using InsertScriptInPageCallback = base::OnceCallback<void(base::Value)>;
+  class ScriptsHandler {
+   public:
+    virtual ~ScriptsHandler() = default;
+    virtual void InsertScriptInPage(const std::string& script,
+                                    InsertScriptInPageCallback cb) = 0;
+  };
+
+  static std::unique_ptr<PsstTabWebContentsObserver> MaybeCreateForWebContents(
+      content::WebContents* contents,
+      content::BrowserContext* browser_context,
+      PrefService* prefs,
+      const int32_t world_id);
+
+  ~PsstTabWebContentsObserver() override;
+  PsstTabWebContentsObserver(const PsstTabWebContentsObserver&) = delete;
+  PsstTabWebContentsObserver& operator=(const PsstTabWebContentsObserver&) =
+      delete;
+
+ private:
+  friend class PsstTabWebContentsObserverUnitTestBase;
+
+  PsstTabWebContentsObserver(content::WebContents* web_contents,
+                             PsstRuleRegistry* registry,
+                             PrefService* prefs,
+                             std::unique_ptr<ScriptsHandler> script_handler);
+
+  bool ShouldInsertScriptForPage(int id);
+  void InsertUserScript(int id, std::unique_ptr<MatchedRule> rule);
+
+  void OnUserScriptResult(int id,
+                          const std::string& policy_script,
+                          base::Value script_result);
+
+  // content::WebContentsObserver overrides
+  void DocumentOnLoadCompletedInPrimaryMainFrame() override;
+  void DidFinishNavigation(content::NavigationHandle* handle) override;
+
+  const raw_ptr<PsstRuleRegistry> registry_;
+  const raw_ptr<PrefService> prefs_;
+  std::unique_ptr<ScriptsHandler> script_handler_;
+
+  base::WeakPtrFactory<PsstTabWebContentsObserver> weak_factory_{this};
+};
+
+}  // namespace psst
+
+#endif  // BRAVE_COMPONENTS_PSST_BROWSER_CONTENT_PSST_TAB_WEB_CONTENTS_OBSERVER_H_
diff --git a/components/psst/browser/content/psst_tab_web_contents_observer_unittest.cc b/components/psst/browser/content/psst_tab_web_contents_observer_unittest.cc
new file mode 100644
index 000000000000..cd7fb056fecf
--- /dev/null
+++ components/psst/browser/content/psst_tab_web_contents_observer_unittest.cc
@@ -0,0 +1,517 @@
+// Copyright (c) 2025 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#include "brave/components/psst/browser/content/psst_tab_web_contents_observer.h"
+
+#include <memory>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "base/memory/ptr_util.h"
+#include "base/run_loop.h"
+#include "base/test/bind.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/values.h"
+#include "brave/components/psst/browser/core/psst_rule_registry.h"
+#include "brave/components/psst/common/features.h"
+#include "brave/components/psst/common/pref_names.h"
+#include "build/build_config.h"
+#include "components/sync_preferences/testing_pref_service_syncable.h"
+#include "content/public/browser/navigation_controller.h"
+#include "content/public/browser/navigation_entry.h"
+#include "content/public/browser/navigation_handle.h"
+#include "content/public/browser/site_instance.h"
+#include "content/public/browser/web_contents_observer.h"
+#include "content/public/test/navigation_simulator.h"
+#include "content/public/test/test_browser_context.h"
+#include "content/public/test/test_renderer_host.h"
+#include "content/public/test/web_contents_tester.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using ::testing::_;
+using ::testing::InvokeArgument;
+
+namespace psst {
+
+class DocumentOnLoadObserver : public content::WebContentsObserver {
+ public:
+  explicit DocumentOnLoadObserver(content::WebContents* web_contents)
+      : content::WebContentsObserver(web_contents) {}
+  void Wait() { loop_.Run(); }
+
+ private:
+  void DocumentOnLoadCompletedInPrimaryMainFrame() override { loop_.Quit(); }
+
+  base::RunLoop loop_;
+};
+
+class MockPsstRuleRegistry final : public PsstRuleRegistry {
+ public:
+  MockPsstRuleRegistry() = default;
+  ~MockPsstRuleRegistry() = default;
+
+  MOCK_METHOD(void,
+              CheckIfMatch,
+              (const GURL&,
+               base::OnceCallback<void(std::unique_ptr<MatchedRule>)>),
+              (override));
+
+  MOCK_METHOD(void,
+              LoadRules,
+              (const base::FilePath& path, PsstRuleRegistry::OnLoadCallback cb),
+              (override));
+};
+
+// testing::InvokeArgument<N> does not work with base::OnceCallback, so we
+// define our own gMock action to run the 2nd argument.
+ACTION_P(CheckIfMatchCallback, loop, match_rule) {
+  std::move(
+      const_cast<base::OnceCallback<void(std::unique_ptr<MatchedRule>)>&>(arg1))
+      .Run(base::WrapUnique(match_rule));
+  loop->Quit();
+}
+
+ACTION_P(CheckIfMatchFailsCallback, loop) {
+  std::move(
+      const_cast<base::OnceCallback<void(std::unique_ptr<MatchedRule>)>&>(arg1))
+      .Run(nullptr);
+  loop->Quit();
+}
+
+class MockPsstScriptsHandler
+    : public PsstTabWebContentsObserver::ScriptsHandler {
+ public:
+  MockPsstScriptsHandler() = default;
+  ~MockPsstScriptsHandler() override = default;
+
+  MOCK_METHOD(void,
+              InsertScriptInPage,
+              (const std::string& script,
+               PsstTabWebContentsObserver::InsertScriptInPageCallback cb),
+              (override));
+};
+
+// testing::InvokeArgument<N> does not work with base::OnceCallback, so we
+// define our own gMock action to run the 2nd argument.
+ACTION_P(InsertScriptInPageCallback, loop, value) {
+  std::move(
+      const_cast<PsstTabWebContentsObserver::InsertScriptInPageCallback&>(arg1))
+      .Run(value.Clone());
+  loop->Quit();
+}
+
+class PsstTabWebContentsObserverUnitTestBase
+    : public content::RenderViewHostTestHarness {
+ public:
+  void SetUp() override {
+    content::RenderViewHostTestHarness::SetUp();
+
+    psst::RegisterProfilePrefs(prefs_.registry());
+    scripts_handler_ = new MockPsstScriptsHandler();
+    rule_registry_ = std::make_unique<MockPsstRuleRegistry>();
+    psst_web_contents_observer_ = base::WrapUnique<PsstTabWebContentsObserver>(
+        new PsstTabWebContentsObserver(
+            web_contents(), rule_registry_.get(), &prefs_,
+            base::WrapUnique<MockPsstScriptsHandler>(scripts_handler_)));
+  }
+
+  void TearDown() override {
+    content::RenderViewHostTestHarness::TearDown();
+    scripts_handler_ = nullptr;
+  }
+
+  MockPsstRuleRegistry& psst_rule_registry() { return *rule_registry_.get(); }
+  MockPsstScriptsHandler& scripts_handler() { return *scripts_handler_; }
+  PrefService* prefs() { return &prefs_; }
+
+  MatchedRule* CreateMatchedRule(const std::string& user_script,
+                                 const std::string& policy_script) {
+    return new MatchedRule("name", user_script, policy_script, 1);
+  }
+
+ protected:
+  base::test::ScopedFeatureList feature_list_;
+
+ private:
+  raw_ptr<MockPsstScriptsHandler> scripts_handler_;  // not owned
+  std::unique_ptr<MockPsstRuleRegistry> rule_registry_;
+  std::unique_ptr<PsstTabWebContentsObserver> psst_web_contents_observer_;
+  sync_preferences::TestingPrefServiceSyncable prefs_;
+};
+
+class PsstTabWebContentsObserverUnitTest
+    : public PsstTabWebContentsObserverUnitTestBase {
+ public:
+  void SetUp() override {
+    feature_list_.InitAndEnableFeature(psst::features::kEnablePsst);
+    PsstTabWebContentsObserverUnitTestBase::SetUp();
+  }
+};
+
+TEST_F(PsstTabWebContentsObserverUnitTest, CreateForRegularBrowserContext) {
+  EXPECT_NE(PsstTabWebContentsObserver::MaybeCreateForWebContents(
+                web_contents(), browser_context(), prefs(), 2),
+            nullptr);
+}
+
+TEST_F(PsstTabWebContentsObserverUnitTest,
+       DontCreateForIncognitoBrowserContext) {
+  content::TestBrowserContext otr_browser_context;
+  otr_browser_context.set_is_off_the_record(true);
+  auto site_instance = content::SiteInstance::Create(&otr_browser_context);
+  auto web_contents = content::WebContentsTester::CreateTestWebContents(
+      &otr_browser_context, site_instance);
+
+  EXPECT_EQ(PsstTabWebContentsObserver::MaybeCreateForWebContents(
+                web_contents.get(), &otr_browser_context, prefs(), 2),
+            nullptr);
+}
+
+TEST_F(PsstTabWebContentsObserverUnitTest,
+       ShouldNotProcessRestoredNavigationEntry) {
+  auto url = GURL("https://example1.com");
+
+  content::NavigationController& controller = web_contents()->GetController();
+  EXPECT_CALL(psst_rule_registry(), CheckIfMatch).Times(0);
+
+  std::unique_ptr<content::NavigationEntry> restored_entry =
+      content::NavigationEntry::Create();
+  restored_entry->SetURL(url);
+  restored_entry->SetTitle(u"Restored Page");
+
+  std::vector<std::unique_ptr<content::NavigationEntry>> entries;
+  entries.push_back(std::move(restored_entry));
+
+  DocumentOnLoadObserver observer(web_contents());
+  controller.Restore(0 /* selected_index */, content::RestoreType::kRestored,
+                     &entries);
+
+  controller.LoadIfNecessary();
+
+  auto navigation_simulator =
+      content::NavigationSimulator::CreateFromPending(controller);
+  navigation_simulator->Commit();
+  observer.Wait();
+}
+
+TEST_F(PsstTabWebContentsObserverUnitTest, ShouldOnlyProcessHttpOrHttps) {
+  EXPECT_CALL(psst_rule_registry(), CheckIfMatch(_, _)).Times(0);
+  {
+    DocumentOnLoadObserver observer(web_contents());
+    content::NavigationSimulator::NavigateAndCommitFromBrowser(
+        web_contents(), GURL("chrome://flags"));
+    observer.Wait();
+  }
+  {
+    DocumentOnLoadObserver observer(web_contents());
+    content::NavigationSimulator::NavigateAndCommitFromBrowser(
+        web_contents(), GURL("about:blank"));
+    observer.Wait();
+  }
+  {
+    DocumentOnLoadObserver observer(web_contents());
+    content::NavigationSimulator::NavigateAndCommitFromBrowser(
+        web_contents(), GURL("file:///somepath"));
+    observer.Wait();
+  }
+  {
+    DocumentOnLoadObserver observer(web_contents());
+    content::NavigationSimulator::NavigateAndCommitFromBrowser(
+        web_contents(), GURL("ftp://example.com"));
+    observer.Wait();
+  }
+}
+
+TEST_F(PsstTabWebContentsObserverUnitTest,
+       ShouldProcessMultipleMainFrameNavigations) {
+  const std::string first_nav_user_script = "user1";
+  const std::string policy_script = "policy";
+  const GURL first_navigation_url("https://example1.com");
+
+  base::RunLoop first_nav_check_loop;
+  EXPECT_CALL(psst_rule_registry(), CheckIfMatch(first_navigation_url, _))
+      .WillOnce(CheckIfMatchCallback(
+          &first_nav_check_loop,
+          CreateMatchedRule(first_nav_user_script, policy_script)));
+
+  base::RunLoop first_nav_user_script_insert_loop;
+  EXPECT_CALL(scripts_handler(), InsertScriptInPage(first_nav_user_script, _))
+      .WillOnce(InsertScriptInPageCallback(&first_nav_user_script_insert_loop,
+                                           base::Value()));
+
+  DocumentOnLoadObserver first_nav_observer(web_contents());
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(
+      web_contents(), first_navigation_url);
+  first_nav_observer.Wait();
+
+  first_nav_check_loop.Run();
+  first_nav_user_script_insert_loop.Run();
+
+  const std::string second_nav_user_script = "user2";
+  const GURL second_navigation_url("https://example2.com");
+  base::RunLoop second_nav_check_loop;
+  EXPECT_CALL(psst_rule_registry(), CheckIfMatch(second_navigation_url, _))
+      .WillOnce(CheckIfMatchCallback(
+          &second_nav_check_loop,
+          CreateMatchedRule(second_nav_user_script, policy_script)));
+
+  base::RunLoop second_nav_user_script_insert_loop;
+  EXPECT_CALL(scripts_handler(), InsertScriptInPage(second_nav_user_script, _))
+      .WillOnce(InsertScriptInPageCallback(&second_nav_user_script_insert_loop,
+                                           base::Value()));
+  EXPECT_CALL(scripts_handler(), InsertScriptInPage(policy_script, _)).Times(0);
+
+  DocumentOnLoadObserver observer(web_contents());
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(
+      web_contents(), second_navigation_url);
+  observer.Wait();
+
+  second_nav_check_loop.Run();
+  second_nav_user_script_insert_loop.Run();
+}
+
+TEST_F(PsstTabWebContentsObserverUnitTest, ShouldProcessRedirectsNavigations) {
+  const std::string user_script = "user";
+  const std::string policy_script = "policy";
+  const GURL url("https://example1.com");
+  const GURL redirect_target("https://redirect.example1.com/");
+
+  EXPECT_CALL(psst_rule_registry(), CheckIfMatch(url, _)).Times(0);
+  base::RunLoop check_loop;
+  EXPECT_CALL(psst_rule_registry(), CheckIfMatch(redirect_target, _))
+      .WillOnce(CheckIfMatchCallback(
+          &check_loop, CreateMatchedRule(user_script, policy_script)));
+
+  base::RunLoop user_script_insert_loop;
+  auto value = base::Value();
+  EXPECT_CALL(scripts_handler(), InsertScriptInPage(user_script, _))
+      .WillOnce(InsertScriptInPageCallback(&user_script_insert_loop,
+                                           std::move(value)));
+  EXPECT_CALL(scripts_handler(), InsertScriptInPage(policy_script, _)).Times(0);
+
+  DocumentOnLoadObserver observer(web_contents());
+  auto simulator =
+      content::NavigationSimulator::CreateBrowserInitiated(url, web_contents());
+  simulator->Redirect(redirect_target);
+  simulator->Commit();
+
+  observer.Wait();
+  check_loop.Run();
+  user_script_insert_loop.Run();
+}
+
+TEST_F(PsstTabWebContentsObserverUnitTest,
+       ShouldNotProcessIfNotPrimaryMainFrame) {
+  // first load the main frame so we can create a subframe
+  GURL url("https://example.com/");
+
+  // call one for the main frame and then not again
+  EXPECT_CALL(psst_rule_registry(), CheckIfMatch(url, _)).Times(1);
+  DocumentOnLoadObserver observer(web_contents());
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+                                                             url);
+  observer.Wait();
+
+  // create sub-frame
+  auto* main_rfh = web_contents()->GetPrimaryMainFrame();
+  auto* child_rfh =
+      content::RenderFrameHostTester::For(main_rfh)->AppendChild("subframe");
+
+  // navigate sub-frame
+  content::NavigationSimulator::CreateRendererInitiated(
+      GURL("https://sub.example.com"), child_rfh)
+      ->Commit();
+}
+
+TEST_F(PsstTabWebContentsObserverUnitTest,
+       ShouldNotProcessIfNavigationNotCommitted) {
+  const GURL url("https://example.com");
+  EXPECT_CALL(psst_rule_registry(), CheckIfMatch(url, _)).Times(0);
+  auto simulator =
+      content::NavigationSimulator::CreateBrowserInitiated(url, web_contents());
+
+  // Simulate navigation start but NOT commit
+  simulator->Start();
+  simulator->Fail(net::ERR_ABORTED);  // Simulates cancel before commit
+}
+
+TEST_F(PsstTabWebContentsObserverUnitTest,
+       ShouldNotProcessIfSameDocumentNavigation) {
+  const GURL url("https://example1.com");
+  // should call once for the initial load and then not again
+  EXPECT_CALL(psst_rule_registry(), CheckIfMatch(url, _)).Times(1);
+  DocumentOnLoadObserver observer(web_contents());
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+                                                             url);
+  observer.Wait();
+
+  auto sim = content::NavigationSimulator::CreateRendererInitiated(
+      GURL(base::JoinString({url.spec(), "anchor"}, "#")),
+      web_contents()->GetPrimaryMainFrame());
+  sim->CommitSameDocument();
+}
+
+TEST_F(PsstTabWebContentsObserverUnitTest, DefaultPrefEnabledShouldProcess) {
+  const GURL url("https://example1.com");
+  EXPECT_CALL(psst_rule_registry(), CheckIfMatch(url, _)).Times(1);
+
+  DocumentOnLoadObserver observer(web_contents());
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+                                                             url);
+  observer.Wait();
+}
+
+TEST_F(PsstTabWebContentsObserverUnitTest, PrefDisabledDontProcess) {
+  const GURL url("https://example1.com");
+  prefs()->SetBoolean(prefs::kPsstEnabled, false);
+  EXPECT_CALL(psst_rule_registry(), CheckIfMatch(url, _)).Times(0);
+  DocumentOnLoadObserver observer(web_contents());
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+                                                             url);
+  observer.Wait();
+}
+
+TEST_F(PsstTabWebContentsObserverUnitTest, CheckIfMatchReturnsNull) {
+  const GURL url("https://example1.com");
+  base::RunLoop check_loop;
+  EXPECT_CALL(psst_rule_registry(), CheckIfMatch(url, _))
+      .WillOnce(CheckIfMatchFailsCallback(&check_loop));
+  EXPECT_CALL(scripts_handler(), InsertScriptInPage).Times(0);
+
+  DocumentOnLoadObserver observer(web_contents());
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+                                                             url);
+  observer.Wait();
+
+  check_loop.Run();
+}
+
+TEST_F(PsstTabWebContentsObserverUnitTest,
+       UserScriptReturnsEmptyHasPolicyScript) {
+  const std::string user_script = "user";
+  const std::string policy_script = "policy";
+  const GURL url("https://example1.com");
+  base::RunLoop check_loop;
+  EXPECT_CALL(psst_rule_registry(), CheckIfMatch(url, _))
+      .WillOnce(CheckIfMatchCallback(
+          &check_loop, CreateMatchedRule(user_script, policy_script)));
+  base::RunLoop user_script_insert_loop;
+  auto value = base::Value();
+
+  EXPECT_CALL(scripts_handler(), InsertScriptInPage(user_script, _))
+      .WillOnce(InsertScriptInPageCallback(&user_script_insert_loop,
+                                           std::move(value)));
+  EXPECT_CALL(scripts_handler(), InsertScriptInPage(policy_script, _)).Times(0);
+
+  DocumentOnLoadObserver observer(web_contents());
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+                                                             url);
+  observer.Wait();
+  check_loop.Run();
+  user_script_insert_loop.Run();
+}
+
+TEST_F(PsstTabWebContentsObserverUnitTest,
+       UserScriptReturnsEmptyNoPolicyScript) {
+  const std::string user_script = "user";
+  const std::string policy_script = "";
+  const GURL url("https://example1.com");
+  base::RunLoop check_loop;
+  EXPECT_CALL(psst_rule_registry(), CheckIfMatch(url, _))
+      .WillOnce(CheckIfMatchCallback(
+          &check_loop, CreateMatchedRule(user_script, policy_script)));
+  base::RunLoop user_script_insert_loop;
+  auto value = base::Value();
+
+  EXPECT_CALL(scripts_handler(), InsertScriptInPage(user_script, _))
+      .WillOnce(InsertScriptInPageCallback(&user_script_insert_loop,
+                                           std::move(value)));
+  EXPECT_CALL(scripts_handler(), InsertScriptInPage(policy_script, _)).Times(0);
+
+  DocumentOnLoadObserver observer(web_contents());
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+                                                             url);
+  observer.Wait();
+
+  check_loop.Run();
+  user_script_insert_loop.Run();
+}
+
+TEST_F(PsstTabWebContentsObserverUnitTest,
+       UserScriptReturnsDictHasPolicyScript) {
+  const std::string user_script = "user";
+  const std::string policy_script = "policy";
+  const GURL url("https://example1.com");
+  base::RunLoop check_loop;
+  EXPECT_CALL(psst_rule_registry(), CheckIfMatch(url, _))
+      .WillOnce(CheckIfMatchCallback(
+          &check_loop, CreateMatchedRule(user_script, policy_script)));
+  base::RunLoop user_script_insert_loop;
+  base::RunLoop policy_script_insert_loop;
+  auto dict = base::Value(base::Value::Dict());
+  auto value = base::Value();
+
+  EXPECT_CALL(scripts_handler(), InsertScriptInPage(user_script, _))
+      .WillOnce(InsertScriptInPageCallback(&user_script_insert_loop,
+                                           std::move(dict)));
+  EXPECT_CALL(scripts_handler(), InsertScriptInPage(policy_script, _))
+      .WillOnce(InsertScriptInPageCallback(&policy_script_insert_loop,
+                                           std::move(value)));
+
+  DocumentOnLoadObserver observer(web_contents());
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+                                                             url);
+  observer.Wait();
+
+  check_loop.Run();
+  user_script_insert_loop.Run();
+  policy_script_insert_loop.Run();
+}
+
+TEST_F(PsstTabWebContentsObserverUnitTest,
+       UserScriptReturnsDictNoPolicyScript) {,
+  const std::string user_script = "user";
+  const std::string policy_script = "";
+  const GURL url("https://example1.com");
+  base::RunLoop check_loop;
+  EXPECT_CALL(psst_rule_registry(), CheckIfMatch(url, _))
+      .WillOnce(CheckIfMatchCallback(
+          &check_loop, CreateMatchedRule(user_script, policy_script)));
+  base::RunLoop user_script_insert_loop;
+  auto dict = base::Value(base::Value::Dict());
+  auto value = base::Value();
+
+  EXPECT_CALL(scripts_handler(), InsertScriptInPage(user_script, _))
+      .WillOnce(InsertScriptInPageCallback(&user_script_insert_loop,
+                                           std::move(dict)));
+  EXPECT_CALL(scripts_handler(), InsertScriptInPage(policy_script, _)).Times(0);
+
+  DocumentOnLoadObserver observer(web_contents());
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+                                                             url);
+  observer.Wait();
+
+  check_loop.Run();
+  user_script_insert_loop.Run();
+}
+
+class PsstTabWebContentsObserverFeatureDisabledUnitTest
+    : public PsstTabWebContentsObserverUnitTestBase {
+ public:
+  void SetUp() override {
+    feature_list_.InitAndDisableFeature(psst::features::kEnablePsst);
+    PsstTabWebContentsObserverUnitTestBase::SetUp();
+  }
+};
+
+TEST_F(PsstTabWebContentsObserverFeatureDisabledUnitTest, DontCreate) {
+  EXPECT_EQ(PsstTabWebContentsObserver::MaybeCreateForWebContents(
+                web_contents(), browser_context(), prefs(), 2),
+            nullptr);
+}
+
+}  // namespace psst
diff --git components/psst/browser/core/BUILD.gn components/psst/browser/core/BUILD.gn
index c98fcf5473d9..f2c6e7f4d9af 100644
--- components/psst/browser/core/BUILD.gn
+++ components/psst/browser/core/BUILD.gn
@@ -3,28 +3,82 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at https://mozilla.org/MPL/2.0/.
 
+import("//brave/components/psst/buildflags/buildflags.gni")
+
+assert(enable_psst)
+
 component("core") {
   output_name = "psst_browser_core"
 
-  defines = [ "IS_PSST_BROWSER_CORE_IMPL" ]
+  public_deps = [ ":headers" ]
+  deps = [ ":core_impl" ]
+}
 
+source_set("headers") {
+  visibility = [ ":*" ]
+  defines = [ "IS_PSST_BROWSER_CORE_IMPL" ]
   sources = [
-    "psst_component_installer.cc",
+    "matched_rule.h",
     "psst_component_installer.h",
-    "psst_rule.cc",
     "psst_rule.h",
-    "psst_rule_registry.cc",
     "psst_rule_registry.h",
   ]
 
   deps = [
     "//base",
+    "//brave/extensions:common",
+    "//url",
+  ]
+}
+
+source_set("core_impl") {
+  visibility = [ ":*" ]
+  defines = [ "IS_PSST_BROWSER_CORE_IMPL" ]
+  sources = [
+    "matched_rule.cc",
+    "psst_component_installer.cc",
+    "psst_rule.cc",
+    "psst_rule_registry_impl.cc",
+    "psst_rule_registry_impl.h",
+    "rule_data_reader.cc",
+    "rule_data_reader.h",
+  ]
+
+  public_deps = [
+    ":headers",
+    "//base",
+    "//url",
+  ]
+
+  deps = [
     "//brave/components/brave_component_updater/browser",
     "//brave/components/psst/common",
     "//brave/extensions:common",
     "//components/component_updater",
+    "//components/prefs",
     "//crypto",
     "//net",
-    "//url",
+  ]
+}
+
+source_set("unit_tests") {
+  testonly = true
+
+  defines = [ "IS_PSST_BROWSER_CORE_IMPL" ]
+
+  sources = [
+    "matched_rule_unittest.cc",
+    "psst_rule_registry_unittest.cc",
+    "psst_rule_unittest.cc",
+    "rule_data_reader_unittest.cc",
+  ]
+
+  deps = [
+    ":core_impl",
+    ":headers",
+    "//base/test:test_support",
+    "//brave/components/psst/common",
+    "//testing/gmock",
+    "//testing/gtest",
   ]
 }
diff --git components/psst/browser/core/DEPS components/psst/browser/core/DEPS
index 2ebcf3ae486a..c1dedb8bb6e9 100644
--- components/psst/browser/core/DEPS
+++ components/psst/browser/core/DEPS
@@ -1,5 +1,6 @@
 include_rules = [
   "+components/component_updater",
   "+crypto",
+  "+components/prefs",
   "+extensions/common",
 ]
diff --git a/components/psst/browser/core/matched_rule.cc b/components/psst/browser/core/matched_rule.cc
new file mode 100644
index 000000000000..d12cd524827e
--- /dev/null
+++ components/psst/browser/core/matched_rule.cc
@@ -0,0 +1,47 @@
+// Copyright (c) 2025 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#include "brave/components/psst/browser/core/matched_rule.h"
+
+#include <memory>
+#include <optional>
+#include <string>
+
+#include "base/memory/ptr_util.h"
+#include "brave/components/psst/browser/core/rule_data_reader.h"
+
+namespace psst {
+
+MatchedRule::MatchedRule(const std::string& name,
+                         const std::string& user_script,
+                         const std::string& policy_script,
+                         int version)
+    : name_(name),
+      user_script_(user_script),
+      policy_script_(policy_script),
+      version_(version) {}
+
+MatchedRule::~MatchedRule() = default;
+
+// static
+std::unique_ptr<MatchedRule> MatchedRule::Create(
+    std::unique_ptr<RuleDataReader> rule_reader,
+    const PsstRule& rule) {
+  CHECK(rule_reader);
+
+  auto user_script =
+      rule_reader->ReadUserScript(rule.name(), rule.user_script_path());
+  auto policy_script =
+      rule_reader->ReadPolicyScript(rule.name(), rule.policy_script_path());
+
+  if (!user_script || !policy_script) {
+    return nullptr;
+  }
+
+  return base::WrapUnique<MatchedRule>(new MatchedRule(
+      rule.name(), user_script.value(), policy_script.value(), rule.version()));
+}
+
+}  // namespace psst
diff --git a/components/psst/browser/core/matched_rule.h b/components/psst/browser/core/matched_rule.h
new file mode 100644
index 000000000000..518dcbc79bb8
--- /dev/null
+++ components/psst/browser/core/matched_rule.h
@@ -0,0 +1,52 @@
+// Copyright (c) 2025 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#ifndef BRAVE_COMPONENTS_PSST_BROWSER_CORE_MATCHED_RULE_H_
+#define BRAVE_COMPONENTS_PSST_BROWSER_CORE_MATCHED_RULE_H_
+
+#include <memory>
+#include <string>
+
+#include "base/component_export.h"
+#include "brave/components/psst/browser/core/psst_rule.h"
+
+namespace psst {
+
+class RuleDataReader;
+
+// Represents the loaded PSST data for PsstRule matched by the URL.
+class COMPONENT_EXPORT(PSST_BROWSER_CORE) MatchedRule {
+ public:
+  ~MatchedRule();
+  MatchedRule(const MatchedRule&) = delete;
+  MatchedRule& operator=(const MatchedRule&) = delete;
+
+  static std::unique_ptr<MatchedRule> Create(
+      std::unique_ptr<RuleDataReader> rule_reader,
+      const PsstRule& rule);
+
+  // Getters.
+  const std::string& user_script() const { return user_script_; }
+  const std::string& policy_script() const { return policy_script_; }
+  int version() const { return version_; }
+  const std::string& name() const { return name_; }
+
+ private:
+  friend class RuleDataReaderUnitTest;
+  friend class PsstTabWebContentsObserverUnitTestBase;
+  MatchedRule(const std::string& name,
+              const std::string& user_script,
+              const std::string& policy_script,
+              int version);
+
+  const std::string name_;
+  const std::string user_script_;
+  const std::string policy_script_;
+  int version_;
+};
+
+}  // namespace psst
+
+#endif  // BRAVE_COMPONENTS_PSST_BROWSER_CORE_MATCHED_RULE_H_
diff --git a/components/psst/browser/core/matched_rule_unittest.cc b/components/psst/browser/core/matched_rule_unittest.cc
new file mode 100644
index 000000000000..1637d46e3ada
--- /dev/null
+++ components/psst/browser/core/matched_rule_unittest.cc
@@ -0,0 +1,136 @@
+// Copyright (c) 2025 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#include "brave/components/psst/browser/core/matched_rule.h"
+
+#include "base/base_paths.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/path_service.h"
+#include "brave/components/psst/browser/core/rule_data_reader.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace psst {
+
+namespace {
+
+constexpr char kPsstJsonFileContent[] = R"([
+        {
+            "name": "a",
+            "include": [
+                "https://a.com/*"
+            ],
+            "exclude": [
+            ],
+            "version": 1,
+            "user_script": "user.js",
+            "policy_script": "policy.js"
+        }
+    ])";
+
+constexpr char kPsstJsonFileNoUserScriptContent[] = R"([
+        {
+            "name": "a",
+            "include": [
+                "https://a.com/*"
+            ],
+            "exclude": [
+            ],
+            "version": 1,
+            "user_script": "",
+            "policy_script": "policy.js"
+        }
+    ])";
+
+constexpr char kPsstJsonFileNoPolicyScriptContent[] = R"([
+        {
+            "name": "a",
+            "include": [
+                "https://a.com/*"
+            ],
+            "exclude": [
+            ],
+            "version": 1,
+            "user_script": "user.js",
+            "policy_script": ""
+        }
+    ])";
+
+std::string ReadFile(const base::FilePath& file_path) {
+  std::string contents;
+  bool success = base::ReadFileToString(file_path, &contents);
+  if (!success || contents.empty()) {
+    VLOG(2) << "ReadFile: cannot " << "read file " << file_path;
+  }
+  return contents;
+}
+}  // namespace
+
+class MatchedRuleTest : public testing::Test {
+ public:
+  void SetUp() override {
+    base::FilePath test_data_dir =
+        base::PathService::CheckedGet(base::DIR_SRC_TEST_DATA_ROOT);
+    test_data_dir_base_ =
+        test_data_dir.AppendASCII("brave/components/test/data/psst");
+  }
+
+  const base::FilePath& GetBasePath() { return test_data_dir_base_; }
+
+  const base::FilePath GetScriptsPath() {
+    return GetBasePath()
+        .Append(base::FilePath::FromUTF8Unsafe("scripts"))
+        .Append(base::FilePath::FromUTF8Unsafe("a"));
+  }
+
+ private:
+  base::FilePath test_data_dir_base_;
+};
+
+TEST_F(MatchedRuleTest, LoadSimpleMatchedRule) {
+  const auto psst_rules = PsstRule::ParseRules(kPsstJsonFileContent);
+  ASSERT_EQ(psst_rules->size(), 1u);
+
+  auto matched_rule = MatchedRule::Create(
+      std::make_unique<RuleDataReader>(GetBasePath()), psst_rules->front());
+
+  EXPECT_TRUE(matched_rule);
+
+  const auto user_script = matched_rule->user_script();
+  ASSERT_FALSE(user_script.empty());
+  EXPECT_EQ(user_script, ReadFile(GetScriptsPath().Append(
+                             base::FilePath::FromUTF8Unsafe("user.js"))));
+  const auto policy_script = matched_rule->policy_script();
+  ASSERT_FALSE(policy_script.empty());
+  EXPECT_EQ(policy_script, ReadFile(GetScriptsPath().Append(
+                               base::FilePath::FromUTF8Unsafe("policy.js"))));
+}
+
+TEST_F(MatchedRuleTest, TryToLoadMatchedRuleWithNoUserOrPolicyScript) {
+  const auto psst_rules_no_user_script =
+      PsstRule::ParseRules(kPsstJsonFileNoUserScriptContent);
+  ASSERT_EQ(psst_rules_no_user_script->size(), 1u);
+  EXPECT_TRUE(psst_rules_no_user_script->front().user_script_path().empty());
+  EXPECT_FALSE(psst_rules_no_user_script->front().policy_script_path().empty());
+
+  auto matched_rule_no_user_script =
+      MatchedRule::Create(std::make_unique<RuleDataReader>(GetBasePath()),
+                          psst_rules_no_user_script->front());
+  EXPECT_FALSE(matched_rule_no_user_script);
+
+  const auto psst_rules_no_policy_script =
+      PsstRule::ParseRules(kPsstJsonFileNoPolicyScriptContent);
+  ASSERT_EQ(psst_rules_no_policy_script->size(), 1u);
+  EXPECT_FALSE(psst_rules_no_policy_script->front().user_script_path().empty());
+  EXPECT_TRUE(
+      psst_rules_no_policy_script->front().policy_script_path().empty());
+
+  auto matched_rule_no_policy_script =
+      MatchedRule::Create(std::make_unique<RuleDataReader>(GetBasePath()),
+                          psst_rules_no_policy_script->front());
+  EXPECT_FALSE(matched_rule_no_policy_script);
+}
+
+}  // namespace psst
diff --git components/psst/browser/core/psst_component_installer.cc components/psst/browser/core/psst_component_installer.cc
index 808e402e3935..5300ffb6d4da 100644
--- components/psst/browser/core/psst_component_installer.cc
+++ components/psst/browser/core/psst_component_installer.cc
@@ -7,25 +7,25 @@
 
 #include <memory>
 #include <string>
-#include <utility>
 #include <vector>
 
 #include "base/base64.h"
 #include "base/containers/to_vector.h"
+#include "base/files/file_util.h"
 #include "base/functional/bind.h"
-#include "base/functional/callback.h"
-#include "base/functional/callback_forward.h"
-#include "base/no_destructor.h"
+#include "base/path_service.h"
 #include "brave/components/brave_component_updater/browser/brave_on_demand_updater.h"
 #include "brave/components/psst/browser/core/psst_rule_registry.h"
 #include "brave/components/psst/common/features.h"
 #include "components/component_updater/component_installer.h"
+#include "components/component_updater/component_updater_paths.h"
 #include "components/component_updater/component_updater_service.h"
+#include "components/prefs/pref_service.h"
 #include "crypto/sha2.h"
 
-namespace psst {
+using brave_component_updater::BraveOnDemandUpdater;
 
-namespace {
+namespace psst {
 
 // Directory structure of PSST component:
 // lhhcaamjbmbijmjbnnodjaknblkiagon/<component version>/
@@ -33,27 +33,13 @@ namespace {
 //  |_ psst.json
 //  |_ scripts/
 //    |_ twitter/
-//        |_ test.js
+//        |_ user.js
 //        |_ policy.js
 //    |_ linkedin/
-//        |_ test.js
+//        |_ user.js
 //        |_ policy.js
 // See psst_rule.cc for the format of psst.json.
 
-constexpr char kPsstComponentName[] =
-    "Brave Privacy Settings Selection for Sites Tool (PSST) Files";
-constexpr char kPsstComponentId[] = "lhhcaamjbmbijmjbnnodjaknblkiagon";
-constexpr char kPsstComponentBase64PublicKey[] =
-    "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAphUFFHyK+"
-    "qUOXSw3OJXRQwKs79bt7zqnmkeFp/szXmmhj6/"
-    "i4fmNiXVaxFuVOryM9OiaVxBIGHjN1BWYCQdylgbmgVTqLWpJAy/AAKEH9/"
-    "Q68yWfQnN5sg1miNir+0I1SpCiT/Dx2N7s28WNnzD2e6/"
-    "7Umx+zRXkRtoPX0xAecgUeyOZcrpZXJ4CG8dTJInhv7Fly/U8V/KZhm6ydKlibwsh2CB588/"
-    "FlvQUzi5ZykXnPfzlsNLyyQ8fy6/+8hzSE5x4HTW5fy3TIRvmDi/"
-    "7HmW+evvuMIPl1gtVe4HKOZ7G8UaznjXBfspszHU1fqTiZWeCPb53uemo1a+rdnSHXwIDAQAB";
-
-}  // namespace
-
 class PsstComponentInstallerPolicy
     : public component_updater::ComponentInstallerPolicy {
  public:
@@ -113,10 +99,11 @@ PsstComponentInstallerPolicy::OnCustomInstall(
 
 void PsstComponentInstallerPolicy::OnCustomUninstall() {}
 
-void PsstComponentInstallerPolicy::ComponentReady(const base::Version& version,
-                                                  const base::FilePath& path,
-                                                  base::Value::Dict manifest) {
-  PsstRuleRegistry::GetInstance()->LoadRules(path);
+void PsstComponentInstallerPolicy::ComponentReady(
+    const base::Version& version,
+    const base::FilePath& install_dir,
+    base::Value::Dict manifest) {
+  PsstRuleRegistry::GetInstance()->LoadRules(install_dir, base::NullCallback());
 }
 
 bool PsstComponentInstallerPolicy::VerifyInstallation(
@@ -147,8 +134,7 @@ bool PsstComponentInstallerPolicy::IsBraveComponent() const {
 }
 
 void RegisterPsstComponent(component_updater::ComponentUpdateService* cus) {
-  if (!base::FeatureList::IsEnabled(psst::features::kBravePsst) || !cus) {
-    // In test, |cus| could be nullptr.
+  if (!base::FeatureList::IsEnabled(psst::features::kEnablePsst) || !cus) {
     return;
   }
 
diff --git components/psst/browser/core/psst_component_installer.h components/psst/browser/core/psst_component_installer.h
index 965c2feed81f..c91ffa943a0d 100644
--- components/psst/browser/core/psst_component_installer.h
+++ components/psst/browser/core/psst_component_installer.h
@@ -14,6 +14,18 @@ class ComponentUpdateService;
 
 namespace psst {
 
+inline constexpr char kPsstComponentName[] =
+    "Brave Privacy Settings Selection for Sites Tool (PSST) Files";
+inline constexpr char kPsstComponentId[] = "lhhcaamjbmbijmjbnnodjaknblkiagon";
+inline constexpr char kPsstComponentBase64PublicKey[] =
+    "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAphUFFHyK+"
+    "qUOXSw3OJXRQwKs79bt7zqnmkeFp/szXmmhj6/"
+    "i4fmNiXVaxFuVOryM9OiaVxBIGHjN1BWYCQdylgbmgVTqLWpJAy/AAKEH9/"
+    "Q68yWfQnN5sg1miNir+0I1SpCiT/Dx2N7s28WNnzD2e6/"
+    "7Umx+zRXkRtoPX0xAecgUeyOZcrpZXJ4CG8dTJInhv7Fly/U8V/KZhm6ydKlibwsh2CB588/"
+    "FlvQUzi5ZykXnPfzlsNLyyQ8fy6/+8hzSE5x4HTW5fy3TIRvmDi/"
+    "7HmW+evvuMIPl1gtVe4HKOZ7G8UaznjXBfspszHU1fqTiZWeCPb53uemo1a+rdnSHXwIDAQAB";
+
 // Registers the PSST component with the component updater.
 COMPONENT_EXPORT(PSST_BROWSER_CORE)
 void RegisterPsstComponent(component_updater::ComponentUpdateService* cus);
diff --git components/psst/browser/core/psst_rule.cc components/psst/browser/core/psst_rule.cc
index ee04e36191b0..c007118653f4 100644
--- components/psst/browser/core/psst_rule.cc
+++ components/psst/browser/core/psst_rule.cc
@@ -25,8 +25,9 @@ namespace {
 // psst.json keys
 constexpr char kInclude[] = "include";
 constexpr char kExclude[] = "exclude";
+constexpr char kName[] = "name";
 constexpr char kVersion[] = "version";
-constexpr char kTestScript[] = "test_script";
+constexpr char kUserScript[] = "user_script";
 constexpr char kPolicyScript[] = "policy_script";
 
 bool GetURLPatternSetFromValue(const base::Value* value,
@@ -61,7 +62,8 @@ PsstRule::~PsstRule() = default;
 PsstRule::PsstRule(const PsstRule& other) {
   include_pattern_set_ = other.include_pattern_set_.Clone();
   exclude_pattern_set_ = other.exclude_pattern_set_.Clone();
-  test_script_path_ = other.test_script_path_;
+  name_ = other.name_;
+  user_script_path_ = other.user_script_path_;
   policy_script_path_ = other.policy_script_path_;
   version_ = other.version_;
 }
@@ -73,8 +75,9 @@ void PsstRule::RegisterJSONConverter(
       kInclude, &PsstRule::include_pattern_set_, GetURLPatternSetFromValue);
   converter->RegisterCustomValueField<extensions::URLPatternSet>(
       kExclude, &PsstRule::exclude_pattern_set_, GetURLPatternSetFromValue);
+  converter->RegisterStringField(kName, &PsstRule::name_);
   converter->RegisterCustomValueField<base::FilePath>(
-      kTestScript, &PsstRule::test_script_path_, GetFilePathFromValue);
+      kUserScript, &PsstRule::user_script_path_, GetFilePathFromValue);
   converter->RegisterCustomValueField<base::FilePath>(
       kPolicyScript, &PsstRule::policy_script_path_, GetFilePathFromValue);
   converter->RegisterIntField(kVersion, &PsstRule::version_);
diff --git components/psst/browser/core/psst_rule.h components/psst/browser/core/psst_rule.h
index 7f07a73c5176..2e1eb8fffe53 100644
--- components/psst/browser/core/psst_rule.h
+++ components/psst/browser/core/psst_rule.h
@@ -6,29 +6,19 @@
 #ifndef BRAVE_COMPONENTS_PSST_BROWSER_CORE_PSST_RULE_H_
 #define BRAVE_COMPONENTS_PSST_BROWSER_CORE_PSST_RULE_H_
 
-#include <memory>
 #include <optional>
 #include <string>
-#include <utility>
 #include <vector>
 
-#include "base/containers/flat_set.h"
+#include "base/component_export.h"
 #include "base/files/file_path.h"
 #include "base/json/json_value_converter.h"
-#include "base/values.h"
 #include "extensions/common/url_pattern_set.h"
 
 class GURL;
 
 namespace psst {
 
-// Holds the loaded script text when a rule is matched.
-struct MatchedRule {
-  std::string test_script;
-  std::string policy_script;
-  int version;
-};
-
 // Format of the psst.json file:
 // [
 //   {
@@ -37,17 +27,18 @@ struct MatchedRule {
 //     ],
 //     "exclude": [
 //     ],
+//     "name": "twitter",
 //     "version": 1,
-//     "test_script": "twitter/test.js",
-//     "policy_script": "twitter/policy.js"
+//     "user_script": "user.js",
+//     "policy_script": "policy.js"
 //   }, ...
 // ]
-// Note that "test_script" and "policy_script" give paths
-// relative to the component under scripts/
+// Note that values for the "_script" keys give paths
+// relative to the component under scripts/<name>/, NOT script contents.
+
 // This class describes a single rule in the psst.json file.
-class PsstRule {
+class COMPONENT_EXPORT(PSST_BROWSER_CORE) PsstRule {
  public:
-  PsstRule();
   ~PsstRule();
   PsstRule(const PsstRule& other);  // needed for std::vector<PsstRule>
 
@@ -63,16 +54,21 @@ class PsstRule {
   bool ShouldInsertScript(const GURL& url) const;
 
   // Getters.
-  const base::FilePath& GetPolicyScript() const { return policy_script_path_; }
-  const base::FilePath& GetTestScript() const { return test_script_path_; }
-  int GetVersion() const { return version_; }
+  const std::string& name() const { return name_; }
+  const base::FilePath& policy_script_path() const {
+    return policy_script_path_;
+  }
+  const base::FilePath& user_script_path() const { return user_script_path_; }
+  int version() const { return version_; }
 
  private:
+  PsstRule();
   extensions::URLPatternSet include_pattern_set_;
   extensions::URLPatternSet exclude_pattern_set_;
+  std::string name_;
   // These are paths (not contents!) relative to the component under scripts/.
   base::FilePath policy_script_path_;
-  base::FilePath test_script_path_;
+  base::FilePath user_script_path_;
   // Used for checking if the last inserted script is the latest version.
   int version_;
 };
diff --git components/psst/browser/core/psst_rule_registry.cc components/psst/browser/core/psst_rule_registry.cc
deleted file mode 100644
index 145552a81de6..000000000000
--- components/psst/browser/core/psst_rule_registry.cc
+++ /dev/null
@@ -1,108 +0,0 @@
-// Copyright (c) 2023 The Brave Authors. All rights reserved.
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-#include "brave/components/psst/browser/core/psst_rule_registry.h"
-
-#include <memory>
-#include <string>
-#include <utility>
-#include <vector>
-
-#include "base/containers/contains.h"
-#include "base/containers/flat_set.h"
-#include "base/files/file_path.h"
-#include "base/files/file_util.h"
-#include "base/functional/callback_forward.h"
-#include "base/logging.h"
-#include "base/memory/singleton.h"
-#include "base/task/thread_pool.h"
-#include "brave/components/psst/browser/core/psst_rule.h"
-#include "brave/components/psst/common/features.h"
-#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
-#include "url/gurl.h"
-#include "url/origin.h"
-
-namespace psst {
-
-namespace {
-
-const base::FilePath::CharType kJsonFile[] = FILE_PATH_LITERAL("psst.json");
-const base::FilePath::CharType kScriptsDir[] = FILE_PATH_LITERAL("scripts");
-
-std::string ReadFile(const base::FilePath& file_path) {
-  std::string contents;
-  bool success = base::ReadFileToString(file_path, &contents);
-  if (!success || contents.empty()) {
-    VLOG(2) << "ReadFile: cannot "
-            << "read file " << file_path;
-  }
-  return contents;
-}
-
-MatchedRule CreateMatchedRule(const base::FilePath& component_path,
-                              const base::FilePath& test_script_path,
-                              const base::FilePath& policy_script_path,
-                              const int version) {
-  auto prefix = base::FilePath(component_path).Append(kScriptsDir);
-  auto test_script = ReadFile(base::FilePath(prefix).Append(test_script_path));
-  auto policy_script =
-      ReadFile(base::FilePath(prefix).Append(policy_script_path));
-  return {test_script, policy_script, version};
-}
-
-}  // namespace
-
-// static
-PsstRuleRegistry* PsstRuleRegistry::GetInstance() {
-  // Check if feature flag is enabled.
-  if (!base::FeatureList::IsEnabled(psst::features::kBravePsst)) {
-    return nullptr;
-  }
-  return base::Singleton<PsstRuleRegistry>::get();
-}
-
-PsstRuleRegistry::PsstRuleRegistry() = default;
-
-PsstRuleRegistry::~PsstRuleRegistry() = default;
-
-void PsstRuleRegistry::CheckIfMatch(
-    const GURL& url,
-    base::OnceCallback<void(MatchedRule)> cb) const {
-  for (const PsstRule& rule : rules_) {
-    if (rule.ShouldInsertScript(url)) {
-      base::ThreadPool::PostTaskAndReplyWithResult(
-          FROM_HERE, {base::MayBlock()},
-          base::BindOnce(&CreateMatchedRule, component_path_,
-                         rule.GetTestScript(), rule.GetPolicyScript(),
-                         rule.GetVersion()),
-          std::move(cb));
-      // Only ever find one matching rule.
-      return;
-    }
-  }
-}
-
-void PsstRuleRegistry::LoadRules(const base::FilePath& path) {
-  SetComponentPath(path);
-  base::ThreadPool::PostTaskAndReplyWithResult(
-      FROM_HERE, {base::MayBlock()},
-      base::BindOnce(&ReadFile, path.Append(kJsonFile)),
-      base::BindOnce(&PsstRuleRegistry::OnLoadRules,
-                     weak_factory_.GetWeakPtr()));
-}
-
-void PsstRuleRegistry::SetComponentPath(const base::FilePath& path) {
-  component_path_ = path;
-}
-
-void PsstRuleRegistry::OnLoadRules(const std::string& contents) {
-  auto parsed_rules = PsstRule::ParseRules(contents);
-  if (!parsed_rules) {
-    return;
-  }
-  rules_ = std::move(parsed_rules.value());
-}
-
-}  // namespace psst
diff --git components/psst/browser/core/psst_rule_registry.h components/psst/browser/core/psst_rule_registry.h
index aa8c9c33fc41..2c168e1ef413 100644
--- components/psst/browser/core/psst_rule_registry.h
+++ components/psst/browser/core/psst_rule_registry.h
@@ -11,56 +11,29 @@
 #include <vector>
 
 #include "base/component_export.h"
-#include "base/gtest_prod_util.h"
-#include "base/memory/raw_ptr.h"
-#include "base/memory/singleton.h"
-#include "base/memory/weak_ptr.h"
+#include "base/functional/callback.h"
+#include "brave/components/psst/browser/core/matched_rule.h"
 #include "brave/components/psst/browser/core/psst_rule.h"
-
-class GURL;
+#include "url/gurl.h"
 
 namespace psst {
-// Needed for testing private methods in PsstTabHelperBrowserTest.
-FORWARD_DECLARE_TEST(PsstTabHelperBrowserTest, NoMatch);
-FORWARD_DECLARE_TEST(PsstTabHelperBrowserTest, RuleMatchTestScriptFalse);
-FORWARD_DECLARE_TEST(PsstTabHelperBrowserTest, RuleMatchTestScriptTrue);
 
-// This class loads and stores the rules from the psst.json file.
-// It is also used for matching based on the URL.
+// Represents the registry of PSST rules.
+// It allows to load the all items from the psst.json file and match them
+// against the URL. For matched rules, it loads rule data (the user.js and
+// policy.js script contents) with using of rule data reader.
 class COMPONENT_EXPORT(PSST_BROWSER_CORE) PsstRuleRegistry {
  public:
-  PsstRuleRegistry(const PsstRuleRegistry&) = delete;
-  PsstRuleRegistry& operator=(const PsstRuleRegistry&) = delete;
-  ~PsstRuleRegistry();
-  static PsstRuleRegistry* GetInstance();  // singleton
-  // Returns the matched PSST rule, if any.
-  void CheckIfMatch(const GURL& url,
-                    base::OnceCallback<void(MatchedRule)> cb) const;
-  // Given a path to psst.json, loads the rules from the file into memory.
-  void LoadRules(const base::FilePath& path);
-
- private:
-  PsstRuleRegistry();
-
-  // These methods are also called by PsstTabHelperBrowserTest.
-  // Given contents of psst.json, loads the rules from the file into memory.
-  // Called by |LoadRules| after the file is read.
-  void OnLoadRules(const std::string& data);
-  // Sets the component path used to resolve the paths to the scripts.
-  void SetComponentPath(const base::FilePath& path);
+  using OnLoadCallback = base::OnceCallback<void(const std::string&,
+                                                 const std::vector<PsstRule>&)>;
+  static PsstRuleRegistry* GetInstance();
 
-  base::FilePath component_path_;
-  std::vector<PsstRule> rules_;
-
-  base::WeakPtrFactory<PsstRuleRegistry> weak_factory_{this};
-
-  // Needed for testing private methods in PsstTabHelperBrowserTest.
-  FRIEND_TEST_ALL_PREFIXES(PsstTabHelperBrowserTest, NoMatch);
-  FRIEND_TEST_ALL_PREFIXES(PsstTabHelperBrowserTest, RuleMatchTestScriptFalse);
-  FRIEND_TEST_ALL_PREFIXES(PsstTabHelperBrowserTest, RuleMatchTestScriptTrue);
-  friend class PsstTabHelperBrowserTest;
+  // Returns the matched PSST rule, if any.
+  virtual void CheckIfMatch(
+      const GURL& url,
+      base::OnceCallback<void(std::unique_ptr<MatchedRule>)> cb) = 0;
 
-  friend struct base::DefaultSingletonTraits<PsstRuleRegistry>;
+  virtual void LoadRules(const base::FilePath& path, OnLoadCallback cb) = 0;
 };
 
 }  // namespace psst
diff --git a/components/psst/browser/core/psst_rule_registry_impl.cc b/components/psst/browser/core/psst_rule_registry_impl.cc
new file mode 100644
index 000000000000..deadd7c669f8
--- /dev/null
+++ components/psst/browser/core/psst_rule_registry_impl.cc
@@ -0,0 +1,92 @@
+// Copyright (c) 2023 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#include "brave/components/psst/browser/core/psst_rule_registry_impl.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/feature_list.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/functional/callback_forward.h"
+#include "base/logging.h"
+#include "base/memory/weak_ptr.h"
+#include "base/no_destructor.h"
+#include "base/task/thread_pool.h"
+#include "brave/components/psst/browser/core/matched_rule.h"
+#include "brave/components/psst/browser/core/psst_rule.h"
+#include "brave/components/psst/browser/core/rule_data_reader.h"
+#include "brave/components/psst/common/features.h"
+#include "url/origin.h"
+
+namespace psst {
+
+namespace {
+
+const base::FilePath::CharType kJsonFile[] = FILE_PATH_LITERAL("psst.json");
+
+std::string ReadFile(const base::FilePath& file_path) {
+  std::string contents;
+  bool success = base::ReadFileToString(file_path, &contents);
+  if (!success || contents.empty()) {
+    VLOG(2) << "ReadFile: cannot " << "read file " << file_path;
+  }
+  return contents;
+}
+
+}  // namespace
+
+// static
+PsstRuleRegistry* PsstRuleRegistry::GetInstance() {
+  static base::NoDestructor<PsstRuleRegistryImpl> instance;
+  return instance.get();
+}
+
+PsstRuleRegistryImpl::PsstRuleRegistryImpl() = default;
+PsstRuleRegistryImpl::~PsstRuleRegistryImpl() = default;
+
+void PsstRuleRegistryImpl::CheckIfMatch(
+    const GURL& url,
+    base::OnceCallback<void(std::unique_ptr<MatchedRule>)> cb) {
+  for (const PsstRule& rule : rules_) {
+    if (rule.ShouldInsertScript(url)) {
+      base::ThreadPool::PostTaskAndReplyWithResult(
+          FROM_HERE, {base::MayBlock()},
+          base::BindOnce(&MatchedRule::Create,
+                         std::make_unique<RuleDataReader>(component_path_),
+                         rule),
+          std::move(cb));
+      // Only ever find one matching rule.
+      return;
+    }
+  }
+}
+
+void PsstRuleRegistryImpl::LoadRules(const base::FilePath& path,
+                                     OnLoadCallback cb) {
+  CHECK(base::FeatureList::IsEnabled(psst::features::kEnablePsst));
+  component_path_ = path;
+
+  base::ThreadPool::PostTaskAndReplyWithResult(
+      FROM_HERE, {base::MayBlock()},
+      base::BindOnce(&ReadFile, path.Append(kJsonFile)),
+      base::BindOnce(&PsstRuleRegistryImpl::OnLoadRules,
+                     weak_factory_.GetWeakPtr(), std::move(cb)));
+}
+
+void PsstRuleRegistryImpl::OnLoadRules(OnLoadCallback cb,
+                                       const std::string& contents) {
+  auto parsed_rules = PsstRule::ParseRules(contents);
+  if (parsed_rules) {
+    rules_ = std::move(parsed_rules.value());
+  }
+
+  std::move(cb).Run(contents, rules_);
+}
+
+}  // namespace psst
diff --git a/components/psst/browser/core/psst_rule_registry_impl.h b/components/psst/browser/core/psst_rule_registry_impl.h
new file mode 100644
index 000000000000..e57c0f22a4f9
--- /dev/null
+++ components/psst/browser/core/psst_rule_registry_impl.h
@@ -0,0 +1,49 @@
+// Copyright (c) 2023 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#ifndef BRAVE_COMPONENTS_PSST_BROWSER_CORE_PSST_RULE_REGISTRY_IMPL_H_
+#define BRAVE_COMPONENTS_PSST_BROWSER_CORE_PSST_RULE_REGISTRY_IMPL_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/functional/callback.h"
+#include "base/functional/callback_forward.h"
+#include "base/memory/weak_ptr.h"
+#include "base/no_destructor.h"
+#include "brave/components/psst/browser/core/matched_rule.h"
+#include "brave/components/psst/browser/core/psst_rule.h"
+#include "brave/components/psst/browser/core/psst_rule_registry.h"
+#include "url/gurl.h"
+
+namespace psst {
+
+class PsstRuleRegistryImpl : public PsstRuleRegistry {
+ public:
+  ~PsstRuleRegistryImpl();
+
+  // PsstRuleRegistry overrides
+  void CheckIfMatch(
+      const GURL& url,
+      base::OnceCallback<void(std::unique_ptr<MatchedRule>)> cb) override;
+  void LoadRules(const base::FilePath& path, OnLoadCallback cb) override;
+
+ private:
+  PsstRuleRegistryImpl();
+  friend base::NoDestructor<PsstRuleRegistryImpl>;
+
+  friend class PsstRuleRegistryUnitTest;
+
+  void OnLoadRules(OnLoadCallback cb, const std::string& data);
+
+  std::vector<PsstRule> rules_;
+  base::FilePath component_path_;
+  base::WeakPtrFactory<PsstRuleRegistryImpl> weak_factory_{this};
+};
+
+}  // namespace psst
+
+#endif  // BRAVE_COMPONENTS_PSST_BROWSER_CORE_PSST_RULE_REGISTRY_IMPL_H_
diff --git a/components/psst/browser/core/psst_rule_registry_unittest.cc b/components/psst/browser/core/psst_rule_registry_unittest.cc
new file mode 100644
index 000000000000..11b257b52ff2
--- /dev/null
+++ components/psst/browser/core/psst_rule_registry_unittest.cc
@@ -0,0 +1,262 @@
+// Copyright (c) 2025 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#include <cstddef>
+#include <memory>
+#include <optional>
+
+#include "base/base_paths.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/functional/callback_forward.h"
+#include "base/path_service.h"
+#include "base/test/mock_callback.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/test/task_environment.h"
+#include "brave/components/psst/browser/core/matched_rule.h"
+#include "brave/components/psst/browser/core/psst_rule_registry_impl.h"
+#include "brave/components/psst/common/features.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace psst {
+
+namespace {
+// Test PSST rules file: brave/components/test/data/psst/psst.json
+constexpr size_t kTestPsstRulesCount = 3;
+constexpr char kPsstUserScriptName[] = "user.js";
+constexpr char kPsstPolicyScriptName[] = "policy.js";
+constexpr char kPsstJsonFileName[] = "psst.json";
+
+std::string ReadFile(const base::FilePath& file_path) {
+  std::string contents;
+  bool success = base::ReadFileToString(file_path, &contents);
+  if (!success || contents.empty()) {
+    VLOG(2) << "ReadFile: cannot " << "read file " << file_path;
+  }
+  return contents;
+}
+}  // namespace
+
+using OnRuleMatchedCallback = base::RepeatingCallback<void(
+    const std::optional<MatchedRule>& matched_rule)>;
+
+class PsstRuleRegistryUnitTest : public testing::Test {
+ public:
+  void SetUp() override {
+    base::FilePath test_data_dir =
+        base::PathService::CheckedGet(base::DIR_SRC_TEST_DATA_ROOT);
+    test_data_dir_base_ =
+        test_data_dir.AppendASCII("brave/components/test/data/psst");
+    scoped_feature_list_.InitAndEnableFeature(features::kEnablePsst);
+  }
+
+  using LoadRulesTestCallback = base::MockCallback<base::OnceCallback<
+      void(const std::string& data, const std::vector<PsstRule>& rules)>>;
+  using CheckIfMatchTestCallback = base::MockCallback<
+      base::OnceCallback<void(std::unique_ptr<MatchedRule>)>>;
+
+  base::FilePath GetTestDataDirBase() const { return test_data_dir_base_; }
+  base::FilePath GetScriptsTestDataDir() const {
+    return GetTestDataDirBase().Append(
+        base::FilePath::FromUTF8Unsafe("scripts"));
+  }
+  base::FilePath GetBrokenTestDataDirBase() const {
+    return GetTestDataDirBase().Append(
+        base::FilePath::FromUTF8Unsafe("wrong_psst"));
+  }
+  PsstRuleRegistryImpl& psst_rule_registry() { return registry_; }
+
+ private:
+  PsstRuleRegistryImpl registry_;
+  base::test::TaskEnvironment task_environment_;
+  base::test::ScopedFeatureList scoped_feature_list_;
+  base::FilePath test_data_dir_base_;
+};
+
+TEST_F(PsstRuleRegistryUnitTest, LoadConcreteRule) {
+  {
+    LoadRulesTestCallback mock_callback;
+    base::RunLoop run_loop;
+    EXPECT_CALL(mock_callback, Run)
+        .Times(1)
+        .WillOnce([&](const std::string& data,
+                      const std::vector<PsstRule>& rules) {
+          EXPECT_EQ(rules.size(), kTestPsstRulesCount);
+          EXPECT_EQ(data,
+                    ReadFile(GetTestDataDirBase().Append(
+                        base::FilePath::FromUTF8Unsafe(kPsstJsonFileName))));
+          run_loop.Quit();
+        });
+
+    psst_rule_registry().LoadRules(GetTestDataDirBase(), mock_callback.Get());
+    run_loop.Run();
+  }
+
+  const auto scripts_path =
+      GetScriptsTestDataDir().Append(base::FilePath::FromUTF8Unsafe("a"));
+
+  base::RunLoop run_loop;
+  CheckIfMatchTestCallback mock_callback;
+  EXPECT_CALL(mock_callback, Run)
+      .Times(1)
+      .WillOnce([&](std::unique_ptr<MatchedRule> matched_rule) {
+        ASSERT_TRUE(matched_rule);
+        EXPECT_EQ(matched_rule->name(), "a");
+        EXPECT_EQ(matched_rule->user_script(),
+                  ReadFile(scripts_path.Append(
+                      base::FilePath::FromUTF8Unsafe(kPsstUserScriptName))));
+        EXPECT_EQ(matched_rule->policy_script(),
+                  ReadFile(scripts_path.Append(
+                      base::FilePath::FromUTF8Unsafe(kPsstPolicyScriptName))));
+        run_loop.Quit();
+      });
+
+  psst_rule_registry().CheckIfMatch(GURL("https://a.test"),
+                                    mock_callback.Get());
+  run_loop.Run();
+}
+
+TEST_F(PsstRuleRegistryUnitTest, CheckIfMatchWithNoRulesLoaded) {
+  CheckIfMatchTestCallback mock_callback;
+  EXPECT_CALL(mock_callback, Run).Times(0);
+  psst_rule_registry().CheckIfMatch(GURL("https://a.test"),
+                                    mock_callback.Get());
+}
+
+TEST_F(PsstRuleRegistryUnitTest, RulesLoading) {
+  LoadRulesTestCallback mock_callback;
+  base::RunLoop run_loop;
+  EXPECT_CALL(mock_callback, Run)
+      .Times(1)
+      .WillOnce(
+          [&](const std::string& data, const std::vector<PsstRule>& rules) {
+            EXPECT_EQ(rules.size(), kTestPsstRulesCount);
+            EXPECT_EQ(data,
+                      ReadFile(GetTestDataDirBase().Append(
+                          base::FilePath::FromUTF8Unsafe(kPsstJsonFileName))));
+            run_loop.Quit();
+          });
+
+  psst_rule_registry().LoadRules(GetTestDataDirBase(), mock_callback.Get());
+  run_loop.Run();
+}
+
+TEST_F(PsstRuleRegistryUnitTest, RulesLoadingEmptyPath) {
+  LoadRulesTestCallback mock_callback;
+  base::RunLoop run_loop;
+  EXPECT_CALL(mock_callback, Run)
+      .Times(1)
+      .WillOnce(
+          [&](const std::string& data, const std::vector<PsstRule>& rules) {
+            EXPECT_TRUE(rules.empty());
+            EXPECT_TRUE(data.empty());
+            run_loop.Quit();
+          });
+
+  psst_rule_registry().LoadRules(base::FilePath(FILE_PATH_LITERAL("")),
+                                 mock_callback.Get());
+  run_loop.Run();
+}
+
+TEST_F(PsstRuleRegistryUnitTest, RulesLoadingBrokenRulesFile) {
+  LoadRulesTestCallback mock_callback;
+  base::RunLoop run_loop;
+  EXPECT_CALL(mock_callback, Run)
+      .Times(1)
+      .WillOnce(
+          [&](const std::string& data, const std::vector<PsstRule>& rules) {
+            EXPECT_TRUE(rules.empty());
+            EXPECT_EQ(data,
+                      ReadFile(GetBrokenTestDataDirBase().Append(
+                          base::FilePath::FromUTF8Unsafe(kPsstJsonFileName))));
+            run_loop.Quit();
+          });
+
+  psst_rule_registry().LoadRules(GetBrokenTestDataDirBase(),
+                                 mock_callback.Get());
+  run_loop.Run();
+}
+
+TEST_F(PsstRuleRegistryUnitTest, RulesLoadingNonExistingPath) {
+  const auto non_existing_path =
+      base::FilePath(FILE_PATH_LITERAL("non-existing-path"));
+  LoadRulesTestCallback mock_callback;
+  base::RunLoop run_loop;
+  EXPECT_CALL(mock_callback, Run)
+      .Times(1)
+      .WillOnce(
+          [&](const std::string& data, const std::vector<PsstRule>& rules) {
+            EXPECT_TRUE(rules.empty());
+            EXPECT_TRUE(data.empty());
+            run_loop.Quit();
+          });
+
+  psst_rule_registry().LoadRules(non_existing_path, mock_callback.Get());
+  run_loop.Run();
+}
+
+TEST_F(PsstRuleRegistryUnitTest, RuleReferencesToNotExistedPath) {
+  {
+    LoadRulesTestCallback mock_callback;
+    base::RunLoop run_loop;
+    EXPECT_CALL(mock_callback, Run)
+        .Times(1)
+        .WillOnce([&](const std::string& data,
+                      const std::vector<PsstRule>& rules) {
+          EXPECT_EQ(rules.size(), kTestPsstRulesCount);
+          EXPECT_EQ(data,
+                    ReadFile(GetTestDataDirBase().Append(
+                        base::FilePath::FromUTF8Unsafe(kPsstJsonFileName))));
+          run_loop.Quit();
+        });
+
+    psst_rule_registry().LoadRules(GetTestDataDirBase(), mock_callback.Get());
+    run_loop.Run();
+  }
+
+  base::RunLoop run_loop;
+  CheckIfMatchTestCallback mock_callback;
+  EXPECT_CALL(mock_callback, Run)
+      .Times(1)
+      .WillOnce([&](std::unique_ptr<MatchedRule> matched_rule) {
+        // Rule has not been loaded correctly(wrong scripts path), so it should
+        // not be matched.
+        ASSERT_FALSE(matched_rule);
+        run_loop.Quit();
+      });
+
+  psst_rule_registry().CheckIfMatch(GURL("https://url.test"),
+                                    mock_callback.Get());
+  run_loop.Run();
+}
+
+TEST_F(PsstRuleRegistryUnitTest, DoNotMatchRuleIfNotExists) {
+  {
+    LoadRulesTestCallback mock_callback;
+    base::RunLoop run_loop;
+    EXPECT_CALL(mock_callback, Run)
+        .Times(1)
+        .WillOnce([&](const std::string& data,
+                      const std::vector<PsstRule>& rules) {
+          EXPECT_EQ(rules.size(), kTestPsstRulesCount);
+          EXPECT_EQ(data,
+                    ReadFile(GetTestDataDirBase().Append(
+                        base::FilePath::FromUTF8Unsafe(kPsstJsonFileName))));
+          run_loop.Quit();
+        });
+
+    psst_rule_registry().LoadRules(GetTestDataDirBase(), mock_callback.Get());
+    run_loop.Run();
+  }
+
+  CheckIfMatchTestCallback mock_callback;
+  EXPECT_CALL(mock_callback, Run).Times(0);
+  psst_rule_registry().CheckIfMatch(GURL("https://notexisted.test"),
+                                    mock_callback.Get());
+}
+
+}  // namespace psst
diff --git a/components/psst/browser/core/psst_rule_unittest.cc b/components/psst/browser/core/psst_rule_unittest.cc
new file mode 100644
index 000000000000..c601adfcb47a
--- /dev/null
+++ components/psst/browser/core/psst_rule_unittest.cc
@@ -0,0 +1,196 @@
+// Copyright (c) 2025 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#include "brave/components/psst/browser/core/psst_rule.h"
+
+#include <cstddef>
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace psst {
+
+namespace {
+
+constexpr char kRules[] = R"([
+        {
+            "name": "a",
+            "include": [
+                "https://a.com/*"
+            ],
+            "exclude": [
+                "https://a.com/exclude/*"
+            ],
+            "version": 1,
+            "user_script": "user.js",
+            "policy_script": "policy.js"
+        }
+    ])";
+
+constexpr char kRulesNoExclude[] = R"([
+        {
+            "name": "b",
+            "include": [
+                "https://b.com/*"
+            ],
+            "exclude": [
+            ],
+            "version": 2,
+            "user_script": "user_script.js",
+            "policy_script": "policy_script.js"
+        }
+    ])";
+
+constexpr char kRulesWithSubdomain[] = R"([
+      {
+          "name": "a",
+          "include": [
+              "https://*.a.com/*"
+          ],
+          "exclude": [
+              "https://a.com/exclude/*"
+          ],
+          "version": 1,
+          "user_script": "user.js",
+          "policy_script": "policy.js"
+      }
+  ])";
+
+constexpr char kRulesMultiple[] = R"([
+        {
+            "name": "a",
+            "include": [
+                "https://a.com/*"
+            ],
+            "version": 1,
+            "user_script": "user.js",
+            "policy_script": "policy.js"
+      },
+      {
+          "name": "b",
+          "include": [
+              "https://b.com/*"
+          ],
+          "exclude": [
+                "https://b.com/exclude/*"
+          ],
+          "version": 2,
+          "user_script": "user_script.js",
+          "policy_script": "policy_script.js"
+      }
+  ])";
+
+}  // namespace
+
+class PsstRuleUnitTest : public testing::Test {};
+
+// need test with more than one rule
+
+TEST_F(PsstRuleUnitTest, ParseRulesWithExclude) {
+  const auto psst_rules_with_exclude = PsstRule::ParseRules(kRules);
+  ASSERT_EQ(psst_rules_with_exclude->size(), 1u);
+
+  auto rule = psst_rules_with_exclude->front();
+
+  EXPECT_EQ(rule.name(), "a");
+  EXPECT_EQ(rule.version(), 1);
+  EXPECT_EQ(rule.user_script_path().value(), FILE_PATH_LITERAL("user.js"));
+  EXPECT_EQ(rule.policy_script_path().value(), FILE_PATH_LITERAL("policy.js"));
+  // include rule
+  EXPECT_TRUE(rule.ShouldInsertScript(GURL("https://a.com/page.html")));
+  EXPECT_FALSE(rule.ShouldInsertScript(GURL("http://a.com/page.html")));
+  EXPECT_FALSE(rule.ShouldInsertScript(GURL("https://b.a.com/page.html")));
+  EXPECT_FALSE(rule.ShouldInsertScript(GURL("https://b.com/a.com")));
+
+  // exclude rule
+  EXPECT_FALSE(
+      rule.ShouldInsertScript(GURL("https://a.com/exclude/page.html")));
+  EXPECT_TRUE(
+      rule.ShouldInsertScript(GURL("https://a.com/blah/exclude/page.html")));
+}
+
+TEST_F(PsstRuleUnitTest, ParseRulesNoExclude) {
+  const auto psst_rules_with_exclude = PsstRule::ParseRules(kRulesNoExclude);
+  ASSERT_EQ(psst_rules_with_exclude->size(), 1u);
+
+  auto rule = psst_rules_with_exclude->front();
+  EXPECT_EQ(rule.name(), "b");
+  EXPECT_EQ(rule.version(), 2);
+  EXPECT_EQ(rule.user_script_path().value(),
+            FILE_PATH_LITERAL("user_script.js"));
+  EXPECT_EQ(rule.policy_script_path().value(),
+            FILE_PATH_LITERAL("policy_script.js"));
+
+  EXPECT_TRUE(rule.ShouldInsertScript(GURL("https://b.com/page.html")));
+  EXPECT_TRUE(rule.ShouldInsertScript(GURL("https://b.com/exclude/page.html")));
+}
+
+TEST_F(PsstRuleUnitTest, ParseRulesWithSubdomain) {
+  const auto psst_rules_with_exclude =
+      PsstRule::ParseRules(kRulesWithSubdomain);
+  ASSERT_EQ(psst_rules_with_exclude->size(), 1u);
+
+  auto rule = psst_rules_with_exclude->front();
+  EXPECT_EQ(rule.name(), "a");
+  EXPECT_EQ(rule.version(), 1);
+  EXPECT_EQ(rule.user_script_path().value(), FILE_PATH_LITERAL("user.js"));
+  EXPECT_EQ(rule.policy_script_path().value(), FILE_PATH_LITERAL("policy.js"));
+
+  // include rule
+  EXPECT_TRUE(rule.ShouldInsertScript(GURL("https://a.com/page.html")));
+  EXPECT_TRUE(rule.ShouldInsertScript(GURL("https://b.a.com/page.html")));
+  EXPECT_FALSE(rule.ShouldInsertScript(GURL("https://a.b.com/page.html")));
+
+  // exclude rule
+  EXPECT_TRUE(
+      rule.ShouldInsertScript(GURL("https://b.a.com/exclude/page.html")));
+  EXPECT_FALSE(
+      rule.ShouldInsertScript(GURL("https://a.com/exclude/page.html")));
+  EXPECT_TRUE(
+      rule.ShouldInsertScript(GURL("https://a.com/blah/exclude/page.html")));
+}
+
+TEST_F(PsstRuleUnitTest, ParseRulesMultipleWithExclude) {
+  const auto psst_rules_with_exclude = PsstRule::ParseRules(kRulesMultiple);
+  ASSERT_EQ(psst_rules_with_exclude->size(), 2u);
+
+  auto rule1 = (*psst_rules_with_exclude)[0];
+  auto rule2 = (*psst_rules_with_exclude)[1];
+
+  EXPECT_EQ(rule1.name(), "a");
+  EXPECT_EQ(rule1.version(), 1);
+  EXPECT_EQ(rule1.user_script_path().value(), FILE_PATH_LITERAL("user.js"));
+  EXPECT_EQ(rule1.policy_script_path().value(), FILE_PATH_LITERAL("policy.js"));
+
+  EXPECT_EQ(rule2.name(), "b");
+  EXPECT_EQ(rule2.version(), 2);
+  EXPECT_EQ(rule2.user_script_path().value(),
+            FILE_PATH_LITERAL("user_script.js"));
+  EXPECT_EQ(rule2.policy_script_path().value(),
+            FILE_PATH_LITERAL("policy_script.js"));
+
+  // rule1
+  EXPECT_TRUE(rule1.ShouldInsertScript(GURL("https://a.com/page.html")));
+  EXPECT_TRUE(
+      rule1.ShouldInsertScript(GURL("https://a.com/exclude/page.html")));
+  EXPECT_FALSE(rule1.ShouldInsertScript(GURL("https://b.com/page.html")));
+
+  // rule2
+  EXPECT_TRUE(rule2.ShouldInsertScript(GURL("https://b.com/page.html")));
+  EXPECT_FALSE(
+      rule2.ShouldInsertScript(GURL("https://b.com/exclude/page.html")));
+  EXPECT_FALSE(rule2.ShouldInsertScript(GURL("https://a.com/page.html")));
+}
+
+TEST_F(PsstRuleUnitTest, ParseRulesInvalidContent) {
+  // empty/non-existent file
+  EXPECT_EQ(PsstRule::ParseRules(""), std::nullopt);
+  // dictionary instead of array
+  EXPECT_EQ(PsstRule::ParseRules("{}"), std::nullopt);
+  // not valid json
+  EXPECT_EQ(PsstRule::ParseRules("fdsa"), std::nullopt);
+}
+
+}  // namespace psst
diff --git a/components/psst/browser/core/rule_data_reader.cc b/components/psst/browser/core/rule_data_reader.cc
new file mode 100644
index 000000000000..384726e01d65
--- /dev/null
+++ components/psst/browser/core/rule_data_reader.cc
@@ -0,0 +1,46 @@
+// Copyright (c) 2025 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#include "brave/components/psst/browser/core/rule_data_reader.h"
+
+#include <optional>
+
+#include "base/files/file_util.h"
+
+namespace psst {
+
+namespace {
+
+const base::FilePath::CharType kScriptsDir[] = FILE_PATH_LITERAL("scripts");
+
+std::optional<std::string> ReadFile(const base::FilePath& file_path) {
+  std::string contents;
+  if (bool success = base::ReadFileToString(file_path, &contents);
+      !success || contents.empty()) {
+    return std::nullopt;
+  }
+  return contents;
+}
+
+}  // namespace
+
+RuleDataReader::RuleDataReader(const base::FilePath& component_path)
+    : prefix_(base::FilePath(component_path).Append(kScriptsDir)) {}
+
+std::optional<std::string> RuleDataReader::ReadUserScript(
+    const std::string& rule_name,
+    const base::FilePath& user_script_path) const {
+  return ReadFile(
+      base::FilePath(prefix_).AppendASCII(rule_name).Append(user_script_path));
+}
+
+std::optional<std::string> RuleDataReader::ReadPolicyScript(
+    const std::string& rule_name,
+    const base::FilePath& policy_script_path) const {
+  return ReadFile(base::FilePath(prefix_).AppendASCII(rule_name).Append(
+      policy_script_path));
+}
+
+}  // namespace psst
diff --git a/components/psst/browser/core/rule_data_reader.h b/components/psst/browser/core/rule_data_reader.h
new file mode 100644
index 000000000000..ebd0f738e689
--- /dev/null
+++ components/psst/browser/core/rule_data_reader.h
@@ -0,0 +1,39 @@
+// Copyright (c) 2025 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#ifndef BRAVE_COMPONENTS_PSST_BROWSER_CORE_RULE_DATA_READER_H_
+#define BRAVE_COMPONENTS_PSST_BROWSER_CORE_RULE_DATA_READER_H_
+
+#include <optional>
+#include <string>
+
+#include "base/files/file_path.h"
+
+namespace psst {
+
+// Represents the reader of the rule data files (user.js and policy.js) for a
+// given rule. The data files are stored in the component directory under
+// "scripts/<rule_name>/user.js" and "scripts/<rule_name>/policy.js".
+class RuleDataReader {
+ public:
+  explicit RuleDataReader(const base::FilePath& component_path);
+  RuleDataReader(const RuleDataReader&) = delete;
+  RuleDataReader& operator=(const RuleDataReader&) = delete;
+  ~RuleDataReader() = default;
+
+  std::optional<std::string> ReadUserScript(
+      const std::string& rule_name,
+      const base::FilePath& user_script_path) const;
+  std::optional<std::string> ReadPolicyScript(
+      const std::string& rule_name,
+      const base::FilePath& policy_script_path) const;
+
+ private:
+  base::FilePath prefix_;
+};
+
+}  // namespace psst
+
+#endif  // BRAVE_COMPONENTS_PSST_BROWSER_CORE_RULE_DATA_READER_H_
diff --git a/components/psst/browser/core/rule_data_reader_unittest.cc b/components/psst/browser/core/rule_data_reader_unittest.cc
new file mode 100644
index 000000000000..49f85de3fe14
--- /dev/null
+++ components/psst/browser/core/rule_data_reader_unittest.cc
@@ -0,0 +1,91 @@
+// Copyright (c) 2025 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#include "brave/components/psst/browser/core/rule_data_reader.h"
+
+#include <optional>
+#include <string>
+
+#include "base/base_paths.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/logging.h"
+#include "base/path_service.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace psst {
+
+namespace {
+
+constexpr base::FilePath::StringViewType kUserScriptPath =
+    FILE_PATH_LITERAL("user.js");
+constexpr base::FilePath::StringViewType kPolicyScriptPath =
+    FILE_PATH_LITERAL("policy.js");
+
+constexpr char kExistingRuleName[] = "a";
+constexpr char kNotExistingRuleName[] = "rule_with_wrong_script_path";
+
+std::string ReadFile(const base::FilePath& file_path) {
+  std::string contents;
+  bool success = base::ReadFileToString(file_path, &contents);
+  if (!success || contents.empty()) {
+    VLOG(2) << "ReadFile: cannot " << "read file " << file_path;
+  }
+  return contents;
+}
+
+}  // namespace
+
+class RuleDataReaderUnitTest : public testing::Test {
+ public:
+  void SetUp() override {
+    base::FilePath test_data_dir =
+        base::PathService::CheckedGet(base::DIR_SRC_TEST_DATA_ROOT);
+    test_data_dir_base_ =
+        test_data_dir.AppendASCII("brave/components/test/data/psst");
+  }
+
+  base::FilePath GetBasePath() { return test_data_dir_base_; }
+
+ private:
+  base::FilePath test_data_dir_base_;
+};
+
+TEST_F(RuleDataReaderUnitTest, LoadComponentScripts) {
+  RuleDataReader crr(GetBasePath());
+  auto scripts_path =
+      GetBasePath()
+          .Append(base::FilePath::FromUTF8Unsafe("scripts"))
+          .Append(base::FilePath::FromUTF8Unsafe(kExistingRuleName));
+
+  const auto user_script = base::FilePath(kUserScriptPath);
+  const auto policy_script = base::FilePath(kPolicyScriptPath);
+
+  auto user_script_content = crr.ReadUserScript(kExistingRuleName, user_script);
+  ASSERT_TRUE(user_script_content);
+  ASSERT_FALSE(user_script_content->empty());
+  EXPECT_EQ(*user_script_content, ReadFile(scripts_path.Append(user_script)));
+
+  auto policy_script_content =
+      crr.ReadPolicyScript(kExistingRuleName, policy_script);
+  ASSERT_TRUE(policy_script_content);
+  ASSERT_FALSE(policy_script_content->empty());
+  EXPECT_EQ(*policy_script_content,
+            ReadFile(scripts_path.Append(policy_script)));
+}
+
+TEST_F(RuleDataReaderUnitTest, TryToLoadWrongWithComponentScriptPath) {
+  RuleDataReader crr(GetBasePath());
+
+  auto user_script =
+      crr.ReadUserScript(kNotExistingRuleName, base::FilePath(kUserScriptPath));
+  ASSERT_FALSE(user_script);
+
+  auto policy_script = crr.ReadPolicyScript(kNotExistingRuleName,
+                                            base::FilePath(kPolicyScriptPath));
+  ASSERT_FALSE(policy_script);
+}
+
+}  // namespace psst
diff --git a/components/psst/buildflags/BUILD.gn b/components/psst/buildflags/BUILD.gn
new file mode 100644
index 000000000000..c16033ef9bb3
--- /dev/null
+++ components/psst/buildflags/BUILD.gn
@@ -0,0 +1,12 @@
+# Copyright (c) 2025 The Brave Authors. All rights reserved.
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at https://mozilla.org/MPL/2.0/.
+
+import("//brave/components/psst/buildflags/buildflags.gni")
+import("//build/buildflag_header.gni")
+
+buildflag_header("buildflags") {
+  header = "buildflags.h"
+  flags = [ "ENABLE_PSST=$enable_psst" ]
+}
diff --git a/components/psst/buildflags/buildflags.gni b/components/psst/buildflags/buildflags.gni
new file mode 100644
index 000000000000..fa63f80a8bd5
--- /dev/null
+++ components/psst/buildflags/buildflags.gni
@@ -0,0 +1,8 @@
+# Copyright (c) 2025 The Brave Authors. All rights reserved.
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at https://mozilla.org/MPL/2.0/.
+
+declare_args() {
+  enable_psst = !is_android && !is_ios
+}
diff --git components/psst/common/BUILD.gn components/psst/common/BUILD.gn
index 3ffd536632ee..8be6c0a51585 100644
--- components/psst/common/BUILD.gn
+++ components/psst/common/BUILD.gn
@@ -10,7 +10,13 @@ component("common") {
   sources = [
     "features.cc",
     "features.h",
+    "pref_names.cc",
+    "pref_names.h",
   ]
 
-  deps = [ "//base" ]
+  deps = [
+    "//base",
+    "//components/pref_registry",
+    "//components/prefs",
+  ]
 }
diff --git a/components/psst/common/DEPS b/components/psst/common/DEPS
new file mode 100644
index 000000000000..8f4f41281284
--- /dev/null
+++ components/psst/common/DEPS
@@ -0,0 +1,3 @@
+include_rules = [
+  "+components/prefs",
+]
\ No newline at end of file
diff --git components/psst/common/features.cc components/psst/common/features.cc
index 8ac6cfab0015..e1f9990bb575 100644
--- components/psst/common/features.cc
+++ components/psst/common/features.cc
@@ -7,6 +7,6 @@
 
 namespace psst::features {
 
-BASE_FEATURE(kBravePsst, "BravePsst", base::FEATURE_DISABLED_BY_DEFAULT);
+BASE_FEATURE(kEnablePsst, "EnablePsst", base::FEATURE_DISABLED_BY_DEFAULT);
 
 }  // namespace psst::features
diff --git components/psst/common/features.h components/psst/common/features.h
index 1afcf666cbf5..0c6e90b5d2e7 100644
--- components/psst/common/features.h
+++ components/psst/common/features.h
@@ -11,7 +11,7 @@
 
 namespace psst::features {
 
-COMPONENT_EXPORT(PSST_COMMON) BASE_DECLARE_FEATURE(kBravePsst);
+COMPONENT_EXPORT(PSST_COMMON) BASE_DECLARE_FEATURE(kEnablePsst);
 
 }  // namespace psst::features
 
diff --git a/components/psst/common/pref_names.cc b/components/psst/common/pref_names.cc
new file mode 100644
index 000000000000..4be5a7876155
--- /dev/null
+++ components/psst/common/pref_names.cc
@@ -0,0 +1,23 @@
+// Copyright (c) 2025 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#include "brave/components/psst/common/pref_names.h"
+
+#include "base/feature_list.h"
+#include "base/strings/string_util.h"
+#include "brave/components/psst/common/features.h"
+#include "components/prefs/pref_registry_simple.h"
+// #include "components/prefs/pref_service.h"
+// #include "components/prefs/scoped_user_pref_update.h"
+
+namespace psst {
+
+void RegisterProfilePrefs(PrefRegistrySimple* registry) {
+  if (base::FeatureList::IsEnabled(psst::features::kEnablePsst)) {
+    registry->RegisterBooleanPref(prefs::kPsstEnabled, true);
+  }
+}
+
+}  // namespace psst
diff --git a/components/psst/common/pref_names.h b/components/psst/common/pref_names.h
new file mode 100644
index 000000000000..5a3d339ae027
--- /dev/null
+++ components/psst/common/pref_names.h
@@ -0,0 +1,25 @@
+// Copyright (c) 2025 The Brave Authors. All rights reserved.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#ifndef BRAVE_COMPONENTS_PSST_COMMON_PREF_NAMES_H_
+#define BRAVE_COMPONENTS_PSST_COMMON_PREF_NAMES_H_
+
+#include "base/component_export.h"
+#include "components/prefs/pref_service.h"
+
+class PrefRegistrySimple;
+
+namespace psst {
+
+namespace prefs {
+inline constexpr char kPsstEnabled[] = "brave.psst.settings.enable_psst";
+}  // namespace prefs
+
+COMPONENT_EXPORT(PSST_COMMON)
+void RegisterProfilePrefs(PrefRegistrySimple* registry);
+
+}  // namespace psst
+
+#endif  // BRAVE_COMPONENTS_PSST_COMMON_PREF_NAMES_H_
diff --git a/test/data/psst-component-data/manifest.json b/components/test/data/psst/manifest.json
similarity index 100%
rename from test/data/psst-component-data/manifest.json
rename to components/test/data/psst/manifest.json
diff --git a/components/test/data/psst/psst.json b/components/test/data/psst/psst.json
diff --git a/components/test/data/psst/psst.json b/components/test/data/psst/psst.json
new file mode 100644
index 000000000000..854316be0cb3
--- /dev/null
+++ components/test/data/psst/psst.json
@@ -0,0 +1,35 @@
+[
+    {
+        "name": "a",
+        "include": [
+            "https://a.test/*"
+        ],
+        "exclude": [
+        ],
+        "version": 1,
+        "user_script": "user.js",
+        "policy_script": "policy.js"
+    },
+    {
+        "name": "b",
+        "include": [
+            "https://b.test/*"
+        ],
+        "exclude": [
+        ],
+        "version": 1,
+        "user_script": "user.js",
+        "policy_script": "policy.js"
+    },
+    {
+        "name": "rule_with_wrong_script_path",
+        "include": [
+            "https://url.test/*"
+        ],
+        "exclude": [
+        ],
+        "version": 1,
+        "user_script": "user.js",
+        "policy_script": "policy.js"
+    }
+]
diff --git test/data/psst-component-data/scripts/a/policy.js components/test/data/psst/scripts/a/policy.js
similarity index 89%
rename from test/data/psst-component-data/scripts/a/policy.js
rename to components/test/data/psst/scripts/a/policy.js
index 6fb162121151..e69f8812f180 100644
--- test/data/psst-component-data/scripts/a/policy.js
+++ components/test/data/psst/scripts/a/policy.js
@@ -3,4 +3,4 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-document.title += 'policy';
+document.title += 'a_policy';
diff --git test/data/psst-component-data/scripts/a/test.js components/test/data/psst/scripts/a/user.js
similarity index 56%
rename from test/data/psst-component-data/scripts/a/test.js
rename to components/test/data/psst/scripts/a/user.js
index 7f3779025b26..7e641e331f23 100644
--- test/data/psst-component-data/scripts/a/test.js
+++ components/test/data/psst/scripts/a/user.js
@@ -4,8 +4,17 @@
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
 (() => {
+  const getUserId = () => {
+    return "user1"
+  }
   return new Promise((resolve) => {
-    document.title = 'test';
-    resolve(true)
+    document.title = 'a_user-';
+    resolve({
+      "user": getUserId(),
+      "requests": [
+        {"name": "privacy setting #1"},
+        {"name": "privacy setting #2"},
+      ]
+    })
   })
-})();
+})();
\ No newline at end of file
diff --git test/data/psst-component-data/scripts/b/policy.js components/test/data/psst/scripts/b/policy.js
similarity index 89%
rename from test/data/psst-component-data/scripts/b/policy.js
rename to components/test/data/psst/scripts/b/policy.js
index 6fb162121151..01e3e4cc96dd 100644
--- test/data/psst-component-data/scripts/b/policy.js
+++ components/test/data/psst/scripts/b/policy.js
@@ -3,4 +3,4 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at http,s://mozilla.org/MPL/2.0/.
 
-document.title += 'policy';
+document.title += 'b_policy';
diff --git test/data/psst-component-data/scripts/b/test.js components/test/data/psst/scripts/b/user.js
similarity index 91%
rename from test/data/psst-component-data/scripts/b/test.js
rename to components/test/data/psst/scripts/b/user.js
index 17a64f6b9ed3..a2bb2a3322e5 100644
--- test/data/psst-component-data/scripts/b/test.js
+++ components/test/data/psst/scripts/b/user.js
@@ -5,7 +5,7 @@
 
 (() => {
   return new Promise((resolve) => {
-    document.title = 'test';
+    document.title = 'b_user-';
     resolve(false)
   })
 })();
diff --git a/components/test/data/psst/wrong_psst/psst.json b/components/test/data/psst/wrong_psst/psst.json
new file mode 100644
index 000000000000..d31a6af287b7
--- /dev/null
+++ components/test/data/psst/wrong_psst/psst.json
@@ -0,0 +1 @@
+broken file content
diff --git test/BUILD.gn test/BUILD.gn
index cf7c7472a7a3..247bd2b8b95f 100644
--- test/BUILD.gn
+++ test/BUILD.gn
@@ -355,6 +355,14 @@ test("brave_unit_tests") {
     ]
   }
 
+  if (is_win) {
+    deps += [
+      "//brave/browser/day_zero_browser_ui_expt:unit_tests",
+      "//brave/components/windows_recall",
+      "//brave/components/windows_recall:unit_tests",
+    ]
+  }
+
   if (enable_tor) {
     deps += [
       "//brave/browser/tor/test:unit_tests",
diff --git test/base/DEPS test/base/DEPS
index 97b81009732b..c5872e79d821 100644
--- test/base/DEPS
+++ test/base/DEPS
@@ -1,5 +1,6 @@
 include_rules = [
   "+brave/components",
+  "+content/public/browser",
   "+chrome/install_static",
   "+components/gcm_driver",
   "+components/prefs",
diff --git test/data/psst-component-data/psst.json test/data/psst-component-data/psst.json
deleted file mode 100644
index d024d3c57791..000000000000
--- test/data/psst-component-data/psst.json
+++ /dev/null
@@ -1,12 +0,0 @@
-[
-    {
-        "include": [
-            "https://a.com/*"
-        ],
-        "exclude": [
-        ],
-        "version": 1,
-        "test_script": "a/test.js",
-        "policy_script": "a/policy.js"
-    }
-]


Description

This PR refactors the PSST (Privacy Site Settings Tool) feature to use buildflags instead of runtime feature checks, and moves PSST tab helper functionality to a new web contents observer pattern. The changes improve the architecture by making PSST compilation conditional and modernizing the component structure.

Possible Issues

  • Syntax Error: Line 457 in psst_tab_web_contents_observer_unittest.cc has an invalid trailing comma in the test name causing compilation failure
  • Complex Refactoring: The extensive changes across multiple components increase risk of integration issues
  • Test Coverage Gaps: Some existing browser tests were removed and replaced with unit tests, potentially reducing integration test coverage
  • Dependency Changes: Moving from runtime to compile-time feature detection could affect existing deployments
Changes

Changes

browser/DEPS & browser/sources.gni

  • Added conditional compilation support for PSST using buildflags
  • Made PSST dependencies conditional on enable_psst flag

browser/about_flags.cc

  • Refactored feature flag from kBravePsst to kEnablePsst
  • Made PSST feature entries conditional on buildflag

browser/brave_profile_prefs.cc & browser/brave_tab_helpers.cc

  • Added conditional PSST preference registration
  • Removed automatic PSST tab helper attachment

browser/ui/tabs/

  • Integrated PSST web contents observer into tab features
  • Added conditional compilation guards

components/psst/browser/content/

  • Major Refactor: Replaced PsstTabHelper with PsstTabWebContentsObserver
  • Added PsstScriptsHandlerImpl for script injection abstraction
  • Renamed "test script" to "user script" throughout codebase
  • Added comprehensive unit tests

components/psst/browser/core/

  • Split rule registry into interface and implementation
  • Added MatchedRule class to encapsulate loaded script data
  • Added RuleDataReader for file system operations
  • Improved error handling and async operations

components/psst/buildflags/

  • New: Added buildflag system to conditionally compile PSST (enable_psst = !is_android && !is_ios)

components/psst/common/

  • Added preference management system
  • Renamed feature flag from kBravePsst to kEnablePsst

Test Data & Browser Tests

  • Moved test data from test/data/psst-component-data/ to components/test/data/psst/
  • Updated test scripts to return objects instead of booleans
  • Replaced browser test with more focused unit tests
sequenceDiagram
    participant BF as BraveTabFeatures
    participant WCO as PsstTabWebContentsObserver
    participant RR as PsstRuleRegistry
    participant SH as PsstScriptsHandlerImpl
    participant WC as WebContents

    Note over BF: Tab initialization
    BF->>WCO: MaybeCreateForWebContents()
    WCO->>WCO: Check feature flag & prefs
    
    Note over WC: Navigation occurs
    WC->>WCO: DidFinishNavigation()
    WCO->>WCO: Mark for processing if valid
    
    WC->>WCO: DocumentOnLoadCompletedInPrimaryMainFrame()
    WCO->>RR: CheckIfMatch(url)
    RR->>RR: Find matching rule
    RR->>WCO: Return MatchedRule
    
    WCO->>SH: InsertScriptInPage(user_script)
    SH->>WC: ExecuteJavaScriptInIsolatedWorld()
    WC->>WCO: OnUserScriptResult(result)
    
    alt If user script returns dict object
        WCO->>SH: InsertScriptInPage(policy_script)
        SH->>WC: ExecuteJavaScriptInIsolatedWorld()
    end

github-actions[bot] avatar Jul 19 '25 16:07 github-actions[bot]

@vadimstruts Thanks for your work on this PR and everyone else involved.

I would just like to remind the importance of keeping PRs small enough so that review cycles are brief, and authors and reviewers don't have to go through many rounds of rework of different parts of the code that get affected by each review request, including retreading the same paths when iterating over conflicting review requests.

For reference going forward: https://github.com/brave/handbook/blob/master/development/small-prs.md

Thanks again for the thorough work on this PR.

cdesouza-chromium avatar Jul 21 '25 18:07 cdesouza-chromium

Released in v1.82.113

brave-builds avatar Jul 24 '25 00:07 brave-builds