[
  {
    "path": ".github/workflows/node.js.yml",
    "content": "# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node\n# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions\n\nname: Tests\n\non:\n  push:\n    branches: [ \"main\" ]\n  pull_request:\n    branches: [ \"main\" ]\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [16.x, 18.x]\n        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/\n\n    steps:\n    - uses: actions/checkout@v3\n    - name: Use Node.js ${{ matrix.node-version }}\n      uses: actions/setup-node@v3\n      with:\n        node-version: ${{ matrix.node-version }}\n        cache: 'npm'\n    - run: npm ci\n    - run: npm test\n"
  },
  {
    "path": ".gitignore",
    "content": "# Local user files\n.DS_Store\n.idea\n.vscode\n\n# Node.js\nnode_modules\n\n# Distribution files\ndist\ndev\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "FUNDING.yml",
    "content": "github: privacy-tech-lab\n"
  },
  {
    "path": "LICENSE.md",
    "content": "MIT License\n\nCopyright (c) 2021 privacy-tech-lab\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <a href=\"https://github.com/privacy-tech-lab/gpc-optmeowt/releases\"><img alt=\"GitHub release (latest by date)\" src=\"https://img.shields.io/github/v/release/privacy-tech-lab/gpc-optmeowt\"></a>\n  <a href=\"https://github.com/privacy-tech-lab/gpc-optmeowt/releases\"><img alt=\"GitHub Release Date\" src=\"https://img.shields.io/github/release-date/privacy-tech-lab/gpc-optmeowt\"></a>\n  <a href=\"https://github.com/privacy-tech-lab/gpc-optmeowt/commits/main\"><img alt=\"GitHub last commit\" src=\"https://img.shields.io/github/last-commit/privacy-tech-lab/gpc-optmeowt\"></a>\n  <a href=\"https://github.com/privacy-tech-lab/gpc-optmeowt/actions/workflows/node.js.yml\"><img alt=\"GitHub Actions\" src=\"https://github.com/privacy-tech-lab/gpc-optmeowt/actions/workflows/node.js.yml/badge.svg?branch=main\"></a>\n  <a href=\"https://github.com/privacy-tech-lab/gpc-optmeowt/issues\"><img alt=\"GitHub issues\" src=\"https://img.shields.io/github/issues-raw/privacy-tech-lab/gpc-optmeowt\"></a>\n  <a href=\"https://github.com/privacy-tech-lab/gpc-optmeowt/issues?q=is%3Aissue+is%3Aclosed\"><img alt=\"GitHub closed issues\" src=\"https://img.shields.io/github/issues-closed-raw/privacy-tech-lab/gpc-optmeowt\"></a>\n  <a href=\"https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\"><img alt=\"GitHub\" src=\"https://img.shields.io/github/license/privacy-tech-lab/gpc-optmeowt\"></a>\n  <a href=\"https://github.com/privacy-tech-lab/gpc-optmeowt/watchers\"><img alt=\"GitHub watchers\" src=\"https://img.shields.io/github/watchers/privacy-tech-lab/gpc-optmeowt?style=social\"></a>\n  <a href=\"https://github.com/privacy-tech-lab/gpc-optmeowt/stargazers\"><img alt=\"GitHub Repo stars\" src=\"https://img.shields.io/github/stars/privacy-tech-lab/gpc-optmeowt?style=social\"></a>\n  <a href=\"https://github.com/privacy-tech-lab/gpc-optmeowt/network/members\"><img alt=\"GitHub forks\" src=\"https://img.shields.io/github/forks/privacy-tech-lab/gpc-optmeowt?style=social\"></a>\n  <a href=\"https://github.com/sponsors/privacy-tech-lab\"><img alt=\"GitHub sponsors\" src=\"https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86\"></a>\n</p>\n  \n<br>\n\n<p align=\"center\">\n  <a href=\"https://privacytechlab.org/\"><img src=\"https://github.com/privacy-tech-lab/gpc-optmeowt/blob/issue-19/src/assets/cat-w-text/optmeow-logo-circle.png\" width=\"150px\" height=\"150px\" alt=\"OptMeowt logo\"></a>\n</p>\n\n# OptMeowt 🐾\n\nOptMeowt (\"Opt Me Out\") is a browser extension for opting you out from web tracking. OptMeowt works by sending Global Privacy Control (GPC) signals to visited websites per the [GPC spec](https://privacycg.github.io/gpc-spec/) that we are developing [at the W3C](https://github.com/privacycg/gpc-spec). In addition, OptMeowt also opts you out from Google's Topics API.\n\n<p align=\"center\">\n  <a href=\"https://addons.mozilla.org/en-US/firefox/addon/optmeowt/\"><img src=\"https://github.com/privacy-tech-lab/optmeowt/blob/main/firefox-add-ons-badge.png\" width=\"172px\" alt=\"Firefox Add Ons badge\"></a>\n  <a href=\"https://chrome.google.com/webstore/detail/optmeowt/hdbnkdbhglahihjdbodmfefogcjbpgbo\"><img src=\"https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/chrome-web-store-badge.png\" width=\"200px\" alt=\"Chrome Web Store badge\"></a>\n<p>\n\nOptMeowt is developed and maintained by Ambrose Vannier (@avan36), Ruby Friedman (@RubyFri), Matthew Rich (@mcrich921), Austin Bosch (@Spongebosch) and Sebastian Zimmeck (@SebastianZimmeck) of the [privacy-tech-lab](https://privacytechlab.org/).\n\nFormer contributors are Francisca Wijaya (@franciscawijaya), Sage Altman (@sagealtman), Matt May (@Mattm27), Ebuka Akubilo (@eakubilo), Samir Cerrato (@samir-cerrato), Nate Levinson (@natelevinson10), Oliver Wang (@OliverWang13), Sophie Eng (@sophieeng), Kate Hausladen (@katehausladen), Jocelyn Wang (@Jocelyn0830), Kuba Alicki (@kalicki1), Stanley Markman (@stanleymarkman), Kiryl Beliauski (@kbeliauski), Daniel Knopf (@dknopf) and Abdallah Salia (@asalia-1).\n\n[1. Research Publications](#1-research-publications)  \n[2. Promo Video](#2-promo-video)  \n[3. How Does OptMeowt Work?](#3-how-does-optmeowt-work)  \n[4. Installing OptMeowt from Source](#4-installing-optmeowt-from-source)  \n[5. Installing OptMeowt for Developers](#5-installing-optmeowt-for-developers)  \n[6. Installing the OptMeowt PETS 2023 Version](#6-installing-the-optmeowt-pets-2023-version)  \n[7. Testing](#7-testing)  \n[8. OptMeowt's Permission Use](#8-optmeowts-permission-use)  \n[9. OptMeowt's Architecture](#9-optmeowts-architecture)  \n[10. Directories in this Repo](#10-directories-in-this-repo)  \n[11. Third Party Libraries](#11-third-party-libraries)  \n[12. Developer Guide](#12-developer-guide)  \n[13. Thank You!](#13-thank-you)\n\n## 1. Research Publications\n\n- Sebastian Zimmeck, [Remarks on the Relevance of Privacy Expectations for Default Opt-out Settings](https://sebastianzimmeck.de/zimmeckEtAlRemarks2026.pdf), IEEE Symposium on Privacy Expectations (ISoPE), New York, New York, 2026, [BibTeX](https://sebastianzimmeck.de/citations.html#zimmeckEtAlGPCRemarks2026Bibtex).\n- Sebastian Zimmeck, Nishant Aggarwal, Zachary Liu, Sage Altman and Konrad Kollnig, [Exercising the CCPA Opt-out Right on Android: Legally Mandated but Practically Challenging](https://sebastianzimmeck.de/zimmeckEtAlGPCAndroid2026.pdf), 26th Privacy Enhancing Technologies Symposium (PETS), Calgary, Canada, July 2026, [BibTeX](https://sebastianzimmeck.de/citations.html#zimmeckEtAlGPCAndroid2026Bibtex)\n- Katherine Hausladen, Oliver Wang, Sophie Eng, Jocelyn Wang, Francisca Wijaya, Matt May and Sebastian Zimmeck, [Websites' Global Privacy Control Compliance at Scale and over Time](https://sebastianzimmeck.de/hausladenEtAlGPCWeb2025.pdf), 34th USENIX Security Symposium (USENIX Security), Seattle, CA, August 2025, [BibTeX](https://sebastianzimmeck.de/citations.html#hausladenEtAlGPCWeb2025Bibtex)\n- Francisca Wijaya, Katherine Hausladen, Matt May, Oliver Wang, Sophie Eng and Sebastian Zimmeck, [Crawl for GPC: An Investigation of CCPA Compliance on the Internet](https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/research/wijayaEtAlCrawlForGPC2024Poster.pdf), Summer Research 2024 Poster Session, Wesleyan University, July 2024\n- Sebastian Zimmeck, Nishant Aggarwal, Zachary Liu and Konrad Kollnig, [From Ad Identifiers to Global Privacy Control: The Status Quo and Future of Opting Out of Ad Tracking on Android](https://arxiv.org/abs/2407.14938), Under Review\n- Sebastian Zimmeck, Eliza Kuller, Chunyue Ma, Bella Tassone and Joe Champeau, [Generalizable Active Privacy Choice: Designing a Graphical User Interface for Global Privacy Control](https://sebastianzimmeck.de/zimmeckEtAlGPC2024.pdf), 24th Privacy Enhancing Technologies Symposium (PETS), Bristol, UK and Online Event, July 2024, [BibTeX](https://sebastianzimmeck.de/citations.html#zimmeckEtAlGPC2024Bibtex)\n- Katherine Hausladen, [Investigating the Current State of CCPA Compliance on the Internet](https://doi.org/10.14418/wes01.2.451), Master's Thesis, Wesleyan University, May 2024\n- Nishant Aggarwal, Wesley Tan, Konrad Kollnig and Sebastian Zimmeck, [The Invisible Threat: Exploring Mobile Privacy Concerns](https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/research/aggarwalEtAlInvisibleThreat2023Poster.pdf), Summer Research 2023 Poster Session, Wesleyan University, July 2023\n- Eliza Kuller, [Privacy Choice Mechanisms and Online Advertising: Can Generalizable Active Privacy Choices and Online Advertising Coexist?](https://doi.org/10.14418/wes01.1.2797), Undergraduate Honors Thesis, Wesleyan University, April 2023\n- Sebastian Zimmeck, Oliver Wang, Kuba Alicki, Jocelyn Wang and Sophie Eng, [Usability and Enforceability of Global Privacy Control](https://sebastianzimmeck.de/zimmeckEtAlGPC2023.pdf), 23rd Privacy Enhancing Technologies Symposium (PETS)\n  Lausanne, Switzerland and Online Event, July 2023, [BibTeX](https://sebastianzimmeck.de/citations.html#zimmeckEtAlGPC2023Bibtex). For installing the OptMeowt version used in this paper, see the [instructions below](https://github.com/privacy-tech-lab/gpc-optmeowt#6-installing-the-optmeowt-pets-2023-version).\n- Isabella Tassone, Chunyue Ma, Eliza Kuller, Joe Champeau and Sebastian Zimmeck, [Enhancing Online Privacy: The Development of Practical Privacy Choice Mechanisms](https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/research/tassoneEtAlEnhancingOnlinePrivacy2022Poster.pdf), Summer Research 2022 Poster Session, Wesleyan University, July 2022\n- Sebastian Zimmeck, [Improving Internet Privacy with Global Privacy Control (GPC)](https://sebastianzimmeck.de/SaTC_PI_Meeting_2022_Poster_GPC_Zimmeck.pdf), 5th NSF Secure and Trustworthy Cyberspace Principal Investigator Meeting (2022 SaTC PI Meeting), Arlington, Virginia, USA, June 2022\n- Kuba Alicki, [Don't Sell Our Data: Exploring CCPA Compliance via Automated Privacy Signal Detection](https://digitalcollections.wesleyan.edu/islandora/dont-sell-our-data-exploring-ccpa-compliance-automated-privacy-signal-detection), Undergraduate Honors Thesis, Wesleyan University, April 2022\n- Eliza Kuller, Chunyue Ma, Isabella Tassone and Sebastian Zimmeck, [Making Online Privacy Choice Mechanisms Effective and Usable](http://summer21.research.wesleyan.edu/2021/07/22/balancing-usability-and-active-choice-while-developing-privacy-permission-schemes/), Summer Research 2021 Poster Session, Wesleyan University, Online, July 2021\n- Sebastian Zimmeck and Kuba Alicki, [Standardizing and Implementing Do Not Sell (Short Paper)](https://sebastianzimmeck.de/zimmeckAndAlicki2020DoNotSell.pdf), 19th Workshop on Privacy in the Electronic Society (WPES), Online Event, November 2020, [BibTeX](https://sebastianzimmeck.de/citations.html#zimmeckAndAlicki2020DoNotSellBibtex)\n\n## 2. Promo Video\n\n[![Watch the Video](https://privacytechlab.org/static/images/OptMeowt_Movie.png)](https://drive.google.com/file/d/1eto77EV13WazpJN1hGXiKKsP2l7oMEu1/view?usp=share_link)\n\n## 3. How Does OptMeowt Work?\n\nOptMeowt sends GPC signals to websites when you browse the web. Such signals must be respected for California consumers per the California Consumer Privacy Act (CCPA), [Regs Section 999.315(d)](https://oag.ca.gov/sites/all/files/agweb/pdfs/privacy/oal-sub-final-text-of-regs.pdf). The number of jurisdictions that require websites to respect GPC signals is increasing. Some websites also respect them even if they are not required to do so.\n\nIn detail, OptMeowt uses the following methods to opt you out:\n\n1. The [GPC header and JS property](https://privacycg.github.io/gpc-spec/).\n2. A `Permissions-Policy` header that opts sites out of Google's [Topics API](https://developer.mozilla.org/en-US/docs/Web/API/Topics_API) on Chromium-based browsers.\n\n**Opting Out of the Topics API:** As all browser vendors are phasing out the use of third-party cookies. In this context Google introduced the [Topics API](https://developer.mozilla.org/en-US/docs/Web/API/Topics_API). The Topics API identifies users' general areas of interest which are then used for personalized advertising. These topics are generated through observing and recording a users' browsing activity. Websites will then receive access to these topics that are stored on users' browsers. To opt you out of the Topics API OptMeowt sends a `Permissions-Policy` header to all the sites you visit. This approach follows [Google's documentation](https://developer.chrome.com/en/docs/privacy-sandbox/topics/#site-opt-out) on how to opt a site out of the Topics API. Note that this functionality of OptMeowt is only available for Chromium browsers as other browsers do not implement the Topics API.\n\n**Customizing which sites receive GPC signals:** For every site you visit OptMeowt will automatically add its domain to the `domain list`. Each newly added domain will receive GPC signals by default. However, you can exclude domains that should not receive GPC signals. This functionality is available on OptMeowt's popup window and settings page.\n\nFor a more in-depth look at how OptMeowt works, check out our [Beginners Guide to OptMeowt](https://docs.google.com/document/d/1H0sA6hK0Q0OLT4Tz_Yp-byHi0U4Ue5DO8k7K-NXnY2Q/edit?usp=sharing). (The document is up to date as of its date. Later changes to OptMeowt are not reflected.)\n\n## 4. Installing OptMeowt from Source\n\nHere are the instructions for installing OptMeowt from the source files in this repo.\n\n### Chrome and Firefox\n\n1. Clone this repo locally with:\n\n   ```bash\n   git clone https://github.com/privacy-tech-lab/gpc-optmeowt.git\n   ```\n\n   You can also download a zipped copy and unzip it.\n\n2. Install [Node.js and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).\n3. From within your local `/gpc-optmeowt/` directory install OptMeowt's dependencies with:\n\n   ```bash\n   npm ci\n   ```\n\n4. Build the project by running:\n\n   ```bash\n   npm run build\n   ```\n\n   This command will create a built for both Chrome and Firefox in `.../gpc-optmeowt/dist/chrome/` and `.../gpc-optmeowt/dist/firefox/`, respectively. `npm run build` will also create packaged versions of OptMeowt in `.../gpc-optmeowt/packages` for distribution on the Chrome Web Store and on Firefox Add-Ons.\n\n### Chrome\n\n5. In Chrome, navigate to the extensions page at `chrome://extensions/`.\n6. Enable `Developer mode` with the slider on the top right corner of the extension page.\n7. Click the `Load unpacked` button in the top left of the page.\n8. Select the directory where you built OptMeowt, by default `/gpc-optmeowt/dist/chrome/` (the directory that contains the `manifest.json`).\n\n### Firefox\n\n5. In Firefox, navigate to the addons page with developer privileges at `about:debugging#/runtime/this-firefox`.\n6. Under `Temporary extensions`, click `Load Temporary Add-on...`.\n7. Select the manifest from the directory where you built OptMeowt, by default `/gpc-optmeowt/dist/firefox/manifest.json/`.\n\n**Note**: OptMeowt is in active development and new features are being added, some of which may cause errors. You can always get the stable release version on the [Chrome Web Store](https://chrome.google.com/webstore/detail/optmeowt/hdbnkdbhglahihjdbodmfefogcjbpgbo) and on [Firefox Add-Ons](https://addons.mozilla.org/en-US/firefox/addon/optmeowt/). You can also disable sending GPC signals to a site in case OptMeowt causes it to break.\n\n## 5. Installing OptMeowt for Developers\n\nTo build the development versions of OptMeowt follow the directions above but replace `npm run build` with:\n\n```bash\nnpm run start\n```\n\nThis command will run the npm script (referenced in `package.json`) that will call Webpack in development mode (Webpack settings are in `webpack.config.js`). `npm run start` will also initiate Webpack servers for both the Firefox and Chrome versions, which will listen for changes as you work and rebuild as necessary.\n\n### 5.1 Webpack\n\nWebpack will build the development versions of OptMeowt into the `dev` subdirectory instead of the `dist` subdirectory. The subdirectories for Chrome and Firefox are `dev/chrome` and `dev/firefox`, respectively.\n\nAlso, when you build for development, the development manifest (in `src/manifest-dev.json`) will be used instead of the distribution manifest (in `src/manifest-dist.json`). The development manifest contains an unsafe eval that we use for our source maps during development. The distribution manifest does not contain this eval. Webpack will select the correct manifest depending on whether you build for development or distribution.\n\nTo include new dependencies you can run:\n\n```bash\nnpm install\n```\n\nRunning this command instead of `npm ci` will include new dependencies in the `package-lock.json`, which is generated from the `package.json`.\n\n### 5.2 Debugging\n\nWe like to use the [Debugger for Firefox](https://marketplace.visualstudio.com/items?itemName=firefox-devtools.vscode-firefox-debug) from within [Visual Studio Code](https://code.visualstudio.com/) when in development to help automating the development and build processes. The default behavior is `F5` to launch and load the extension in the browser. There is a similar extension that you can use for Chrome, [JavaScript Debugger](https://marketplace.visualstudio.com/items?itemName=ms-vscode.js-debug), which is already included in Visual Studio Code by default. Make sure to follow the online documentation on writing the correct `.vscode/launch.json` file, or other necessary settings files, in order to properly load OptMeowt with the debugger.\n\n### 5.3 Developing on Windows\n\nWe have built most of our codebase in macOS, so path variables and similar code may cause the build to break in other OSs, in particular Windows. We recommend using macOS or installing a Linux OS if you will be working with the codebase in any significant manner.\n\n## 6. Installing the OptMeowt PETS 2023 Version\n\nThe version of OptMeowt used in our 2023 PETS paper, [Usability and Enforceability of Global Privacy Control](https://sebastianzimmeck.de/zimmeckEtAlGPC2023.pdf), can be found in our [v3.0.0-paper release](https://github.com/privacy-tech-lab/gpc-optmeowt/releases/tag/v3.0.0-paper). To view the v3.0.0-paper code, you can [look at the repo here](https://github.com/privacy-tech-lab/gpc-optmeowt/tree/v3.0.0-paper). Instructions for building the extension locally are the same as stated above per our [Firefox instructions](https://github.com/privacy-tech-lab/gpc-optmeowt/tree/main#firefox). To activate Analysis mode in the v3.0.0-paper release press the `Protection Mode` label in the popup. In addition, Analysis mode requires other privacy extensions or browsers to be disabled. For further detailed information on how to use analysis mode, please refer to [our methodology](https://github.com/privacy-tech-lab/gpc-optmeowt/tree/v4.0.1/#4-analysis-mode-firefox-only).\n\nAnalysis mode used to be part of the OptMeowt extension but is now part of the [GPC Web Crawler](https://github.com/privacy-tech-lab/gpc-web-crawler), which you can use to analyze websites' GPC compliance at scale.\n\n## 7. Testing\n\nOptMeowt uses the [Mocha](https://mochajs.org/) framework as well as [Puppeteer](https://pptr.dev/) to execute its testing and continuous integration. The continuous integration is built into the OptMeowt repo with Github Actions. The [Actions tab](https://github.com/privacy-tech-lab/gpc-optmeowt/actions) shows all workflows and past unit test checks for previous PRs.\n\nThe test responsible for checking OptMeowt's ability to set the GPC signal can not be run with GitHub Actions. You can run it locally with:\n\n```bash\nnpm test\n```\n\nUsing Puppeteer this command will launch an automated headful browser on Chromium testing the Chrome GPC signal against the [GPC reference server](https://global-privacy-control.vercel.app/).\n\n### 7.1 Running Automated Unit Tests\n\n**Locally:**\nYou can run unit tests locally.\n\n1. Clone this repo locally or download a zipped copy and unzip it.\n2. Make sure npm is up to date by running `npm -v` to check the version and updating follow [the instructions on the npm site](https://docs.npmjs.com/try-the-latest-stable-version-of-npm), depending on your operating system.\n3. Run tests with:\n\n   ```bash\n   npm test\n   ```\n\n4. If Puppeteer is not installed, run:\n\n   ```bash\n   npm install\n   ```\n\n**Continuous Integration:**\nThe continuous integration is built into the OptMeowt repo. Therefore, no changes to the extension environment are needed to run new tests.\n\n### 7.2 Manual UI testing\n\nThe following procedure is for testing the OptMeowt extension UI, which cannot be automated. They are recommended to be performed manually as follows:\n\n1. Download the version of the extension you want to test through `npm run start`. Then, download the unpacked dev version for your browser.\n2. Navigate to a site with the well-known file, like <https://global-privacy-control.vercel.app/>\n3. Click on the OptMeowt symbol in the top right of your browser.\n   - [ ] TEST 1: The symbol for the cat should be solid green.\n   - [ ] TEST 2: The URL of the website should be written under the \"Protection Mode\" banner.\n   - [ ] TEST 3: Global Privacy Control should be enabled.\n   - [ ] TEST 4: There should be a blue number detailing the number of domains receiving signals.\n4. Click on the drop down for \"3rd Party Domains\".\n   - [ ] TEST 5: There should be sites that show up with Global Privacy Control switched on.\n5. Navigate out of the \"3rd Party Domains\" drop down and click on the \"Website Response\" drop down\n   - [ ] TEST 6: There should be text showing that GPC Signals were accepted.\n   - [ ] TEST 7: Switch \"Dark Mode\" on and off and ensure the popup is correctly changing colors.\n6. Navigate to the top of the popup and click on the \"More\" symbol (image: Sliders) to go to the Settings page.\n7. In the main settings page, click on \"Disable\" and open the popup.\n   - [ ] TEST 8: The popup should be fully grayed out and showing the popup disabled.\n8. In the website, move to the Domainlist page.\n   - [ ] TEST 9: There should be multiple domains showing in the Domainlist tab.\n9. Go back to the main settings page and export Domainlist.\n   - [ ] TEST 10: Check the exported Domainlist and the Domainlist in the settings page to make sure the websites match up.\n\n### 7.3 Creating a New Test\n\n1. Navigate to `.../gpc-optmeowt/test/`. Then navigate to the folder in the test directory that corresponds to the tested function's location in the extension source code.\n2. Create a new file in the matching folder. Name the file with the format `FUNCTION_NAME.test.js`.\n   For example, if testing a function named `sum` located in the folder `.../src/math`, create the test called `sum.test.js` in the folder `.../test/math`\n3. Write test using [ECMAScript formatting](https://nodejs.org/api/esm.html).\n\n## 8. OptMeowt's Permission Use\n\n**Note**: We do not collect any data from you. Third parties will also not receive your data. The permissions OptMeowt is using are required for opting you out. To that end, OptMeowt uses the following permissions:\n\n```json\n\"permissions\": [\n    \"declarativeNetRequest\",\n    \"webRequest\",\n    \"webRequestBlocking\",\n    \"webNavigation\",\n    \"<all_urls>\",\n    \"storage\",\n    \"activeTab\",\n    \"tabs\",\n    \"scripting\"\n  ]\n```\n\n- `declarativeNetRequest`: Allows OptMeowt to modify rules, allowing us to send the GPC header\n- `webRequest`: Pauses outgoing HTTP requests to append opt out headers\n- `webRequestBlocking`: Allows an extension to intercept and potentially block, modify, or redirect web requests before they are completed\n- `webNavigation`: Similar to `webRequest`, allows OptMeowt to check when navigation requests are made to reset processes\n- `<all_urls>`: Gives OptMeowt permission to access and interact with the content and data of any website visited by the browser\n- `storage`: Allows OptMeowt to save your opt out preferences in your browser\n- `activeTab`: Allows OptMeowt to set opt out signals on your active browser tab\n- `tabs`: Allows OptMeowt to keep track of HTTP headers per tab to show you the opt out status of the current site in a popup\n- `scripting`: Allows OptMeowt to declare content scripts and send the GPC DOM signal\n\n## 9. OptMeowt's Architecture\n\nDetailed information on OptMeowt's architecture is available in a [separate readme](https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/README_ARCHITECTURE.md).\n\n**Note**: The architecture readme is only current as of its commit date.\n\n## 10. Directories in this Repo\n\nHere are the main directories in this repo:\n\n- `src/`: Main contents of the OptMeowt browser extension.\n- `src/assets`: Graphical elements of the extension, including logos and button images.\n- `src/background`: Listeners for events and logic for sending privacy signals.\n- `src/data`: Definitions of headers and privacy flags.\n- `src/options`: UI elements and scripts for the supplemental options page.\n- `src/popup`: UI elements and scripts for the popup inside the extensions bar.\n- `src/theme`: Dark and light mode themes.\n- `ui-mockup`: Contains PDF and XD files demonstrating the preliminary mockup of OptMeowt.\n\n## 11. Third Party Libraries\n\nOptMeowt uses various [third party libraries](https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/package.json). We thank the developers.\n\n## 12. Developer Guide\n\nIf you have questions about OptMeowt's functionality or have found a bug, please check out our [FAQ \\ Known quirks](https://github.com/privacy-tech-lab/gpc-optmeowt/wiki/FAQ-%5C-Known-quirks) page on the [Wiki](https://github.com/privacy-tech-lab/gpc-optmeowt/wiki). If you cannot find what you are looking for, feel free to open an issue, and we will address it.\n\n**Note**: When viewing your browser's console on a site, a 404 error status code regarding the domain's GPC status file (`/.well-known/gpc.json`) may be shown. This behavior is normal and will occur (1) on domains that do not support GPC and (2) on domains that support GPC but do not host a `/.well-known/gpc.json` file.\n\n## 13. Thank You!\n\n<p align=\"center\"><strong>We would like to thank our supporters!</strong></p><br>\n\n<p align=\"center\">Major financial support provided by the National Science Foundation.</p>\n\n<p align=\"center\">\n  <a href=\"https://nsf.gov/awardsearch/showAward?AWD_ID=2055196\">\n    <img class=\"img-fluid\" src=\"./nsf.png\" height=\"100px\" alt=\"National Science Foundation Logo\">\n  </a>\n</p>\n\n<p align=\"center\">Additional financial support provided by the Alfred P. Sloan Foundation, Wesleyan University, and the Anil Fernando Endowment.</p>\n\n<p align=\"center\">\n  <a href=\"https://sloan.org/grant-detail/9631\">\n    <img class=\"img-fluid\" src=\"./sloan_logo.jpg\" height=\"70px\" alt=\"Sloan Foundation Logo\">\n  </a>\n  <a href=\"https://www.wesleyan.edu/mathcs/cs/index.html\">\n    <img class=\"img-fluid\" src=\"./wesleyan_shield.png\" height=\"70px\" alt=\"Wesleyan University Logo\">\n  </a>\n</p>\n\n<p align=\"center\">Conclusions reached or positions taken are our own and not necessarily those of our financial supporters, its trustees, officers, or staff.</p>\n\n##\n\n<p align=\"center\">\n  <a href=\"https://privacytechlab.org/\"><img src=\"./plt_logo.png\" width=\"200px\" height=\"200px\" alt=\"privacy-tech-lab logo\"></a>\n<p>\n"
  },
  {
    "path": "README_ARCHITECTURE.md",
    "content": "# Architecture Overview\n\n```txt\nsrc\n├── assets       # Static images & files\n├── background      # Manages the background script processes\n│   ├── protection\n│   │   ├── background.js\n│   │   ├── listeners-chrome.js\n│   │   ├── listeners-firefox.js\n│   │   ├── protection-ff.js\n│   │   └── protection.js\n│   ├── control.js\n│   └── storage.js\n├── common       # Manages header sending and rules\n│   ├── editDomainlist.js\n│   └── editRules.js\n├── content-scripts     # Runs processes on site on adds DOM signal\n│   ├── injection\n│   │   └── gpc-dom.js\n│   ├── registration\n│   │   └── gpc-dom.js\n│   └── contentScript.js\n├── data       # Stores constant data (DNS signals, settings, etc.)\n│   ├── defaultSettings.js\n│   ├── headers.js\n│   └── regex.js\n├── manifests      # Stores manifests\n│   ├── chrome\n│   │   ├── manifest-dev.json\n│   │   └── manifest-dist.json\n│   ├── firefox\n│   │   ├── manifest-dev.json\n│   │   └── manifest-dist.json\n├── options       # Options page frontend\n│   ├── components\n│   │   ├── scaffold-component.html\n│   │   └── util.js\n│   ├── views\n│   │   ├── about-view\n│   │   │   ├── about-view.html\n│   │   │   └── about-view.js\n│   │   ├── domainlist-view\n│   │   │   ├── domainlist-view.html\n│   │   │   └── domainlist-view.js\n│   │   ├── main-view\n│   │   │   ├── main-view.html\n│   │   │   └── main-view.js\n│   │   └── settings-view\n│   │       ├── settings-view.html\n│   │       └── settings-view.js\n│   ├── dark-mode.css\n│   ├── options.html\n│   ├── options.js\n│   └── styles.css\n├── popup       # Popup page frontend\n│   ├── popup.html\n│   ├── popup.js\n│   └── styles.css\n├── rules       # Manages universal rules\n│   ├── gpc_exceptions_rules.json\n│   └── universal_gpc_rules.json\n└── theme       # Contains darkmode\n    └── darkmode.js\ntest\n└── background\n    └── gpc.test.js\n```\n\nThe following source folders have detailed descriptions further in the document.\n\n[background](#background)\\\n[common](#common)\\\n[content-scripts](#content-scripts)\\\n[data](#data)\\\n[manifests](#manifests)\\\n[options](#options)\\\n[popup](#popup)\\\n[rules](#rules)\\\n[theme](#theme)\n\n## background\n\n1. `protection`\n2. `control.js`\n3. `storage.js`\n\n### `src/background/protection`\n\n1. `background.js`\n2. `listeners-chrome.js`\n3. `listeners-firefox.js`\n4. `protection.js`\n5. `protection-ff.js`\n\n#### `protection/background.js`\n\nInitializes the protection mode listeners.\n\n#### `protection/listeners-chrome.js` and `protection/listeners-firefox.js`\n\nCreates listeners for Chrome and Firefox, respectively.\n\n#### `protection/protection.js`\n\nManages the domain list with functions like `logData();`, `updateDomainlistAndSignal();`, `pullToDomainlistCache();`, `syncDomainlists();`. Also responsible for supplying the popup with the proper information with `dataToPopup();`. Also creates listeners to watch the popup for domain list changes.\n\n#### `protection/protection-ff.js`\n\nManages the domain list for Firefox.\n\n### `background/control.js`\n\nUses `protection.js` to turn the extension on and off.\n\n### `background/storage.js`\n\nHandles storage uploads and downloads.\n\n## common\n\n1. `editDomainlist.js`\n2. `editRules.js`\n\nThis folder holds common internal API's to be used throughout the extension.\n\n### `common/editDomainlist.js`\n\nIs an internal API to be used for editing a users domain list.\n\n### `common/editRules.js`\n\nIs an internal API to be used for editing rules that allow us to send the GPC header.\n\n## content-scripts\n\n1. `injection`\n2. `registration`\n3. `contentScript.js`\n\nThis folder contains our main content script and methods for injecting the GPC signal into the DOM.\n\n### `src/content-scripts/injection`\n\n1. `gpc-dom.js`\n\n`gpc-dom.js` injects the DOM signal.\n\n### `src/content-scripts/registration`\n\n1. `gpc-dom.js`\n\nThis file injects `injection/gpc-dom.js` into the page using a static script. (Based on [this stack overflow thread](https://stackoverflow.com/questions/9515704/use-a-content-script-to-access-the-page-context-variables-and-functions))\n\n### `content-scripts/contentScript.js`\n\nThis runs on every page and sends information to signal background processes.\n\n## data\n\n1. `defaultSettings.js`\n2. `headers.js`\n3. `regex.js`\n\nThis folder contains static data.\n\n### `data/defaultSettings.js`\n\nContains the default OptMeowt settings.\n\n### `data/headers.js`\n\nContains the default headers to be attached to online requests.\n\n### `data/regex.js`\n\nContains regular expressions for finding \"do not sell\" links and related privacy signals.\n\n## manifests\n\n1. `chrome`\n2. `firefox`\n\nContains the extension manifests\n\n### `manifests/chrome`\n\n1. `manifest-dev.json`\n2. `manifest-dist.json`\n\nContains the development and distribution manifests for Chrome\n\n### `manifests/firefox`\n\n1. `manifest-dev.json`\n2. `manifest-dist.json`\n\nContains the development and distribution manifests for Firefox\n\n## options\n\n1. `components`\n2. `views`\n3. `dark-mode.css`\n4. `options.html`\n\nThis folder contains all of the frontend code\n\n### `options/components`\n\n1. `scaffold-component.html`\n2. `util.js`\n\nThis folder contains the basic layout of every options page and helper functions to help render the pages.\n\n### `options/views`\n\n1. `about-view`\n2. `domainlist-view`\n3. `main-view`\n4. `settings-view`\n\nContains all frontend and implementation of the settings pages.\n\n#### `views/about-view`\n\n1. `about-view.html`\n2. `about-view.js`\n\nBuilds the \"about\" page\n\n#### `views/domainlist-view`\n\n1. `domainlist-view.html`\n2. `domainlist-view.js`\n\nBuilds the domain list page\n\n#### `views/main-view`\n\n1. `main-view.html`\n2. `main-view.js`\n\nBuilds the main options page\n\n#### `views/settings-view`\n\n1. `settings-view.html`\n2. `settings-view.js`\n\nBuilds the settings page\n\n### `options/dark-mode.css`\n\nContains the dark-mode styles for OptMeowt.\n\n### `options/options.html` and `options/options.js`\n\nIs the entry point for the main options page.\n\n### `options/styles.css`\n\nContains the basic styles for OptMeowt.\n\n## popup\n\n1. `popup.html`\n2. `popup.js`\n3. `styles.css`\n\nContains the frontend and implementation for the OptMeowt popup.\n\n## rules\n\n1. `gpc_exception_rules.json`\n2. `universal_gpc_rules.json`\n\nContains rule framework for sending GPC headers to sites.\n\n## theme\n\n1. `darkmode.js`\n\nContains the dark mode functionality.\n\n**Links to APIs:**\n\nChrome: [webRequest](https://developer.chrome.com/docs/extensions/reference/webRequest/) and [webNavigation](https://developer.chrome.com/docs/extensions/reference/webNavigation/)\n\nFirefox: [webRequest](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest) and [webNavigation](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webNavigation)\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"optmeowt\",\n  \"version\": \"6.1.0\",\n  \"description\": \"A privacy extension that allows users to exercise rights under GPC\",\n  \"main\": \"index.js\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"prestart\": \"rimraf dev\",\n    \"start\": \"concurrently -k npm:start:firefox  npm:start:chrome\",\n    \"start:firefox\": \"webpack --watch --mode development --env firefox\",\n    \"start:chrome\": \"webpack --watch --mode development --env chrome\",\n    \"prebuild\": \"rimraf dist && mkdir dist && mkdir dist/packages\",\n    \"build\": \"npm run build:firefox && npm run build:chrome\",\n    \"build:firefox\": \"webpack --mode production --env firefox\",\n    \"build:chrome\": \"webpack --mode production --env chrome\",\n    \"postbuild:firefox\": \"cd dist/firefox && zip -rFSX ../packages/ff-optmeowt-$npm_package_version.zip * -x '*.git*' -x '*.DS_Store*' -x '*.txt*'\",\n    \"postbuild:chrome\": \"cd dist/chrome && zip -rFSX ../packages/chrome-optmeowt-$npm_package_version.zip * -x '*.git*' -x '*.DS_Store*' -x '*.txt*'\",\n    \"test\": \"mocha $(find test -name '*.js')\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/privacy-tech-lab/gpc-optmeowt.git\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"bugs\": {\n    \"url\": \"https://github.com/privacy-tech-lab/gpc-optmeowt/issues\"\n  },\n  \"homepage\": \"https://github.com/privacy-tech-lab/gpc-optmeowt#readme\",\n  \"dependencies\": {\n    \"animate.css\": \"^4.1.1\",\n    \"darkmode-js\": \"^1.5.7\",\n    \"file-saver\": \"^2.0.5\",\n    \"idb\": \"^7.1.1\",\n    \"mocha\": \"^10.8.2\",\n    \"mustache\": \"^4.2.0\",\n    \"path\": \"^0.12.7\",\n    \"psl\": \"^1.8.0\",\n    \"puppeteer\": \"^22.15.0\",\n    \"rimraf\": \"^3.0.2\",\n    \"tippy.js\": \"^6.3.7\",\n    \"uikit\": \"3.6.9\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.21.3\",\n    \"@babel/preset-env\": \"^7.20.2\",\n    \"babel-loader\": \"^9.1.2\",\n    \"clean-webpack-plugin\": \"^4.0.0\",\n    \"concurrently\": \"^6.2.1\",\n    \"copy-webpack-plugin\": \"^14.0.0\",\n    \"css-loader\": \"^5.2.7\",\n    \"file-loader\": \"^6.2.0\",\n    \"html-webpack-plugin\": \"^5.3.2\",\n    \"prettier\": \"^2.3.2\",\n    \"string-replace-loader\": \"^3.0.3\",\n    \"style-loader\": \"^2.0.0\",\n    \"wait-on\": \"^7.2.0\",\n    \"webpack\": \"^5.105.0\",\n    \"webpack-cli\": \"^4.8.0\",\n    \"webpack-dev-server\": \"^5.2.4\",\n    \"workbox-webpack-plugin\": \"^7.3.0\"\n  },\n  \"overrides\": {\n    \"serialize-javascript\": \"^7.0.4\"\n  },\n  \"resolutions\": {\n    \"ws\": \"^8.17.1\"\n  }\n}"
  },
  {
    "path": "src/background/control.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\ncontrol.js\n================================================================================\ncontrol.js manages persistent data, message liseteners, in particular\nto manage the state & functionality mode of the extension\n*/\n\nimport {\n  init as initProtection_ff,\n  halt as haltProtection_ff,\n} from \"./protection/protection-ff.js\";\nimport {\n  init as initProtection_cr,\n  halt as haltProtection_cr,\n} from \"./protection/protection.js\";\nimport { defaultSettings } from \"../data/defaultSettings.js\";\nimport { stores, storage } from \"./storage.js\";\nimport { reloadDynamicRules } from \"../common/editRules.js\";\n\nimport {\n  debug_domainlist_and_dynamicrules,\n  updateRemovalScript,\n} from \"../common/editDomainlist.js\";\n\nasync function enable() {\n  var initProtection = initProtection_cr;\n  initProtection();\n}\n\nfunction disable() {\n  var haltProtection = haltProtection_cr;\n  haltProtection();\n}\n\n/******************************************************************************/\n// Initializers\n\n// This is the very first thing the extension runs\n(async () => {\n    chrome.scripting.registerContentScripts([\n      {\n        id: \"1\",\n        matches: [\"<all_urls>\"],\n        excludeMatches:[\"https://example.com/\"],\n        js: [\"content-scripts/registration/gpc-dom.js\"],\n        runAt: \"document_start\",\n      }\n    ]);\n\n  // Check if the browser is Firefox\nif (\"$BROWSER\" == \"firefox\") {\n  chrome.runtime.onInstalled.addListener(function (details) {\n    if (details.reason === 'install') {\n      chrome.runtime.openOptionsPage((result) => {});\n    }\n  });\n  }\n  // Initializes the default settings\n  let settingsDB = await storage.getStore(stores.settings);\n  for (let setting in defaultSettings) {\n    if (typeof settingsDB[setting] === \"undefined\") {\n      await storage.set(stores.settings, defaultSettings[setting], setting);\n    }\n  }\n  const localSettings = await chrome.storage.local.get(\n    \"WELLKNOWN_CHECK_ENABLED\"\n  );\n  if (typeof localSettings.WELLKNOWN_CHECK_ENABLED === \"undefined\") {\n    await chrome.storage.local.set({\n      WELLKNOWN_CHECK_ENABLED: defaultSettings[\"WELLKNOWN_CHECK_ENABLED\"],\n    });\n  }\n\n  let isEnabled = await storage.get(stores.settings, \"IS_ENABLED\");\n\n  if (isEnabled) {\n    // Turns on the extension\n    enable();\n    updateRemovalScript();\n    reloadDynamicRules();\n  }\n\n})();\n\n/******************************************************************************/\n// Mode listeners\n\n// (1) Handle extension activeness is changed by calling all halt\n// \t - Make sure that I switch extensionmode and separate it from mode.domainlist\n// (2) Handle extension functionality with listeners and message passing\n\n/**\n * Listeners for information from --POPUP-- or --OPTIONS-- page\n * This is the main \"hub\" for message passing between the extension components\n * https://developer.chrome.com/docs/extensions/mv3/messaging/\n */\nchrome.runtime.onMessage.addListener(async function (\n  message\n) {\n  if (message.msg === \"TURN_ON_OFF\") {\n    let isEnabled = message.data.isEnabled; // can be undefined\n\n    if (isEnabled) {\n      await storage.set(stores.settings, true, \"IS_ENABLED\");\n      enable();\n    } else {\n      await storage.set(stores.settings, false, \"IS_ENABLED\");\n      disable();\n    }\n  }\n\n  if (message.msg === \"CHANGE_IS_DOMAINLISTED\") {\n    let isDomainlisted = message.data.isDomainlisted; // can be undefined // not used\n  }\n});\n"
  },
  {
    "path": "src/background/protection/background.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\nbackground.js\n================================================================================\nbackground.js is the main background script handling OptMeowt's\nmain opt-out functionality\n*/\n\nimport { enableListeners, disableListeners } from \"./listeners-$BROWSER.js\";\nimport { stores, storage } from \"../storage.js\";\nimport { defaultSettings } from \"../../data/defaultSettings.js\";\n\n// We could alt. use this in place of \"building\" for chrome/ff, just save it to settings in storage\nvar userAgent =\n  window.navigator.userAgent.indexOf(\"Firefox\") > -1 ? \"moz\" : \"chrome\";\n\n/******************************************************************************/\n\n/**\n * Enables extension functionality and sets site listeners\n * Information regarding the functionality and timing of webRequest and webNavigation\n * can be found on Mozilla's & Chrome's API docuentation sites (also linked above)\n *\n * The actual listeners are located in `listeners-(chosen browser).js`\n * The functions called on event occurance are located in `events.js`\n *\n * HIERARCHY:   manifest.json --> protection --> background.js --> listeners-$BROWSER.js --> events.js\n */\n\n/******************************************************************************/\n\n/**\n * Initializes the extension\n * Place all initialization necessary, as high level as can be, here.\n */\nasync function init() {\n  enableListeners();\n}\n\nfunction halt() {\n  disableListeners();\n}\n\n/******************************************************************************/\n\nexport const background = {\n  init,\n  halt,\n};\n"
  },
  {
    "path": "src/background/protection/listeners-chrome.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\nlisteners-chrome.js\n================================================================================\nlisteners-chrome.js holds the on-page-visit listeners for chrome that activate \nour main functionality\n*/\n\n// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/onBeforeRequest\n// https://developer.chrome.com/docs/extensions/reference/webRequest/\n// This is the extraInfoSpec array of strings\nconst CHROME_REQUEST_SPEC = [\"requestHeaders\", \"extraHeaders\"];\nconst CHROME_RESPONSE_SPEC = [\"responseHeaders\", \"extraHeaders\"];\n// This is the filter object\nconst FILTER = { urls: [\"<all_urls>\"] };\n\n/**\n * Enables extension functionality and sets site listeners\n * Information regarding the functionality and timing of webRequest and webNavigation\n * can be found on Mozilla's & Chrome's API docuentation sites (also linked above)\n *\n * The functions called on event occurance are located in `events.js`\n */\nfunction enableListeners(callbacks) {\n  const {\n    onBeforeSendHeaders,\n    onHeadersReceived,\n    onBeforeNavigate,\n    onCommitted,\n    onCompleted,\n  } = callbacks;\n\n  // (4) global Chrome listeners\n  chrome.webRequest.onBeforeSendHeaders.addListener(\n    onBeforeSendHeaders,\n    FILTER,\n    CHROME_REQUEST_SPEC\n  );\n  chrome.webRequest.onHeadersReceived.addListener(\n    onHeadersReceived,\n    FILTER,\n    CHROME_RESPONSE_SPEC\n  );\n  chrome.webNavigation.onBeforeNavigate.addListener(onBeforeNavigate, FILTER);\n  chrome.webNavigation.onCommitted.addListener(onCommitted, FILTER);\n  chrome.webNavigation.onCompleted.addListener(onCompleted, FILTER);\n}\n\n/**\n * Disables background listeners\n */\nfunction disableListeners(callbacks) {\n  const {\n    onBeforeSendHeaders,\n    onHeadersReceived,\n    onBeforeNavigate,\n    onCommitted,\n    onCompleted,\n  } = callbacks;\n\n  chrome.webRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders);\n  chrome.webRequest.onHeadersReceived.removeListener(onHeadersReceived);\n  chrome.webNavigation.onBeforeNavigate.removeListener(onBeforeNavigate);\n  chrome.webNavigation.onCommitted.removeListener(onCommitted);\n  chrome.webNavigation.onCompleted.removeListener(onCompleted);\n}\n\nexport { enableListeners, disableListeners };\n"
  },
  {
    "path": "src/background/protection/listeners-firefox.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\nlisteners-firefox.js\n================================================================================\nlisteners-firefox.js holds the on-page-visit listeners for firefox that activate \nour main functionality\n*/\n\n// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/onBeforeRequest\n// https://developer.chrome.com/docs/extensions/reference/webRequest/\n// This is the extraInfoSpec array of strings\nconst MOZ_REQUEST_SPEC = [\"requestHeaders\", \"blocking\"];\nconst MOZ_RESPONSE_SPEC = [\"responseHeaders\", \"blocking\"];\n\n// This is the filter object\nconst FILTER = { urls: [\"<all_urls>\"] };\n\n/**\n * Enables extension functionality and sets site listeners\n * Information regarding the functionality and timing of webRequest and webNavigation\n * can be found on Mozilla's & Chrome's API docuentation sites (also linked above)\n *\n * The functions called on event occurance are located in `events.js`\n */\nfunction enableListeners(callbacks) {\n  const {\n    onBeforeSendHeaders,\n    onHeadersReceived,\n    onBeforeNavigate,\n    onCommitted,\n    onCompleted,\n  } = callbacks;\n\n  // (4) global Firefox listeners\n  chrome.webRequest.onBeforeSendHeaders.addListener(\n    onBeforeSendHeaders,\n    FILTER,\n    MOZ_REQUEST_SPEC\n  );\n  chrome.webRequest.onHeadersReceived.addListener(\n    onHeadersReceived,\n    FILTER,\n    MOZ_RESPONSE_SPEC\n  );\n  chrome.webNavigation.onBeforeNavigate.addListener(onBeforeNavigate);\n  chrome.webNavigation.onCommitted.addListener(onCommitted);\n  chrome.webNavigation.onCompleted.addListener(onCompleted);\n}\n\n/**\n * Disables background listeners\n */\nfunction disableListeners(callbacks) {\n  const {\n    onBeforeSendHeaders,\n    onHeadersReceived,\n    onBeforeNavigate,\n    onCommitted,\n    onCompleted,\n  } = callbacks;\n\n  chrome.webRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders);\n  chrome.webRequest.onHeadersReceived.removeListener(onHeadersReceived);\n  chrome.webNavigation.onBeforeNavigate.removeListener(onBeforeNavigate);\n  chrome.webNavigation.onCommitted.removeListener(onCommitted);\n  chrome.webNavigation.onCompleted.removeListener(onCompleted);\n}\n\nexport { enableListeners, disableListeners };\n"
  },
  {
    "path": "src/background/protection/protection-ff.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\nprotection.js\n================================================================================\nprotection.js (1) Implements our per-site functionality for the background listeners\n              (2) Handles cached values & message passing to popup & options page\n*/\n\nimport { stores, storage } from \"../storage.js\";\nimport { defaultSettings } from \"../../data/defaultSettings.js\";\nimport { headers } from \"../../data/headers.js\";\nimport { enableListeners, disableListeners } from \"./listeners-$BROWSER.js\";\nimport psl from \"psl\";\nimport { isWellknownCheckEnabled } from \"../../common/settings.js\";\n\n/******************************************************************************/\n/******************************************************************************/\n/**********             # Initializers (cached values)               **********/\n/******************************************************************************/\n/******************************************************************************/\n\nvar domainlist = {}; // Caches & mirrors domainlist in storage\nvar isDomainlisted = defaultSettings[\"IS_DOMAINLISTED\"];\nvar tabs = {};          // Caches all tab infomration, i.e. requests, etc. \nvar wellknown = {};     // Caches wellknown info to be sent to popup\nvar signalPerTab = {};  // Caches if a signal is sent to render the popup icon\nvar activeTabID = 0;    // Caches current active tab id\nvar sendSignal = true;  // Caches if the signal can be sent to the curr domain\nvar domPrev3rdParties = {}; //stores all the 3rd parties by domain (resets when you quit chrome)\nvar globalParsedDomain;\n\nasync function reloadVars() {\n  let storedDomainlisted = await storage.get(\n    stores.settings,\n    \"IS_DOMAINLISTED\"\n  );\n  if (storedDomainlisted) {\n    isDomainlisted = storedDomainlisted;\n  }\n}\n\nreloadVars();\n\n\n/******************************************************************************/\n/******************************************************************************/\n/**********       # Lisetener callbacks - Main functionality         **********/\n/******************************************************************************/\n/******************************************************************************/\n\n/*\n * The four following functions are all related to the four main listeners in\n * `background.js`. These four functions implement all the other helper\n * functions below\n */\n\nconst listenerCallbacks = {\n  /**\n   * Handles all signal processessing prior to sending request headers\n   * @param {object} details - retrieved info passed into callback\n   * @returns {array} details.requestHeaders from addHeaders\n   */\n  onBeforeSendHeaders: async (details) => {\n    await updateDomainlist(details);\n\n    if (true) {\n      signalPerTab[details.tabId] = true;\n      return addHeaders(details);\n    }\n  },\n\n  /**\n   * @param {object} details - retrieved info passed into callback\n   */\n  onHeadersReceived: (details) => {\n    logData(details);\n  },\n\n  /**\n   * @param {object} details - retrieved info passed into callback\n   */\n  onBeforeNavigate: (details) => {\n    // Resets certain cached info\n    if (details.frameId === 0) {\n      wellknown[details.tabId] = null;\n      signalPerTab[details.tabId] = false;\n      tabs[activeTabID].REQUEST_DOMAINS = {};\n    }\n  },\n\n  /**\n   * Adds DOM property\n   * @param {object} details - retrieved info passed into callback\n   */\n  onCommitted: async (details) => {\n    if (true) {\n      addDomSignal(details);\n      updatePopupIcon(details);\n    }\n  },\n}; // closes listenerCallbacks object\n\n/******************************************************************************/\n/******************************************************************************/\n/**********      # Listener helper fxns - Main functionality         **********/\n/******************************************************************************/\n/******************************************************************************/\n\n/**\n * Attaches headers from `headers.js` to details.requestHeaders\n * @param {object} details - retrieved info passed into callback\n * @returns {array} details.requestHeaders\n */\nfunction addHeaders(details) {\n  console.log(\"addHeaders called\");\n  for (let signal in headers) {\n    let s = headers[signal];\n    details.requestHeaders.push({ name: s.name, value: s.value });\n  }\n  return { requestHeaders: details.requestHeaders };\n}\n\n/**\n * Runs `dom.js` to attach DOM signal\n * @param {object} details - retrieved info passed into callback\n */\nfunction addDomSignal(details) {\n  console.log(\"addDomSignal called\");\n  chrome.scripting.executeScript(details.tabId, {\n    file: \"../../content-scripts/injection/gpc-dom.js\",\n    frameId: details.frameId, // Supposed to solve multiple injections\n    // as opposed to allFrames: true\n    runAt: \"document_start\",\n  });\n}\n\nfunction getCurrentParsedDomain() {\n  return new Promise((resolve, reject) => {\n    try {\n      chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {\n        let tab = tabs[0];\n        let url = new URL(tab.url);\n        let parsed = psl.parse(url.hostname);\n        let domain = parsed.domain;\n        globalParsedDomain = domain;  // for global scope variable\n        resolve(domain);\n      });\n    } catch(e) {\n      reject();\n    }\n  })\n}\n\n/**\n * Checks whether a particular domain should receive a DNS signal\n * (1) Parse url to get domain for domainlist\n * (2) Update domains by adding current domain to domainlist in storage.\n * (3) Updates the 3rd party list for the currentDomain\n * (4) Check to see if we should send signal.\n * \n * Currently, it only adds to domainlist store as NULL if it doesnt exist\n * @param {Object} details - callback object according to Chrome API\n */\n async function updateDomainlist(details) {\n  let url = new URL(details.url);\n  let parsedUrl = psl.parse(url.hostname);\n  let parsedDomain = parsedUrl.domain;\n  if (parsedDomain == null || parsedDomain == undefined) {\n    return;\n  }\n\n  let parsedDomainVal = domainlist[parsedDomain];\n  if (parsedDomainVal === undefined) {\n    storage.set(stores.domainlist, null, parsedDomain); // Sets to storage async\n    domainlist[parsedDomain] = null; // Sets to cache\n    parsedDomainVal = null;\n  }\n\n  //get the current parsed domain--this is used to store 3rd parties (using globalParsedDomain variable)\n \n  let currentDomain = await getCurrentParsedDomain(); \n  //initialize the objects\n  if (!(activeTabID in domPrev3rdParties)){\n    domPrev3rdParties[activeTabID] = {};\n  }\n  if (!(currentDomain in domPrev3rdParties[activeTabID]) ){\n    domPrev3rdParties[activeTabID][currentDomain] = {};\n  }\n  //as they come in, add the parsedDomain to the object with null value (just a placeholder)\n  domPrev3rdParties[activeTabID][currentDomain][parsedDomain] = null;\n  \n\n  (isDomainlisted) \n    ? ((parsedDomainVal === null) ? sendSignal = true : sendSignal = false)\n    : sendSignal = true;\n}\n\nfunction updatePopupIcon(details) {\n  if (wellknown[details.tabId] === undefined) {\n    wellknown[details.tabId] = null;\n  }\n  if (wellknown[details.tabId] === null) {\n    chrome.browserAction.setIcon({\n      tabId: details.tabId,\n      path: \"assets/face-icons/optmeow-face-circle-green-ring-128.png\",\n    });\n  }\n}\n\nfunction logData(details) {\n  let url = new URL(details.url);\n  let parsed = psl.parse(url.hostname);\n\n  if (tabs[details.tabId] === undefined) {\n    tabs[details.tabId] = { DOMAIN: null, REQUEST_DOMAINS: {}, TIMESTAMP: 0 };\n    tabs[details.tabId].REQUEST_DOMAINS[parsed.domain] = {\n      URLS: {},\n      RESPONSE: details.responseHeaders,\n      TIMESTAMP: details.timeStamp,\n    };\n    tabs[details.tabId].REQUEST_DOMAINS[parsed.domain].URLS = {\n      URL: details.url,\n      RESPONSE: details.responseHeaders,\n    };\n  } else {\n    if (tabs[details.tabId].REQUEST_DOMAINS[parsed.domain] === undefined) {\n      tabs[details.tabId].REQUEST_DOMAINS[parsed.domain] = {\n        URLS: {},\n        RESPONSE: details.responseHeaders,\n        TIMESTAMP: details.timeStamp,\n      };\n      tabs[details.tabId].REQUEST_DOMAINS[parsed.domain].URLS[details.url] = {\n        RESPONSE: details.responseHeaders,\n      };\n    } else {\n      tabs[details.tabId].REQUEST_DOMAINS[parsed.domain].URLS[details.url] = {\n        RESPONSE: details.responseHeaders,\n      };\n    }\n  }\n}\n\nasync function pullToDomainlistCache() {\n  let domain;\n  let domainlistKeys = await storage.getAllKeys(stores.domainlist);\n  let domainlistValues = await storage.getAll(stores.domainlist);\n  for (let key in domainlistKeys) {\n    domain = domainlistKeys[key];\n    domainlist[domain] = domainlistValues[key];\n  }\n}\n\nasync function syncDomainlists() {\n  // (1) Reconstruct a domainlist indexedDB object from storage\n  // (2) Iterate through local domainlist\n  // --- If item in cache NOT in domainlistKeys/domainlistDB, add to storage\n  //     via storage.set()\n  // (3) Iterate through all domain keys in indexedDB domainlist\n  // --- If key NOT in cached domainlist, add to cached domainlist\n\n  let domainlistKeys = await storage.getAllKeys(stores.domainlist);\n  let domainlistValues = await storage.getAll(stores.domainlist);\n  let domainlistDB = {};\n  let domain;\n  for (let key in domainlistKeys) {\n    domain = domainlistKeys[key];\n    domainlistDB[domain] = domainlistValues[key];\n  }\n\n  for (let domainKey in domainlist) {\n    if (!domainlistDB[domainKey]) {\n      await storage.set(stores.domainlist, domainlist[domainKey], domainKey);\n    }\n  }\n\n  for (let domainKey in domainlistDB) {\n    if (!domainlist[domainKey]) {\n      domainlist[domainKey] = domainlistDB[domainKey];\n    }\n  }\n}\n\n/**\n * whether the curr site should get privacy signals\n * (We need to try and make a synchronous version, esp. for DOM issue & related\n * message passing with the contentscript which injects the DOM signal)\n * @returns {bool} sendSignal\n */\nasync function sendPrivacySignal(domain) {\n  let sendSignal;\n  const extensionEnabled = await storage.get(stores.settings, \"IS_ENABLED\");\n  const extensionDomainlisted = await storage.get(\n    stores.settings,\n    \"IS_DOMAINLISTED\"\n  );\n  const domainDomainlisted = await storage.get(stores.domainlist, domain);\n\n  if (extensionEnabled) {\n    if (extensionDomainlisted) {\n      // Recall we must flip the value of the domainlisted domain\n      // due to how to how defined domainlisted values, corresponding to MV3\n      // declarativeNetRequest rule exceptions\n      // (i.e., null => no rule exists, valued => exception rule exists)\n      sendSignal = !domainDomainlisted ? true : false;\n    } else {\n      sendSignal = true;\n    }\n  } else {\n    sendSignal = false;\n  }\n  return sendSignal;\n}\n\n/******************************************************************************/\n/******************************************************************************/\n/**********          # Message Passing - Popup helper fxns           **********/\n/******************************************************************************/\n/******************************************************************************/\n\nfunction handleSendMessageError() {\n  const error = chrome.runtime.lastError;\n  if (error) {\n    console.warn(error.message);\n  }\n}\n\n// Info back to popup\nfunction dataToPopup() {\n\n  let requestsData = {};  \n  \n  if (tabs[activeTabID] !== undefined) {\n\n    requestsData = domPrev3rdParties[activeTabID][globalParsedDomain];\n    console.log(\"requests by tabID:\", domPrev3rdParties);\n  }\n\n  chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {\n    let tabID = tabs[0][\"id\"];\n    let wellknownData = wellknown[tabID];\n\n    let popupData = {\n      requests: requestsData,\n      wellknown: wellknownData,\n    };\n\n    chrome.runtime.sendMessage(\n      {\n        msg: \"POPUP_PROTECTION_DATA\",\n        data: popupData,\n      },\n      handleSendMessageError\n    );\n  });\n}\n\n/******************************************************************************/\n/******************************************************************************/\n/**********                   # Message passing                      **********/\n/******************************************************************************/\n/******************************************************************************/\n\n/**\n * Currently only handles syncing domainlists between storage and memory\n * This runs when the popup disconnects from the background page\n * @param {Port} port\n */\nfunction onConnectHandler(port) {\n  if (port.name === \"POPUP\") {\n    port.onDisconnect.addListener(function () {\n      syncDomainlists();\n    });\n  }\n}\n\n/**\n * This is currently only to handle adding the GPC DOM signal.\n * I'm not sure how to fit it into an async call, it doesn't want to connect.\n * It would be nice to merge the two onMessage handlers.\n * TODO: This method still seems to have a timing issue. Doesn't always show DOM signal as thumbs up on reference site.\n * @returns {Bool} true (lets us send asynchronous responses to senders)\n */\nfunction onMessageHandlerSynchronous(message, sender, sendResponse) {\n  if (message.msg === \"APPEND_GPC_PROP\") {\n    let url = new URL(sender.origin);\n    let parsed = psl.parse(url.hostname);\n    let domain = parsed.domain;\n\n    const r = sendPrivacySignal(domain);\n    r.then((r) => {\n      const response = {\n        msg: \"APPEND_GPC_PROP_RESPONSE\",\n        sendGPC: r,\n      };\n      sendResponse(response);\n    });\n  }\n  return true;\n}\n\n/**\n * Listeners for information from --POPUP-- or --OPTIONS-- page\n * This is the main \"hub\" for message passing between the extension components\n * https://developer.chrome.com/docs/extensions/mv3/messaging/\n */\nasync function onMessageHandlerAsync(message, sender, sendResponse) {\n  if (message.msg === \"GET_WELLKNOWN_CHECK_ENABLED\") {\n    const enabled = await isWellknownCheckEnabled();\n    await chrome.storage.local.set({ WELLKNOWN_CHECK_ENABLED: enabled });\n    sendResponse({ enabled });\n    return true;\n  }\n  if (message.msg === \"TOGGLE_WELLKNOWN_CHECK\") {\n    const enabled = message.data?.enabled !== false;\n    await storage.set(stores.settings, enabled, \"WELLKNOWN_CHECK_ENABLED\");\n    await chrome.storage.local.set({ WELLKNOWN_CHECK_ENABLED: enabled });\n    if (!enabled) {\n      await storage.clear(stores.wellknownInformation);\n      wellknown = {};\n    }\n  }\n  if (message.msg === \"CHANGE_IS_DOMAINLISTED\") {\n    isDomainlisted = message.data.isDomainlisted;\n    storage.set(stores.settings, isDomainlisted, \"IS_DOMAINLISTED\");\n  }\n  if (message.msg === \"SET_TO_DOMAINLIST\") {\n    let { domain, key } = message.data;\n    domainlist[domain] = key; // Sets to cache\n    storage.set(stores.domainlist, key, domain); // Sets to long term storage\n  }\n  if (message.msg === \"REMOVE_FROM_DOMAINLIST\") {\n    let domain = message.data;\n    delete domainlist[domain];\n  }\n  if (message.msg === \"POPUP_PROTECTION\") {\n    dataToPopup();\n  }\n  if (message.msg === \"CONTENT_SCRIPT_WELLKNOWN\") {\n    const wellknownCheckEnabled = await isWellknownCheckEnabled();\n    if (!wellknownCheckEnabled) {\n      return true;\n    }\n    let tabID = sender.tab.id;\n    wellknown[tabID] = message.data;\n    if (wellknown[tabID][\"gpc\"] === true) {\n      setTimeout(() => {}, 10000);\n      if (signalPerTab[tabID] === true) {\n        chrome.browserAction.setIcon({\n          tabId: tabID,\n          path: \"assets/face-icons/optmeow-face-circle-green-128.png\",\n        });\n      }\n    }\n  }\n\n  if (message.msg === \"CONTENT_SCRIPT_TAB\") {\n    let url = new URL(sender.origin);\n    let parsed = psl.parse(url.hostname);\n    let domain = parsed.domain;\n    let tabID = sender.tab.id;\n    if (tabs[tabID] === undefined) {\n      tabs[tabID] = {\n        DOMAIN: domain,\n        REQUEST_DOMAINS: {},\n        TIMESTAMP: message.data,\n      };\n    } else if (tabs[tabID].DOMAIN !== domain) {\n      tabs[tabID].DOMAIN = domain;\n      let urls = tabs[tabID][\"REQUEST_DOMAINS\"];\n      for (let key in urls) {\n        if (urls[key][\"TIMESTAMP\"] >= message.data) {\n          tabs[tabID][\"REQUEST_DOMAINS\"][key] = urls[key];\n        } else {\n          delete tabs[tabID][\"REQUEST_DOMAINS\"][key];\n        }\n      }\n      tabs[tabID][\"TIMESTAMP\"] = message.data;\n    }\n  }\n  if (message.msg === \"FORCE_RELOAD\") {\n    pullToDomainlistCache();\n  }\n  return true; // Async callbacks require this\n}\n\nfunction initMessagePassing() {\n  chrome.runtime.onConnect.addListener(onConnectHandler);\n  chrome.runtime.onMessage.addListener(onMessageHandlerAsync);\n  chrome.runtime.onMessage.addListener(onMessageHandlerSynchronous);\n}\n\nfunction closeMessagePassing() {\n  chrome.runtime.onConnect.removeListener(onConnectHandler);\n  chrome.runtime.onMessage.removeListener(onMessageHandlerAsync);\n  chrome.runtime.onMessage.removeListener(onMessageHandlerSynchronous);\n}\n\n/******************************************************************************/\n/******************************************************************************/\n/**********       # Other initializers - run once per enable         **********/\n/******************************************************************************/\n/******************************************************************************/\n\n/**\n * Listener for tab switch that updates the cached current tab variable\n */\nfunction onActivatedProtectionMode(info) {\n  activeTabID = info.tabId;\n}\n\n// Handles misc. setup & setup listeners\nfunction initSetup() {\n  pullToDomainlistCache();\n\n  // Runs on startup to initialize the cached current tab variable\n  chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {\n    if (tabs.id) {\n      activeTabID = tabs.id;\n    }\n  });\n\n  chrome.tabs.onActivated.addListener(onActivatedProtectionMode);\n}\n\nfunction closeSetup() {\n  chrome.tabs.onActivated.removeListener(onActivatedProtectionMode);\n}\n\n/**\n * Inteded to facilitate transitioning between analysis & protection modes\n */\nfunction wipeLocalVars() {\n  domainlist = {}; // Caches & mirrors domainlist in storage\n  tabs = {}; // Caches all tab infomration, i.e. requests, etc.\n  wellknown = {}; // Caches wellknown info to be sent to popup\n  signalPerTab = {}; // Caches if a signal is sent to render the popup icon\n  activeTabID = 0; // Caches current active tab id\n  sendSignal = false; // Caches if the signal can be sent to the curr domain\n}\n\n/******************************************************************************/\n/******************************************************************************/\n/**********           # Exportable init / halt functions             **********/\n/******************************************************************************/\n/******************************************************************************/\n\nexport function init() {\n  reloadVars();\n  enableListeners(listenerCallbacks);\n  initMessagePassing();\n  initSetup();\n}\n\n\nexport function halt() {\n  disableListeners(listenerCallbacks);\n  closeMessagePassing();\n  closeSetup();\n  wipeLocalVars();\n}\n"
  },
  {
    "path": "src/background/protection/protection.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\nprotection.js\n================================================================================\nprotection.js (1) Implements our per-site functionality for the background listeners\n          (2) Handles cached values & message passing to popup & options page\n*/\n\nimport { stores, storage } from \"./../storage.js\";\nimport { defaultSettings } from \"../../data/defaultSettings.js\";\nimport { enableListeners, disableListeners } from \"./listeners-$BROWSER.js\";\nimport psl from \"psl\";\n\nimport {\n  addDynamicRule,\n  deleteDynamicRule,\n  reloadDynamicRules,\n} from \"../../common/editRules.js\";\nimport { isWellknownCheckEnabled, isComplianceCheckEnabled, getUserState } from \"../../common/settings.js\";\nimport { fetchComplianceData, isCacheValid } from \"../../data/complianceData.js\";\n\n/******************************************************************************/\n/******************************************************************************/\n/**********             # Initializers (cached values)               **********/\n/******************************************************************************/\n/******************************************************************************/\n\nvar domainlist = {}; // Caches & mirrors domainlist in storage\nvar isDomainlisted = defaultSettings[\"IS_DOMAINLISTED\"];\nvar tabs = {};          // Caches all tab infomration, i.e. requests, etc. \nvar wellknown = {};     // Caches wellknown info to be sent to popup\nvar signalPerTab = {};  // Caches if a signal is sent to render the popup icon\nvar activeTabID = 0;    // Caches current active tab id\nvar sendSignal = true;  // Caches if the signal can be sent to the curr domain\nvar domPrev3rdParties = {}; //stores all the 3rd parties by domain (resets when you quit chrome)\nvar globalParsedDomain;\nvar setup = false;\n// complianceData cache is now handled by the IndexedDB store \"stores.complianceData\"\nconst DEFAULT_NO_DATA_STATUS = {\n  status: 'no_data',\n  details: 'We do not have data for this site.',\n  lastChecked: null\n};\n\nasync function reloadVars() {\n  let storedDomainlisted = await storage.get(\n    stores.settings,\n    \"IS_DOMAINLISTED\"\n  );\n  if (storedDomainlisted) {\n    isDomainlisted = storedDomainlisted;\n  }\n}\n\nreloadVars();\n\n/******************************************************************************/\n/******************************************************************************/\n/**********       # Lisetener callbacks - Main functionality         **********/\n/******************************************************************************/\n/******************************************************************************/\n\n/*\n * The four following functions are all related to the four main listeners in\n * `background.js`. These four functions implement all the other helper\n * functions below\n */\n\nconst listenerCallbacks = {\n  /**\n   * Handles all signal processessing prior to sending request headers\n   * @param {object} details - retrieved info passed into callback\n   * @returns {array}\n   */\n  onBeforeSendHeaders: async (details) => {\n    await updateDomainlist(details);\n  },\n\n  /**\n   * @param {object} details - retrieved info passed into callback\n   */\n  onHeadersReceived: async (details) => {\n    //if (!setup){\n    //initSetup();\n    //}\n    await logData(details);\n    await sendData();\n\n\n\n  },\n\n  /**\n   * @param {object} details - retrieved info passed into callback\n   */\n  onBeforeNavigate: (details) => {\n    // Resets certain cached info\n  },\n\n  /**\n   * Adds DOM property\n   * @param {object} details - retrieved info passed into callback\n   */\n  onCommitted: async (details) => {\n    await updateDomainlist(details);\n  },\n\n  onCompleted: async (details) => {\n    await sendData();\n    await handleComplianceCheck(details);\n  }\n\n}; // closes listenerCallbacks object\n\n/******************************************************************************/\n/******************************************************************************/\n/**********      # Listener helper fxns - Main functionality         **********/\n/******************************************************************************/\n/******************************************************************************/\n\n\n/**\n * Fetches compliance data if not cached or cache is stale\n * @returns {Promise<Object|null>} - Metadata map containing stateCode or null if disabled/error\n */\nasync function getComplianceData() {\n  const stateCode = await getUserState();\n  if (!stateCode || stateCode === 'none') {\n    return null;\n  }\n\n  // Check if we have valid cached data in IndexedDB\n  const metadata = await storage.get(stores.complianceData, '_metadata');\n  \n  // Invalidate cache if state changed\n  if (metadata && metadata.stateCode && metadata.stateCode !== stateCode) {\n    await storage.clear(stores.complianceData);\n  } else if (metadata && isCacheValid(metadata.fetchedAt)) {\n    return metadata; // Cache is valid, return metadata to signify readiness\n  }\n\n  // Fetch fresh data for the selected state\n  try {\n    const result = await fetchComplianceData(stateCode);\n\n    // fetchComplianceData returns { error: 'fetch_error' } on network/server failure\n    if (result.error === 'fetch_error') {\n      console.warn(`Compliance data unavailable for ${stateCode} (server/network error)`);\n      return { _fetchError: true };\n    }\n\n    // Save individual domains to indexedDB instead of holding a huge object in memory\n    const dataKeys = Object.keys(result.data);\n    for (const key of dataKeys) {\n      await storage.set(stores.complianceData, result.data[key], key);\n    }\n\n    // Store metadata in storage (including viewUrl for the popup's dataset link)\n    const newMetadata = {\n      fetchedAt: result.fetchedAt,\n      count: result.count,\n      stateCode,\n      viewUrl: result.viewUrl || null,\n    };\n    await storage.set(stores.complianceData, newMetadata, '_metadata');\n\n    // Notify the popup that fresh compliance data is ready to be painted!\n    chrome.runtime.sendMessage({\n      msg: \"COMPLIANCE_DATA_READY\"\n    }).catch(e => {\n      // Ignored: Popup might be closed\n    });\n\n    console.log(`Fetched ${stateCode} compliance data for ${result.count} domains`);\n    return newMetadata;\n  } catch (error) {\n    console.error('Failed to fetch compliance data:', error);\n    return null;\n  }\n}\n\n/**\n * Looks up and stores compliance status for current domain after page load\n * @param {Object} details - callback object from onCompleted listener\n */\nasync function handleComplianceCheck(details) {\n  // Only check main frame navigations\n  if (details.frameId !== 0) return;\n\n  const stateCode = await getUserState();\n  if (!stateCode || stateCode === 'none') {\n    return;\n  }\n\n  try {\n    const url = new URL(details.url);\n    const parsed = psl.parse(url.hostname);\n    const domain = parsed.domain;\n\n    if (!domain) return;\n\n    console.log('Running compliance check for:', domain);\n\n    const metadata = await storage.get(stores.complianceData, '_metadata');\n    const cacheIsCold = !metadata || !isCacheValid(metadata.fetchedAt);\n    if (cacheIsCold) {\n      await storage.set(stores.settings, true, 'COMPLIANCE_LOADING');\n      // Tell popup we are loading\n      chrome.runtime.sendMessage({ msg: \"COMPLIANCE_DATA_LOADING\" }).catch(() => {});\n    }\n\n    // Wrap fetch + process + store in one try/finally so COMPLIANCE_LOADING is\n    // only cleared AFTER the domain status is in storage. Clearing it earlier\n    // caused the popup poll to read before the write completed (\"Not in Dataset\").\n    try {\n      const complianceDataMeta = await getComplianceData();\n\n      // Server/network was unreachable — store fetch_error so popup can show it\n      if (complianceDataMeta && complianceDataMeta._fetchError) {\n        await storage.set(stores.complianceData, { status: 'fetch_error' }, domain);\n        return;\n      }\n\n      if (!complianceDataMeta) {\n        console.log('No compliance data available');\n        return;\n      }\n\n      const status = await storage.get(stores.complianceData, domain) || DEFAULT_NO_DATA_STATUS;\n\n      console.log('Compliance status for', domain, ':', status.status);\n\n      // Write status to storage FIRST, then clear loading flag\n      await storage.set(stores.complianceData, status, domain);\n    } finally {\n      // Only clear after the write above completes (or on any error path)\n      if (cacheIsCold) {\n        await storage.set(stores.settings, false, 'COMPLIANCE_LOADING');\n        // Notify popup that load is successful and store is populated\n        chrome.runtime.sendMessage({ msg: \"COMPLIANCE_DATA_READY\" }).catch(() => {});\n      }\n    }\n  } catch (error) {\n    console.debug('Error in compliance check:', error);\n    // Ensure loading flag is cleared on unexpected errors\n    await storage.set(stores.settings, false, 'COMPLIANCE_LOADING');\n  }\n}\n\nasync function sendData() {\n  let activeTab = await chrome.tabs.query({ active: true, currentWindow: true });\n  let activeTabID = activeTab.length > 0 ? activeTab[0].id : null;\n\n  if (activeTabID === null) {\n    return;\n  }\n\n  let currentDomain = await getCurrentParsedDomain();\n  if (!currentDomain) {\n    return;\n  }\n\n  const partiesForTab = domPrev3rdParties?.[activeTabID];\n  const info = partiesForTab ? partiesForTab[currentDomain] : null;\n\n  if (!info) {\n    await storage.delete(stores.thirdParties, currentDomain);\n    return;\n  }\n\n  const data = Object.keys(info).filter(Boolean);\n  await storage.set(stores.thirdParties, data, currentDomain);\n\n}\n\n\nfunction getCurrentParsedDomain() {\n  return new Promise((resolve) => {\n    chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {\n      try {\n        const tab = tabs && tabs[0];\n        if (!tab || !tab.url) {\n          return resolve(null);\n        }\n        const url = new URL(tab.url);\n        const parsed = psl.parse(url.hostname);\n        const domain = parsed && parsed.domain ? parsed.domain : null;\n        globalParsedDomain = domain;  // for global scope variable\n        resolve(domain);\n      } catch (e) {\n        resolve(null);\n      }\n    });\n  });\n}\n\n\n/**\n * Checks whether a particular domain should receive a DNS signal\n * (1) Parse url to get domain for domainlist\n * (2) Update domains by adding current domain to domainlist in storage.\n * (3) Updates the 3rd party list for the currentDomain\n * (4) Check to see if we should send signal.\n * \n * Currently, it only adds to domainlist store as NULL if it doesnt exist\n * @param {Object} details - callback object according to Chrome API\n */\nasync function updateDomainlist(details) {\n  if (!details || !details.url) {\n    return;\n  }\n\n  let parsedDomain;\n  try {\n    let url = new URL(details.url);\n    let parsedUrl = psl.parse(url.hostname);\n    parsedDomain = parsedUrl.domain;\n  } catch (e) {\n    return;\n  }\n\n  if (!parsedDomain) {\n    return;\n  }\n\n  let currDomainValue = await storage.get(stores.domainlist, parsedDomain);\n  let id = details.tabId;\n\n  if (currDomainValue === undefined) {\n    await storage.set(stores.domainlist, null, parsedDomain); // Sets to storage async\n  }\n\n  let currentDomain = await getCurrentParsedDomain();\n  if (!currentDomain) {\n    return;\n  }\n\n  //get the current parsed domain--this is used to store 3rd parties (using globalParsedDomain variable)\n  if (!(id in domPrev3rdParties)) {\n    domPrev3rdParties[id] = {};\n  }\n  if (!(currentDomain in domPrev3rdParties[id])) {\n    domPrev3rdParties[id][currentDomain] = {};\n  }\n  //as they come in, add the parsedDomain to the object with null value (just a placeholder)\n  domPrev3rdParties[id][currentDomain][parsedDomain] = null;\n\n\n}\n\nfunction updatePopupIcon(tabId) {\n  chrome.action.setIcon({\n    tabId: tabId,\n    path: \"assets/face-icons/optmeow-face-circle-green-ring-128.png\",\n  });\n}\n\nasync function logData(details) {\n  let url = new URL(details.url);\n  let parsed = psl.parse(url.hostname);\n\n  if (tabs[details.tabId] === undefined) {\n    tabs[details.tabId] = { DOMAIN: null, REQUEST_DOMAINS: {}, TIMESTAMP: 0 };\n    tabs[details.tabId].REQUEST_DOMAINS[parsed.domain] = {\n      URLS: {},\n      RESPONSE: details.responseHeaders,\n      TIMESTAMP: details.timeStamp,\n    };\n    tabs[details.tabId].REQUEST_DOMAINS[parsed.domain].URLS = {\n      URL: details.url,\n      RESPONSE: details.responseHeaders,\n    };\n  } else {\n    if (tabs[details.tabId].REQUEST_DOMAINS[parsed.domain] === undefined) {\n      tabs[details.tabId].REQUEST_DOMAINS[parsed.domain] = {\n        URLS: {},\n        RESPONSE: details.responseHeaders,\n        TIMESTAMP: details.timeStamp,\n      };\n      tabs[details.tabId].REQUEST_DOMAINS[parsed.domain].URLS[details.url] = {\n        RESPONSE: details.responseHeaders,\n      };\n    } else {\n      tabs[details.tabId].REQUEST_DOMAINS[parsed.domain].URLS[details.url] = {\n        RESPONSE: details.responseHeaders,\n      };\n    }\n  }\n\n}\n\nasync function pullToDomainlistCache() {\n  let domain;\n  let domainlistKeys = await storage.getAllKeys(stores.domainlist);\n  let domainlistValues = await storage.getAll(stores.domainlist);\n\n  for (let key in domainlistKeys) {\n    domain = domainlistKeys[key];\n    domainlist[domain] = domainlistValues[key];\n  }\n}\n\nasync function syncDomainlists() {\n  // (1) Reconstruct a domainlist indexedDB object from storage\n  // (2) Iterate through local domainlist\n  // --- If item in cache NOT in domainlistKeys/domainlistDB, add to storage\n  //     via storage.set()\n  // (3) Iterate through all domain keys in indexedDB domainlist\n  // --- If key NOT in cached domainlist, add to cached domainlist\n\n  let domainlistKeys = await storage.getAllKeys(stores.domainlist);\n  let domainlistValues = await storage.getAll(stores.domainlist);\n  let domainlistDB = {};\n  let domain;\n  for (let key in domainlistKeys) {\n    domain = domainlistKeys[key];\n    domainlistDB[domain] = domainlistValues[key];\n  }\n\n  for (let domainKey in domainlist) {\n    if (!domainlistDB[domainKey]) {\n      await storage.set(stores.domainlist, domainlist[domainKey], domainKey);\n    }\n  }\n\n  for (let domainKey in domainlistDB) {\n    if (!domainlist[domainKey]) {\n      domainlist[domainKey] = domainlistDB[domainKey];\n    }\n  }\n}\n\n/**\n * whether the curr site should get privacy signals\n * (We need to try and make a synchronous version, esp. for DOM issue & related\n * message passing with the contentscript which injects the DOM signal)\n * @returns {bool} sendSignal\n */\nasync function sendPrivacySignal(domain) {\n  let sendSignal;\n  const extensionEnabled = await storage.get(stores.settings, \"IS_ENABLED\");\n  const extensionDomainlisted = await storage.get(\n    stores.settings,\n    \"IS_DOMAINLISTED\"\n  );\n  const domainDomainlisted = await storage.get(stores.domainlist, domain);\n\n  if (extensionEnabled) {\n    if (extensionDomainlisted) {\n      // Recall we must flip the value of the domainlisted domain\n      // due to how to how defined domainlisted values, corresponding to MV3\n      // declarativeNetRequest rule exceptions\n      // (i.e., null => no rule exists, valued => exception rule exists)\n      sendSignal = !domainDomainlisted ? true : false;\n    } else {\n      sendSignal = true;\n    }\n  } else {\n    sendSignal = false;\n  }\n  return sendSignal;\n}\n\n/******************************************************************************/\n/******************************************************************************/\n/**********          # Message Passing - Popup helper fxns           **********/\n/******************************************************************************/\n/******************************************************************************/\n\nfunction handleSendMessageError() {\n  const error = chrome.runtime.lastError;\n  if (error) {\n    console.warn(error.message);\n  }\n}\nasync function dataToPopupHelper() {\n  //data gets sent back every time the popup is clicked\n  let domain = await getCurrentParsedDomain();\n  if (!domain) {\n    return [];\n  }\n\n  let parties = await storage.get(stores.thirdParties, domain);\n  if (!Array.isArray(parties)) {\n    return [];\n  }\n\n  return parties;\n}\n\n// Info back to popup\nasync function dataToPopup(wellknownData) {\n  let requestsData = await dataToPopupHelper(); //get requests from the helper\n  chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {\n    let popupData = {\n      requests: requestsData,\n      wellknown: wellknownData,\n    };\n\n    chrome.runtime.sendMessage(\n      {\n        msg: \"POPUP_PROTECTION_DATA\",\n        data: popupData,\n      },\n      handleSendMessageError\n    );\n  });\n}\n\nasync function dataToPopupRequests() {\n  let requestsData = await dataToPopupHelper(); //get requests from the helper\n  console.log(\"requests data in DTPR: \", requestsData)\n\n  chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {\n    chrome.runtime.sendMessage(\n      {\n        msg: \"POPUP_PROTECTION_DATA_REQUESTS\",\n        data: requestsData,\n      },\n      handleSendMessageError\n    );\n  });\n}\n\n/******************************************************************************/\n/******************************************************************************/\n/**********                   # Message passing                      **********/\n/******************************************************************************/\n/******************************************************************************/\n\n/**\n * Currently only handles syncing domainlists between storage and memory\n * This runs when the popup disconnects from the background page\n * @param {Port} port\n */\nfunction onConnectHandler(port) {\n  if (port.name === \"POPUP\") {\n    port.onDisconnect.addListener(function () {\n      syncDomainlists();\n    });\n  }\n}\n\n/**\n * This is currently only to handle adding the GPC DOM signal.\n * I'm not sure how to fit it into an async call, it doesn't want to connect.\n * It would be nice to merge the two onMessage handlers.\n * TODO: This method still seems to have a timing issue. Doesn't always show DOM signal as thumbs up on reference site.\n * @returns {Bool} true (lets us send asynchronous responses to senders)\n */\nfunction onMessageHandlerSynchronous(message, sender, sendResponse) {\n  if (message.msg === \"APPEND_GPC_PROP\") {\n    let url = new URL(sender.origin);\n    let parsed = psl.parse(url.hostname);\n    let domain = parsed.domain;\n\n    const r = sendPrivacySignal(domain);\n    r.then((r) => {\n      const response = {\n        msg: \"APPEND_GPC_PROP_RESPONSE\",\n        sendGPC: r,\n      };\n      sendResponse(response);\n    });\n  }\n  //return true;\n}\n\n/**\n * Listeners for information from --POPUP-- or --OPTIONS-- page\n * This is the main \"hub\" for message passing between the extension components\n * https://developer.chrome.com/docs/extensions/mv3/messaging/\n  */\nasync function onMessageHandlerAsync(message, sender, sendResponse) {\n  if (message.msg === \"GET_WELLKNOWN_CHECK_ENABLED\") {\n    const enabled = await isWellknownCheckEnabled();\n    await chrome.storage.local.set({ WELLKNOWN_CHECK_ENABLED: enabled });\n    sendResponse({ enabled });\n    return true;\n  }\n  if (message.msg === \"USER_STATE_CHANGE\") {\n    console.log(\"User state changed, triggering immediate compliance fetch...\");\n\n    // Trigger fetch and update current tab\n    (async () => {\n      try {\n        // Set loading flag\n        await storage.set(stores.settings, true, \"COMPLIANCE_LOADING\");\n        chrome.runtime.sendMessage({ msg: \"COMPLIANCE_DATA_LOADING\" }).catch(() => {});\n\n        const dataMeta = await getComplianceData();\n        const tabs = await chrome.tabs.query({ active: true, currentWindow: true });\n        if (tabs.length > 0 && tabs[0].url) {\n          const url = new URL(tabs[0].url);\n          const parsed = psl.parse(url.hostname);\n          const domain = parsed.domain;\n          if (domain) {\n            // Server/network error — propagate fetch_error to the popup\n            if (dataMeta && dataMeta._fetchError) {\n              await storage.set(stores.complianceData, { status: 'fetch_error' }, domain);\n              console.warn('Compliance config server unreachable; stored fetch_error for active domain');\n            } else if (dataMeta) {\n              const status = await storage.get(stores.complianceData, domain) || DEFAULT_NO_DATA_STATUS;\n              await storage.set(stores.complianceData, status, domain);\n              console.log(`Updated compliance storage for active domain: ${domain}`);\n            }\n          }\n        }\n      } catch (e) {\n        console.error(\"Failed to fetch/update compliance data:\", e);\n      } finally {\n        // Clear loading flag\n        await storage.set(stores.settings, false, \"COMPLIANCE_LOADING\");\n        chrome.runtime.sendMessage({ msg: \"COMPLIANCE_DATA_READY\" }).catch(() => {});\n      }\n    })();\n    return true;\n  }\n  if (message.msg === \"TOGGLE_WELLKNOWN_CHECK\") {\n    const enabled = message.data?.enabled !== false;\n    await storage.set(stores.settings, enabled, \"WELLKNOWN_CHECK_ENABLED\");\n    await chrome.storage.local.set({ WELLKNOWN_CHECK_ENABLED: enabled });\n    if (!enabled) {\n      await storage.clear(stores.wellknownInformation);\n      wellknown = {};\n    }\n  }\n  if (message.msg === \"CHANGE_IS_DOMAINLISTED\") {\n    let isDomainlisted = message.data.isDomainlisted;\n    storage.set(stores.settings, isDomainlisted, \"IS_DOMAINLISTED\");\n  }\n  if (message.msg === \"SET_TO_DOMAINLIST\") {\n    let { domain, key } = message.data;\n    domainlist[domain] = key; // Sets to cache\n    addDynamicRule(id, domain);\n    storage.set(stores.domainlist, key, domain); // Sets to long term storage\n  }\n  if (message.msg === \"POPUP_PROTECTION_REQUESTS\") {\n    console.log(\"info queried\");\n    await dataToPopupRequests();\n  }\n  if (message.msg === \"CONTENT_SCRIPT_WELLKNOWN\") {\n    const wellknownCheckEnabled = await isWellknownCheckEnabled();\n    if (!wellknownCheckEnabled) {\n      return true;\n    }\n    // sender.origin not working for Firefox MV3, instead added a new message argument, message.origin_url\n    //let url = new URL(sender.origin);\n    let url = new URL(message.origin_url);\n    let parsed = psl.parse(url.hostname);\n    let domain = parsed.domain;\n\n    let tabID = sender.tab.id;\n    let wellknown = [];\n    let sendSignal = await storage.get(stores.domainlist, domain);\n\n    wellknown[tabID] = message.data;\n    let wellknownData = message.data;\n\n    await storage.set(stores.wellknownInformation, wellknownData, domain);\n\n    //await sendData();\n\n    if (wellknown[tabID] === null && sendSignal == null) {\n      updatePopupIcon(tabID);\n    } else if (wellknown[tabID][\"gpc\"] === true && sendSignal == null) {\n      chrome.action.setIcon({\n        tabId: tabID,\n        path: \"assets/face-icons/optmeow-face-circle-green-128.png\",\n      });\n    }\n    chrome.runtime.onMessage.addListener(async function (message, _, __) {\n      if (message.msg === \"POPUP_PROTECTION\") {\n        await dataToPopup(wellknownData);\n      }\n    });\n  }\n\n  if (message.msg === \"CONTENT_SCRIPT_TAB\") {\n    let url = new URL(sender.origin);\n    let parsed = psl.parse(url.hostname);\n    let domain = parsed.domain;\n    let tabID = sender.tab.id;\n    if (tabs[tabID] === undefined) {\n      tabs[tabID] = {\n        DOMAIN: domain,\n        REQUEST_DOMAINS: {},\n        TIMESTAMP: message.data,\n      };\n    } else if (tabs[tabID].DOMAIN !== domain) {\n      tabs[tabID].DOMAIN = domain;\n      let urls = tabs[tabID][\"REQUEST_DOMAINS\"];\n      for (let key in urls) {\n        if (urls[key][\"TIMESTAMP\"] >= message.data) {\n          tabs[tabID][\"REQUEST_DOMAINS\"][key] = urls[key];\n        } else {\n          delete tabs[tabID][\"REQUEST_DOMAINS\"][key];\n        }\n      }\n      tabs[tabID][\"TIMESTAMP\"] = message.data;\n    }\n  }\n  return true; // Async callbacks require this\n}\n\nfunction initMessagePassing() {\n  chrome.runtime.onConnect.addListener(onConnectHandler);\n  chrome.runtime.onMessage.addListener(onMessageHandlerAsync);\n  chrome.runtime.onMessage.addListener(onMessageHandlerSynchronous);\n}\n\nfunction closeMessagePassing() {\n  chrome.runtime.onConnect.removeListener(onConnectHandler);\n  chrome.runtime.onMessage.removeListener(onMessageHandlerAsync);\n  chrome.runtime.onMessage.removeListener(onMessageHandlerSynchronous);\n}\n\n/******************************************************************************/\n/******************************************************************************/\n/**********       # Other initializers - run once per enable         **********/\n/******************************************************************************/\n/******************************************************************************/\n\n/**\n * Listener for tab switch that updates the cached current tab variable\n */\nfunction onActivatedProtectionMode(info) {\n  activeTabID = info.tabId;\n  console.log(\"onActivatedProtectionMode called\");\n}\n\n// Handles misc. setup & setup listeners\nfunction initSetup() {\n  pullToDomainlistCache();\n\n  // Runs on startup to initialize the cached current tab variable\n  chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {\n    if (tabs.id) {\n      activeTabID = tabs.id;\n    }\n  });\n\n  chrome.tabs.onActivated.addListener(onActivatedProtectionMode);\n  setup = true;\n}\n\nfunction closeSetup() {\n  chrome.tabs.onActivated.removeListener(onActivatedProtectionMode);\n}\n\n/**\n * Inteded to facilitate transitioning between analysis & protection modes\n */\nfunction wipeLocalVars() {\n  domainlist = {}; // Caches & mirrors domainlist in storage\n  tabs = {}; // Caches all tab infomration, i.e. requests, etc.\n  wellknown = {}; // Caches wellknown info to be sent to popup\n  signalPerTab = {}; // Caches if a signal is sent to render the popup icon\n  activeTabID = 0; // Caches current active tab id\n  sendSignal = false; // Caches if the signal can be sent to the curr domain\n}\n\n/******************************************************************************/\n/******************************************************************************/\n/**********           # Exportable init / halt functions             **********/\n/******************************************************************************/\n/******************************************************************************/\n\nexport function init() {\n  reloadVars();\n  enableListeners(listenerCallbacks);\n  initMessagePassing();\n  initSetup();\n}\n\nexport function halt() {\n  disableListeners(listenerCallbacks);\n  closeMessagePassing();\n  closeSetup();\n  wipeLocalVars();\n}\n"
  },
  {
    "path": "src/background/storage.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\nstorage.js\n================================================================================\nstorage.js handles OptMeowt's reads/writes of data to some local location\n*/\n\nimport { openDB } from \"idb\";\nimport { reloadDynamicRules } from \"../common/editRules.js\";\nimport pkg from 'file-saver';\nconst { saveAs } = pkg;\n\n/******************************************************************************/\n/**************************  Enumerated settings  *****************************/\n/******************************************************************************/\n\n// In general, these functions should be use with async / await for\n// syntactic sweetness & synchronous data handling\n// i.e., await storage.set(stores.settings, extensionMode.enabled, 'MODE')\nconst stores = Object.freeze({\n  settings: \"SETTINGS\",\n  domainlist: \"DOMAINLIST\",\n  thirdParties: \"THIRDPARTIES\",\n  wellknownInformation: \"WELLKNOWNDATA\",\n  complianceData: \"COMPLIANCEDATA\",\n});\n\n/******************************************************************************/\n/*************************  Main Storage Functions  ***************************/\n/******************************************************************************/\n\nconst dbPromise = openDB(\"extensionDB\", 2, {\n  upgrade: function dbPromiseInternal(db, oldVersion) {\n    // Create stores that don't exist yet\n    if (!db.objectStoreNames.contains(stores.domainlist)) {\n      db.createObjectStore(stores.domainlist);\n    }\n    if (!db.objectStoreNames.contains(stores.settings)) {\n      db.createObjectStore(stores.settings);\n    }\n    if (!db.objectStoreNames.contains(stores.thirdParties)) {\n      db.createObjectStore(stores.thirdParties);\n    }\n    if (!db.objectStoreNames.contains(stores.wellknownInformation)) {\n      db.createObjectStore(stores.wellknownInformation);\n    }\n    // New in version 2\n    if (oldVersion < 2 && !db.objectStoreNames.contains(stores.complianceData)) {\n      db.createObjectStore(stores.complianceData);\n    }\n  },\n});\n\nconst storage = {\n  async get(store, key) {\n    if (typeof key === \"undefined\") {\n      return undefined;\n    }\n    return (await dbPromise).get(store, key);\n  },\n  async getAll(store) {\n    return (await dbPromise).getAll(store);\n  },\n  async getAllKeys(store) {\n    return (await dbPromise).getAllKeys(store);\n  },\n  // returns an object containing the given store\n  async getStore(store) {\n    const storeValues = await storage.getAll(store);\n    const storeKeys = await storage.getAllKeys(store);\n    let storeCopy = {};\n    let key;\n    for (let index in storeKeys) {\n      key = storeKeys[index];\n      storeCopy[key] = storeValues[index];\n    }\n    return storeCopy;\n  },\n  async set(store, value, key) {\n    if (typeof key === \"undefined\") {\n      return undefined;\n    }\n    return (await dbPromise).put(store, value, key);\n  },\n  async delete(store, key) {\n    if (typeof key === \"undefined\") {\n      return undefined;\n    }\n    return (await dbPromise).delete(store, key);\n  },\n  async clear(store) {\n    return (await dbPromise).clear(store);\n  },\n};\n\n/******************************************************************************/\n/*********************  Importing/Exporting Domain List  **********************/\n/******************************************************************************/\n\nasync function handleDownload() {\n  const DOMAINLIST = await storage.getStore(stores.domainlist);\n  let MANIFEST = chrome.runtime.getManifest();\n  let data = {\n    VERSION: MANIFEST.version,\n    DOMAINLIST: DOMAINLIST,\n  };\n\n  let blob = new Blob([JSON.stringify(data, null, 4)], {\n    type: \"text/plain;charset=utf-8\",\n  });\n  saveAs(blob, \"OptMeowt_backup.json\");\n}\n\n/**\n * Sets-up the process for importing a saved domainlist backup\n */\nasync function startUpload() {\n  document.getElementById(\"upload-domainlist\").value = \"\";\n  document.getElementById(\"upload-domainlist\").click();\n}\n\n/**\n * Imports and updates the domainlist in local storage with an imported backup\n */\nasync function handleUpload() {\n  await storage.clear(stores.domainlist);\n  const file = this.files[0];\n  const fr = new FileReader();\n  fr.onload = function (e) {\n    const UPLOADED_DATA = JSON.parse(e.target.result);\n    let version = UPLOADED_DATA.VERSION;\n    let domainlist = UPLOADED_DATA.DOMAINLIST;\n    version = version.split(\".\");\n\n    let domainlist_keys = Object.keys(domainlist);\n    let domainlist_vals = Object.values(domainlist);\n    for (let i = 0; i < domainlist_keys.length; i++) {\n      try {\n        storage.set(stores.domainlist, domainlist_vals[i], domainlist_keys[i]);\n      } catch (error) {\n        alert(\"Error loading list\");\n      }\n    }\n    // hardcode if it is the new version // check\n    if (Number(version[0]) >= 3) {\n      reloadDynamicRules();\n      updateRemovalScript();\n    } else {\n      chrome.runtime.sendMessage({\n        msg: \"FORCE_RELOAD\",\n      });\n    }\n  };\n  fr.readAsText(file);\n}\n\n/******************************************************************************/\n/******************************************************************************/\n/******************************************************************************/\n\nexport { handleDownload, startUpload, handleUpload, stores, storage };\n"
  },
  {
    "path": "src/common/editDomainlist.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\neditDomainlist.js\n================================================================================\neditDomainlist.js is an internal API modifying the domainlist / modifying the\ndomainlist simultaneously with the dynamic ruleset\n*/\n\nimport { storage, stores } from \"../background/storage.js\";\nimport {\n  deleteAllDynamicRules,\n  deleteDynamicRule,\n  addDynamicRule,\n  getFreshId,\n} from \"./editRules.js\";\n\n/* # Debugging */\n// debug_domainlist_and_dynamicrules\n// print_rules_and_domainlist\n\n/******************************************************************************/\n/******************************************************************************/\n/**********                  # Standard Operation                    **********/\n/******************************************************************************/\n/******************************************************************************/\n\nasync function updateRemovalScript() {\n    let ex_matches = [\"https://example.org/foo/bar.html\"];\n    let domain;\n    let domainValue;\n    const domainlistKeys = await storage.getAllKeys(stores.domainlist);\n    const domainlistValues = await storage.getAll(stores.domainlist);\n    for (let index in domainlistKeys) {\n      domain = domainlistKeys[index];\n      domainValue = domainlistValues[index];\n      if (domainValue != null) {\n        ex_matches.push(\"https://\" + domain + \"/*\");\n        ex_matches.push(\"https://www.\" + domain + \"/*\");\n      }\n    }\n    chrome.scripting\n      .updateContentScripts([\n        {\n          id: \"1\",\n          matches: [\"<all_urls>\"],\n          excludeMatches: ex_matches,\n          js: [\"content-scripts/registration/gpc-dom.js\"],\n          runAt: \"document_start\",\n        },\n      ])\n      .then(() => {});\n  }\n\nasync function createCS(domain){\n    let script = await chrome.scripting.getRegisteredContentScripts({\n    });\n\n    let ex_matches = script[0].excludeMatches;\n\n    ex_matches.push(\"https://\" + domain + \"/*\");\n    ex_matches.push(\"https://www.\" + domain + \"/*\");\n\n    await chrome.scripting.updateContentScripts([\n      {\n        id: \"1\",\n        matches: [\"<all_urls>\"],\n        excludeMatches: ex_matches,\n        js: [\"content-scripts/registration/gpc-dom.js\"],\n        runAt: \"document_start\",\n      },\n    ])\n    .then(() => {});\n  }\n\nasync function deleteCS(domain){\n    let script = await chrome.scripting.getRegisteredContentScripts({\n    });\n    let ex_matches = script[0].excludeMatches;\n    function removeItemOnce(arr, value) {\n      var index = arr.indexOf(value);\n      if (index > -1) {\n        arr.splice(index, 1);\n      }\n      return arr;\n    }\n\n    ex_matches = removeItemOnce(ex_matches,\"https://\" + domain + \"/*\");\n    ex_matches = removeItemOnce(ex_matches,\"https://www.\" + domain + \"/*\");\n    await chrome.scripting.updateContentScripts([\n      {\n        id: \"1\",\n        matches: [\"<all_urls>\"],\n        excludeMatches: ex_matches,\n        js: [\"content-scripts/registration/gpc-dom.js\"],\n        runAt: \"document_start\",\n      },\n    ])\n    .then(() => {});\n  }\n\nasync function deleteDomainlistAndDynamicRules() {\n  await storage.clear(stores.domainlist);\n    deleteAllDynamicRules();\n  }\n\nasync function addDomainToDomainlistAndRules(domain) {\n  let id = 1;\n    id = await getFreshId();\n    addDynamicRule(id, domain); // add the rule for the chosen domain\n    createCS(domain);\n    await storage.set(stores.domainlist, id, domain); // record what rule the domain is associated to\n}\n\nasync function removeDomainFromDomainlistAndRules(domain) {\n    let id = await storage.get(stores.domainlist, domain);\n    deleteDynamicRule(id);\n    deleteCS(domain);\n    await storage.set(stores.domainlist, null, domain);\n  }\n\n/******************************************************************************/\n/******************************************************************************/\n/**********                      # Debugging                         **********/\n/******************************************************************************/\n/******************************************************************************/\n\nasync function debug_domainlist_and_dynamicrules() {\n  let sampleSites = [ // not called\n    \"a.com\",\n    \"b.com\",\n    \"c.com\",\n    \"d.com\",\n    \"e.com\",\n    \"f.com\",\n    \"g.com\",\n    \"h.com\",\n    \"i.com\",\n    \"j.com\",\n  ];\n  await deleteDomainlistAndDynamicRules();\n  await print_rules_and_domainlist();\n  addDynamicRule(2, \"nytimes.com\"); // add the rule for the chosen domain\n  await storage.set(stores.domainlist, 2, \"nytimes.com\"); // record what rule the domain is associated to\n\n  await print_rules_and_domainlist();\n  await addDomainToDomainlistAndRules(\"a.com\");\n  await addDomainToDomainlistAndRules(\"b.com\");\n  await addDomainToDomainlistAndRules(\"c.com\");\n  await addDomainToDomainlistAndRules(\"d.com\");\n  await addDomainToDomainlistAndRules(\"e.com\");\n  await addDomainToDomainlistAndRules(\"f.com\");\n  await addDomainToDomainlistAndRules(\"g.com\");\n  await addDomainToDomainlistAndRules(\"h.com\");\n  await addDomainToDomainlistAndRules(\"i.com\");\n  await addDomainToDomainlistAndRules(\"j.com\");\n  await print_rules_and_domainlist();\n}\n\nasync function print_rules_and_domainlist() {\n  let rules = await chrome.declarativeNetRequest.getDynamicRules();\n  let domainlist = await storage.getStore(stores.domainlist);\n  console.log(\n    \"Here are the curr dynamic rules:\",\n    rules,\n    \"Here is our curr domainlist: \",\n    domainlist\n  );\n}\n\n/******************************************************************************/\n/******************************************************************************/\n/******************************************************************************/\n\nexport {\n  deleteDomainlistAndDynamicRules,\n  addDomainToDomainlistAndRules,\n  removeDomainFromDomainlistAndRules,\n  updateRemovalScript,\n  debug_domainlist_and_dynamicrules,\n  print_rules_and_domainlist,\n  deleteCS,\n  createCS\n};\n"
  },
  {
    "path": "src/common/editRules.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\nimport { storage, stores } from \"../background/storage.js\";\n\n/*\neditRules.js\n================================================================================\neditRules.js is an internal API for adding/removing GPC-exclusion dynamic rules\n*/\n\n/**\n * Gets fresh rule ID for new DeclarativeNetRequest dynamic rule\n * Pulls from already set dynamic rules as opposed to domainlist values\n *\n * NOTE:  Does not 'reserve' the ID. If it isn't used on the client side,\n *        getFreshId() will spit out the same val next call.\n * @returns {Promise<(number|null)>} - number of fresh ID, null if non available\n */\nexport async function getFreshId() {\n  const MAX_RULES =\n    chrome.declarativeNetRequest.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES;\n  const rules = await chrome.declarativeNetRequest.getDynamicRules();\n  let freshId = null;\n  let usedRuleIds = [];\n\n  for (let i in rules) {\n    usedRuleIds.push(rules[i][\"id\"]);\n  }\n  usedRuleIds.sort((a, b) => {\n    return a - b;\n  }); // Necessary for next for loop\n\n  // Make sure the ID starts at 1 (I think 0 is reserved?)\n  for (let i = 1; i < MAX_RULES; i++) {\n    if (i !== usedRuleIds[i - 1]) {\n      freshId = i; // We have found the first nonzero, unused id\n      break;\n    }\n  }\n  return freshId;\n}\n\n/**\n * Deletes GPC-exclusion rule from rule set\n * Does NOT remove from domainlist\n * (see declarativeNetRequest)\n * @param {number} id - rule id\n */\nexport async function deleteDynamicRule(id) {\n  let UpdateRuleOptions = { removeRuleIds: [id] };\n  await chrome.declarativeNetRequest.updateDynamicRules(UpdateRuleOptions);\n}\n\n/**\n * Deletes all GPC-exclusion dynamic rules\n * (see declarativeNetRequest)\n */\nexport async function deleteAllDynamicRules() {\n  let MAX_RULES =\n    chrome.declarativeNetRequest.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES;\n  let UpdateRuleOptions = { removeRuleIds: [...Array(MAX_RULES).keys()] };\n  await chrome.declarativeNetRequest.updateDynamicRules(UpdateRuleOptions);\n}\n\n/**\n * Adds domain as a rule to be excluded from receiving GPC signals\n * Note id should be fresh, o/w it will overwrite existing rule\n * (see getFreshId, declarativeNetRequest)\n * @param {number} id - rule id\n * @param {string} domain - domain to associate with id\n */\nexport async function addDynamicRule(id, domain) {\n  let UpdateRuleOptions = {\n    addRules: [\n      {\n        id: id,\n        priority: 2,\n        action: {\n          type: \"modifyHeaders\",\n          requestHeaders: [\n            { \"header\": \"Sec-GPC\", \"operation\": \"remove\" },\n            { \"header\": \"DNT\", \"operation\": \"remove\" },\n            { \"header\": \"Permissions-Policy\", \"operation\": \"remove\"}\n          ],\n        },\n        condition: {\n          urlFilter: domain,\n          resourceTypes: [\n            \"main_frame\",\n            \"sub_frame\",\n            \"stylesheet\",\n            \"script\",\n            \"image\",\n            \"font\",\n            \"object\",\n            \"xmlhttprequest\",\n            \"ping\",\n            \"csp_report\",\n            \"media\",\n            \"websocket\",\n            \"other\",\n          ],\n        },\n      },\n    ],\n    removeRuleIds: [id],\n  };\n  await chrome.declarativeNetRequest.updateDynamicRules(UpdateRuleOptions);\n  return;\n}\n\n/**\n * Deletes all rules, queries current domainlist, and re-adds all rules\n * - Useful when replacing the domainlist via an import/export\n * - Remember rules as of v3.0.0 are 'exclusion' rules, i.e. excluded from\n *   receiving GPC or other opt-outs.\n */\nexport async function reloadDynamicRules() {\n    deleteAllDynamicRules();\n    let domainlist = await storage.getStore(stores.domainlist);\n\n    let promises = [];\n    Object.keys(domainlist).forEach(async (domain) => {\n      promises.push(\n        new Promise(async (resolve, reject) => {\n          let id = domainlist[domain];\n          if (id) {\n            await addDynamicRule(id, domain);\n          }\n          resolve();\n        })\n      );\n    });\n  }\n"
  },
  {
    "path": "src/common/settings.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\nsettings.js\n================================================================================\nShared helpers related to extension settings.\n*/\n\nimport { storage, stores } from \"../background/storage.js\";\n\n/**\n * Returns whether the well-known check is enabled.\n * Defaults to true unless explicitly disabled.\n * @returns {Promise<boolean>}\n */\nexport async function isWellknownCheckEnabled() {\n  const enabled = await storage.get(stores.settings, \"WELLKNOWN_CHECK_ENABLED\");\n  return enabled !== false;\n}\n\n/**\n * Returns the user's selected state code (CA, CO, CT, NJ) or null if not set.\n * A value of \"none\" means the user explicitly chose not to be in a covered state.\n * @returns {Promise<string|null>}\n */\nexport async function getUserState() {\n  return await storage.get(stores.settings, \"USER_STATE\") || null;\n}\n\n/**\n * Returns whether the compliance check should be shown.\n * True when a valid state is selected (not null, not \"none\").\n * @returns {Promise<boolean>}\n */\nexport async function isComplianceCheckEnabled() {\n  const state = await getUserState();\n  return state !== null && state !== \"none\";\n}\n"
  },
  {
    "path": "src/content-scripts/contentScript.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\ncontentScripts.js\n================================================================================\ncontentScripts.js runs on every page and passes data to the background page\nhttps://developer.chrome.com/extensions/content_scripts\n*/\n\n// Here is a resource I used to help setup the inject script functionality as\n// well as setup message listeners to pass data back to the background\n// https://www.freecodecamp.org/news/chrome-extension-message-passing-essentials/\n\n/******************************************************************************/\n/******************************************************************************/\n/**********              # USPAPI call helper functions              **********/\n/******************************************************************************/\n/******************************************************************************/\n\n\n// To be injected to call the USPAPI function in analysis mode\nconst uspapi = `\n  try {\n    __uspapi('getUSPData', 1, (data) => {\n      let currURL = document.URL\n      window.postMessage({ type: \"USPAPI_TO_CONTENT_SCRIPT\", result: data, url: currURL });\n    });\n  }\n`;\n\nconst uspapiRequest = `\n  try {\n    __uspapi('getUSPData', 1, (data) => {\n      let currURL = document.URL\n      window.postMessage({ type: \"USPAPI_TO_CONTENT_SCRIPT_REQUEST\", result: data, url: currURL });\n    });\n  } catch (e) {\n    window.postMessage({ type: \"USPAPI_TO_CONTENT_SCRIPT_REQUEST\", result: \"USPAPI_FAILED\" });\n  }\n`;\n\nfunction injectScript(script) {\n  const scriptElem = document.createElement(\"script\");\n  scriptElem.innerHTML = script;\n  document.documentElement.prepend(scriptElem);\n}\n\nasync function isWellknownCheckEnabled() {\n  try {\n    const { WELLKNOWN_CHECK_ENABLED } = await chrome.storage.local.get(\n      \"WELLKNOWN_CHECK_ENABLED\"\n    );\n    if (typeof WELLKNOWN_CHECK_ENABLED === \"boolean\") {\n      return WELLKNOWN_CHECK_ENABLED;\n    }\n  } catch (error) {\n    print(error);\n  }\n  try {\n    const response = await chrome.runtime.sendMessage({\n      msg: \"GET_WELLKNOWN_CHECK_ENABLED\",\n    });\n    if (response && typeof response.enabled === \"boolean\") {\n      return response.enabled;\n    }\n  } catch (error) {\n    print(error);\n  }\n  // If we can't determine the setting, err on the side of not fetching.\n  return false;\n}\n\n/******************************************************************************/\n/******************************************************************************/\n/**********                   # Main functionality                   **********/\n/******************************************************************************/\n/******************************************************************************/\n\nasync function getWellknown(url) {\n  const response = await fetch(`${url.origin}/.well-known/gpc.json`);\n  new_url = url = JSON.parse(JSON.stringify(url));\n  let wellknownData;\n  try {\n    wellknownData = await response.json();\n  } catch {\n    wellknownData = null;\n  }\n  chrome.runtime.sendMessage({\n    msg: \"CONTENT_SCRIPT_WELLKNOWN\",\n    data: wellknownData,\n    origin_url: new_url\n  });\n}\n\n/**\n * Passes info to background scripts for processing via messages\n * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendMessage\n * There are other ways to do this, but I use an IIFE to run everything at once\n * https://developer.mozilla.org/en-US/docs/Glossary/IIFE\n */\n(() => {\n  /*   MAIN CONTENT SCRIPT PROCESSES GO HERE   */\n\n  let url = new URL(location); // location object\n  isWellknownCheckEnabled().then((shouldCheck) => {\n    if (shouldCheck) {\n      getWellknown(url);\n    }\n  });\n})();\n\n/******************************************************************************/\n/******************************************************************************/\n/**********    # Message passing from injected script via window     **********/\n/******************************************************************************/\n/******************************************************************************/\n\nchrome.runtime.onMessage.addListener(function (message, sender, sendResponse) { // check unused arguments\n  if (message.msg === \"USPAPI_FETCH_REQUEST\") {\n    injectScript(uspapiRequest);\n  }\n});\n\nwindow.addEventListener(\n  \"message\",\n  function (event) {\n    if (\n      event.data.type == \"USPAPI_TO_CONTENT_SCRIPT\"\n    ) {\n      chrome.runtime.sendMessage({\n        msg: \"USPAPI_TO_BACKGROUND\",\n        data: event.data.result,\n        location: this.location.href,\n      });\n    }\n    if (event.data.type == \"USPAPI_TO_CONTENT_SCRIPT_REQUEST\") {\n      chrome.runtime.sendMessage({\n        msg: \"USPAPI_TO_BACKGROUND_FROM_FETCH_REQUEST\",\n        data: event.data.result,\n        location: this.location.href,\n      });\n    }\n  },\n  false\n);\n"
  },
  {
    "path": "src/content-scripts/injection/gpc-dom.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\ninjection/gpc-dom.js\n================================================================================\ninjection/gpc-dom.js is the static script injected by a related \ncontent script (registered from the extension service worker) for full DOM access\n*/\n\n/**\n * Sets Global Privacy Control (GPC) JavaScript property on the DOM\n */\nfunction setDomSignal() {\n  try {\n    var GPCVal = true;\n    \n    const GPCDomVal = `Object.defineProperties(Navigator.prototype, \n      { \"globalPrivacyControl\": {\n\t\t   get: () => ${GPCVal},\n\t\t   configurable: true,\n\t\t   enumerable: true\n\t   }});\n    document.currentScript.parentElement.removeChild(document.currentScript);\n\t   `;\n\n    const GPCDomElem = document.createElement(\"script\");\n    GPCDomElem.innerHTML = GPCDomVal;\n    document.documentElement.prepend(GPCDomElem);\n\n  } catch (e) {\n    console.error(`Failed to set DOM signal: ${e}`);\n  }\n}\n\nsetDomSignal();\n"
  },
  {
    "path": "src/content-scripts/registration/gpc-dom.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\nregistration/gpc-dom.js\n================================================================================\nregistration/gpc-dom.js is the content script, registered via the \nextension service worker, that injects another static script to provide full\nDOM access and permissions\n*/\n\n// High Level:\n// Static script that will be injected onto a page to allow it full access, not\n// an isolated-world access (see Chrome extension API docs on isolated worlds).\n// Necessary to inject the GPC JS property on a page via full DOM permission.\n\n// Requirements:\n// - INJECTION_SCRIPT must also be defined under \"web_accessible_resources\"\n// - This url must be a (semi) absolute path from the compiled project to the script\n//   (Please see webpack output file directory structure)\n// - This script must be registered from the extension service worker w/ same URL\nconst INJECTION_SCRIPT = \"content-scripts/injection/gpc-dom.js\";\n\n// Based on\n// https://stackoverflow.com/questions/9515704/use-a-content-script-to-access-the-page-context-variables-and-functions\nfunction injectStaticScript() {\n  let s = document.createElement(\"script\");\n  s.src = chrome.runtime.getURL(INJECTION_SCRIPT);\n  s.online = function () {\n    this.remove();\n  };\n  document.documentElement.prepend(s);\n}\ninjectStaticScript();\n"
  },
  {
    "path": "src/data/complianceData.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\ncomplianceData.js\n================================================================================\nFetches and processes GPC compliance data from state-specific hosted CSV files.\nCSV URLs are read dynamically from states.json hosted on GitHub so they can be\nupdated server-side without requiring an extension update.\n\nCompliance is determined by set membership and signal content:\n  - Domain in noncompliant_sites        → 'non_compliant'\n  - Domain in all_sites, signals found  → 'compliant'\n  - Domain in all_sites, all signals null (nothing to measure against)   → 'no_signals'\n  - Domain in neither                   → 'no_data'  (handled by caller)\n  - Network/server error                → 'fetch_error' (handled by caller)\n\nThe 8 signal columns checked for null:\n  uspapi_before_gpc, uspapi_after_gpc,\n  usp_cookies_before_gpc, usp_cookies_after_gpc,\n  OptanonConsent_before_gpc, OptanonConsent_after_gpc,\n  gpp_before_gpc, gpp_after_gpc\n\nWhen the all_sites CSV includes a `compliance_classification` column (per the\nschema in gpc-web-ui at client/background_reading/COMPLIANCE_CLASSIFICATION_OVERVIEW.md\n— https://github.com/privacy-tech-lab/gpc-web-ui/blob/master/client/background_reading/COMPLIANCE_CLASSIFICATION_OVERVIEW.md),\nthe parsed JSON object is attached to each domain entry as `classification` so\nthe popup can show per-family status (USPS, OptanonConsent, Well-known, GPP).\n*/\n\nconst STATES_JSON_URL =\n  'https://raw.githubusercontent.com/privacy-tech-lab/gpc-web-ui/master/client/public/states.json'; // Permalink to raw states.json — use raw.githubusercontent.com so fetch() gets JSON, not an HTML page\n\n// Human-readable state names (used to look up entries in states.json)\nexport const STATE_NAMES = {\n  CA: 'California',\n  CO: 'Colorado',\n  CT: 'Connecticut',\n  NJ: 'New Jersey',\n};\n\n\n\n// Cache TTL: 24 hours\nconst CACHE_TTL_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Converts a GitHub blob URL to a raw.githubusercontent.com URL so fetch()\n * receives the raw file content rather than an HTML page.\n * e.g. https://github.com/ORG/REPO/blob/SHA/path/file.csv\n *   →  https://raw.githubusercontent.com/ORG/REPO/SHA/path/file.csv\n * @param {string} blobUrl\n * @returns {string}\n */\nfunction toRawUrl(blobUrl) {\n  // Handle GitHub blob URLs\n  const githubMatch = blobUrl.match(\n    /^https:\\/\\/github\\.com\\/([^/]+\\/[^/]+)\\/blob\\/(.+)$/\n  );\n  if (githubMatch) {\n    return `https://raw.githubusercontent.com/${githubMatch[1]}/${githubMatch[2]}`;\n  }\n  // If it's already a raw URL or something else, return as-is\n  return blobUrl;\n}\n\n/**\n * Fetches states.json from the remote host to retrieve current CSV URLs.\n * @returns {Promise<Object>} - Parsed states.json content\n */\nasync function fetchStatesConfig() {\n  const response = await fetch(STATES_JSON_URL);\n  if (!response.ok) {\n    throw new Error(`Failed to fetch states.json (${response.status})`);\n  }\n  return response.json();\n}\n\n// The 8 privacy-signal column names we check for in all_sites\nconst SIGNAL_COLUMNS = [\n  'uspapi_before_gpc',\n  'uspapi_after_gpc',\n  'usp_cookies_before_gpc',\n  'usp_cookies_after_gpc',\n  'optanonconsent_before_gpc',\n  'optanonconsent_after_gpc',\n  'gpp_before_gpc',\n  'gpp_after_gpc',\n];\n\n/**\n * Returns true if a column value counts as \"null\" (no signal present).\n * @param {string|undefined} val\n * @returns {boolean}\n */\nfunction isNullSignal(val) {\n  return !val || val.trim() === '' || val.trim().toLowerCase() === 'null';\n}\n\n/**\n * Parses a single CSV line, honoring double-quote-wrapped fields with `\"\"`\n * escapes. Required because some columns (urlClassification, third_party_urls,\n * compliance_classification, …) contain quoted JSON with embedded commas.\n * Naive `line.split(',')` mis-aligns columns past the first quoted field.\n * Assumes no embedded newlines inside quoted fields, which holds for these CSVs.\n * @param {string} line\n * @returns {string[]}\n */\nfunction parseCSVLine(line) {\n  const out = [];\n  let cur = '';\n  let inQuotes = false;\n  for (let i = 0; i < line.length; i++) {\n    const ch = line[i];\n    if (inQuotes) {\n      if (ch === '\"') {\n        if (line[i + 1] === '\"') { cur += '\"'; i++; }\n        else { inQuotes = false; }\n      } else {\n        cur += ch;\n      }\n    } else if (ch === ',') {\n      out.push(cur); cur = '';\n    } else if (ch === '\"' && cur === '') {\n      inQuotes = true;\n    } else {\n      cur += ch;\n    }\n  }\n  out.push(cur);\n  return out;\n}\n\n/**\n * Parses the `compliance_classification` field, which the upstream crawler\n * currently emits as a Python repr (single quotes, None/True/False) rather\n * than real JSON. We coerce to JSON before parsing so the field is usable.\n * Returns null if the value can't be parsed under either dialect.\n *\n * TODO: the blind `'` → `\"` replace will mangle any string value that contains\n * an apostrophe. Today the schema only carries enum statuses with no free-form\n * strings, so this is safe, but the real fix is upstream: have the crawler\n * emit real JSON (json.dumps) instead of a Python repr in this column.\n * @param {string} raw\n * @returns {object|null}\n */\nfunction parseClassification(raw) {\n  try { return JSON.parse(raw); } catch (_) {}\n  const jsonish = raw\n    .replace(/\\bNone\\b/g, 'null')\n    .replace(/\\bTrue\\b/g, 'true')\n    .replace(/\\bFalse\\b/g, 'false')\n    .replace(/'/g, '\"');\n  try { return JSON.parse(jsonish); } catch (_) { return null; }\n}\n\n/**\n * Fetches a CSV from the given URL and returns a Set of domain strings.\n * Used for the noncompliant_sites CSV where we only need domain membership.\n * Assumes a column header named \"domain\" (case-insensitive).\n * @param {string} url - Direct download URL for the CSV\n * @returns {Promise<Set<string>>} - Set of domain names found in the CSV\n */\nasync function fetchDomainSet(url) {\n  const response = await fetch(url);\n  if (!response.ok) {\n    throw new Error(`Failed to fetch CSV (${response.status}): ${url}`);\n  }\n\n  const csvText = await response.text();\n\n  if (csvText.trim().startsWith('<!DOCTYPE html>') || csvText.includes('Google Drive - Virus scan warning')) {\n    throw new Error('CSV fetch returned HTML (possible Google Drive virus-scan redirect)');\n  }\n\n  const lines = csvText.split(/\\r?\\n/);\n  if (lines.length < 2) return new Set();\n\n  // Find the index of the \"domain\" column from the header row\n  const headers = lines[0].split(',').map(h => h.trim().toLowerCase());\n  const domainIndex = headers.indexOf('domain');\n  if (domainIndex === -1) {\n    throw new Error('CSV does not contain a \"domain\" column');\n  }\n\n  const domains = new Set();\n  for (let i = 1; i < lines.length; i++) {\n    const line = lines[i].trim();\n    if (!line) continue;\n    // Simple split — domain names don't contain commas or quotes\n    const cols = line.split(',');\n    const domain = cols[domainIndex] ? cols[domainIndex].trim() : null;\n    if (domain) {\n      domains.add(domain);\n    }\n  }\n\n  return domains;\n}\n\n/**\n * Fetches the all_sites CSV and returns a Map of domain →\n *   { allNull: boolean, classification: object|null }.\n * - allNull is true when all 8 privacy-signal columns are null/empty, meaning\n *   we could not observe any consent mechanism to measure GPC compliance against.\n * - classification is the parsed `compliance_classification` JSON object when\n *   the column is present and parseable, otherwise null.\n * @param {string} url - Direct download URL for the all_sites CSV\n * @returns {Promise<Map<string, {allNull: boolean, classification: object|null}>>}\n */\nasync function fetchAllSitesData(url) {\n  const response = await fetch(url);\n  if (!response.ok) {\n    throw new Error(`Failed to fetch CSV (${response.status}): ${url}`);\n  }\n\n  const csvText = await response.text();\n\n  if (csvText.trim().startsWith('<!DOCTYPE html>') || csvText.includes('Google Drive - Virus scan warning')) {\n    throw new Error('CSV fetch returned HTML (possible Google Drive virus-scan redirect)');\n  }\n\n  const lines = csvText.split(/\\r?\\n/);\n  if (lines.length < 2) return new Map();\n\n  const headers = parseCSVLine(lines[0]).map(h => h.trim().toLowerCase());\n  const domainIndex = headers.indexOf('domain');\n  if (domainIndex === -1) {\n    throw new Error('all_sites CSV does not contain a \"domain\" column');\n  }\n\n  // Map each signal column name to its index (-1 if not present)\n  const signalIndices = SIGNAL_COLUMNS.map(col => headers.indexOf(col));\n  const classificationIndex = headers.indexOf('compliance_classification');\n\n  const result = new Map();\n  for (let i = 1; i < lines.length; i++) {\n    const line = lines[i];\n    if (!line || !line.trim()) continue;\n    const cols = parseCSVLine(line);\n    const domain = cols[domainIndex] ? cols[domainIndex].trim() : null;\n    // Skip rows where domain itself is missing or the literal string \"null\"\n    // (happens when the crawler couldn't resolve the domain, e.g. status=\"not added\")\n    if (!domain || domain.toLowerCase() === 'null') continue;\n\n    // A domain is allNull only if every signal column is null/empty\n    const allNull = signalIndices.every(idx => idx === -1 || isNullSignal(cols[idx]));\n\n    let classification = null;\n    if (classificationIndex !== -1) {\n      const raw = cols[classificationIndex];\n      if (raw && raw.trim() && raw.trim().toLowerCase() !== 'null') {\n        classification = parseClassification(raw);\n      }\n    }\n\n    result.set(domain, { allNull, classification });\n  }\n\n  return result;\n}\n\n/**\n * Fetches and processes compliance data for a specific state.\n * First fetches states.json from the remote host to get current CSV URLs,\n * then fetches both all_sites and noncompliant_sites CSVs in parallel.\n * Determines status by set membership and signal content:\n *   - in noncompliant_sites              → 'non_compliant'\n *   - in all_sites, signals detected     → 'compliant'\n *   - in all_sites, all signals null     → 'no_signals'\n * Domains absent from all_sites are not included in the returned map;\n * the caller treats missing entries as 'no_data'.\n * Any network or server error returns { error: 'fetch_error' }.\n *\n * @param {string} stateCode - Two-letter state code (CA, CO, CT, NJ)\n * @returns {Promise<Object>} - { data, fetchedAt, stateCode, count } or { error: 'fetch_error' }\n */\nexport async function fetchComplianceData(stateCode) {\n  const stateName = STATE_NAMES[stateCode];\n  if (!stateName) {\n    throw new Error(`Unknown state code: ${stateCode}`);\n  }\n\n  // Fetch states.json from the remote host to get current CSV URLs\n  let statesConfig;\n  try {\n    statesConfig = await fetchStatesConfig();\n  } catch (error) {\n    console.error('Could not reach states.json config server:', error);\n    return { error: 'fetch_error' };\n  }\n\n  const stateEntry = statesConfig[stateName];\n  if (!stateEntry || !stateEntry.all_sites || !stateEntry.noncompliant_sites) {\n    console.error(`states.json does not contain a valid entry for: ${stateName}`);\n    return { error: 'fetch_error' };\n  }\n\n  // Convert GitHub blob URLs from states.json to raw content URLs\n  let allSitesUrl, noncompliantUrl;\n  try {\n    allSitesUrl = toRawUrl(stateEntry.all_sites);\n    noncompliantUrl = toRawUrl(stateEntry.noncompliant_sites);\n  } catch (error) {\n    console.error('Failed to parse CSV URLs from states.json:', error);\n    return { error: 'fetch_error' };\n  }\n\n  console.log(`Fetching ${stateCode} compliance data (all_sites + noncompliant_sites)...`);\n\n  let allSitesData, noncompliantDomains;\n  try {\n    [allSitesData, noncompliantDomains] = await Promise.all([\n      fetchAllSitesData(allSitesUrl),\n      fetchDomainSet(noncompliantUrl),\n    ]);\n  } catch (error) {\n    console.error(`Failed to fetch compliance CSVs for ${stateCode}:`, error);\n    return { error: 'fetch_error' };\n  }\n\n  console.log(`${stateCode}: ${allSitesData.size} all_sites, ${noncompliantDomains.size} noncompliant_sites`);\n\n  const complianceMap = {};\n\n  for (const [domain, { allNull, classification }] of allSitesData) {\n    let status;\n    if (noncompliantDomains.has(domain)) {\n      status = 'non_compliant';\n    } else if (allNull) {\n      // All 8 signal columns were null during the crawl — we can't assess compliance\n      status = 'no_signals';\n    } else {\n      status = 'compliant';\n    }\n    complianceMap[domain] = {\n      status,\n      details: '',\n      classification, // null when CSV lacks the column or value is JSON null\n    };\n  }\n\n  return {\n    data: complianceMap,\n    fetchedAt: Date.now(),\n    stateCode,\n    count: Object.keys(complianceMap).length,\n    // The view URL for this state's all_sites dataset (from states.json)\n    viewUrl: stateEntry.all_sites,\n  };\n}\n\n/**\n * Checks if cached data is still valid\n * @param {number} fetchedAt - Timestamp when data was fetched\n * @returns {boolean} - True if cache is still valid\n */\nexport function isCacheValid(fetchedAt) {\n  if (!fetchedAt) return false;\n  return (Date.now() - fetchedAt) < CACHE_TTL_MS;\n}\n"
  },
  {
    "path": "src/data/defaultSettings.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\ndefaultSettings.js \n================================================================================\ndefaultSettings.js exports the default global extension settings\n*/\n\nexport const defaultSettings = {\n  BROWSER: \"$BROWSER\",\n  DOMAINLIST_PRESSED: false,\n  IS_DOMAINLISTED: false,\n  IS_ENABLED: true,\n  TUTORIAL_SHOWN: true,\n  REQUEST_PERMISSIONS_SHOWN: false,\n  TUTORIAL_SHOWN_IN_POPUP: true,\n  WELLKNOWN_CHECK_ENABLED: true,\n  COMPLIANCE_CHECK_ENABLED: true,\n};\n"
  },
  {
    "path": "src/data/headers.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\nheaders.js\n================================================================================\nheaders.js exports all opt-out headers to be attached per request\n*/\n\n// headers must contain a name and a value\nexport const headers = {\n  \"Sec-GPC\": {\n    name: \"Sec-GPC\",\n    value: \"1\",\n  },\n  \"Disable-Topics\": {\n    name: 'permissions-policy',\n    value: 'interest-cohort=()'\n  }\n};\n"
  },
  {
    "path": "src/manifests/chrome/manifest-dev.json",
    "content": "{\n  \"name\": \"OptMeowt\",\n  \"author\": \"privacy-tech-lab\",\n  \"version\": \"6.1.0\",\n  \"description\": \"OptMeowt allows Web users to make use of their rights to opt out from the sale and sharing of personal data\",\n  \"permissions\": [\n    \"declarativeNetRequest\",\n    \"webRequest\",\n    \"webNavigation\",\n    \"storage\",\n    \"activeTab\",\n    \"tabs\",\n    \"scripting\"\n  ],\n  \"declarative_net_request\": {\n    \"rule_resources\": [\n      {\n        \"id\": \"universal_GPC\",\n        \"enabled\": true,\n        \"path\": \"rules/universal_gpc_rules.json\"\n      },\n      {\n        \"id\": \"GPC_exceptions\",\n        \"enabled\": true,\n        \"path\": \"rules/gpc_exceptions_rules.json\"\n      }\n    ]\n  },\n  \"host_permissions\": [\n    \"<all_urls>\"\n  ],\n  \"icons\": {\n    \"128\": \"assets/face-icons/icon128-face-circle.png\"\n  },\n  \"action\": {\n    \"default_title\": \"OptMeowt\",\n    \"default_popup\": \"popup.html\"\n  },\n  \"content_scripts\": [\n    {\n      \"matches\": [\"<all_urls>\"],\n      \"js\": [\"content-scripts/contentScript.js\"],\n      \"run_at\": \"document_start\"\n    }\n  ],\n  \"options_ui\": {\n    \"page\": \"options.html\",\n    \"open_in_tab\": true\n  },\n  \"background\": {\n    \"service_worker\": \"background.bundle.js\"\n  },\n  \"web_accessible_resources\": [{\n\t  \"resources\": [\"content-scripts/injection/gpc-dom.js\"],\n\t  \"matches\": [\"<all_urls>\"]\n\t}],\n  \"manifest_version\": 3,\n  \"incognito\": \"spanning\",\n  \"content_security_policy\": {\n    \"extension_pages\": \"script-src 'self'; object-src 'self'\",\n    \"sandbox\": \"sandbox allow-scripts; script-src 'self' 'unsafe-eval'; object-src 'self'\"\n  }\n}\n"
  },
  {
    "path": "src/manifests/chrome/manifest-dist.json",
    "content": "{\n  \"name\": \"OptMeowt\",\n  \"author\": \"privacy-tech-lab\",\n  \"version\": \"6.1.0\",\n  \"description\": \"OptMeowt allows Web users to make use of their rights to opt out from the sale and sharing of personal data\",\n  \"permissions\": [\n    \"declarativeNetRequest\",\n    \"webRequest\",\n    \"webNavigation\",\n    \"storage\",\n    \"activeTab\",\n    \"tabs\",\n    \"scripting\"\n  ],\n  \"declarative_net_request\": {\n    \"rule_resources\": [\n      {\n        \"id\": \"universal_GPC\",\n        \"enabled\": true,\n        \"path\": \"rules/universal_gpc_rules.json\"\n      },\n      {\n        \"id\": \"GPC_exceptions\",\n        \"enabled\": true,\n        \"path\": \"rules/gpc_exceptions_rules.json\"\n      }\n    ]\n  },\n  \"host_permissions\": [\n    \"<all_urls>\"\n  ],\n  \"icons\": {\n    \"128\": \"assets/face-icons/icon128-face-circle.png\"\n  },\n  \"action\": {\n    \"default_title\": \"OptMeowt\",\n    \"default_popup\": \"popup.html\"\n  },\n  \"content_scripts\": [\n    {\n      \"matches\": [\"<all_urls>\"],\n      \"js\": [\"content-scripts/contentScript.js\"],\n      \"run_at\": \"document_start\"\n    }\n  ],\n  \"options_ui\": {\n    \"page\": \"options.html\",\n    \"open_in_tab\": true\n  },\n  \"background\": {\n    \"service_worker\": \"background.bundle.js\"\n  },\n  \"web_accessible_resources\": [{\n\t  \"resources\": [\"content-scripts/injection/gpc-dom.js\"],\n\t  \"matches\": [\"<all_urls>\"]\n\t}],\n  \"manifest_version\": 3,\n  \"incognito\": \"spanning\",\n  \"content_security_policy\": {\n    \"extension_pages\": \"script-src 'self'; object-src 'self'\",\n    \"sandbox\": \"sandbox allow-scripts; script-src 'self' 'unsafe-eval'; object-src 'self'\"\n  }\n}\n"
  },
  {
    "path": "src/manifests/firefox/manifest-dev.json",
    "content": "{\n  \"name\": \"OptMeowt\",\n  \"author\": \"privacy-tech-lab\",\n  \"version\": \"6.1.0\",\n  \"description\": \"OptMeowt allows Web users to make use of their rights to opt out from the sale and sharing of personal data\",\n  \"permissions\": [\n    \"webRequestBlocking\",\n    \"declarativeNetRequest\",\n    \"webRequest\",\n    \"webNavigation\",\n    \"storage\",\n    \"activeTab\",\n    \"tabs\",\n    \"scripting\"\n  ],\n  \"declarative_net_request\": {\n    \"rule_resources\": [\n      {\n        \"id\": \"universal_GPC\",\n        \"enabled\": true,\n        \"path\": \"rules/universal_gpc_rules.json\"\n      },\n      {\n        \"id\": \"GPC_exceptions\",\n        \"enabled\": true,\n        \"path\": \"rules/gpc_exceptions_rules.json\"\n      }\n    ]\n  },\n  \"host_permissions\": [\n    \"<all_urls>\"\n  ],\n  \"icons\": {\n    \"128\": \"assets/face-icons/icon128-face-circle.png\"\n  },\n  \"action\": {\n    \"default_title\": \"OptMeowt\",\n    \"default_popup\": \"popup.html\"\n  },\n  \"content_scripts\": [\n    {\n      \"matches\": [\"<all_urls>\"],\n      \"js\": [\"content-scripts/contentScript.js\"],\n      \"run_at\": \"document_start\"\n    }\n  ],\n  \"options_ui\": {\n    \"page\": \"options.html\",\n    \"open_in_tab\": true\n  },\n  \"background\": {\n    \"scripts\": [\"background.bundle.js\"]\n  },\n  \"web_accessible_resources\": [{\n\t  \"resources\": [\"content-scripts/injection/gpc-dom.js\"],\n\t  \"matches\": [\"<all_urls>\"]\n\t}],\n  \"manifest_version\": 3,\n  \"incognito\": \"spanning\",\n  \"content_security_policy\": {\n    \"extension_pages\": \"script-src 'self'; object-src 'self'\"\n  },\n  \"browser_specific_settings\": {\n    \"gecko\": {\n      \"id\": \"{7f22397f-fb61-47e2-9e4b-4ddd98faa275}\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/manifests/firefox/manifest-dist.json",
    "content": "{\n  \"name\": \"OptMeowt\",\n  \"author\": \"privacy-tech-lab\",\n  \"version\": \"6.1.0\",\n  \"description\": \"OptMeowt allows Web users to make use of their rights to opt out from the sale and sharing of personal data\",\n  \"permissions\": [\n    \"webRequestBlocking\",\n    \"declarativeNetRequest\",\n    \"webRequest\",\n    \"webNavigation\",\n    \"storage\",\n    \"activeTab\",\n    \"tabs\",\n    \"scripting\"\n  ],\n  \"declarative_net_request\": {\n    \"rule_resources\": [\n      {\n        \"id\": \"universal_GPC\",\n        \"enabled\": true,\n        \"path\": \"rules/universal_gpc_rules.json\"\n      },\n      {\n        \"id\": \"GPC_exceptions\",\n        \"enabled\": true,\n        \"path\": \"rules/gpc_exceptions_rules.json\"\n      }\n    ]\n  },\n  \"host_permissions\": [\n    \"<all_urls>\"\n  ],\n  \"icons\": {\n    \"128\": \"assets/face-icons/icon128-face-circle.png\"\n  },\n  \"action\": {\n    \"default_title\": \"OptMeowt\",\n    \"default_popup\": \"popup.html\"\n  },\n  \"content_scripts\": [\n    {\n      \"matches\": [\"<all_urls>\"],\n      \"js\": [\"content-scripts/contentScript.js\"],\n      \"run_at\": \"document_start\"\n    }\n  ],\n  \"options_ui\": {\n    \"page\": \"options.html\",\n    \"open_in_tab\": true\n  },\n  \"background\": {\n    \"scripts\": [\"background.bundle.js\"]\n  },\n  \"web_accessible_resources\": [{\n\t  \"resources\": [\"content-scripts/injection/gpc-dom.js\"],\n\t  \"matches\": [\"<all_urls>\"]\n\t}],\n  \"manifest_version\": 3,\n  \"incognito\": \"spanning\",\n  \"content_security_policy\": {\n    \"extension_pages\": \"script-src 'self'; object-src 'self'\"\n  },\n  \"browser_specific_settings\": {\n    \"gecko\": {\n      \"id\": \"{7f22397f-fb61-47e2-9e4b-4ddd98faa275}\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/options/components/scaffold-component.html",
    "content": "<!--\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n-->\n\n<!--\nscaffold-component.html\n================================================================================\nscaffold-component.html defines the layout of each scrollable content\n-->\n\n<!--\n    This template is passed into every subpage in the options page via\n    the given JS function. The correct information for each page is inflated\n    in the sections below, and then the newly filled scaffold template is\n    passed into the options page and rendered.\n -->\n<template id=\"scaffold-component\">\n  <div class=\"uk-container\" id=\"scaffold\" style=\"animation-duration: 250ms\">\n    <div class=\"uk-grid-large\">\n      <!-- Heading and subtitle -->\n      <div class=\"first-column uk-margin-medium-bottom\">\n        <h1 class=\"uk-heading-medium text-color-darker\" style=\"display: inline\">\n          {{ title }}\n        </h1>\n        <div class=\"uk-text-lead uk-text-light text-color\">{{ subtitle }}</div>\n      </div>\n      <!-- Body -->\n      <div class=\"first-column\" id=\"scaffold-component-body\">\n        <!-- Body content inflated here -->\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "src/options/components/util.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\nutil.js\n================================================================================\nutil.js contains global helper functions to help render the options page\n*/\n\n/**\n * Get local html file as string\n * @param {string} path - location of HTML template\n * @returns {string|none} - Returns the stringified HTML template or\n *                          prints an error\n */\nimport Mustache from \"mustache\";\n\nexport async function fetchTemplate(path) {\n  let response = await fetch(path);\n  let data = await response.text();\n  return data;\n}\n\n/**\n * Parse string to html document\n * @param {string} template - stringified HTML template\n * @returns {HTMLDocument} - also a Document\n */\nexport function parseTemplate(template) {\n  let parser = new DOMParser();\n  let doc = parser.parseFromString(template, \"text/html\");\n  return doc;\n}\n\n/**\n * Fetches and parses html document; returns selected html\n * @param {string} path - location of document to be parsed\n * @param {string} id - name of the element in doc to be selected\n *                      after it is parsed\n * @returns {Object} - element object related to the id parameter\n */\nexport async function fetchParse(path, id) {\n  let template = await fetchTemplate(path);\n  return parseTemplate(template).getElementById(id);\n}\n\n/**\n * Renders and parse html document; returns selected html\n * @param {string} template - stringified HTML doc template\n * @param {Object} data - specifically a `headings` object\n * @param {string} id - id of an element in an HTML doc\n * @returns {Object} - element object related to the id parameter\n */\nexport function renderParse(template, data, id) {\n  let renderedTemplate = Mustache.render(template, data);\n  return parseTemplate(renderedTemplate).getElementById(id);\n}\n"
  },
  {
    "path": "src/options/dark-mode.css",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n.darkmode--activated {\n  background-color: #252522 !important;\n  color: #eee !important;\n}\n\n.darkmode--activated .bg-light {\n  background-color: #333 !important;\n}\n\n.darkmode--activated .bg-white {\n  background-color: #111 !important;\n}\n\n.darkmode--activated .bg-black {\n  background-color: #eee !important;\n}\n\n.darkmode--activated .text-color-darker {\n  color: #dddddd !important;\n}\n\n.darkmode--activated .text-color {\n  color: #adadad !important;\n}\n\n.darkmode--activated .question {\n  color: #dddddd !important;\n}\n\n.darkmode--activated .answer {\n  color: #adadad !important;\n}\n\n.darkmode--activated .divide {\n  color: #ffff00 !important;\n}\n\n.darkmode--activated .uk-list-divide {\n  color: #ffff00 !important;\n}\n\n.darkmode--activated .blue-heading {\n  color: #93caf2 !important;\n}\n\n.darkmode--activated a {\n  text-decoration: underline;\n  color: #fff !important;\n}\n\n.darkmode--activated .uk-modal-body,\n.darkmode--activated .uk-modal-footer,\n.darkmode--activated .uk-modal-header {\n  background-color: #252522 !important;\n}\n\n/********************************************************/\n\n/*\nRadio buttons style settings page\n*/\n.darkmode--activated input[type=\"radio\"] {\n  width: 35px;\n  height: 35px;\n  transition: all ease 0.5s;\n}\n\n.darkmode--activated input[type=\"radio\"]:checked {\n  transition: all ease 0.25s;\n  background-image: var(--accent-color);\n  box-shadow: 0 6px 12px #222221;\n}\n\n.darkmode--activated input[type=\"radio\"]:hover:not(:checked) {\n  transition: all ease 0.25s;\n  box-shadow: 0 6px 12px #222221;\n  background-color: #cbcbcb;\n  /* border-color: transparent; */\n}\n\n/*\nSettings page\n*/\n.darkmode--activated .navbar-item.active {\n  color: #3d87e9;\n}\n\n/*\n'settings-view' domainlist import & export button style\n*/\n.darkmode--activated .importexport-button {\n  background-color: #252522 !important;\n  border-color: #cbcbcb !important;\n  color: #cbcbcb !important;\n  box-shadow: 0 8px 16px -1px #111111 !important;\n}\n.darkmode--activated .importexport-button:hover {\n  background-color: #d4d4d4 !important;\n  border-color: #d4d4d4 !important;\n  color: #4e4e4d !important;\n  box-shadow: 0 6px 12px #303030 !important;\n}\n\n.darkmode--activated .domainlist-navbar {\n  background-color: #252522 !important;\n  color: #cbcbcb !important;\n}\n\n.darkmode--activated .analysis-navbar {\n  background-color: #252522 !important;\n  color: #cbcbcb !important;\n}\n\n.darkmode--activated .sticky {\n  box-shadow: 0 8px 8px -10px #111111 !important;\n}\n\n/********************************************************/\n\n/*\nButtons and Switches\n*/\n\n.darkmode--activated .optMode {\n  border: 1px solid rgb(238, 238, 238);\n  color: rgb(238, 238, 238);\n  box-shadow: 0 3px 6px -1px #111111 !important;\n}\n\n.darkmode--activated .optMode:hover {\n  color: var(--text-white) !important;\n}\n\n.darkmode--activated .search {\n  background-color: #333 !important;\n  border-color: #333 !important;\n  color: #cbcbcb;\n}\n\n.darkmode--activated .button {\n  background-color: rgb(70, 70, 70) !important;\n  border: 1px solid rgb(70, 70, 70) !important;\n  transition: all 0.3s ease;\n  color: #eee !important;\n}\n\n.darkmode--activated .uk-badge {\n  background-color: rgb(37, 37, 34) !important;\n}\n\n.darkmode--activated .button:hover {\n  background-color: rgb(143, 17, 17) !important;\n  border: 1px solid rgb(143, 17, 17) !important;\n  transition: all 0.3s ease;\n  color: #eee !important;\n}\n\n.darkmode--activated .switch input:checked + span {\n  background: #37be70 !important;\n  box-shadow: 0 0px 0px 0px rgba(72, 234, 139, 0.2) !important;\n}\n\n.darkmode--activated .switch input + span {\n  box-shadow: 0 0px 0px 0px rgba(72, 234, 139, 0.2) !important;\n}\n\n.darkmode--activated .domain-list:hover {\n  background-color: #5f5f5d !important;\n  color: white !important;\n}\n\n.darkmode--activated .dropdown-tab:hover {\n  background-color: #3f3f3c !important;\n  color: #eee !important;\n}\n\n.darkmode--activated .dropdown-tab-click {\n  background-color: #3f3f3c !important;\n  color: #eee !important;\n}\n\n.darkmode--activated .wellknown-bg {\n  background-color: #3f3f3c !important;\n  border-color: #3f3f3c !important;\n  color: #eeeeee;\n}\n\n/* .darkmode--activated .dark-checkbox {\n  background-color: #333 !important;\n  border: 1px solid #333 !important;\n  color: #eee !important;\n} */\n\n/* input[type=\"radio\"]:checked, .darkmode--activated {\n  box-shadow: 0 6px 12px #000 !important;\n}\ninput[type='radio']:hover:not(:checked), .darkmode--activated {\n  box-shadow: 0 6px 12px #000 !important;\n} */\n"
  },
  {
    "path": "src/options/options.html",
    "content": "<!--\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n-->\n\n<!--\noptions.html\n================================================================================\noptions.html is the html scaffolding for OptMeowt's options page\n-->\n\n<!--\n    This is the bare bones options.html page before the `main view` is\n    loaded and before any main content is loaded.\n-->\n<!DOCTYPE html>\n<html>\n  <head>\n    <title>OptMeowt</title>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n  </head>\n\n  <!-- This is the entry point to 'Options' -->\n\n  <body>\n    <!-- Content will be inflated here-->\n  </body>\n</html>\n"
  },
  {
    "path": "src/options/options.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\noptions.js\n================================================================================\noptions.js starts the process of rendering the main options page\n*/\n\nimport { mainView } from \"./views/main-view/main-view.js\";\n\n// CSS TO JS IMPORTS\nimport \"../../node_modules/uikit/dist/css/uikit.min.css\";\nimport \"../../node_modules/animate.css/animate.min.css\";\nimport \"./styles.css\";\n\n// HTML TO JS IMPORTS - TOP OF `popup.html`\nimport \"../../node_modules/uikit/dist/js/uikit.js\";\nimport \"../../node_modules/uikit/dist/js/uikit-icons.js\";\nimport \"../../node_modules/mustache/mustache.js\";\nimport \"../../node_modules/@popperjs/core/dist/umd/popper.js\";\nimport \"../../node_modules/tippy.js/dist/tippy-bundle.umd.js\";\n\n/**\n * Intializes scripts that build the options page\n */\ndocument.addEventListener(\"DOMContentLoaded\", (event) => {\n  mainView(); // check event\n});\n"
  },
  {
    "path": "src/options/styles.css",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\nstyles.css\n================================================================================\nstyles.css is the main css page for OptMeowt's options page\n*/\n\n@import \"./dark-mode.css\";\n\n/********************************************************/\n\n/*\nColors\n*/\n:root {\n  --accent-color: #4472c4;\n  --accent-color-lighter-80: #dae3f3;\n  --accent-color-lighter-60: #b4c7e7;\n  --accent-color-lighter-40: #8faadc;\n  --accent-color-darker-25: #2f5597;\n  --accent-color-darker-50: #203864;\n\n  /* --text-color: #888fa1; */\n  --text-color: #5a647d;\n  /* --text-color-darker: #5a647d ; */\n  --text-color-darker: #353b4a;\n  --text-color-inactive: #d3d3d3;\n\n  --highlight-light: #f2f2f2;\n\n  --text-gray: rgb(89, 98, 127);\n}\n\n.text-color-darker {\n  color: var(--text-color-darker);\n}\n\n.text-color {\n  color: var(--text-color);\n}\n\n/********************************************************/\n\n/*\nBody style\n*/\nhtml {\n  background-color: inherit;\n}\n\nbody {\n  color: var(--text-color);\n  user-select: none;\n}\n\npre,\ncode {\n  white-space: pre-line;\n}\n\na {\n  text-decoration: underline;\n}\n\n/********************************************************/\n\n/*\nAbout page Q/A heading style\n*/\n.question {\n  color: var(--text-color-darker);\n  font-size: x-large;\n  user-select: none;\n}\n\n.answer {\n  color: var(--text-color);\n  font-size: medium;\n  user-select: none;\n}\n\n/********************************************************/\n\n/*\nNavbar item style\n*/\n.navbar-item {\n  color: var(--text-color-inactive);\n  opacity: 1;\n  cursor: pointer;\n  transition: all ease 0.5s;\n}\n\n.navbar-item.active {\n  color: var(--accent-color);\n}\n\n/********************************************************/\n\n/*\nSticky Domain List Navbar\n*/\n.domainlist-navbar {\n  background-color: white;\n  padding-top: 5px;\n}\n/*\nSticky Analysis List Navbar\n*/\n\n/* Added with  */\n.sticky {\n  position: fixed;\n  top: 0;\n  width: 100%;\n  transition: all ease 0.25s;\n  box-shadow: 0 8px 8px -10px var(--accent-color-lighter-60);\n  z-index: 100;\n}\n\n/********************************************************/\n\n/*\nRadio buttons style\n*/\ninput[type=\"radio\"] {\n  width: 35px;\n  height: 35px;\n  transition: all ease 0.5s;\n}\n\ninput[type=\"radio\"]:checked {\n  transition: all ease 0.25s;\n  background-image: var(--accent-color);\n  box-shadow: 0 4px 8px var(--accent-color-lighter-60);\n}\n\ninput[type=\"radio\"]:hover:not(:checked) {\n  transition: all ease 0.25s;\n  box-shadow: 0 4px 8px var(--accent-color-lighter-60);\n  border-color: transparent;\n}\n\n/********************************************************/\n\n/*\n`domainlist-view` options page checkbox style\n*/\n.check {\n  /* appearance: none; */\n  transform: scale(1.25);\n  /* transform-style: inherit; */\n  /* z-index: -10; */\n}\n\n/********************************************************/\n\n/*\n'settings-view' domainlist import & export button style\n*/\n.importexport-button {\n  background-color: white;\n  border-color: #888fa1;\n  color: #888fa1;\n  padding: 12px 16px;\n  text-align: center;\n  text-decoration-color: none;\n  font-size: 14px;\n  display: inline-block;\n  border-radius: 10px;\n  border-style: solid;\n  box-shadow: 0 8px 16px -1px rgba(211, 211, 211, 0.2);\n  outline: none;\n  transition: all ease 0.25s;\n}\n.importexport-button:hover {\n  background-color: var(--accent-color);\n  border-color: var(--accent-color);\n  color: white;\n  box-shadow: 0 6px 12px var(--accent-color-lighter-60);\n  transition: all ease 0.25s;\n}\n\n.button:hover {\n  background-color: #df3131 !important;\n  border: 1px solid #df3131 !important;\n  transition: all 0.3s ease;\n  color: #fff !important;\n}\n\n.uspStringElem {\n  margin: auto;\n  padding-top: 10px;\n  padding-bottom: 10px;\n  padding-right: 8px;\n  padding-left: 8px;\n  background-color: white;\n  border: 1px solid var(--text-gray);\n  color: var(--text-gray);\n  text-align: center;\n}\n\n.uspStringElem:hover {\n  background-color: white;\n  border: 1px solid var(--text-gray);\n  color: var(--text-gray);\n}\n\n.uspStringElem:active {\n  background-color: white;\n  border: 1px solid var(--text-gray);\n  color: var(--text-gray);\n}\n\n/********************************************************/\n\n/* Animated iOS style switch\nhttps://codepen.io/aaroniker/pen/oaQdQZ\n*/\n.switch {\n  cursor: pointer;\n}\n.switch input {\n  display: none;\n}\n.switch input + span {\n  width: 48px;\n  height: 28px;\n  border-radius: 14px;\n  transition: all 0.3s ease;\n  display: block;\n  position: relative;\n  background: #888fa1;\n  box-shadow: 0 8px 16px -1px rgba(211, 211, 211, 0.2);\n}\n.switch input + span:before,\n.switch input + span:after {\n  content: \"\";\n  display: block;\n  position: absolute;\n  transition: all 0.3s ease;\n}\n.switch input + span:before {\n  top: 5px;\n  left: 5px;\n  width: 18px;\n  height: 18px;\n  border-radius: 9px;\n  border: 5px solid #fff;\n}\n.switch input + span:after {\n  top: 5px;\n  left: 32px;\n  width: 6px;\n  height: 18px;\n  border-radius: 40%;\n  transform-origin: 50% 50%;\n  background: #fff;\n  opacity: 0;\n}\n.switch input + span:active {\n  transform: scale(0.92);\n}\n.switch input:checked + span {\n  background: #48ea8b;\n  box-shadow: 0 8px 16px -1px rgba(72, 234, 139, 0.2);\n}\n.switch input:checked + span:before {\n  width: 0px;\n  border-radius: 3px;\n  margin-left: 27px;\n  border-width: 3px;\n  background: #fff;\n}\n.switch input:checked + span:after {\n  animation: blobChecked 0.35s linear forwards 0.2s;\n}\n.switch input:not(:checked) + span:before {\n  animation: blob 0.85s linear forwards 0.2s;\n}\n@keyframes blob {\n  0%,\n  100% {\n    transform: scale(1);\n  }\n  30% {\n    transform: scale(1.12, 0.94);\n  }\n  60% {\n    transform: scale(0.96, 1.06);\n  }\n}\n@keyframes blobChecked {\n  0% {\n    opacity: 1;\n    transform: scaleX(1);\n  }\n  30% {\n    transform: scaleX(1.44);\n  }\n  70% {\n    transform: scaleX(1.18);\n  }\n  50%,\n  99% {\n    transform: scaleX(1);\n    opacity: 1;\n  }\n  100% {\n    transform: scaleX(1);\n    opacity: 0;\n  }\n}\n* {\n  box-sizing: border-box;\n}\n*:before,\n*:after {\n  box-sizing: border-box;\n}\n\n*:before,\n*:after {\n  box-sizing: border-box;\n}\n\n.switch-smaller input + span {\n  width: 48px;\n  height: 28px;\n  border-radius: 14px;\n  transition: all 0.3s ease;\n  display: block;\n  position: relative;\n  background: #888fa1;\n  box-shadow: 0 8px 16px -1px rgba(211, 211, 211, 0.2);\n  transform: scale(0.7);\n}\n.switch-smaller input + span:active {\n  transform: scale(0.65);\n}\n\n/*========================================================================================*/\n/*Walkthrough popups css*/\n\n.tippy-box[data-theme~=\"custom-1\"] {\n  background-color: #87cefa;\n  box-shadow: 10px 10px 5px 0px rgba(0, 0, 0, 0.43);\n  color: white;\n  border-radius: 5px;\n  padding: 10px;\n  text-align: left;\n  float: left;\n}\n\n.tippy-box[data-theme~=\"custom-1\"] button {\n  color: white;\n}\n\n.tippy-box[data-theme~=\"custom-1\"][data-placement^=\"top\"]\n  > .tippy-arrow::before {\n  border-top-color: #87cefa;\n}\n.tippy-box[data-theme~=\"custom-1\"][data-placement^=\"bottom\"]\n  > .tippy-arrow::before {\n  border-bottom-color: #87cefa;\n}\n.tippy-box[data-theme~=\"custom-1\"][data-placement^=\"left\"]\n  > .tippy-arrow::before {\n  border-left-color: #87cefa;\n}\n.tippy-box[data-theme~=\"custom-1\"][data-placement^=\"right\"]\n  > .tippy-arrow::before {\n  border-right-color: #87cefa;\n}\n"
  },
  {
    "path": "src/options/views/about-view/about-view.html",
    "content": "<!--\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n-->\n\n\n<!--\nabout-view.html\n================================================================================\nabout-view.html contains the 'about' view template to be loaded into the\noptions page when clicked\n-->\n\n\n<!--\n    This is the body content inflated into scaffold-component.html when\n    the `about view` is called by a user from within the options page.\n-->\n<template id=\"about-view\">\n  <div>\n    <p class=\"uk-width-3-4\" style=\"font-size: medium;\">\n      OptMeowt (\"Opt Me Out\") 🐾 is a browser extension for sending\n      Global Privacy Control signals to websites that you visit.\n      It is part of [Global Privacy Control](https://globalprivacycontrol.org/),\n      a Global Privacy Control standardization effort we are spearheading.\n    </br></br>\n      Feel free to read more about how OptMeowt works in our\n      <a href=\"https://github.com/privacy-tech-lab/gpc-optmeowt\">GitHub repo</a>.\n    </p>\n\n    <p class=\"uk-width-3-4 question\">\n      <b>Do I need an account to use OptMeowt?</b>\n    </p>\n    <p class=\"uk-width-3-4 answer\">\n      You do not need an account to use OptMeowt.\n    </p>\n\n    <p class=\"uk-width-3-4 question\">\n      <b>How do I update OptMeowt?</b>\n    </p>\n    <p class=\"uk-width-3-4 answer\">\n      If you installed OptMeowt via the <a href=\"https://chrome.google.com/webstore/detail/optmeowt/hdbnkdbhglahihjdbodmfefogcjbpgbo\">Google Chrome Web Store</a> or as a \n      <a href=\"https://addons.mozilla.org/en-US/firefox/addon/optmeowt/\">Firefox Browser Add-on</a>,\n      updates should be pushed automatically to you. We will also push updates\n      regularly to <a href=\"https://github.com/privacy-tech-lab/gpc-optmeowt\">\n      OptMeowt's GitHub repo</a>. \n    </p>\n\n    <p class=\"uk-width-3-4 question\">\n      <b>Who are you?</b>\n    </p>\n    <p class=\"uk-width-3-4 answer\">\n      We are academic researchers at\n      <a href=\"https://privacytechlab.org/\">\n        Wesleyan University's privacy-tech-lab</a>.\n      </br>\n        Feel free to take a look at our other projects on the\n      <a href=\"https://github.com/privacy-tech-lab\">\n        privacy-tech-lab GitHub</a>.\n    </p>\n\n    <p class=\"uk-width-3-4 question\">\n      <b>Where can I reach you?</b>\n    </p>\n    <p class=\"uk-width-3-4 answer\">\n      Feel free to shoot us an email @\n      <a href=\"mailto:sebastian@privacytechlab.org\">\n        sebastian@privacytechlab.org</a> with any feedback or questions!\n    </p>\n\n    <p class=\"uk-width-3-4 question\">\n      <b>Do you have an FAQ or a place to report bugs?</b>\n    </p>\n    <p class=\"uk-width-3-4 answer\">\n      If you have questions about OptMeowt's functionality or \n      believe you may have found a bug, please check out our \n<a href=\"https://github.com/privacy-tech-lab/gpc-optmeowt/wiki/FAQ-%5C-Known-quirks\">FAQ \\ Known quirks</a> \n      page on our GitHub \n<a href=\"https://github.com/privacy-tech-lab/gpc-optmeowt/wiki\">Wiki</a> \n      to see if we have already addressed the issue. \n      If you cannot find what you are looking for, please feel free \n      to open an issue and we will address it as soon as we can! \n    </p>\n\n    <p class=\"uk-width-3-4 answer\">\n      Note: OptMeowt is a work in progress ...\n    </p>\n  </div>\n</template>\n"
  },
  {
    "path": "src/options/views/about-view/about-view.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\nabout-view.js\n================================================================================\nabout-view.js loads about-view.html when clicked on the options page\n*/\n\nimport { renderParse, fetchParse } from \"../../components/util.js\";\n\n/**\n * @typedef headings\n * @property {string} headings.title - Title of the given page\n * @property {string} headings.subtitle - Subtitle of the given page\n */\nconst headings = {\n  title: \"About\",\n  subtitle: \"Learn more about OptMeowt\",\n};\n\n/**\n * Renders the `About` view in the options page\n * @param {string} scaffoldTemplate - stringified HTML template\n */\nexport async function aboutView(scaffoldTemplate) {\n  const body = renderParse(scaffoldTemplate, headings, \"scaffold-component\");\n  let content = await fetchParse(\n    \"./views/about-view/about-view.html\",\n    \"about-view\"\n  );\n\n  document.getElementById(\"content\").innerHTML = body.innerHTML;\n  document.getElementById(\"scaffold-component-body\").innerHTML =\n    content.innerHTML;\n}\n"
  },
  {
    "path": "src/options/views/domainlist-view/domainlist-view.html",
    "content": "<!--\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n-->\n\n\n<!--\ndomainlist-view.html\n================================================================================\ndomainlist-view.html contains the 'domainlist' view template to be loaded into the\noptions page when clicked\n-->\n\n\n<!--\n    This is the body content inflated into scaffold-component.html when\n    the `domainlist view` is called by a user from within the options page.\n-->\n<template id=\"domainlist-view\">\n  <div uk-grid>\n    <div class=\"uk-width-3-4\" style=\"margin-top: 0px;\">\n      \n      <!-- Navbar with \"Delete All\" button -->\n      <div class=\"domainlist-navbar\" id=\"domainlist-navbar\" style=\"display: flex; justify-content: space-between; align-items: center; width: 100%; padding: 5px;\">\n        <div class=\"uk-text-lead uk-text-bold text-color-darker\">\n          Domains\n        </div>\n        <button\n          id=\"delete-all-domains\"\n          class=\"uk-badge button\"\n          type=\"button\"\n          style=\"\n            padding: 5px 10px;\n            background-color: white;\n            border: 1px solid #e06d62;\n            color: #e06d62;\n            position: absolute; /* Remove from the normal flow */\n            right: 0; /* Place it all the way to the right */\n            margin-right: 119px; /* Fine-tune with additional spacing */\n          \"\n        >\n          Delete All\n        </button>\n        </div>\n        <br>\n      </div>\n\n      <!-- Domainlist -->\n      <ul\n        class=\"uk-list uk-list-divider uk-width-1-1\"\n        style=\"margin-top: 0px; margin-bottom: 0px;\"\n        id=\"domainlist-main\"\n      >\n      <li>\n        <div uk-grid class=\"uk-grid-small uk-width-1-1\" style=\"font-size: medium;\">\n          <div class=\"domain uk-width-expand\" >\n            Loading...\n          </div>\n          <div style=\"\n            margin-right: auto;\n            margin-left: auto;\n            margin-top: auto;\n            margin-bottom: auto;\n            \"\n          >\n        </div>\n      </li>\n      </ul>\n      \n      <!-- Custom Confirmation Modal -->\n      <div id=\"confirm-modal\" class=\"hidden\">\n        <div id=\"modal-content\">\n          <p>Are you sure you want to delete all domains?</p>\n          <button id=\"confirm-yes\">Yes</button>\n          <button id=\"confirm-no\">No</button>\n        </div>\n      </div>\n\n      <style>\n        #confirm-modal {\n          position: fixed;\n          top: 0;\n          left: 0;\n          width: 100%;\n          height: 100%;\n          background-color: rgba(0, 0, 0, 0.5);\n          display: flex;\n          justify-content: center;\n          align-items: center;\n        }\n        #modal-content {\n          background-color: white;\n          padding: 20px;\n          border-radius: 8px;\n          text-align: center;\n        }\n\n        #modal-content p {\n          color: #333; /* Darker text color */\n        }\n        \n        #confirm-modal.hidden {\n          display: none;\n        }\n      </style>\n\n      <!-- Custom Alert Modal -->\n      <div id=\"alert-modal\" class=\"hidden\">\n        <div id=\"alert-modal-content\">\n          <p></p>\n          <button id=\"alert-ok\">OK</button>\n        </div>\n      </div>\n      <style>\n      #alert-modal {\n        position: fixed;\n        top: 0;\n        left: 0;\n        width: 100%;\n        height: 100%;\n        background-color: rgba(0, 0, 0, 0.5);\n        display: flex;\n        justify-content: center;\n        align-items: center;\n      }\n      #alert-modal-content {\n        background-color: white;\n        padding: 20px;\n        border-radius: 8px;\n        text-align: center;\n      }\n      \n      #alert-modal-content p {\n        color: #333; /* Darker text color */\n      }\n\n      #alert-modal.hidden {\n        display: none;\n      }\n      #alert-ok {\n        background-color: #e06d62;\n        color: white;\n        border: none;\n        padding: 10px 20px;\n        border-radius: 5px;\n        cursor: pointer;\n      }\n      #alert-ok:hover {\n        background-color: #d05c52;\n        }\n        </style>\n\n        </div>\n      </div>\n    </template>\n"
  },
  {
    "path": "src/options/views/domainlist-view/domainlist-view.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\ndomainlist-view.js\n================================================================================\ndomainlist-view.js loads domainlist-view.html when clicked on the options page\n*/\n\nimport { storage, stores } from \"../../../background/storage.js\";\nimport { renderParse, fetchParse } from \"../../components/util.js\";\n\nimport {\n  addDomainToDomainlistAndRules,\n  removeDomainFromDomainlistAndRules,\n  updateRemovalScript,\n  deleteCS\n} from \"../../../common/editDomainlist.js\";\nimport { reloadDynamicRules } from \"../../../common/editRules.js\";\n\n/******************************************************************************/\n/***************************** Toggle Functions *******************************/\n/******************************************************************************/\n\n/**\n * Generates the HTML that will build the domainlist switch for a given\n * domain in the domainlist\n * @param {string} domain - Any given domain\n * @param {(number|null)} id - Dynamic rule ID if domainlisted as \"excluded\"\n * @return {string} - The stringified checkbox HTML compontent\n */\nexport function buildToggle(domain, id) {\n  let toggle;\n  if (!id) {\n    toggle = `<input type=\"checkbox\" id=\"${domain}\" checked />`;\n  } else {\n    toggle = `<input type=\"checkbox\" id=\"${domain}\" />`;\n  }\n  return toggle;\n}\n\n/**\n * Creates an event listener that toggles a given domain's stored value in\n * the domainlist if a user clicks on the object with the given element ID\n * @param {string} elementId - HTML element to be linked to the listener\n * @param {string} domain - domain to be changed in domainlist\n */\nexport async function toggleListener(elementId, domain) {\n  document.getElementById(elementId).addEventListener(\"click\", async () => {\n    const domainId = await storage.get(stores.domainlist, domain);\n    if (domainId == null) {\n      await addDomainToDomainlistAndRules(domain);\n    } else {\n      await removeDomainFromDomainlistAndRules(domain);\n    }\n\n    chrome.runtime.sendMessage({\n      msg: \"FORCE_RELOAD\",\n    });\n  });\n}\n\nfunction showConfirmModal(message, callback) {\n  const modal = document.getElementById(\"confirm-modal\");\n  const yesButton = document.getElementById(\"confirm-yes\");\n  const noButton = document.getElementById(\"confirm-no\");\n\n  // Set the message in the modal\n  modal.querySelector(\"p\").textContent = message;\n\n  // Show the modal\n  modal.classList.remove(\"hidden\");\n\n  // Handle \"Yes\" button click\n  yesButton.onclick = () => {\n      callback(true);  // Pass true to the callback if \"Yes\" was clicked\n      modal.classList.add(\"hidden\");  // Hide the modal\n  };\n\n  // Handle \"No\" button click\n  noButton.onclick = () => {\n      callback(false);  // Pass false to the callback if \"No\" was clicked\n      modal.classList.add(\"hidden\");  // Hide the modal\n  };\n}\n\nfunction showAlert(message, callback) {\n  const modal = document.getElementById(\"alert-modal\");\n  const okButton = document.getElementById(\"alert-ok\");\n\n  // Set the message in the modal\n  modal.querySelector(\"p\").textContent = message;\n\n  // Show the modal\n  modal.classList.remove(\"hidden\");\n\n  // Handle \"OK\" button click\n  okButton.onclick = () => {\n      callback();  // Call the callback after the alert is dismissed\n      modal.classList.add(\"hidden\");  // Hide the modal\n  };\n}\n\n/**\n * Creates the specific Domain List toggles as well as the perm delete\n * buttons for each domain\n */\nasync function createToggleListeners() {\n  const domainlistKeys = await storage.getAllKeys(stores.domainlist);\n  const domainlistValues = await storage.getAll(stores.domainlist);\n  let domain;\n  let domainValue;\n  for (let index in domainlistKeys) {\n    domain = domainlistKeys[index];\n    domainValue = domainlistValues[index];\n    // MAKE SURE THE ID MATCHES EXACTLY\n    toggleListener(domain, domain);\n    deleteButtonListener(domain);\n  }\n}\n\n/**\n * Delete buttons for each domain\n * @param {string} domain\n */\n function deleteButtonListener(domain) {\n  document\n    .getElementById(`delete ${domain}`)\n    .addEventListener(\"click\", async () => {\n      const deletePrompt = `Are you sure you would like to delete this domain from the Domain List?`;\n      const successPrompt = `Successfully deleted ${domain} from the Domain List.`;\n\n      showConfirmModal(deletePrompt, async (confirmed) => {\n        if (confirmed) {\n          // Proceed with deletion if user confirms\n          await storage.delete(stores.domainlist, domain);\n\n          reloadDynamicRules();\n          updateRemovalScript();\n          deleteCS();\n\n          // Replacing alert() with custom showAlert()\n          showAlert(successPrompt, () => {\n            document.getElementById(`li ${domain}`).remove();\n          });\n        }\n      });\n    });\n}\n/******************************************************************************/\n\n/**\n * @typedef headings\n * @property {string} headings.title - Title of the given page\n * @property {string} headings.subtitle - Subtitle of the given page\n */\nconst headings = {\n  title: \"Domain List\",\n  subtitle:\n    \"Toggle which domains you would like to receive Global Privacy Control signals in Protection Mode\",\n};\n\n/**\n * Creates the event listeners for the `domainlist` page buttons and options\n */\nasync function eventListeners() {\n  await createToggleListeners();\n\n  document.getElementById(\"delete-all-domains\").addEventListener(\"click\", async () => {\n    const deletePrompt = `Are you sure you would like to delete all domains from the Domain List?`;\n    const successPrompt = `Successfully deleted all domains from the Domain List.`;\n\n    showConfirmModal(deletePrompt, async (confirmed) => {\n        if (confirmed) {\n            // If user clicks \"Yes\", proceed with deletion\n            const domainlistKeys = await storage.getAllKeys(stores.domainlist);\n            \n            for (let domain of domainlistKeys) {\n                await storage.delete(stores.domainlist, domain);\n            }\n\n            reloadDynamicRules();\n            updateRemovalScript();\n            deleteCS();\n\n            // Show success message using the custom alert modal\n            showAlert(successPrompt, () => {\n                document.getElementById(\"domainlist-main\").innerHTML = \"\";  // Clears the list visually\n            });\n        } else {\n            // No action taken if user clicks \"No\"\n        }\n    });\n});\n\n  window.onscroll = function () {\n    stickyNavbar();\n  };\n  var nb = document.getElementById(\"domainlist-navbar\");\n  var sticky = nb.offsetTop;\n\n  /**\n   * Sticky navbar\n   */\n  function stickyNavbar() {\n    if (window.pageYOffset >= sticky) {\n      nb.classList.add(\"sticky\");\n    } else {\n      nb.classList.remove(\"sticky\");\n    }\n  }\n}\n\n/**\n * Builds the list of domains in the domainlist, and their respective\n * options, to be displayed\n */\nasync function buildList() {\n  let items = \"\";\n  let domain;\n  let domainValue;\n  const domainlistKeys = await storage.getAllKeys(stores.domainlist);\n  const domainlistValues = await storage.getAll(stores.domainlist);\n  for (let index in domainlistKeys) {\n    domain = domainlistKeys[index];\n    domainValue = domainlistValues[index];\n    items +=\n      `\n    <li id=\"li ${domain}\">\n      <div uk-grid class=\"uk-grid-small uk-width-1-1\" style=\"font-size: medium;\">\n        <div>\n          <label class=\"switch\">\n          ` +\n      buildToggle(domain, domainValue) +\n      `\n            <span></span>\n          </label>\n        </div>\n        <div class=\"domain uk-width-expand\">\n          ${domain}\n        </div>\n        <div style=\"\n          margin-right: 5px;\n          margin-left: 5px;\n          margin-top: auto;\n          margin-bottom: auto;\n          \"\n        >\n          <label class=\"switch\" >\n            <span></span>\n          </label>\n        </div>\n          <button\n            id=\"delete ${domain}\"\n            class=\"uk-badge button\"\n            type=\"button\"\n            style=\"\n              margin-right: 5px;\n              margin-left: 5px;\n              margin-top: auto;\n              margin-bottom: auto;\n              padding-right: 5px;\n              padding-left: 5px;\n              background-color: white;\n              border: 1px solid #e06d62;\n              color: #e06d62;\n            \"\n          >\n            Delete\n          </button>\n      </div>\n    </li>\n          `;\n  }\n  document.getElementById(\"domainlist-main\").innerHTML = items;\n}\n\n/**\n * Renders the `domain list` view in the options page\n * @param {string} scaffoldTemplate - stringified HTML template\n */\nexport async function domainlistView(scaffoldTemplate) {\n  const body = renderParse(scaffoldTemplate, headings, \"scaffold-component\");\n  let content = await fetchParse(\n    \"./views/domainlist-view/domainlist-view.html\",\n    \"domainlist-view\"\n  );\n\n  document.getElementById(\"content\").innerHTML = body.innerHTML;\n  document.getElementById(\"scaffold-component-body\").innerHTML =\n    content.innerHTML;\n\n  await buildList();\n  eventListeners();\n}\n"
  },
  {
    "path": "src/options/views/main-view/main-view.html",
    "content": "<!--\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n-->\n\n<!--\nmain-view.html\n================================================================================\nmain-view.html defines the main structure of the about page and handles\nnavigation\n-->\n\n<!--\n    This page defines the parts of the options page that are not the main\n    content, specifically the persistent navigation bar that connects to\n    every other page.\n-->\n<template id=\"main-view\">\n  <div uk-height-viewport class=\"uk-padding-large uk-overflow-hidden\">\n    <div class=\"uk-flex uk-flex-center@l\">\n      <div\n        class=\"uk-grid-collapse uk-width-expand\"\n        uk-grid\n        style=\"max-width: 1366px; min-width: 320px\"\n      >\n        <!-- Width spacer for sidebar -->\n        <div class=\"uk-width-small\"></div>\n        <!-- Sidebar -->\n        <div class=\"uk-width-small uk-position-fixed\">\n          <!-- Logo -->\n          <div class=\"first-column\">\n            <img\n              src=\"../../../assets/cat-w-text/optmeow-logo-circle.png\"\n              alt=\"Logo\"\n              style=\"width: 125px; height: 125px\"\n            />\n          </div>\n          <!-- Navigation bar -->\n          <div\n            class=\"first-column uk-flex-inline uk-width-expand\"\n            style=\"\n              border-right: 1px solid #e5e5e5;\n              padding-bottom: 128px;\n              padding-top: 32px;\n              padding-left: 10px;\n            \"\n          >\n            <ul class=\"uk-nav uk-nav-default\">\n              <div class=\"uk-transition-toggle uk-h4\" tabindex=\"0\">\n                <li\n                  class=\"uk-transition-scale-up navbar-item\"\n                  id=\"main-view-settings\"\n                >\n                  Settings\n                </li>\n              </div>\n              <div class=\"uk-transition-toggle uk-h4\" tabindex=\"0\">\n                <li\n                  class=\"uk-transition-scale-up navbar-item\"\n                  id=\"main-view-domainlist\"\n                >\n                  Domain List\n                </li>\n              </div>\n              <div class=\"uk-transition-toggle uk-h4\" tabindex=\"0\">\n                <li\n                  class=\"uk-transition-scale-up navbar-item\"\n                  id=\"main-view-about\"\n                >\n                  About\n                </li>\n              </div>\n            </ul>\n          </div>\n        </div>\n        <!-- Scrollable Content -->\n        <div class=\"uk-width-expand\" id=\"content\">\n          <!-- Content will be inflated here-->\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "src/options/views/main-view/main-view.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\nmain-view.js\n================================================================================\nmain-view.js handles the navigation between different parts of the options page\nand loads them when called through the navigation bar\n*/\n\n\nimport { fetchTemplate, parseTemplate } from \"../../components/util.js\";\nimport { settingsView } from \"../settings-view/settings-view.js\";\nimport { domainlistView } from \"../domainlist-view/domainlist-view.js\";\nimport { aboutView } from \"../about-view/about-view.js\";\nimport { storage, stores } from \"../../../background/storage.js\";\nimport Darkmode from \"../../../theme/darkmode.js\";\n\n/**\n * Opens the `Settings` page\n * @param {string} bodyTemplate - stringified HTML template\n */\nasync function displaySettings(bodyTemplate) {\n  settingsView(bodyTemplate);\n  document.querySelector(\".navbar-item.active\").classList.remove(\"active\");\n  document.querySelector(\"#main-view-settings\").classList.add(\"active\");\n}\n\n/**\n * Opens the `Domainlist` page\n * @param {string} bodyTemplate - stringified HTML template\n */\nfunction displayDomainlist(bodyTemplate) {\n  domainlistView(bodyTemplate);\n  document.querySelector(\".navbar-item.active\").classList.remove(\"active\");\n  document.querySelector(\"#main-view-domainlist\").classList.add(\"active\");\n}\n\n/**\n * Opens the `Display` page\n * @param {string} bodyTemplate - stringified HTML template\n */\nfunction displayAbout(bodyTemplate) {\n  aboutView(bodyTemplate);\n  document.querySelector(\".navbar-item.active\").classList.remove(\"active\");\n  document.querySelector(\"#main-view-about\").classList.add(\"active\");\n}\n\n/**\n * Prepares the `Main` page elements and intializes the default `Settings` page\n */\nexport async function mainView() {\n  let docTemplate = await fetchTemplate(\"./views/main-view/main-view.html\");\n  const bodyTemplate = await fetchTemplate(\n    \"./components/scaffold-component.html\"\n  );\n  document.body.innerHTML =\n    parseTemplate(docTemplate).getElementById(\"main-view\").innerHTML;\n\n  let domainlistPressed = await storage.get(\n    stores.settings,\n    \"DOMAINLIST_PRESSED\"\n  );\n\n  if (!domainlistPressed) {\n    settingsView(bodyTemplate); // First page\n    document.querySelector(\"#main-view-settings\").classList.add(\"active\");\n  } else if (domainlistPressed) {\n    domainlistView(bodyTemplate); // First page\n    await storage.set(stores.settings, false, \"DOMAINLIST_PRESSED\");\n    document.querySelector(\"#main-view-domainlist\").classList.add(\"active\");}\n\n  document\n    .getElementById(\"main-view-settings\")\n    .addEventListener(\"click\", () => displaySettings(bodyTemplate));\n  document\n    .getElementById(\"main-view-domainlist\")\n    .addEventListener(\"click\", () => displayDomainlist(bodyTemplate));\n  document\n    .getElementById(\"main-view-about\")\n    .addEventListener(\"click\", () => displayAbout(bodyTemplate));\n\n  // DARK MODE\n  const darkmode = new Darkmode();\n\n  //Listener: Listens for a message sent by popup.js\n  chrome.runtime.onMessage.addListener(function (\n    message,\n    sender,\n    sendResponse\n  ) {\n    if (message.msg === \"DARKSWITCH_PRESSED\") {\n      darkmode.toggle();\n    }\n    if (message.msg === \"SHOW_TUTORIAL\") {\n      displaySettings(bodyTemplate);\n    }\n  });\n}\n"
  },
  {
    "path": "src/options/views/settings-view/settings-view.html",
    "content": "<!--\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n-->\n\n\n<!--\nsettings-view.html\n================================================================================\nsettings-view.html contains the 'settings' view template to be loaded into the\noptions page when clicked\n-->\n\n\n<!--\n    This is the body content inflated into scaffold-component.html when\n    the `settings view` is called by a user from within the options page.\n-->\n<template id=\"settings-view\">\n  <div id=\"welcome-modal\" uk-modal>\n    <div class=\"uk-modal-dialog\">\n      <button class=\"uk-modal-close-default\" type=\"button\" uk-close></button>\n      <div class=\"uk-modal-header uk-flex uk-flex-column uk-flex-middle\">\n        <img src=\"../../../assets/face-icons/icon64-face-circle.png\" alt=\"\" style=\"margin: 0px;\">\n        <h2 class=\"uk-align-center text-color-darker\">Welcome to OptMeowt!</h2>\n      </div>\n      <div class=\"uk-modal-body\" style=\"font-size: 15px; text-align: center;\">\n        OptMeowt has been successfully installed on your browser! Would you like a quick walkthrough of the options\n        page?\n      </div>\n      <div class=\"uk-modal-footer\">\n        <button id=\"modal-button-1\" class=\"text-color uk-button uk-button-default uk-align-right\"\n          style=\"margin: 0px 10px;\">NOT NOW</button>\n        <button id=\"modal-button-2\" class=\"text-color uk-button uk-button-default uk-align-right\"\n          style=\"margin: 0px 10px;\">OK</button>\n      </div>\n    </div>\n  </div>\n\n  <div id=\"permission-modal\" uk-modal>\n    <div class=\"uk-modal-dialog\">\n      <button class=\"uk-modal-close-default\" type=\"button\" uk-close></button>\n      <div class=\"uk-modal-header uk-flex uk-flex-column uk-flex-middle\">\n        <img src=\"../../../assets/face-icons/icon64-face-circle.png\" alt=\"\" style=\"margin: 0px;\">\n        <h2 class=\"uk-align-center text-color-darker\">Welcome to OptMeowt!</h2>\n      </div>\n      <div class=\"uk-modal-body\" style=\"font-size: 15px; text-align: center;\">\n        OptMeowt requires host permissions to be enabled to function correctly, please enable these permissions using\n        the button below. Note that OptMeowt does not collect your data!\n      </div>\n      <div class=\"uk-modal-footer\">\n        <button id=\"modal-button-4\" class=\"text-color uk-button uk-button-default\"\n          style=\"display: block; margin: 0 auto;\">ENABLE</button>\n      </div>\n    </div>\n  </div>\n\n  <div>\n    <label>\n      <div uk-grid class=\"uk-grid-column-small\" style=\"margin-left: 0px;\">\n        <div class=\"uk-container\" style=\"margin: auto;\">\n          <input class=\"uk-radio\" type=\"radio\" name=\"radio\" id=\"settings-view-radio0\" />\n        </div>\n        <div uk-grid class=\"uk-grid-row-collapse uk-width-expand\">\n          <div class=\"uk-first-column uk-width-1-1 uk-text-bold settings-popup1\" style=\"font-size: large;\"\n            id=\"settings-view-radio0-text\">\n            Enable\n          </div>\n          <div class=\"uk-first-column uk-width-1-1\" style=\"font-size: small;\">\n            Sends 'Global Privacy Control' signals to every visited domain\n          </div>\n        </div>\n      </div>\n    </label>\n    <br />\n    <label>\n      <div uk-grid class=\"uk-grid-column-small\" style=\"margin-left: 0px;\">\n        <div class=\"uk-container\" style=\"margin: auto;\">\n          <input class=\"uk-radio\" type=\"radio\" name=\"radio\" id=\"settings-view-radio2\" />\n        </div>\n        <div uk-grid class=\"uk-grid-row-collapse uk-width-expand\">\n          <div class=\"uk-first-column uk-width-1-1 uk-text-bold tutorial-tooltip1\" style=\"font-size: large;\">\n            Domain List\n          </div>\n          <div class=\"uk-first-column uk-width-1-1\" style=\"font-size: small;\">\n            Sends 'Global Privacy Control' signals according to the custom Domain List\n          </div>\n        </div>\n      </div>\n    </label>\n    <br />\n    <label>\n      <div uk-grid class=\"uk-grid-column-small\" style=\"margin-left: 0px;\">\n        <div class=\"uk-container\" style=\"margin: auto;\">\n          <input class=\"uk-radio\" type=\"radio\" name=\"radio\" id=\"settings-view-radio1\" />\n        </div>\n        <div uk-grid class=\"uk-grid-row-collapse uk-width-expand\">\n          <div class=\"uk-first-column uk-width-1-1 uk-text-bold\" style=\"font-size: large;\"\n            id=\"settings-view-radio1-text\">\n            Disable\n          </div>\n          <div class=\"uk-first-column uk-width-1-1\" style=\"font-size: small;\">\n            Does not send any 'Global Privacy Control' signals\n          </div>\n        </div>\n      </div>\n    </label>\n  </div>\n  </div>\n  <div class=\"uk-text-small uk-text-muted\" style=\"font-size: x-small; margin-top: 10px;\">\n    ** Please note that the privacy preferences you configure in OptMeowt may be influenced by global browser settings,\n    like enabling GPC browser-wide.\n  </div>\n  <hr style=\"margin-top: 5px; margin-bottom: 5px;\">\n  <div style=\"font-weight: bold; font-size: medium; padding-bottom: 10px;\">\n    Advanced Options\n  </div>\n  <div class=\"uk-margin-small\">\n    <label>\n      <div uk-grid class=\"uk-grid-column-small\" style=\"margin-left: 0px;\">\n        <div class=\"uk-container\" style=\"margin: auto;\">\n          <input class=\"uk-checkbox\" type=\"checkbox\" id=\"wellknown-check-toggle\" />\n        </div>\n        <div uk-grid class=\"uk-grid-row-collapse uk-width-expand\">\n          <div class=\"uk-first-column uk-width-1-1 uk-text-bold\" style=\"font-size: medium;\">\n            Check for /.well-known/gpc.json\n          </div>\n          <div class=\"uk-first-column uk-width-1-1\" style=\"font-size: small;\">\n            Disable to stop the request and hide this info in the popup\n          </div>\n        </div>\n      </div>\n    </label>\n  </div>\n  <div class=\"uk-margin-small\">\n    <label>\n      <div uk-grid class=\"uk-grid-column-small\" style=\"margin-left: 0px;\">\n        <div uk-grid class=\"uk-grid-row-collapse uk-width-expand\">\n          <div class=\"uk-first-column uk-width-1-1 uk-text-bold\" style=\"font-size: medium;\">\n            GPC Compliance State\n          </div>\n          <div class=\"uk-first-column uk-width-1-1\" style=\"font-size: small;\">\n            Select your state to see compliance data, or choose \"None\" to hide\n          </div>\n          <div class=\"uk-first-column uk-width-1-1\" style=\"margin-top: 6px;\">\n            <select id=\"state-select\" class=\"uk-select\" style=\"width: 200px; font-size: small;\">\n              <option value=\"none\">None (hidden)</option>\n              <option value=\"CA\">California</option>\n              <option value=\"CO\">Colorado</option>\n              <option value=\"CT\">Connecticut</option>\n              <option value=\"NJ\">New Jersey</option>\n            </select>\n          </div>\n        </div>\n      </div>\n    </label>\n  </div>\n  <div uk-grid>\n    <div>\n      <button id=\"download-button\" class=\"importexport-button\">\n        Export Domain List\n        </a>\n    </div>\n    <div>\n      <button id=\"upload-button\" class=\"importexport-button tutorial-tooltip2\">\n        Import Domain List from File\n      </button>\n      <input hidden type=\"file\" accept=\".json\" id=\"upload-domainlist\">\n      </input>\n    </div>\n  </div>\n\n  <br>\n\n  <div id=\"thank-you-modal\" uk-modal>\n    <div class=\"uk-modal-dialog\">\n      <button class=\"uk-modal-close-default\" type=\"button\" uk-close></button>\n      <div class=\"uk-modal-header uk-flex uk-flex-column uk-flex-middle\">\n        <img src=\"../../../assets/face-icons/optmeow-face-circle-green-ring-128.png\" alt=\"\">\n      </div>\n      <div class=\"uk-modal-body\" style=\"font-size: 15px; text-align: center;\">\n        <h2 class=\"text-color-darker\">Enjoy Using OptMeowt!</h2>\n      </div>\n      <div class=\"uk-modal-footer\">\n        <button id=\"modal-button-3\" class=\"text-color uk-button uk-button-default uk-align-center\">Learn More About\n          Us</button>\n      </div>\n    </div>\n  </div>\n\n\n</template>"
  },
  {
    "path": "src/options/views/settings-view/settings-view.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\nsettings-view.js\n================================================================================\nsettings-view.js loads settings-view.html when clicked on the options page\n*/\n\nimport { renderParse, fetchParse } from \"../../components/util.js\";\nimport {\n  handleDownload,\n  startUpload,\n  handleUpload,\n  stores,\n  storage,\n} from \"../../../background/storage.js\";\n\n// Used in tutorial\nimport UIkit from \"../../../../node_modules/uikit/dist/js/uikit.js\";\nimport tippy from \"../../../../node_modules/tippy.js/dist/tippy-bundle.umd.js\";\n\nimport \"../../../../node_modules/file-saver/src/FileSaver.js\";\nimport Darkmode from \"darkmode-js\"; // check darkmode\nimport {\n  addDynamicRule,\n  deleteAllDynamicRules,\n  reloadDynamicRules,\n} from \"../../../common/editRules.js\";\nimport { isWellknownCheckEnabled } from \"../../../common/settings.js\";\nimport { updateRemovalScript } from \"../../../common/editDomainlist.js\";\n\n\n\n/**\n * @typedef headings\n * @property {string} headings.title - Title of the given page\n * @property {string} headings.subtitle - Subtitle of the given page\n */\nconst headings = {\n  title: \"Settings\",\n  subtitle: \"Adjust extension settings\",\n};\n\n\n/**\n * Creates the event listeners for the `Settings` page buttons and options\n */\nfunction eventListeners() {\n  document\n    .getElementById(\"settings-view-radio0\")\n    .addEventListener(\"click\", () => {\n      chrome.runtime.sendMessage({\n        msg: \"TURN_ON_OFF\",\n        data: { isEnabled: true },\n      });\n      chrome.runtime.sendMessage({\n        msg: \"CHANGE_IS_DOMAINLISTED\",\n        data: { isDomainlisted: false },\n      });\n      chrome.scripting.updateContentScripts([\n        {\n          id: \"1\",\n          matches: [\"<all_urls>\"],\n          excludeMatches: [],\n          js: [\"content-scripts/registration/gpc-dom.js\"],\n          runAt: \"document_start\",\n        },\n      ]);\n      deleteAllDynamicRules();\n    });\n  document\n    .getElementById(\"settings-view-radio1\")\n    .addEventListener(\"click\", () => {\n      chrome.runtime.sendMessage({\n        msg: \"TURN_ON_OFF\",\n        data: { isEnabled: false },\n      });\n      chrome.runtime.sendMessage({\n        msg: \"CHANGE_IS_DOMAINLISTED\",\n        data: { isDomainlisted: false },\n      });\n      chrome.scripting.updateContentScripts([\n        {\n          id: \"1\",\n          matches: [\"https://example.com/foo/bar.html\"],\n          excludeMatches: [],\n          js: [\"content-scripts/registration/gpc-dom.js\"],\n          runAt: \"document_start\",\n        },\n      ]);\n      addDynamicRule(4999, \"*\");\n    });\n  document\n    .getElementById(\"settings-view-radio2\")\n    .addEventListener(\"click\", () => {\n      chrome.runtime.sendMessage({\n        msg: \"TURN_ON_OFF\",\n        data: { isEnabled: true },\n      });\n      chrome.runtime.sendMessage({\n        msg: \"CHANGE_IS_DOMAINLISTED\",\n        data: { isDomainlisted: true },\n      });\n      updateRemovalScript();\n      reloadDynamicRules();\n    });\n  document\n    .getElementById(\"download-button\")\n    .addEventListener(\"click\", handleDownload);\n  document\n    .getElementById(\"wellknown-check-toggle\")\n    .addEventListener(\"change\", async (event) => {\n      const enabled = event.target.checked;\n      await storage.set(\n        stores.settings,\n        enabled,\n        \"WELLKNOWN_CHECK_ENABLED\"\n      );\n      await chrome.storage.local.set({ WELLKNOWN_CHECK_ENABLED: enabled });\n      chrome.runtime.sendMessage({\n        msg: \"TOGGLE_WELLKNOWN_CHECK\",\n        data: { enabled },\n      });\n    });\n  document\n    .getElementById(\"state-select\")\n    .addEventListener(\"change\", async (event) => {\n      const stateCode = event.target.value;\n      await storage.set(\n        stores.settings,\n        stateCode,\n        \"USER_STATE\"\n      );\n      // Clear cached compliance data when state changes\n      await storage.clear(stores.complianceData);\n\n      // Set loading flag immediately\n      await storage.set(stores.settings, true, \"COMPLIANCE_LOADING\");\n\n      // Notify background script to start fetching new data immediately\n      chrome.runtime.sendMessage({ msg: \"USER_STATE_CHANGE\" });\n    });\n  document.getElementById(\"upload-button\").addEventListener(\"click\", () => {\n    const verify = confirm(\n      `This option will load a list of domains from a file, clearing all domains currently in the list.\\\\n Do you wish to continue?`\n    );\n    if (verify) {\n      startUpload();\n    }\n  });\n  document\n    .getElementById(\"upload-domainlist\")\n    .addEventListener(\"change\", handleUpload, false);\n\n  chrome.runtime.onMessage.addListener(async function (message, _, __) {\n    if (message.msg === \"SHOW_TUTORIAL\") {\n      if (\"$BROWSER\" == \"chrome\") {\n        chrome.tabs.reload();\n      } else {\n        await storage.set(stores.settings, true, \"TUTORIAL_SHOWN\");\n        walkthrough();\n      }\n    }\n  });\n}\n\n\n/******************************************************************************/\n\n/*\n * Gives user a walkthrough of install page on first install\n */\n\n\nfunction walkthrough() {\n  let modal = UIkit.modal(\"#welcome-modal\");\n  modal.show();\n\n  document.getElementById(\"modal-button-1\").onclick = function () {\n    modal.hide();\n  };\n\n  document.getElementById(\"modal-button-2\").onclick = function () {\n    modal.hide();\n    tippy(\".tutorial-tooltip1\", {\n      content:\n        \"<p>Set which sites should receive a Global Privacy Control signal<p>  <button class='uk-button uk-button-default'>Next</button>\",\n      allowHTML: true,\n      trigger: \"manual\",\n      placement: \"right\",\n      offset: [0, -600],\n      duration: 1000,\n      theme: \"custom-1\",\n      onHide(instance) {\n        trigger2();\n      },\n    });\n    let tooltip =\n      document.getElementsByClassName(\"tutorial-tooltip1\")[0]._tippy;\n    tooltip.show();\n  };\n\n  function trigger2() {\n    tippy(\".tutorial-tooltip2\", {\n      content:\n        \"<p>Import and export your customized list of sites that should receive a signal<p>  <button class='uk-button uk-button-default'>Next</button>\",\n      allowHTML: true,\n      trigger: \"manual\",\n      duration: 1000,\n      theme: \"custom-1\",\n      placement: \"right\",\n      offset: [0, 60],\n      onHide() {\n        trigger4();\n      },\n    });\n    let tooltip =\n      document.getElementsByClassName(\"tutorial-tooltip2\")[0]._tippy;\n    tooltip.show();\n  }\n\n  function trigger4() {\n    let modal = UIkit.modal(\"#thank-you-modal\");\n    modal.show();\n    document.getElementById(\"modal-button-3\").onclick = () => {\n      chrome.tabs.create(\n        { url: \"https://privacytechlab.org/\" },\n        function (tab) { }\n      );\n    };\n  }\n}\n\n/*\n * Request host permissions upon install\n */\n\nasync function requestPermissionsButton() {\n  try {\n    // Request permissions\n    const response = await browser.permissions.request({\n      origins: [\"<all_urls>\"] // Allows host permissions\n    });\n\n    // Check if permissions were granted or refused\n    if (response) {\n      console.log(\"Permissions were granted\");\n      storage.set(stores.settings, true, \"REQUEST_PERMISSIONS_SHOWN\");\n    } else {\n      console.log(\"Permissions were refused\");\n    }\n\n    // Retrieve current permissions after the request\n    const currentPermissions = await browser.permissions.getAll();\n    console.log(`Current permissions:`, currentPermissions);\n  } catch (error) {\n    console.error('Error requesting permissions:', error);\n  }\n}\n\nfunction requestPermissions() {\n  let modal = UIkit.modal('#permission-modal');\n  modal.show();\n  document.getElementById(\"modal-button-4\").onclick = () => {\n    requestPermissionsButton();\n    modal.hide();\n  }\n}\n\n/******************************************************************************/\n\n/**\n * Renders the `Settings` view in the options page\n * @param {string} scaffoldTemplate - stringified HTML template\n */\nexport async function settingsView(scaffoldTemplate) {\n  const body = renderParse(scaffoldTemplate, headings, \"scaffold-component\");\n  let content = await fetchParse(\n    \"./views/settings-view/settings-view.html\",\n    \"settings-view\"\n  );\n\n  document.getElementById(\"content\").innerHTML = body.innerHTML;\n  document.getElementById(\"scaffold-component-body\").innerHTML =\n    content.innerHTML;\n\n  // Render correct extension mode radio button\n  const isEnabled = await storage.get(stores.settings, \"IS_ENABLED\");\n  const isDomainlisted = await storage.get(stores.settings, \"IS_DOMAINLISTED\");\n  const wellknownCheckEnabled = await isWellknownCheckEnabled();\n\n  if (isEnabled) {\n    isDomainlisted\n      ? (document.getElementById(\"settings-view-radio2\").checked = true)\n      : (document.getElementById(\"settings-view-radio0\").checked = true);\n  } else {\n    document.getElementById(\"settings-view-radio1\").checked = true;\n  }\n\n  document.getElementById(\"wellknown-check-toggle\").checked =\n    wellknownCheckEnabled;\n\n  const userState = await storage.get(stores.settings, \"USER_STATE\");\n  document.getElementById(\"state-select\").value = userState || \"none\";\n\n  eventListeners();\n\n  const tutorialShown = await storage.get(stores.settings, \"TUTORIAL_SHOWN\");\n  if (!tutorialShown) {\n    walkthrough();\n  }\n  storage.set(stores.settings, true, \"TUTORIAL_SHOWN\");\n\n  if (\"$BROWSER\" == \"firefox\") {\n    const requestShown = await storage.get(stores.settings, \"REQUEST_PERMISSIONS_SHOWN\");\n    if (!requestShown) {\n      requestPermissions();\n    }\n  }\n}\n"
  },
  {
    "path": "src/popup/popup.html",
    "content": "<!--\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n-->\n\n<!--\npopup.html\n================================================================================\npopup.html is the html scaffolding for OptMeowt's popup page\n-->\n\n<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <title>OptMeowt</title>\n  <meta charset=\"UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n</head>\n\n<!-- This is the entry point to 'Popup' -->\n\n<body class=\"uk-padding-large\" style=\"width: 320px\">\n  <!-- State Selection Overlay (first-time setup) -->\n  <div id=\"state-selection-overlay\" style=\"display: none;\">\n    <div class=\"state-overlay-content\">\n      <img src=\"./assets/cat-w-text/optmeow-logo-circle.png\" height=\"36\" width=\"36\" alt=\"Logo\" />\n      <h3 class=\"state-overlay-title\">See how sites treat your data by selecting a state</h3>\n      <p class=\"state-overlay-desc\">\n        GPC compliance data is available for select states.\n      </p>\n      <div class=\"state-buttons\">\n        <button class=\"state-btn\" data-state=\"CA\">🌴 California</button>\n        <button class=\"state-btn\" data-state=\"CO\">🏔️ Colorado</button>\n        <button class=\"state-btn\" data-state=\"CT\">🌳 Connecticut</button>\n        <button class=\"state-btn\" data-state=\"NJ\">🏖️ New Jersey</button>\n      </div>\n      <button id=\"state-skip-link\" class=\"state-btn state-btn-skip\">Skip — I'm not in these states</button>\n    </div>\n  </div>\n\n  <!-- Header -->\n  <div id=\"pop-up-header\" uk-grid class=\"uk-grid-small\">\n    <div>\n      <a href=\"https://privacytechlab.org/\" target=\"_blank\">\n        <img src=\"./assets/cat-w-text/optmeow-logo-circle.png\" height=\"32\" width=\"32\" alt=\"Logo\" />\n      </a>\n    </div>\n    <div class=\"uk-width-expand\" style=\"font-size: medium; font-weight: bold; margin: auto\" id=\"start-tutorial\">\n      OptMeowt\n    </div>\n    <div class=\"uk-container\" style=\"margin: auto; padding: 0; padding-right: 8px\" uk-tooltip=\"\" id=\"enable-disable\">\n      <img src=\"\" height=\"20\" width=\"20\" alt=\"enable-disable\" uk-svg id=\"img\" />\n    </div>\n    <div class=\"uk-container\" style=\"margin: auto; padding: 0; padding-right: 8px\" uk-tooltip=\"More\" id=\"more\">\n      <img src=\"./assets/options-2-outline.svg\" height=\"20\" width=\"20\" alt=\"more\" uk-svg />\n    </div>\n    <div class=\"uk-container\" style=\"margin: auto; padding: 0\" uk-tooltip=\"Tutorial\" id=\"tour\">\n      <img src=\"../assets/question-mark.svg\" height=\"20\" width=\"20\" alt=\"tour\" uk-svg id=\"img\" />\n    </div>\n  </div>\n  <hr id=\"divider-1\" class=\"divide\" class=\"uk-margin-small-top\" />\n\n  <!-- Popup body content -->\n  <div class=\"uk-inline uk-width-expand\">\n    <!-- Content -->\n    <div id=\"content\">\n      <!-- Domain List UI elements here -->\n\n\n      <div id=\"domain-title\" style=\"overflow-wrap: break-word; display: inline-block\" class=\"\n            blue-heading\n            uk-flex-inline\n            uk-width-1-1\n            uk-flex-center\n            uk-text-center\n            uk-text-large\n            uk-text-bold\n            uk-text-truncate\n          \">\n        <!-- Active tab domain inflated here -->\n      </div>\n\n      <!-- Use this to inflate \n            (1) DNS status in protection mode \n             -->\n      <div id=\"more-info-body\">\n        <div uk-grid style=\"margin-top: 4%; margin-bottom: 4%\">\n          <div id=\"more-info-text\" class=\"uk-width-expand uk-margin-auto-vertical\"\n            style=\"font-weight: bold; font-size: medium\">\n            <!-- Inflate more info status here -->\n          </div>\n          <div>\n            <div uk-grid>\n              <div class=\"uk-width-auto\">\n                <label class=\"switch tooltip-1\" id=\"switch-label\">\n                  <!-- Populate switch preference here -->\n                </label>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <hr id=\"divider-2\" class=\"divide\" style=\"margin: 0px\" />\n\n      <!-- stats about visited domains -->\n      <div uk-grid class=\"uk-width-expand uk-text-center\" style=\"font-size: medium; padding: 10px; margin-left: 0px\">\n        <div id=\"visited-domains-stats\" class=\"uk-text-center\"></div>\n      </div>\n      <hr id=\"divider-3\" class=\"divide\" style=\"margin: 0px\" />\n\n      <!-- Dropdown 1 -->\n      <div id=\"dropdown-1\" uk-grid class=\"dropdown-tab\" style=\"padding: 15px; margin-right: -30px\">\n        <div class=\"uk-container\" style=\"margin: auto; padding: 0; padding-left: 30px\" uk-tooltip=\"Dropdown\">\n          <img id=\"dropdown-chevron-1\" src=\"./assets/chevron-down.svg\" height=\"15\" width=\"15\" alt=\"dropdown\" uk-svg />\n        </div>\n        <div id=\"dropdown-1-text\" class=\"uk-width-expand uk-text-left\"\n          style=\"font-weight: bold; font-size: medium; margin-right: 20px\">\n          <!-- Inflate dropdown 1 text info here -->\n        </div>\n      </div>\n      <hr class=\"divide\" id=\"divider-4\" style=\"margin: 0px; display: none\" />\n\n      <ul id=\"dropdown-1-expandable\" class=\"uk-list\" style=\"display: none\">\n        <!-- Inflate dropdown 1 info here -->\n      </ul>\n      <hr id=\"divider-5\" class=\"divide\" style=\"margin: 0px; display: none\" />\n\n      <!-- Dropdown 2 -->\n      <div id=\"dropdown-2\" uk-grid class=\"dropdown-tab\" style=\"padding: 15px; margin-right: -30px\">\n        <div class=\"uk-container\" style=\"margin: auto; padding: 0; padding-left: 30px\" uk-tooltip=\"Dropdown\">\n          <img id=\"dropdown-chevron-2\" src=\"./assets/chevron-down.svg\" height=\"15\" width=\"15\" alt=\"dropdown\" uk-svg />\n        </div>\n        <div id=\"dropdown-2-text\" class=\"uk-width-expand uk-text-left\"\n          style=\"font-weight: bold; font-size: medium; margin-right: 20px\">\n          <!-- Inflate dropdown 2 text info here -->\n        </div>\n      </div>\n      <hr class=\"divide\" id=\"divider-6\" style=\"margin: 0px; display: none\" />\n\n      <ul id=\"dropdown-2-expandable\" class=\"uk-list\" style=\"display: none\">\n        <!-- Inflate dropdown 2 info here -->\n      </ul>\n      <hr id=\"divider-7\" class=\"divide\" style=\"margin: 0px; display: none\" />\n\n      <!-- Compliance Status (always visible) -->\n      <div id=\"compliance-section\" style=\"display: none;\">\n        <hr class=\"divide\" style=\"margin: 0px\" />\n        <div id=\"compliance-status-content\" style=\"padding: 10px 15px;\">\n          <!-- Compliance badge and details rendered by popup.js -->\n        </div>\n      </div>\n\n      <!-- Domain List link (Protection mode only) -->\n      <div id=\"domain-list\" uk-grid class=\"domain-list\" style=\"padding: 15px; margin-right: -30px\">\n        <div id=\"domain-list-text\" class=\"uk-width-expand uk-text-center\"\n          style=\"font-weight: bold; font-size: medium; margin-right: 20px\">\n          Domain List\n        </div>\n      </div>\n\n      <hr id=\"divider-10\" class=\"divide\" style=\"margin: 0px\" />\n\n      <!-- GPC Web UI link -->\n      <div id=\"gpc-web-ui-link\" uk-grid class=\"domain-list\" style=\"padding: 15px; margin-right: -30px\">\n        <div class=\"uk-width-expand uk-text-center\"\n          style=\"font-weight: bold; font-size: medium; margin-right: 20px\">\n          See Web Compliance Data\n        </div>\n      </div>\n\n      <hr class=\"divide\" style=\"margin: 0px\" />\n      <!-- Dark Mode -->\n      <div uk-grid class=\"uk-margin-top\" style=\"margin-bottom: 4%\">\n        <div class=\"uk-width-expand uk-margin-auto-vertical\" style=\"font-weight: bold; font-size: medium\">\n          Dark Mode\n        </div>\n        <div uk-grid>\n          <div class=\"uk-width-auto\">\n            <label class=\"custom-control-label switch\" for=\"darkSwitch\" text=\"Dark Mode Switch\">\n              <input type=\"checkbox\" class=\"custom-control-input\" id=\"darkSwitch\" />\n              <span></span>\n            </label>\n          </div>\n        </div>\n      </div>\n\n      <br />\n      <div id=\"gpc-branding\">\n        <a href=\"https://globalprivacycontrol.org/\" target=\"_blank\">\n          <img src=\"./assets/gpc-logo.svg\" style=\"width: 60%\" alt=\"gpc-logo\" uk-svg id=\"gpc-logo\" />\n        </a>\n      </div>\n    </div>\n    <!-- Extension is disabled message (when content is hidden) -->\n    <div id=\"extension-disabled-message\" class=\"uk-position-cover uk-flex uk-flex-center uk-flex-middle\"\n      style=\"transition: all ease 0.25s; opacity: 0\">\n      <div class=\"uk-container uk-text-center\">\n        <div style=\"font-size: medium; font-weight: bold; margin-bottom: 4px\">\n          Extension Disabled\n        </div>\n        <div>\n          You may enable the extension by clicking the play button above.\n        </div>\n      </div>\n    </div>\n  </div>\n</body>\n\n</html>"
  },
  {
    "path": "src/popup/popup.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\npopup.js\n================================================================================\npopup.js supplements and renders complex elements on popup.html\n*/\n\nimport { stores, storage } from \"../background/storage.js\";\nimport { isWellknownCheckEnabled, getUserState } from \"../common/settings.js\";\nimport { STATE_NAMES } from \"../data/complianceData.js\";\nimport \"../../node_modules/uikit/dist/css/uikit.min.css\";\nimport \"../../node_modules/animate.css/animate.min.css\";\nimport \"./styles.css\";\nimport psl from \"psl\";\nimport \"../../node_modules/uikit/dist/js/uikit.js\";\nimport \"../../node_modules/uikit/dist/js/uikit-icons.js\";\nimport \"../../node_modules/@popperjs/core/dist/umd/popper.js\";\nimport tippy from \"../../node_modules/tippy.js/dist/tippy-bundle.umd.js\";\nimport UIkit from \"uikit\";\nimport Darkmode from \"../theme/darkmode.js\";\n\nimport {\n  addDomainToDomainlistAndRules,\n  removeDomainFromDomainlistAndRules,\n  updateRemovalScript,\n} from \"../common/editDomainlist.js\";\n\nimport { reloadDynamicRules, addDynamicRule } from \"../common/editRules.js\";\n\n// Global scope settings variables\nvar isEnabled;\nvar isDomainlisted;\nvar parsedDomain;\n\n// Protection mode data\nvar domainsInfo;\nvar wellknownInfo;\n\n// Darkmode\nconst darkmode = new Darkmode();\n\n\n/******************************************************************************/\n/******************************************************************************/\n/**********       # First-to-load popup components (essential)       **********/\n/******************************************************************************/\n/******************************************************************************/\n\n\n//Init: initialize darkmode button (NOTE: accesses global scope `mode`)\nfunction generateDarkmodeElement() {\n  let darkSwitch = document.getElementById(\"darkSwitch\");\n  let darkmodeText = \"\";\n  if (darkmode.isActivated()) {\n    darkmodeText = `<input\n      type=\"checkbox\"\n      class=\"custom-control-input\"\n      id=\"darkSwitch\" \n      checked\n      />`;\n  } else {\n    darkmodeText = `<input\n      type=\"checkbox\"\n      class=\"custom-control-input\"\n      id=\"darkSwitch\"\n      />`;\n  }\n  darkSwitch.outerHTML = darkmodeText;\n\n  document.getElementById(\"darkSwitch\").addEventListener(\"click\", () => {\n    chrome.runtime.sendMessage({\n      msg: \"DARKSWITCH_PRESSED\",\n    });\n    darkmode.toggle();\n  });\n}\n\n// Fetches the current domain\nfunction getCurrentParsedDomain() {\n  return new Promise((resolve, reject) => {\n    try {\n      chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {\n        let tab = tabs[0];\n        let url = new URL(tab.url);\n        let parsed = psl.parse(url.hostname);\n        let domain = parsed.domain;\n        parsedDomain = domain; // for global scope variable\n        resolve(domain);\n      });\n    } catch (e) {\n      reject();\n    }\n  });\n}\n\n/**\n * In sync with global scope `parsedDomain`\n * @param {String} parsedDomain\n */\nfunction renderFirstPartyDomain(parsedDomain) {\n  if (parsedDomain) {\n    document.getElementById(\"domain-title\").innerHTML = parsedDomain;\n    initPopUpWalkthrough();\n  } else {\n    document.getElementById(\"domain-title\").style.display = \"none\";\n  }\n}\n\n// Extension on/off renderer helper\n\nfunction renderExtensionIsEnabledDisabled(isEnabled, isDomainlisted) {\n  if (isEnabled === undefined || isDomainlisted === undefined) {\n    document.getElementById(\"img\").src = \"../assets/play-circle-outline.svg\";\n    document\n      .getElementById(\"enable-disable\")\n      .setAttribute(\"uk-tooltip\", \"Enable\");\n  } else if (isEnabled) {\n    document.getElementById(\"img\").src = \"../assets/pause-circle-outline.svg\";\n    document\n      .getElementById(\"enable-disable\")\n      .setAttribute(\"uk-tooltip\", \"Disable\");\n    document.getElementById(\"content\").style.opacity = \"1\";\n    document.getElementById(\"extension-disabled-message\").style.opacity = \"0\";\n    document.getElementById(\"extension-disabled-message\").style.display =\n      \"none\";\n  } else {\n    document.getElementById(\"extension-disabled-message\").style.display = \"\";\n    document.getElementById(\"img\").src = \"../assets/play-circle-outline.svg\";\n    document\n      .getElementById(\"enable-disable\")\n      .setAttribute(\"uk-tooltip\", \"Enable\");\n    document.getElementById(\"content\").style.opacity = \"0.1\";\n    document.getElementById(\"extension-disabled-message\").style.opacity = \"1\";\n  }\n}\n\nfunction turnonoff(isEnabled) {\n  if (isEnabled) {\n    chrome.scripting.updateContentScripts([\n      {\n        id: \"1\",\n        matches: [\"https://example.com/foo/bar.html\"],\n        excludeMatches: [],\n        js: [\"content-scripts/registration/gpc-dom.js\"],\n        runAt: \"document_start\",\n      },\n    ]);\n    addDynamicRule(4999, \"*\");\n  } else {\n    updateRemovalScript();\n    reloadDynamicRules();\n  }\n}\n\nfunction listenerExtensionIsEnabledDisabledButton(\n  isEnabled,\n  isDomainlisted,\n  mode\n) {\n  document\n    .getElementById(\"enable-disable\")\n    .addEventListener(\"click\", async () => {\n      isEnabled = await storage.get(stores.settings, \"IS_ENABLED\");\n\n      if (isEnabled) {\n        document.getElementById(\"extension-disabled-message\").style.display =\n          \"\";\n        document.getElementById(\"img\").src =\n          \"../assets/play-circle-outline.svg\";\n        document\n          .getElementById(\"enable-disable\")\n          .setAttribute(\"uk-tooltip\", \"Enable\");\n        document.getElementById(\"content\").style.opacity = \"0.1\";\n        document.getElementById(\"extension-disabled-message\").style.opacity =\n          \"1\";\n        chrome.runtime.sendMessage({\n          msg: \"TURN_ON_OFF\",\n          data: { isEnabled: false },\n        });\n      } else {\n        document.getElementById(\"img\").src =\n          \"../assets/pause-circle-outline.svg\";\n        document\n          .getElementById(\"enable-disable\")\n          .setAttribute(\"uk-tooltip\", \"Disable\");\n        document.getElementById(\"content\").style.opacity = \"1\";\n        document.getElementById(\"extension-disabled-message\").style.opacity =\n          \"0\";\n        document.getElementById(\"extension-disabled-message\").style.display =\n          \"none\";\n        chrome.runtime.sendMessage({\n          msg: \"TURN_ON_OFF\",\n          data: { isEnabled: true },\n        });\n      }\n      turnonoff(isEnabled);\n    });\n}\n\n// Domain counter for Protection mode helper\n\nasync function renderDomainCounter() {\n  const domainlistValues = await storage.getAll(stores.domainlist);\n  let count = Object.keys(domainlistValues).filter((key) => {\n    return domainlistValues[key] == null;\n  }).length;\n  document.getElementById(\"visited-domains-stats\").innerHTML = `\n    <p id = \"domain-count\" class=\"blue-heading\" style=\"font-size:25px;\n    font-weight: bold\">${count}</p> Domains Receiving Signals\n  `;\n}\n\n// First party domain and the Global Privacy Control listener helper\n\nasync function renderFirstPartyDomainDNSToggle() {\n  let checkbox = \"\";\n  let text = \"\";\n  if (parsedDomain) {\n    try {\n      const parsedDomainValue = await storage.get(\n        stores.domainlist,\n        parsedDomain\n      );\n\n      if (!parsedDomainValue) {\n        checkbox = `<input type=\"checkbox\" id=\"input\" checked/><span></span>`;\n        text = \"Global Privacy Control\";\n      } else {\n        checkbox = `<input type=\"checkbox\" id=\"input\"/><span></span>`;\n        text = \"Global Privacy Control Off\";\n      }\n      document.getElementById(\"switch-label\").innerHTML = checkbox;\n      document.getElementById(\"more-info-text\").innerHTML = text;\n    } catch (e) {\n      console.error(e);\n      document.getElementById(\"switch-label\").innerHTML = checkbox;\n      document.getElementById(\"more-info-text\").innerHTML = text;\n    }\n  } else {\n    document.getElementById(\"switch-label\").innerHTML = checkbox;\n    document.getElementById(\"more-info-text\").innerHTML = text;\n  }\n}\n\nasync function listenerFirstPartyDomainDNSToggleCallback() {\n  chrome.runtime.sendMessage({ msg: \"TURN_ON_OFF\", data: { isEnabled: true } });\n  chrome.runtime.sendMessage({\n    msg: \"CHANGE_IS_DOMAINLISTED\",\n    data: { isDomainlisted: true },\n  });\n  const parsedDomainValue = await storage.get(stores.domainlist, parsedDomain);\n  let elemString = \"\";\n  if (!parsedDomainValue) {\n    elemString = \"Global Privacy Control Disabled\";\n    await addDomainToDomainlistAndRules(parsedDomain);\n  } else {\n    elemString = \"Global Privacy Control\";\n    await removeDomainFromDomainlistAndRules(parsedDomain);\n  }\n\n  document.getElementById(\"more-info-text\").innerHTML = elemString;\n}\n\nfunction listenerFirstPartyDomainDNSToggle() {\n  document\n    .getElementById(\"switch-label\")\n    .addEventListener(\"click\", listenerFirstPartyDomainDNSToggleCallback);\n}\n\nfunction removeFirstPartyDomainDNSToggle() {\n  document\n    .getElementById(\"switch-label\")\n    .removeEventListener(\"click\", listenerFirstPartyDomainDNSToggleCallback);\n  document.getElementById(\"switch-label\").innerHTML = \"\";\n  document.getElementById(\"more-info-text\").innerHTML = \"\";\n}\n\n// Dropdown helpers\n\nfunction renderDropdown1Toggle() {\n  if (\n    document.getElementById(\"dropdown-1-expandable\").style.display === \"none\"\n  ) {\n    document.getElementById(\"dropdown-chevron-1\").src =\n      \"../assets/chevron-down.svg\";\n    document.getElementById(\"dropdown-1-expandable\").style.display = \"none\";\n    document\n      .getElementById(\"dropdown-1\")\n      .classList.remove(\"dropdown-tab-click\");\n    document.getElementById(\"divider-4\").style.display = \"none\";\n  } else {\n    document.getElementById(\"dropdown-chevron-1\").src =\n      \"../assets/chevron-up.svg\";\n    document.getElementById(\"dropdown-1-expandable\").style.display = \"\";\n    document.getElementById(\"dropdown-1\").classList.add(\"dropdown-tab-click\");\n    document.getElementById(\"divider-4\").style.display = \"\";\n  }\n}\n\nfunction renderDropdown2Toggle() {\n  if (\n    document.getElementById(\"dropdown-2-expandable\").style.display === \"none\"\n  ) {\n    document.getElementById(\"dropdown-chevron-2\").src =\n      \"../assets/chevron-down.svg\";\n    document.getElementById(\"dropdown-2-expandable\").style.display = \"none\";\n    document\n      .getElementById(\"dropdown-2\")\n      .classList.remove(\"dropdown-tab-click\");\n    document.getElementById(\"divider-6\").style.display = \"none\";\n  } else {\n    document.getElementById(\"dropdown-chevron-2\").src =\n      \"../assets/chevron-up.svg\";\n    document.getElementById(\"dropdown-2-expandable\").style.display = \"\";\n    document.getElementById(\"dropdown-2\").classList.add(\"dropdown-tab-click\");\n    document.getElementById(\"divider-6\").style.display = \"\";\n  }\n}\n\nfunction listenerDropdown1ToggleCallback() {\n  if (\n    document.getElementById(\"dropdown-1-expandable\").style.display === \"none\"\n  ) {\n    document.getElementById(\"dropdown-chevron-1\").src =\n      \"../assets/chevron-up.svg\";\n    document.getElementById(\"dropdown-1-expandable\").style.display = \"\";\n    document.getElementById(\"dropdown-1\").classList.add(\"dropdown-tab-click\");\n    document.getElementById(\"divider-4\").style.display = \"\";\n  } else {\n    document.getElementById(\"dropdown-chevron-1\").src =\n      \"../assets/chevron-down.svg\";\n    document.getElementById(\"dropdown-1-expandable\").style.display = \"none\";\n    document\n      .getElementById(\"dropdown-1\")\n      .classList.remove(\"dropdown-tab-click\");\n    document.getElementById(\"divider-4\").style.display = \"none\";\n  }\n}\n\nfunction listenerDropdown2ToggleCallback() {\n  if (\n    document.getElementById(\"dropdown-2-expandable\").style.display === \"none\"\n  ) {\n    document.getElementById(\"dropdown-chevron-2\").src =\n      \"../assets/chevron-up.svg\";\n    document.getElementById(\"dropdown-2-expandable\").style.display = \"\";\n    document.getElementById(\"dropdown-2\").classList.add(\"dropdown-tab-click\");\n    document.getElementById(\"divider-6\").style.display = \"\";\n  } else {\n    document.getElementById(\"dropdown-chevron-2\").src =\n      \"../assets/chevron-down.svg\";\n    document.getElementById(\"dropdown-2-expandable\").style.display = \"none\";\n    document\n      .getElementById(\"dropdown-2\")\n      .classList.remove(\"dropdown-tab-click\");\n    document.getElementById(\"divider-6\").style.display = \"none\";\n  }\n}\n\nfunction listenerDropdown1Toggle() {\n  document\n    .getElementById(\"dropdown-1\")\n    .addEventListener(\"click\", listenerDropdown1ToggleCallback);\n}\n\nfunction listenerDropdown2Toggle() {\n  document\n    .getElementById(\"dropdown-2\")\n    .addEventListener(\"click\", listenerDropdown2ToggleCallback);\n}\n\nfunction removeListenerDropdown1Toggle() {\n  document\n    .getElementById(\"dropdown-1\")\n    .removeEventListener(\"click\", listenerDropdown1ToggleCallback);\n}\n\nfunction removeListenerDropdown2Toggle() {\n  document\n    .getElementById(\"dropdown-2\")\n    .removeEventListener(\"click\", listenerDropdown2ToggleCallback);\n}\n\n\n/******************************************************************************/\n/******************************************************************************/\n/**********                 # Inflates main content                  **********/\n/******************************************************************************/\n/******************************************************************************/\n\n/**\n * Redraws the popup for protection mode\n */\nasync function showProtectionInfo() {\n  removeFirstPartyDomainDNSToggle();\n  removeListenerDropdown1Toggle();\n  removeListenerDropdown2Toggle();\n  document.getElementById(\"switch-label\").innerHTML = \"\";\n  document.getElementById(\"more-info-body\").style.display = \"\";\n  document.getElementById(\"more-info-text\").innerHTML = \"Global Privacy Control On!\";\n  document.getElementById(\"dropdown-1\").style.display = \"\";\n  document.getElementById(\"dropdown-1-text\").innerHTML = \"3rd Party Domains\";\n  document.getElementById(\"dropdown-2-text\").innerHTML = \"Well-known Data\";\n  document.getElementById(\"dropdown-1-expandable\").innerHTML = \"\";\n  document.getElementById(\"dropdown-2-expandable\").innerHTML = \"\";\n  document.getElementById(\"dropdown-1-expandable\").style.display = \"none\";\n  document.getElementById(\"dropdown-2-expandable\").style.display = \"none\";\n  document.getElementById(\"divider-6\").style.display = \"none\";\n  document.getElementById(\"divider-7\").style.display = \"none\";\n  document.getElementById(\"visited-domains-stats\").style.display = \"\";\n  document.getElementById(\"domain-list\").style.display = \"\";\n  document.getElementById(\"gpc-web-ui-link\").style.display = \"\";\n\n  const wellknownCheckEnabled = await isWellknownCheckEnabled();\n  document.getElementById(\"dropdown-2\").style.display = wellknownCheckEnabled\n    ? \"\"\n    : \"none\";\n\n  // Generate `Global Privacy Control` elem\n  renderDomainCounter(); // Render \"X domains receiving signals\" info section\n  renderFirstPartyDomainDNSToggle(); // Render 1P domain \"DNS Enabled/Disabled\" text+toggle\n\n  // Listeners associated with the buttons / toggles rendered above\n  listenerFirstPartyDomainDNSToggle();\n  listenerDropdown1Toggle();\n  if (wellknownCheckEnabled) {\n    listenerDropdown2Toggle();\n  }\n\n  let domain = await getCurrentParsedDomain();\n  if (!domain) {\n    await buildDomains([]);\n    if (wellknownCheckEnabled) {\n      await buildWellKnown(null);\n    }\n    return;\n  }\n\n  let parties = await storage.get(stores.thirdParties, domain);\n  let wellknown = wellknownCheckEnabled\n    ? await storage.get(stores.wellknownInformation, domain)\n    : null;\n\n  await buildDomains(parties);\n  if (wellknownCheckEnabled) {\n    await buildWellKnown(wellknown ?? null);\n  }\n\n  const userState = await getUserState();\n  const complianceEnabled = userState && userState !== 'none';\n\n  if (complianceEnabled) {\n    // Check initial loading state\n    const isLoading = await storage.get(stores.settings, \"COMPLIANCE_LOADING\");\n    if (isLoading) {\n      await buildComplianceStatusLoading(userState);\n    } else {\n      // Data might already be loaded\n      const metadata = await storage.get(stores.complianceData, '_metadata');\n      const viewUrl = metadata?.viewUrl || null;\n      const complianceStatus = await storage.get(stores.complianceData, domain);\n      await buildComplianceStatus(complianceStatus ?? null, userState, viewUrl);\n    }\n    document.getElementById(\"compliance-section\").style.display = \"\";\n\n    // Add a local listener specifically for this popup view\n    // so it updates when the background script says it's downloading/ready\n    chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {\n      if (message.msg === \"COMPLIANCE_DATA_LOADING\") {\n        await buildComplianceStatusLoading(userState);\n      } else if (message.msg === \"COMPLIANCE_DATA_READY\") {\n        const metadata = await storage.get(stores.complianceData, '_metadata');\n        const freshViewUrl = metadata?.viewUrl || null;\n        const newStatus = await storage.get(stores.complianceData, domain);\n        await buildComplianceStatus(newStatus ?? null, userState, freshViewUrl);\n      }\n    });\n\n  } else {\n    document.getElementById(\"compliance-section\").style.display = \"none\";\n  }\n\n\n\n  // chrome.runtime.sendMessage({\n  //   msg: \"POPUP_PROTECTION_REQUESTS\",\n  //   data: null,\n  // });\n}\n\n/**\n * In sync with global scope `mode`\n * @param {Modes} mode - from modes.js\n */\n\n/**\n * Initializes the popup window after DOM content is loaded\n * @param {Object} event - contains information about the event\n */\ndocument.addEventListener(\"DOMContentLoaded\", async (event) => {\n  // Check if state has been selected (first-time setup)\n  const userState = await getUserState();\n\n  if (!userState) {\n    // Show state selection overlay\n    document.getElementById(\"state-selection-overlay\").style.display = \"flex\";\n\n    // State button listeners\n    document.querySelectorAll(\".state-btn\").forEach(btn => {\n      btn.addEventListener(\"click\", async () => {\n        const stateCode = btn.dataset.state;\n        await storage.set(stores.settings, stateCode, \"USER_STATE\");\n        await storage.clear(stores.complianceData);\n        await storage.set(stores.settings, true, \"COMPLIANCE_LOADING\");\n        await chrome.runtime.sendMessage({ msg: \"USER_STATE_CHANGE\" });\n        document.getElementById(\"state-selection-overlay\").style.display = \"none\";\n        // Re-initialize popup with state selected\n        location.reload();\n      });\n    });\n\n    // Skip link\n    document.getElementById(\"state-skip-link\").addEventListener(\"click\", async (e) => {\n      e.preventDefault();\n      await storage.set(stores.settings, \"none\", \"USER_STATE\");\n      document.getElementById(\"state-selection-overlay\").style.display = \"none\";\n      location.reload();\n    });\n\n    return; // Don't initialize rest of popup\n  }\n\n  isEnabled = await storage.get(stores.settings, \"IS_ENABLED\");\n  isDomainlisted = await storage.get(stores.settings, \"IS_DOMAINLISTED\");\n  parsedDomain = await getCurrentParsedDomain(); // This must happen first\n\n  renderExtensionIsEnabledDisabled(isEnabled, isDomainlisted); // Render global ENABLED/DISABLED mode\n  listenerExtensionIsEnabledDisabledButton(isEnabled);\n\n  renderFirstPartyDomain(parsedDomain); // Render 1P domain\n\n  generateDarkmodeElement(); // Render darkmode\n  showProtectionInfo();\n});\n\n/******************************************************************************/\n/******************************************************************************/\n/**********          # Second-to-load components (dropdowns)         **********/\n/******************************************************************************/\n/******************************************************************************/\n\n/**\n * Builds the listener to enable toggling 3rd party domains on/off in domainlist\n * @param {String} requestDomain - the domain related to the element which\n * the listener should be attached\n */\nfunction addThirdPartyDomainDNSToggleListener(requestDomain) {\n  document\n    .getElementById(`input-${requestDomain}`)\n    .addEventListener(\"click\", async () => {\n      chrome.runtime.sendMessage({\n        msg: \"TURN_ON_OFF\",\n        data: { isEnabled: true },\n      });\n      chrome.runtime.sendMessage({\n        msg: \"CHANGE_IS_DOMAINLISTED\",\n        data: { isDomainlisted: true },\n      });\n      const requestDomainValue = await storage.get(\n        stores.domainlist,\n        requestDomain\n      );\n      let elemString = \"\";\n      if (!requestDomainValue) {\n        elemString = \"Global Privacy Control Disabled\";\n        addDomainToDomainlistAndRules(requestDomain);\n      } else {\n        elemString = \"Global Privacy Control\";\n        removeDomainFromDomainlistAndRules(requestDomain);\n      }\n\n      document.getElementById(`dns-enabled-text-${requestDomain}`).innerHTML =\n        elemString;\n    });\n}\n\n/**\n * Builds the requested domains HTML of the popup window\n * @param {Object} requests - Contains all request domains for the current tab\n * (requests = tabs[activeTabID].requestDomainS; passed from background page)\n */\nasync function buildDomains(requests) {\n  let domain = await getCurrentParsedDomain();\n  let items = \"\";\n  const domainlistKeys = await storage.getAllKeys(stores.domainlist);\n  const domainlistValues = await storage.getAll(stores.domainlist);\n  const requestList = Array.isArray(requests) ? requests : [];\n\n  // Iterate through requests array\n  for (let i = 0; i < requestList.length; i++) {\n    const requestDomain = requestList[i]; // Get the domain name from the request array\n\n    if (requestDomain != domain) {\n      let checkbox = \"\";\n      let text = \"\";\n      // Find correct index\n      let index = domainlistKeys.indexOf(requestDomain);\n      if (index > -1 && !domainlistValues[index]) {\n        checkbox = `<input type=\"checkbox\" id=\"input-${requestDomain}\" checked/>`;\n        text = \"Global Privacy Control\";\n      } else {\n        checkbox = `<input type=\"checkbox\" id=\"input-${requestDomain}\"/>`;\n        text = \"Global Privacy Control Disabled\";\n      }\n\n      items += `\n        <li>\n          <div\n            class=\"blue-heading uk-flex-inline uk-width-1-1 uk-flex-center uk-text-center uk-text-bold uk-text-truncate\"\n            style=\"font-size: medium\"\n            id=\"domain\"\n          >\n            ${requestDomain}\n          </div>\n          <div uk-grid  style=\"margin-top: 4%; \">\n            <div\n              id=\"dns-enabled-text-${requestDomain}\"\n              class=\"uk-width-expand uk-margin-auto-vertical\"\n              style=\"font-size: small;\"\n            >\n              ${text}\n            </div>\n            <div>\n              <div uk-grid>\n                <div class=\"uk-width-auto\">\n                  <label class=\"switch switch-smaller\" id=\"switch-label-${requestDomain}\">\n                    <!-- Populate switch preference here -->\n                    ${checkbox}\n                    <span class=\"tooltip-1\"></span>\n                  </label>\n                </div>\n              </div>\n            </div>\n          </div>\n          <!-- Response info -->\n          <div uk-grid uk-grid-row-collapse style=\"margin-top:0px;\">\n          </div>\n        </li>\n      `;\n    }\n  }\n  document.getElementById(\"dropdown-1-expandable\").innerHTML = items;\n\n  // Sets the 3rd party domain toggle listeners\n  for (let i = 0; i < requestList.length; i++) {\n    const requestDomain = requestList[i];\n    if (requestDomain != domain) {\n      addThirdPartyDomainDNSToggleListener(requestDomain);\n    }\n  }\n}\n\n/**\n * Builds the Well Known HTML for the popup window\n * @param {Object} requests - Contains all well-known info in current tab\n * (requests passed from contentScript.js page as of v1.1.3)\n */\nasync function buildWellKnown(requests) {\n  let explainer;\n  const data =\n    requests && typeof requests === \"object\" ? requests : null;\n\n  /*\n  This iterates over the cases of possible combinations of\n  having found a GPC policy, and whether or not a site respects\n  the signal or not, setting both the `explainer` and `tabDetails`\n  for GPC v1\n  */\n  if (data !== null && data[\"gpc\"] === true) {\n    explainer = `\n      <li>\n        <p class=\"uk-text-center uk-text-small uk-text-bold\">\n          GPC Signals Accepted\n        </p>\n      </li>\n      <li>\n        <p class=\"uk-text-center uk-text-small\">\n          This website says it respects GPC signals\n        </p>\n      </li>\n      `;\n  } else if (data !== null && data[\"gpc\"] === false) {\n    explainer = `\n      <li>\n        <p class=\"uk-text-center uk-text-small uk-text-bold\">\n          GPC Signals Rejected\n        </p>\n      </li>\n      <li>\n        <p class=\"uk-text-center uk-text-small\">\n          This website does not respect GPC signals\n        </p>\n      </li>\n      `;\n  } else {\n    explainer = `\n      <li>\n        <p class=\"uk-text-center uk-text-small uk-text-bold\">\n          GPC Policy Missing\n        </p>\n      </li>\n      <li>\n        <p class=\"uk-text-center uk-text-small\">\n          It seems this website does not have a GPC signal processing policy!\n        </p>\n      </li>\n      `;\n  }\n\n  let wellknown =\n    data !== null && data[\"gpc\"] !== null\n      ? `\n    <li class=\"uk-text-center uk-text-small\">\n      Here is the GPC policy:\n    </li>\n    <li>\n      <pre class=\"wellknown-bg\">\n        ${JSON.stringify(data, null, 4)\n        .replace(/['\"\\{\\}\\n]/g, \"\")\n        .replace(/,/g, \"\\n\")}\n      </pre>\n    </li>\n  `\n      : ``;\n\n  document.getElementById(\n    \"dropdown-2-expandable\"\n  ).innerHTML = `${explainer} ${wellknown}`;\n}\n\n/**\n * Builds the Compliance Status Loading HTML for the popup window\n * @param {string} stateCode - The state code being loaded\n */\nasync function buildComplianceStatusLoading(stateCode) {\n  const container = document.getElementById(\"compliance-status-content\");\n  const stateName = STATE_NAMES[stateCode] || stateCode;\n  const stateLabel = `<p class=\"compliance-state-label\">What We Observed · ${stateName}</p>`;\n\n  container.innerHTML = `\n    <div class=\"compliance-inline\">\n      ${stateLabel}\n      <div class=\"uk-text-center uk-margin-small-top\">\n        <div uk-spinner=\"ratio: 0.8\"></div>\n        <p class=\"compliance-status-text\" style=\"margin-top: 8px; font-style: italic;\">Loading ${stateName} compliance data...</p>\n      </div>\n    </div>\n  `;\n}\n\n// Maps a schema status string to a (label, css class) for the status pill.\n// `null`/missing means no privacy string of that family was detected.\nconst CLASSIFICATION_STATUS_META = {\n  opted_out:        { label: 'Opted Out',        cls: 'status-opted-out' },\n  did_not_opt_out:  { label: 'Did Not Opt Out',  cls: 'status-did-not-opt-out' },\n  invalid_missing:  { label: 'Invalid/Missing',  cls: 'status-invalid' },\n  invalid:          { label: 'Invalid',          cls: 'status-invalid' },\n  not_applicable:   { label: 'Not Applicable',   cls: 'status-na' },\n};\nconst CLASSIFICATION_NONE_META  = { label: 'None',  cls: 'status-none' };\nconst CLASSIFICATION_MIXED_META = { label: 'Mixed', cls: 'status-mixed' };\n\nfunction metaForStatus(status) {\n  if (!status) return CLASSIFICATION_NONE_META;\n  // Fallback label is a fixed string (not `status`) so unexpected/malformed\n  // values from the CSV can't be injected into innerHTML downstream.\n  return CLASSIFICATION_STATUS_META[status] || { label: 'Unknown', cls: 'status-none' };\n}\n\n// GPP ships a list of per-(state, field) classifications. Collapse to one\n// status: same across all → that status; different → \"Mixed\".\nfunction aggregateGppMeta(gpp) {\n  if (!gpp || !Array.isArray(gpp.classifications) || gpp.classifications.length === 0) {\n    return CLASSIFICATION_NONE_META;\n  }\n  const statuses = new Set(gpp.classifications.map(c => c.status));\n  if (statuses.size === 1) return metaForStatus([...statuses][0]);\n  return CLASSIFICATION_MIXED_META;\n}\n\nfunction classificationRowHtml(family, meta) {\n  return `\n    <div class=\"classification-row\">\n      <span class=\"classification-family\">${family}</span>\n      <span class=\"classification-status ${meta.cls}\">${meta.label}</span>\n    </div>\n  `;\n}\n\nfunction buildClassificationHtml(classification) {\n  return `\n    <div class=\"compliance-classifications\">\n      ${classificationRowHtml('USPS',            metaForStatus(classification.usps?.status))}\n      ${classificationRowHtml('Optanon Consent', metaForStatus(classification.optanonConsent?.status))}\n      ${classificationRowHtml('Well-known',      metaForStatus(classification.wellKnown?.status))}\n      ${classificationRowHtml('GPP',             aggregateGppMeta(classification.gpp))}\n    </div>\n  `;\n}\n\n// Roll the four families (GPP flattened) into a single top-line verdict:\n// any did_not_opt_out → \"does_not_honor\"; else any opted_out → \"honors\";\n// else \"unknown\" (all None, or only invalid/not_applicable).\nfunction computeOverallVerdict(classification) {\n  const statuses = [];\n  if (classification.usps?.status)            statuses.push(classification.usps.status);\n  if (classification.optanonConsent?.status)  statuses.push(classification.optanonConsent.status);\n  if (classification.wellKnown?.status)       statuses.push(classification.wellKnown.status);\n  if (Array.isArray(classification.gpp?.classifications)) {\n    for (const c of classification.gpp.classifications) {\n      if (c.status) statuses.push(c.status);\n    }\n  }\n  if (statuses.includes('did_not_opt_out')) return 'does_not_honor';\n  if (statuses.includes('opted_out'))       return 'honors';\n  return 'unknown';\n}\n\nfunction overallBadgeHtml(classification) {\n  const verdict = computeOverallVerdict(classification);\n  if (verdict === 'honors') {\n    return '<span class=\"compliance-status-badge compliance-compliant\">🟢 Likely Honors GPC</span>';\n  }\n  if (verdict === 'does_not_honor') {\n    return '<span class=\"compliance-status-badge compliance-non-compliant\">🔴 Likely Does Not Honor GPC</span>';\n  }\n  return '<span class=\"compliance-status-badge compliance-no-signals\">🟡 Could Not Determine</span>';\n}\n\n/**\n * Builds the Compliance Status HTML for the popup window\n * @param {Object} status - Compliance status object from storage\n * @param {string} stateCode - The state code\n * @param {string|null} viewUrl - URL to the all_sites dataset (from states.json metadata)\n */\nasync function buildComplianceStatus(status, stateCode, viewUrl) {\n  const container = document.getElementById(\"compliance-status-content\");\n  const stateName = STATE_NAMES[stateCode] || stateCode;\n  const datasetUrl = viewUrl || '#';\n  const stateLabel = `<p class=\"compliance-state-label\">What We Observed · ${stateName}</p>`;\n\n  if (!status || status.status === 'no_data') {\n    container.innerHTML = `\n      <div class=\"compliance-inline\">\n        ${stateLabel}\n        <span class=\"compliance-status-badge compliance-no-data\">⚪ Not in Dataset</span>\n        <p class=\"compliance-status-text\">We do not have data for this site.</p>\n        <a class=\"compliance-link\" href=\"${datasetUrl}\" target=\"_blank\">View ${stateName} dataset →</a>\n      </div>\n    `;\n    return;\n  }\n\n  if (status.status === 'fetch_error') {\n    container.innerHTML = `\n      <div class=\"compliance-inline\">\n        ${stateLabel}\n        <span class=\"compliance-status-badge compliance-unknown\">🟠 Data Unavailable</span>\n        <p class=\"compliance-status-text\">Could not reach the compliance data server. Check your connection and try again.</p>\n      </div>\n    `;\n    return;\n  }\n\n  // New schema-based per-family classification (preferred when the dataset has it).\n  if (status.classification) {\n    container.innerHTML = `\n      <div class=\"compliance-inline\">\n        ${stateLabel}\n        ${overallBadgeHtml(status.classification)}\n        ${buildClassificationHtml(status.classification)}\n        <a class=\"compliance-link\" href=\"${datasetUrl}\" target=\"_blank\">View ${stateName} dataset →</a>\n      </div>\n    `;\n    return;\n  }\n\n  // Fallback: legacy summary badge for datasets without the classification column.\n  let badge = '';\n  let statusText = '';\n  if (status.status === 'compliant') {\n    badge = '<span class=\"compliance-status-badge compliance-compliant\">🟢 Likely Honors GPC</span>';\n    statusText = '';\n  } else if (status.status === 'non_compliant') {\n    badge = '<span class=\"compliance-status-badge compliance-non-compliant\">🔴 Likely Ignores GPC</span>';\n    statusText = 'This site has consent tools but did not appear to respond to GPC during our crawl.';\n  } else if (status.status === 'no_signals') {\n    badge = '<span class=\"compliance-status-badge compliance-no-signals\">🟡 Could Not Determine</span>';\n    statusText = 'No consent signals were detected on this site during our crawl, so we cannot assess GPC compliance.';\n  } else {\n    badge = '<span class=\"compliance-status-badge compliance-unknown\">🔵 Inconclusive</span>';\n    statusText = 'This site is in the dataset but results were inconclusive.';\n  }\n\n  const details = status.details || '';\n\n  container.innerHTML = `\n    <div class=\"compliance-inline\">\n      ${stateLabel}\n      ${badge}\n      <p class=\"compliance-status-text\">${statusText}</p>\n      ${details ? `<p class=\"compliance-details\">${details}</p>` : ''}\n      <a class=\"compliance-link\" href=\"${datasetUrl}\" target=\"_blank\">View ${stateName} dataset →</a>\n    </div>\n  `;\n}\n\n/******************************************************************************/\n/******************************************************************************/\n/**********                   # Message handling                     **********/\n/******************************************************************************/\n/******************************************************************************/\n\n/**\n * Listens for messages from background page that call functions to populate\n * the popup badge counter and build the popup domain list HTML, respectively\n */\nchrome.runtime.onMessage.addListener(function (message, _, __) {\n  if (message.msg === \"POPUP_PROTECTION_DATA\") {\n    let { requests, wellknown } = message.data;\n    domainsInfo = requests;\n    wellknownInfo = wellknown;\n    //buildDomains(requests);\n    //buildWellKnown(wellknown);\n  }\n  if (message.msg === \"POPUP_PROTECTION_DATA_REQUESTS\") {\n    let requests = message.data;\n    buildDomains(requests);\n  }\n});\n\n// Initializes the process to add to domainlist, via the background script\n// This is to ensure all processes happen correctly\nfunction setToDomainlist(d, k) {\n  chrome.runtime.sendMessage({\n    msg: \"SET_TO_DOMAINLIST\",\n    data: { domain: d, key: k },\n  });\n}\n\n/******************************************************************************/\n/******************************************************************************/\n/**********                  # Tutorial walkthorugh                  **********/\n/******************************************************************************/\n/******************************************************************************/\n\n// Walkthrough function\nfunction popUpWalkthrough() {\n  let contentStr =\n    \"Toggle this switch to enable or disable sending Global Privacy Control signals to this site in Protection mode\";\n  tippy(\".tooltip-1\", {\n    content: contentStr,\n    trigger: \"manual\",\n    placement: \"bottom\",\n    duration: 1000,\n    theme: \"custom-2\",\n    maxWidth: 250,\n  });\n  let tooltip = document.getElementsByClassName(\"tooltip-1\")[0]._tippy;\n  tooltip.show();\n}\n\n// Init: Check to see if we should do tutorial\nasync function initPopUpWalkthrough() {\n  const tutorialShownInPopup = await storage.get(\n    stores.settings,\n    \"TUTORIAL_SHOWN_IN_POPUP\"\n  );\n\n  if (!tutorialShownInPopup) {\n    popUpWalkthrough();\n\n    storage.set(stores.settings, true, \"TUTORIAL_SHOWN_IN_POPUP\");\n  }\n}\n\n/******************************************************************************/\n/******************************************************************************/\n/**********           # Misc. initializers and listeners             **********/\n/******************************************************************************/\n/******************************************************************************/\n\n// Listener: Opens options page\ndocument.getElementById(\"more\").addEventListener(\"click\", () => {\n  chrome.runtime.openOptionsPage();\n});\n\n// Listener: Opens tutorial\nif (\"$BROWSER\" == \"chrome\") {\n  document.getElementById(\"tour\").addEventListener(\"click\", async () => {\n    await storage.set(stores.settings, false, \"TUTORIAL_SHOWN\");\n\n    chrome.runtime.sendMessage({\n      msg: \"SHOW_TUTORIAL\",\n    });\n  });\n\n  document.getElementById(\"tour\").addEventListener(\"click\", async () => {\n    await storage.set(stores.settings, false, \"TUTORIAL_SHOWN\");\n    setTimeout(chrome.runtime.openOptionsPage, 100);\n  });\n} else {\n  document.getElementById(\"tour\").addEventListener(\"click\", () => {\n    chrome.runtime.sendMessage({\n      msg: \"SHOW_TUTORIAL\",\n    });\n\n    storage.set(stores.settings, false, \"TUTORIAL_SHOWN\");\n  });\n\n  document.getElementById(\"tour\").addEventListener(\"click\", () => {\n    chrome.runtime.sendMessage({\n      msg: \"SHOW_TUTORIAL\",\n    });\n\n    storage.set(stores.settings, false, \"TUTORIAL_SHOWN\");\n\n    chrome.runtime.openOptionsPage();\n  });\n}\n\n// Listener: Opens domainlist in options page\ndocument.getElementById(\"domain-list\").addEventListener(\"click\", async () => {\n  await storage.set(stores.settings, true, \"DOMAINLIST_PRESSED\");\n  chrome.runtime.openOptionsPage();\n});\n\n// Listener: Opens GPC Web UI with current domain and user's selected state\ndocument.getElementById(\"gpc-web-ui-link\").addEventListener(\"click\", async () => {\n  const userState = await getUserState();\n  const stateParam = userState && userState !== \"none\" ? `&state=${userState}` : \"\";\n  if (parsedDomain) {\n    chrome.tabs.create({ url: `https://gpc-web-ui.vercel.app/?url=${parsedDomain}${stateParam}` });\n  } else {\n    chrome.tabs.create({ url: `https://gpc-web-ui.vercel.app/${stateParam ? `?${stateParam.slice(1)}` : \"\"}` });\n  }\n});\n"
  },
  {
    "path": "src/popup/styles.css",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\nstyles.css\n================================================================================\nstyles.css is the main css page for OptMeowt's popup page\n*/\n\n:root {\n  --text-gray: rgb(89, 98, 127);\n}\n\n.small-warning-box {\n  color: white;\n  background-color: #db4437;\n  padding-left: 10px;\n  padding-right: 5px;\n  padding-top: 5px;\n  padding-bottom: 5px;\n  border-radius: 5px;\n  text-align: left;\n}\n\n.importexport-button {\n  background-color: white;\n  border-color: #888fa1;\n  color: #888fa1;\n  padding: 12px 16px;\n  text-align: center;\n  text-decoration-color: none;\n  font-size: 14px;\n  display: inline-block;\n  border-radius: 10px;\n  border-style: solid;\n  box-shadow: 0 8px 16px -1px rgba(211, 211, 211, 0.2);\n  outline: none;\n  transition: all ease 0.25s;\n}\n\n.importexport-button:hover {\n  background-color: var(--accent-color);\n  border-color: var(--accent-color);\n  color: white;\n  box-shadow: 0 6px 12px var(--accent-color-lighter-60);\n  transition: all ease 0.25s;\n}\n\n.button:hover {\n  background-color: #df3131 !important;\n  border: 1px solid #df3131 !important;\n  transition: all 0.3s ease;\n  color: #fff !important;\n}\n\n.uspStringElem {\n  margin: auto;\n  padding-top: 10px;\n  padding-bottom: 10px;\n  padding-right: 8px;\n  padding-left: 8px;\n  background-color: white;\n  border: 1px solid var(--text-gray);\n  color: var(--text-gray);\n  text-align: center;\n}\n\n.uspStringElem:hover {\n  background-color: white;\n  border: 1px solid var(--text-gray);\n  color: var(--text-gray);\n}\n\n.uspStringElem:active {\n  background-color: white;\n  border: 1px solid var(--text-gray);\n  color: var(--text-gray);\n}\n\n\n\n/*\nSVG photo assets style\n*/\n@import \"../options/styles.css\";\n\nsvg {\n  fill: #d3d3d3;\n  transition: all ease 0.5s;\n}\n\nsvg:hover {\n  fill: #5a647d;\n  transition: all ease 0.5s;\n}\n\n/********************************************************/\n\n/*\nBlue Heading\n*/\n.blue-heading {\n  color: #4472c4;\n}\n\n/*=======================================================================================*/\n/*tutorial popup css*/\n.tippy-box[data-theme~=\"custom-2\"] {\n  background-color: #87cefa;\n  box-shadow: 10px 10px 5px 0px rgba(0, 0, 0, 0.31);\n  color: white;\n  padding: 10px;\n  border-radius: 5px;\n  text-align: left;\n  float: left;\n}\n\n.tippy-box[data-theme~=\"custom-2\"][data-placement^=\"bottom\"]>.tippy-arrow::before {\n  border-bottom-color: #87cefa;\n}\n\n/********************************************************/\n\n/*\nAnimated 3rd party domains/domain list buttons/links\n*/\n\n.domain-list:hover {\n  background-color: var(--accent-color);\n  color: white;\n  transition: ease-out 0.1s;\n}\n\n\n.dropdown-tab:hover {\n  background-color: var(--highlight-light);\n  color: var(--text-color-darker);\n  transition: ease-out 0.1s;\n}\n\n.dropdown-tab-click {\n  background-color: var(--highlight-light);\n  color: var(--text-color-darker);\n}\n\n/********************************************************/\n\n/*\nAccepted/rejected text on popup\n*/\n\n.status-text-red {\n  color: red;\n  /* border: solid red 2px;\n  border-radius: 14px;\n  box-shadow: 0 8px 16px -1px rgba(211, 211, 211, .2);\n  outline: none; */\n}\n\n.divide {\n  border: 0;\n  /* height: 1px;\n  background: black; */\n}\n\n/* .uk-list-divide {\n  border: 0;\n  height: 1px;\n  background: black;\n} */\n\n/********************************************************/\n\n/*\nCompliance status indicators\n*/\n\n.compliance-status-badge {\n  display: inline-block;\n  padding: 4px 10px;\n  border-radius: 12px;\n  font-size: small;\n  font-weight: bold;\n  margin-bottom: 8px;\n}\n\n.compliance-compliant {\n  background-color: #d4edda;\n  color: #155724;\n  border: 1px solid #c3e6cb;\n}\n\n.compliance-non-compliant {\n  background-color: #f8d7da;\n  color: #721c24;\n  border: 1px solid #f5c6cb;\n}\n\n.compliance-no-signals {\n  background-color: #fff3cd;\n  color: #856404;\n  border: 1px solid #ffeeba;\n}\n\n.compliance-no-data {\n  background-color: #e2e3e5;\n  color: #383d41;\n  border: 1px solid #d6d8db;\n}\n\n.compliance-unknown {\n  background-color: #cce5ff;\n  color: #004085;\n  border: 1px solid #b8daff;\n}\n\n.compliance-inline {\n  text-align: center;\n}\n\n.compliance-status-text {\n  font-size: small;\n  margin-top: 6px;\n  margin-bottom: 4px;\n}\n\n.compliance-details {\n  font-size: x-small;\n  color: var(--text-gray);\n  margin-top: 2px;\n  margin-bottom: 4px;\n}\n\n.compliance-link {\n  font-size: 10px;\n  color: #999;\n  display: inline-block;\n  margin-top: 8px;\n  text-decoration: none;\n  transition: color 0.2s;\n}\n\n.compliance-link:hover {\n  text-decoration: underline;\n  color: #666;\n}\n\n.compliance-section-divider {\n  display: none;\n  /* Removed based on feedback */\n}\n\n.compliance-state-label {\n  font-size: 10px;\n  font-weight: 700;\n  text-transform: uppercase;\n  letter-spacing: 1.5px;\n  color: #999;\n  margin-top: 24px;\n  margin-bottom: 12px;\n  display: block;\n}\n\n/*\nPer-family compliance classification rows\n(USPS, Optanon Consent, Well-known, GPP)\n*/\n\n.compliance-classifications {\n  display: flex;\n  flex-direction: column;\n  gap: 6px;\n  margin: 4px 0 10px;\n  text-align: left;\n}\n\n.classification-row {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  font-size: 12px;\n  padding: 4px 2px;\n}\n\n.classification-family {\n  font-weight: 600;\n  color: var(--text-color, inherit);\n}\n\n.classification-status {\n  display: inline-block;\n  padding: 2px 8px;\n  border-radius: 10px;\n  font-size: 11px;\n  font-weight: 600;\n  line-height: 1.4;\n  border: 1px solid transparent;\n}\n\n.classification-status.status-opted-out {\n  background-color: rgba(40, 167, 69, 0.15);\n  color: #2e8540;\n  border-color: rgba(40, 167, 69, 0.35);\n}\n\n.classification-status.status-did-not-opt-out {\n  background-color: rgba(220, 53, 69, 0.15);\n  color: #c0392b;\n  border-color: rgba(220, 53, 69, 0.35);\n}\n\n.classification-status.status-invalid {\n  background-color: rgba(255, 152, 0, 0.15);\n  color: #b76f00;\n  border-color: rgba(255, 152, 0, 0.35);\n}\n\n.classification-status.status-mixed {\n  background-color: rgba(0, 123, 255, 0.15);\n  color: #1565c0;\n  border-color: rgba(0, 123, 255, 0.35);\n}\n\n.classification-status.status-na,\n.classification-status.status-none {\n  background-color: rgba(120, 120, 120, 0.12);\n  color: #888;\n  border-color: rgba(120, 120, 120, 0.25);\n}\n\n/*\nState selection overlay\n*/\n\n#state-selection-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  background: var(--body-bg, #fff);\n  z-index: 1000;\n  overflow-y: auto;\n}\n\n.state-overlay-content {\n  text-align: center;\n  padding: 16px 20px;\n  min-height: 100%;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n}\n\n.state-overlay-title {\n  font-size: 16px;\n  font-weight: bold;\n  margin-bottom: 4px;\n  margin-top: 8px;\n}\n\n.state-overlay-desc {\n  font-size: 12px;\n  color: var(--text-gray, #666);\n  margin-bottom: 12px;\n  line-height: 1.3;\n}\n\n.state-buttons {\n  display: flex;\n  flex-direction: column;\n  gap: 6px;\n  margin-bottom: 12px;\n  width: 100%;\n}\n\n.state-btn {\n  padding: 8px 12px;\n  border: 1.5px solid #d0d0d0;\n  border-radius: 8px;\n  background: var(--body-bg, #fff);\n  color: var(--text-color, #333);\n  font-size: 13px;\n  font-weight: 500;\n  cursor: pointer;\n  transition: all 0.15s ease;\n}\n\n.state-btn:hover {\n  border-color: #4a90d9;\n  background: #eef4fb;\n}\n\n.state-btn-skip {\n  border-style: dashed;\n  color: var(--text-gray, #888);\n  font-size: 12px;\n  margin-top: 4px;\n}\n\n.state-btn-skip:hover {\n  border-color: #999;\n  background: #f5f5f5;\n}"
  },
  {
    "path": "src/rules/gpc_exceptions_rules.json",
    "content": "[\n\t{\n\t\t\"id\": 2,\n\t\t\"priority\": 1,\n\t\t\"action\": {\n\t\t  \"type\": \"modifyHeaders\",\n\t\t  \"requestHeaders\": [\n        { \"header\": \"Sec-GPC\", \"operation\": \"remove\" },\n        { \"header\": \"Permissions-Policy\", \"operation\": \"remove\"}\n\t\t  ]\n\t\t},\n\t\t\"condition\": { \n\t\t  \"urlFilter\": \"https://example.com\",\n\t\t  \"resourceTypes\": [\n        \"main_frame\", \n        \"sub_frame\",\n        \"stylesheet\",\n        \"script\",\n        \"image\",\n        \"font\",\n        \"object\",\n        \"xmlhttprequest\",\n        \"ping\",\n        \"csp_report\",\n        \"media\",\n        \"websocket\",\n        \"other\"\n\t\t  ]\n\t\t}\n\t}\n]\n"
  },
  {
    "path": "src/rules/universal_gpc_rules.json",
    "content": "[\n  {\n    \"id\": 1,\n    \"priority\": 1,\n    \"action\": {\n      \"type\": \"modifyHeaders\",\n      \"requestHeaders\": [\n        { \"header\": \"Sec-GPC\", \"operation\": \"set\", \"value\": \"1\" },\n        { \"header\": \"Permissions-Policy\", \"operation\": \"set\", \"value\": \"browsing-topics=()\"}\n      ]\n    },\n    \"condition\": { \n      \"urlFilter\": \"*\",\n      \"resourceTypes\": [\n        \"main_frame\", \n        \"sub_frame\",\n        \"stylesheet\",\n        \"script\",\n        \"image\",\n        \"font\",\n        \"object\",\n        \"xmlhttprequest\",\n        \"ping\",\n        \"csp_report\",\n        \"media\",\n        \"websocket\",\n        \"other\"\n      ]\n    }\n  }\n]\n"
  },
  {
    "path": "src/theme/darkmode.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\ndarkmode.js\n================================================================================\ndarkmode.js is a snippet of code based on the `darkmode.js` source file \nlocated at https://github.com/sandoche/Darkmode.js/blob/master/src/darkmode.js\n\ndarkmode.js implements a way to store the darkmode option in local storage and\nonly keeps one class that is appended to the HTML body on toggle. All CSS is \napplied via `dark-mode.css` and the `darkmode--activated` attribute.\n\nGitHub Repo: https://github.com/sandoche/Darkmode.js\n*/\n\nexport const IS_BROWSER = typeof window !== \"undefined\";\n\nexport default class Darkmode {\n  constructor(options) {\n    if (!IS_BROWSER) {\n      return;\n    }\n\n    const defaultOptions = {\n      saveInCookies: true,\n      autoMatchOsTheme: true,\n    };\n\n    options = Object.assign({}, defaultOptions, options);\n\n    const preferedThemeOs =\n      options.autoMatchOsTheme &&\n      window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n    const darkmodeActivated =\n      window.localStorage.getItem(\"darkmode\") === \"true\";\n    const darkmodeNeverActivatedByAction =\n      window.localStorage.getItem(\"darkmode\") === null;\n\n    if (\n      (darkmodeActivated === true && options.saveInCookies) ||\n      (darkmodeNeverActivatedByAction && preferedThemeOs)\n    ) {\n      document.body.classList.add(\"darkmode--activated\");\n    }\n  }\n\n  toggle() {\n    if (!IS_BROWSER) {\n      return;\n    }\n    const isDarkmode = this.isActivated();\n\n    document.body.classList.toggle(\"darkmode--activated\");\n    window.localStorage.setItem(\"darkmode\", !isDarkmode);\n  }\n\n  isActivated() {\n    if (!IS_BROWSER) {\n      return null;\n    }\n    return document.body.classList.contains(\"darkmode--activated\");\n  }\n}\n"
  },
  {
    "path": "test/background/cookieRemoval.test.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\ncookieRemoval.test.js\n================================================================================\nVerifies that legacy cookie-based opt-out code was fully removed when the\nextension dropped IAB/NAI cookie support.\n*/\n\nimport assert from \"assert\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst repoRoot = path.resolve(__dirname, \"../../\");\n\nconst resolvePath = (relativePath) => path.resolve(repoRoot, relativePath);\nconst readFile = (relativePath) =>\n  fs.readFileSync(resolvePath(relativePath), \"utf8\");\n\n/**\n * Recursively searches a directory for a given string.\n * Stops early once the string is found.\n * NOTE: Only used on the `src` tree which is small enough for sync traversal.\n */\nfunction containsInDir(dirPath, needle) {\n  const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n  for (const entry of entries) {\n    if (entry.name.startsWith(\".\")) {\n      continue;\n    }\n    const fullPath = path.join(dirPath, entry.name);\n    if (entry.isDirectory()) {\n      if (containsInDir(fullPath, needle)) {\n        return true;\n      }\n    } else {\n      const content = fs.readFileSync(fullPath, \"utf8\");\n      if (content.includes(needle)) {\n        return true;\n      }\n    }\n  }\n  return false;\n}\n\nit(\"confirms legacy cookie source files were removed\", () => {\n  const removedFiles = [\n    \"src/background/cookiesIAB.js\",\n    \"src/background/protection/cookiesOnInstall.js\",\n    \"src/background/storageCookies.js\",\n    \"src/data/cookie_list.js\",\n  ];\n\n  removedFiles.forEach((filePath) => {\n    assert.strictEqual(\n      fs.existsSync(resolvePath(filePath)),\n      false,\n      `Expected ${filePath} to be removed`\n    );\n  });\n});\n\ndescribe(\"Check parsing of IAB signal\", () => {\n  it(\"should not reference parse helpers in chrome protection background\", () => {\n    const content = readFile(\"src/background/protection/protection.js\");\n    assert.ok(\n      !content.includes(\"parseIAB\") && !content.includes(\"cookiesIAB\"),\n      \"chrome protection background still references legacy parse helpers\"\n    );\n  });\n\n  it(\"should not reference parse helpers in firefox protection background\", () => {\n    const content = readFile(\"src/background/protection/protection-ff.js\");\n    assert.ok(\n      !content.includes(\"parseIAB\") && !content.includes(\"cookiesIAB\"),\n      \"firefox protection background still references legacy parse helpers\"\n    );\n  });\n\n  it(\"should not reference parse helpers in popup UI state\", () => {\n    const content = readFile(\"src/popup/popup.js\");\n    assert.ok(\n      !content.includes(\"parseIAB\") && !content.includes(\"cookiesIAB\"),\n      \"popup still references legacy parse helpers\"\n    );\n  });\n\n  it(\"should not ship legacy parse tests\", () => {\n    assert.strictEqual(\n      fs.existsSync(resolvePath(\"test/background/parseIAB.test.js\")),\n      false,\n      \"parseIAB.test.js should have been removed\"\n    );\n  });\n});\n\ndescribe(\"Checks if cookie is stored per domain/subdomain\", () => {\n  const storageContent = readFile(\"src/background/storage.js\");\n\n  it(\"should not import storageCookies helper\", () => {\n    assert.ok(\n      !storageContent.includes(\"storageCookies\"),\n      \"storage.js still imports storageCookies helper\"\n    );\n  });\n\n  it(\"should guard storage.get against undefined keys\", () => {\n    const guardRegex =\n      /async get\\(store, key\\)[\\s\\S]*?if \\(typeof key === \"undefined\"\\)/;\n    assert.ok(\n      guardRegex.test(storageContent),\n      \"storage.get does not guard against undefined keys\"\n    );\n  });\n\n  it(\"should guard storage.set against undefined keys\", () => {\n    const guardRegex =\n      /async set\\(store, value, key\\)[\\s\\S]*?if \\(typeof key === \"undefined\"\\)/;\n    assert.ok(\n      guardRegex.test(storageContent),\n      \"storage.set does not guard against undefined keys\"\n    );\n  });\n\n  it(\"should guard storage.delete against undefined keys\", () => {\n    const guardRegex =\n      /async delete\\(store, key\\)[\\s\\S]*?if \\(typeof key === \"undefined\"\\)/;\n    assert.ok(\n      guardRegex.test(storageContent),\n      \"storage.delete does not guard against undefined keys\"\n    );\n  });\n\n  it(\"should not call addCookiesForGivenDomain helper\", () => {\n    assert.ok(\n      !storageContent.includes(\"addCookiesForGivenDomain\"),\n      \"storage.js still references addCookiesForGivenDomain\"\n    );\n  });\n\n  it(\"should not call deleteCookiesForGivenDomain helper\", () => {\n    assert.ok(\n      !storageContent.includes(\"deleteCookiesForGivenDomain\"),\n      \"storage.js still references deleteCookiesForGivenDomain\"\n    );\n  });\n});\n\ndescribe(\"Check different IAB signals for validity\", () => {\n  const srcRoot = resolvePath(\"src\");\n\n  it(\"should not reference isValidSignalIAB helper in source files\", () => {\n    assert.strictEqual(\n      containsInDir(srcRoot, \"isValidSignalIAB\"),\n      false,\n      \"Found isValidSignalIAB reference in src\"\n    );\n  });\n\n  it(\"should not reference makeCookieIAB helper in source files\", () => {\n    assert.strictEqual(\n      containsInDir(srcRoot, \"makeCookieIAB\"),\n      false,\n      \"Found makeCookieIAB reference in src\"\n    );\n  });\n\n  it(\"should not reference pruneCookieIAB helper in source files\", () => {\n    assert.strictEqual(\n      containsInDir(srcRoot, \"pruneCookieIAB\"),\n      false,\n      \"Found pruneCookieIAB reference in src\"\n    );\n  });\n\n  it(\"should not reference legacy cookie_list dataset\", () => {\n    assert.strictEqual(\n      containsInDir(srcRoot, \"cookie_list\"),\n      false,\n      \"Found cookie_list reference in src\"\n    );\n  });\n});\n"
  },
  {
    "path": "test/background/gpc.test.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\ngpc.test.js\n================================================================================\ngpc.test.js tests the GPC signal head-fully using Puppeteer and Chromium\n*/\n\n/**\n *  Tests for testing GPC signals\n */\n\nimport assert from \"assert\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport puppeteer from \"puppeteer\";\nimport { fileURLToPath } from \"url\";\n\nlet browser;\n\nif (!process.env.CI) {\n  describe(\"GPC test\", function () {\n    this.timeout(20000);\n    before(async () => {\n      const puppeteerOps = {\n        headless: false,\n      };\n      const args = [];\n\n      const __filename = fileURLToPath(import.meta.url);\n      const __dirname = path.dirname(__filename);\n      const projectRoot = path.resolve(__dirname, \"../../\");\n      let extensionPath = path.resolve(projectRoot, \"dev/chrome\");\n\n      if (!fs.existsSync(extensionPath)) {\n        extensionPath = path.resolve(projectRoot, \"dist/chrome\");\n      }\n\n      if (!fs.existsSync(extensionPath)) {\n        throw new Error(\n          \"Unable to locate an OptMeowt build at dev/chrome or dist/chrome. \" +\n            \"Run `npm run start:chrome` or `npm run build:chrome` before running this test.\"\n        );\n      }\n\n      args.push(\"--disable-extensions-except=\" + extensionPath);\n      args.push(\"--load-extension=\" + extensionPath);\n\n      puppeteerOps.args = args;\n      browser = await puppeteer.launch(puppeteerOps);\n      await new Promise((resolve) => setTimeout(resolve, 1000));\n    });\n    after(async () => {\n      await browser.close();\n    });\n\n    it(\"Tests whether the GPC and header signal are properly set\", async () => {\n      const page = await browser.newPage();\n\n      await page.goto(`https://global-privacy-control.vercel.app/`);\n\n      await page.reload();\n\n      const gpc = await page.evaluate(async () => {\n        await new Promise((resolve) => setTimeout(resolve, 1000));\n\n        return (async () => {\n          return navigator.globalPrivacyControl;\n        })();\n      });\n\n      const header = await page.evaluate(async () => {\n        function getElementByXpath(path) {\n          return document.evaluate(\n            path,\n            document,\n            null,\n            XPathResult.FIRST_ORDERED_NODE_TYPE,\n            null\n          ).singleNodeValue;\n        }\n\n        return getElementByXpath(\"/html/body/section[2]/div/div[1]/div/h3\")\n          .innerText;\n      });\n\n      assert.equal(header, 'Header present \\nSec-GPC: \"1\"');\n      assert.equal(gpc, true);\n    });\n  });\n}\n"
  },
  {
    "path": "webpack.config.js",
    "content": "/*\nLicensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md\nprivacy-tech-lab, https://privacytechlab.org/\n*/\n\n/*\nconst CopyPlugin = require(\"copy-webpack-plugin\");\nconst TerserPlugin = require(\"terser-webpack-plugin\");\nconst HtmlWebpackPlugin = require(\"html-webpack-plugin\");\nconst { CleanWebpackPlugin } = require(\"clean-webpack-plugin\");\nconst path = require(\"path\");\n*/\n\n\nimport CopyPlugin from \"copy-webpack-plugin\";\nimport TerserPlugin from \"terser-webpack-plugin\";\nimport HtmlWebpackPlugin from \"html-webpack-plugin\";\nimport { CleanWebpackPlugin } from \"clean-webpack-plugin\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n\n// ! Implement a \"frontend\" export in order to use a dev serve\n// ! Implement terser for production\n// ! Implement file loader for assets\n\nexport default (env, argv) => {\n  const browser = env.chrome ? \"chrome\" : \"firefox\"; // default to firefox build\n  const isProduction = argv.mode == \"production\"; // sets bool depending on build\n\n  return {\n    name: \"background\",\n    // This is useful, plus we need it b/c otherwise we get an \"unsafe eval\" problem\n    entry: {\n      background: \"./src/background/control.js\",\n      popup: \"./src/popup/popup.js\",\n      options: \"./src/options/options.js\",\n    },\n    output: {\n      filename: \"[name].bundle.js\",\n      path: path.resolve(\n        __dirname,\n        `${isProduction ? \"dist\" : \"dev\"}/${browser}`\n      ),\n    },\n    devtool: isProduction ? \"source-map\" : \"cheap-source-map\",\n    devServer: {\n      open: true,\n      host: \"localhost\",\n    },\n    optimization: {\n      minimize: true,\n      minimizer: [new TerserPlugin()],\n    },\n    module: {\n      rules: [\n        {\n          // compile for the correct browser\n          test: /\\.js$/,\n          exclude: /node_modules/,\n          loader: \"string-replace-loader\",\n          options: {\n            search: /\\$BROWSER/g,\n            replace: browser,\n          },\n        },\n        {\n          test: /\\.js$/,\n          exclude: /node_modules/,\n          use: {\n            loader: \"babel-loader\",\n          },\n        },\n        {\n          test: /\\.css$/,\n          use: [\"style-loader\", \"css-loader\"],\n        },\n        {\n          test: /\\.(png|svg|jpe?g|gif)$/,\n          loader: \"file-loader\",\n          options: {\n            outputPath: \"assets/\",\n            publicPath: \"assets/\",\n            name: \"[name].[ext]\",\n          },\n        },\n      ],\n    },\n\n    // All of our \"extra\" stuff is currently being copies over\n    // When time permits, lets have everything compile correclty\n    plugins: [\n      new CleanWebpackPlugin(),\n      new CopyPlugin({\n        patterns: [\n          {\n            context: path.resolve(__dirname, \"src\"),\n            from: \"assets\",\n            to: \"assets\",\n          },\n        ],\n      }),\n      new CopyPlugin({\n        patterns: [\n          {\n            context: path.resolve(__dirname, \"src\"),\n            from: \"content-scripts\",\n            to: \"content-scripts\",\n          },\n        ],\n      }),\n      new CopyPlugin({\n        patterns: [\n          {\n            context: path.resolve(\n              __dirname,\n              env.chrome ? \"src/manifests/chrome\" : \"src/manifests/firefox\"\n            ),\n            from: isProduction ? \"manifest-dist.json\" : \"manifest-dev.json\",\n            to: \"manifest.json\",\n          },\n        ],\n      }),\n      new CopyPlugin({\n        patterns: [\n          {\n            context: path.resolve(__dirname, \"src\"),\n            from: \"rules\",\n            to: \"rules\",\n          },\n        ],\n      }),\n      new CopyPlugin({\n        patterns: [\n          {\n            context: path.resolve(__dirname, \"src/options\"),\n            from: \"views\",\n            to: \"views\",\n          },\n        ],\n      }),\n      new CopyPlugin({\n        patterns: [\n          {\n            context: path.resolve(__dirname, \"src/options\"),\n            from: \"components\",\n            to: \"components\",\n          },\n        ],\n      }),\n      new HtmlWebpackPlugin({\n        filename: \"options.html\",\n        template: \"src/options/options.html\",\n        chunks: [\"options\"],\n      }),\n      new HtmlWebpackPlugin({\n        filename: \"popup.html\",\n        template: \"src/popup/popup.html\",\n        chunks: [\"popup\"],\n      }),\n    ],\n  };\n};\n"
  }
]