From 24b7073dbedad41f860623a7056f32f351e37899 Mon Sep 17 00:00:00 2001
From: "mic (spark-01)" <85814106+q1@users.noreply.github.com>
Date: Sat, 11 Jul 2026 12:04:36 -0700
Subject: [PATCH 1/4] Recode Cookie Quick Manager 0.6.0
---
.github/workflows/ci.yml | 19 +
.gitignore | 3 +
README.md | 237 ++---
package-lock.json | 523 +++++------
package.json | 24 +-
qa/PARITY_REPORT.md | 274 ++----
qa/chromium-qa.mjs | 886 ++++++++++++++++--
qa/firefox-baseline-rdp.py | 55 +-
qa/fixture-server.mjs | 6 +-
scripts/build.mjs | 35 +
src/_locales/de/messages.json | 37 +-
src/_locales/en/messages.json | 14 +-
src/_locales/fr/messages.json | 14 +-
src/api.js | 692 +++++---------
src/background-script.js | 273 +++---
src/browser-shim.js | 5 +
src/cookies.html | 18 +-
src/cookies.js | 257 +++--
src/core.js | 517 ++++++++++
src/export.js | 378 ++++----
src/manifest.firefox.json | 48 +
src/manifest.json | 12 +-
src/menu.html | 17 +-
src/menu.js | 33 +-
src/options.html | 38 +-
src/options.js | 251 +++--
src/platform.js | 9 +-
src/service-worker.js | 3 +-
src/static/css/bootstrap-theme.min.css | 6 -
src/static/css/jquery-ui.min.css | 7 -
src/static/js/bootstrap-treeview-1.2.0.min.js | 1 -
src/static/js/bootstrap-treeview.min.css | 1 -
test/api.test.js | 109 +++
test/background.test.js | 163 ++++
test/core.test.js | 289 ++++++
test/platform.test.js | 32 +
test/repository.test.js | 46 +
37 files changed, 3458 insertions(+), 1874 deletions(-)
create mode 100644 .github/workflows/ci.yml
create mode 100644 scripts/build.mjs
create mode 100644 src/browser-shim.js
create mode 100644 src/core.js
create mode 100644 src/manifest.firefox.json
delete mode 100644 src/static/css/bootstrap-theme.min.css
delete mode 100644 src/static/css/jquery-ui.min.css
delete mode 100644 src/static/js/bootstrap-treeview-1.2.0.min.js
delete mode 100644 src/static/js/bootstrap-treeview.min.css
create mode 100644 test/api.test.js
create mode 100644 test/background.test.js
create mode 100644 test/core.test.js
create mode 100644 test/platform.test.js
create mode 100644 test/repository.test.js
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..ba7b849
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,19 @@
+name: CI
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 24
+ cache: npm
+ - run: npm ci
+ - run: npx playwright install --with-deps chromium
+ - run: npm run check
diff --git a/.gitignore b/.gitignore
index b6288b1..2a75508 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,10 @@
dist/
build/
+build-firefox/
node_modules/
qa/certs/
+__pycache__/
+*.py[cod]
qa/firefox-baseline-extension/
.playwright-cqm-profile/
poc/
diff --git a/README.md b/README.md
index e4638cd..19779a1 100644
--- a/README.md
+++ b/README.md
@@ -1,211 +1,140 @@
-# Overview
+# Cookie Quick Manager
-Cookie Quick Manager is a complete manager for cookies accumulated during browsing.
-It allows you to view, edit, create, delete, backup, restore, and search cookies by domain names.
-The current codebase is being ported to Chromium as a Manifest V3 extension while preserving the original Firefox-oriented feature set as closely as Chromium allows.
-In addition, the LocalStorage of the page viewed can be deleted (see below).
+Cookie Quick Manager is a privacy-oriented browser extension for viewing, searching, creating, editing, deleting, protecting, exporting, and importing cookies. It also provides a popup action for clearing the active page's LocalStorage.
-Cookie Quick Manager is designed for developers, testers or people
-concerned about their privacy on the Internet.
+This fork modernizes the original extension as a shared Manifest V3 codebase with target-specific builds for:
-This WebExtension is compatible with Firefox 57 and is inspired by addons like [Cookies Manager+](https://addons.mozilla.org/fr/firefox/addon/cookies-manager-plus/) and [Advanced Cookie Manager](https://addons.mozilla.org/fr/firefox/addon/cookie-manager/) whose development has been discontinued due to the withdrawal of the support for "Legacy" extensions.
+- Chromium 130+
+- Firefox desktop 140+
+- Firefox for Android 142+
-**November 2018: Cookie Quick Manager is now available on Android!**
+The project remains GPLv3 and retains attribution to the original Cookie Quick Manager project by Ysard.
-
-
-
+## What it supports
+- Host-only and domain-scoped cookies
+- Session and persistent cookies
+- Secure, HttpOnly, and SameSite attributes
+- Exact path, store, host/domain, First-Party Isolation, and partition identity
+- Chromium partitioned cookies (CHIPS), including visible partition scope
+- Firefox containers/contextual identities and First-Party Isolation controls
+- Search by domain, name, and value, with optional subdomain grouping
+- JSON and Netscape `cookies.txt` import/export
+- Exact cookie protection and restoration after an explicit site deletion
+- Bulk deletion of only the cookies currently visible through the active filters
+- Split-incognito Chromium stores when the user enables Allow in Incognito
+- Popup counts and exact-origin LocalStorage clearing
-# Chromium / Chrome status
+Natural cookie expiry and browser eviction are intentionally not reversed. Protection is for explicit deletion, not a way to make expired cookies immortal.
-This branch now targets **Chromium Manifest V3**.
+## Browser-specific behavior
-Most of the original functionality can be preserved, but Chromium does not expose exact equivalents for two Firefox-only capabilities:
+Chromium uses an extension service worker and generic cookie stores. Firefox uses background scripts and retains its container and privacy APIs. The Firefox-only First-Party Isolation UI is hidden on Chromium.
-* **Containers / Contextual Identities** → replaced by generic **cookie stores** (default store, and a separate incognito/private store when Chromium allows it).
-* **First-Party Isolation** → Firefox-only. The related Firefox setting is not exposed on Chromium, so that UI is hidden there.
+The manager opens in a tab by default. Windowed mode uses a normal extension popup window. Chromium users must enable Allow in Incognito before private-store cookies are available to the extension.
-Other important Chromium-specific notes:
+## Install and build
-* The background logic now runs in an **extension service worker**.
-* The old Firefox panel window behavior is replaced by opening the manager in a **tab** by default, or a regular Chromium **popup window** when windowed mode is selected.
-* To manage private/incognito cookies in Chromium, users must enable **Allow in Incognito** for the extension.
-
-
-# Development / local loading
-
-## Build the Chromium extension
+The project explicitly uses npm:
```bash
-npm install
-npm run build
+npm ci
+npm run build:all
```
-This creates a loadable unpacked extension in `build/`.
-
-## Load unpacked in Chromium
-
-1. Open `chrome://extensions`
-2. Enable **Developer mode**
-3. Click **Load unpacked**
-4. Select the `build/` directory
+Artifacts:
-## Local QA fixture site
+- `build/` — unpacked Chromium extension
+- `build-firefox/` — unpacked Firefox extension
+- `dist/cookie_quick_manager-chromium.zip` — after `npm run package`
+- `dist/cookie_quick_manager-firefox.zip` — after `npm run package:firefox`
-The repository includes a local fixture server used to QA cookie, LocalStorage, subdomain, and protection behavior deterministically.
+For Chromium, open `chrome://extensions`, enable Developer mode, choose Load unpacked, and select `build/`.
-Generate a development certificate once:
+For Firefox, load `build-firefox/` temporarily from `about:debugging`, or run:
```bash
-npm run fixture:cert
+npx web-ext run --source-dir=build-firefox --firefox=/usr/bin/firefox --no-input
```
-Start the HTTP + HTTPS fixture server:
+## Test and release checks
+
+Run the complete local gate:
```bash
-npm run fixture:start
+npm run check
```
-Fixture entry points:
-
-* `http://lvh.me:4173/`
-* `http://sub.lvh.me:4173/`
-* `https://lvh.me:4443/`
-* `https://sub.lvh.me:4443/`
+This gate:
-`lvh.me` and its subdomains resolve to `127.0.0.1`, which makes them convenient for domain/subdomain cookie testing.
+- runs the browser-independent unit and adapter contracts;
+- syntax-checks source, QA, and build scripts;
+- builds Chromium and Firefox artifacts;
+- lints the Firefox artifact with `web-ext`;
+- launches the unpacked Chromium extension in Playwright and runs the full browser regression suite.
-## Focused Chromium QA
+The Chromium QA runner generates its ignored development certificate when needed, starts the loopback-only fixture server, maps `lvh.me` locally in Chromium, and stops the fixture process it owns. A separately running fixture is reused.
-Run the current automated Chromium regression checks:
+Focused commands:
```bash
+npm run test:unit
npm run qa:chromium
+npm test
```
-The script loads the unpacked MV3 extension into Playwright Chromium and verifies:
+The browser suite covers real manager, options, import/export, protection, and popup paths. Its scenarios include exact host/domain and path collisions, stale-value rotation, site-specific parent-domain filtering, aging backups, nameless cookies, uppercase/BOM Netscape files, partition round-trips and UI disambiguation, settings replacement/reset, hostile markup, and actual popup LocalStorage clearing.
-* fixture seeding on the QA site
-* domain listing in the manager UI
-* protected-cookie restoration
-* cookie editing
-* JSON export/import round-trip
-* secure cookie creation on HTTPS
-* Chromium options behavior (including hidden Firefox-only FPI control)
+See [qa/PARITY_REPORT.md](qa/PARITY_REPORT.md) for dated evidence and the remaining browser matrix limitations.
-## Firefox baseline parity check
+### Manual fixture use
-To compare against the old Firefox build:
-
-1. Extract / launch the baseline extension with `web-ext`, for example:
-
-```bash
-unzip -q -o dist/cookie_quick_manager-0.5rc2.zip -d qa/firefox-baseline-extension
-npx web-ext run --source-dir=qa/firefox-baseline-extension --firefox=/usr/bin/firefox --start-url=http://lvh.me:4173/ --no-input
-```
-
-2. In another terminal, run the remote-debugging parity probe:
+The fixture can also be run interactively:
```bash
-python3 qa/firefox-baseline-rdp.py
+npm run fixture:cert
+npm run fixture:start
```
-See `qa/PARITY_REPORT.md` for the latest recorded parity summary.
-
-
-# Features
-
-* User friendly: Clear and structured user interface. Each parameter and functionality is described when the mouse is over the element.
-* Windowed and tab mode: Choose the opening in a tab to get a wider view.
-* Transparency and security: The source code is free (under GPLv3) and # published on a public platform, the only way to allow reviews and external contributions.
-* Search: A user can search for cookies of a domain and subdomains which depend on it.
-* Edit/Create: All the attributes of a cookie can be modified: domain, path, name, value, expiration date, as well as secure and httponly flags.
-* Delete: Remove the cookies of the current website in two clicks.
-* Export: The export and import of a cookie or cookies from a domain in JSON or Netscape format is just as easy.
-* First-Party Isolation: Supported with some limitations (due to API bugs) on Firefox 59, 60, and 61, and without limitations on Firefox 62 (scheduled on September 2018).
-* Stores / contexts: On Firefox, contextual identities (containers) are supported. On Chromium, the equivalent UI works with browser cookie stores instead.
-* SameSite: The SameSite flag is supported. This is a partial protection against the risks associated with Cross-Site Request Forgery (CSRF) and Cross-Site Script Inclusion (XSSI) attacks, implemented since Firefox 63.
-* Cookie protection: Delete cookies except protected ones, with two clicks at anytime from the website you are viewing. An option can also prevent cookies from being deleted by the sites themselves.
-* Protection of session cookies: Session cookies can be protected in two clicks to prevent accidental logout from websites after cleaning normal cookies.
-* Cleaning and privacy: Can automatically delete all cookies at startup.
-* LocalStorage: Keys/values of the page viewed can be deleted.
-
-
-# Privacy
-
-This addon does not store or leak any personal information.
-
-It requires the following permissions to operate:
-
-* Host permission for all urls: This allows you to edit the cookies and to delete the Localstorage of any site visited.
-* Cookies: Allows access to the browser's cookie store.
-* ActiveTab: Allows access to the currently consulted url, and its favicon if it exists.
-* Scripting: Used on Chromium to read the LocalStorage item count from the current tab.
-* Storage: Allows the storage of the following user settings:
- - size of the windowed mode,
- - protected cookies (only the name of the domains),
- - the template used to import/export the cookies,
- - the skin.
-* Browsing data: Enables extensions to clear the data that is accumulated while the user is browsing (LocalStorage here).
-* Contextual Identities: Firefox only. Allows the addon to list the containers.
-* Privacy: Firefox only. Access and modify various privacy-related browser settings (the FirstPartyIsolation flag here).
-* ClipboardWrite (optional): Allows the export of cookies to the clipboard from Firefox 63.
-
-
-# About cookie protection
-
-The protection of cookies is limited to the current addon actions, to the deletions that can be made by the sites themselves, or to the deletions made in the browser's "Cookies and Site Data" options.
-This means that if you choose the browser option to delete all cookies when it closes, the addon will be unable to restore them (the method used by the browser does not send the necessary signal to notify the addon). However, a similar option is reimplemented in the addon itself in order to keep only the protected cookies when restarting the browser.
-
-
-# What is "Delete Current Site Local Storage" on the popup menu?
-
-This item allows a user to delete LocalStorage keys from the viewed page.
-
-The LocalStorage is a quite new feature of HTML5 that allows developers to create data in your browser using JavaScript. Cookies are just one type of storage among others.
-You will also find the term "SessionStorage", a LocalStorage where data is stored temporarily (deleted on browser restart), but the important thing is that LocalStorage is persistent, and cleared only at the discretion of the visited websites.
-You may erase all of the LocalStorage store by following the procedure described in the [documentation of Firefox](https://support.mozilla.org/en-US/kb/delete-cookies-remove-info-websites-stored#w_delete-all-cookies), and by selecting "Offline Website Data".
-
-From the point of view of privacy and security:
-
-This kind of persistant data (even after clearing cache), was invented to store small data allowing the operation of online applications, but also offers new and better accurate ways of tracking thanks to the memorization of private or identifying data.
-
-
-# Support & source code
-
-The extension is still in development with the launch of Firefox Quantum; questions, bug reports and feature requests are open on the [GitHub repository](https://github.com/ysard/cookie-quick-manager/issues).
+Entry points:
+- `http://lvh.me:4173/`
+- `http://sub.lvh.me:4173/`
+- `https://lvh.me:4443/`
+- `https://sub.lvh.me:4443/`
-# How to contribute ?
+The server binds to `127.0.0.1` by default.
-1. You can contribute by reporting bugs or problems encountered by creating an issue on [this page] (https://github.com/ysard/cookie-quick-manager/issues)
+## Privacy and permissions
+The extension does not transmit browsing data or cookie contents. Settings and cookie-protection identity metadata are stored locally. Cookie values are not placed in extension storage.
-2. You can translate the application into your language.
-The files are at this address:
-https://github.com/ysard/cookie-quick-manager/tree/fpi/src/_locales
+Permissions are used as follows:
-You must fork the repository and then make a pull-request with your changes.
-See this documentation with illustrations:
+- `cookies` and `` — inspect and modify cookies for sites the user visits
+- `activeTab` and `scripting` — count and clear LocalStorage for the exact active origin
+- `storage` — settings and protected-cookie identity metadata
+- `clipboardWrite` (optional) — copy exported cookies
+- `contextualIdentities` and `privacy` (Firefox only) — containers and First-Party Isolation
-- https://help.github.com/en/articles/fork-a-repo
-- https://help.github.com/en/articles/creating-a-pull-request-from-a-fork
+Protection metadata includes domain, name, path, store, host/domain scope, and partition/FPI scope. Chromium shares `storage.local` between regular and split-incognito extension processes, but the cookie jars remain separate.
-Then execute the following commands:
+Browser-level clear-on-close policies may delete cookies before an extension can preserve them. The add-on's own startup cleanup instead retains protected identities.
- cd cookie-quick-manager
- make get_missing_from_new_language LOCALE = "from"
+## Compatibility notes
-(here, `de` is the locale code for Deutch language)
+- Current JSON exports retain store, FPI, SameSite, host/domain, and partition metadata.
+- Older exports containing malformed URLs such as `https://.example.com/` are repaired during import.
+- Structurally valid expired persistent records are skipped while the rest of an aging backup is restored.
+- Netscape import accepts standard case-insensitive `TRUE`/`FALSE` flags and `#HttpOnly_` entries.
+- Firefox uses a fork-specific extension ID and declares no data collection in its manifest.
-The previous command will create a file in `src/_locales//messages.json`,
-then display the list of elements to be translated.
-The file does not need to be translated to 100% to be used!
+## Contributing
-You must then make your pull request on github.
+Issues and pull requests belong at [q1/cookie-quick-manager](https://github.com/q1/cookie-quick-manager). Translation files are under `src/_locales/`.
+The historical upstream is [ysard/cookie-quick-manager](https://github.com/ysard/cookie-quick-manager).
-# License
+## License
-[GPLv3](https://github.com/ysard/cookie-quick-manager/blob/master/LICENSE").
\ No newline at end of file
+[GNU General Public License v3.0](LICENSE)
diff --git a/package-lock.json b/package-lock.json
index 1daf09b..d8b2236 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,16 +1,15 @@
{
"name": "cookie-quick-manager",
- "version": "0.5.0",
+ "version": "0.6.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cookie-quick-manager",
- "version": "0.5.0",
+ "version": "0.6.0",
"devDependencies": {
- "playwright": "^1.58.2",
- "web-ext": "^10.0.0",
- "webextension-polyfill": "^0.12.0"
+ "playwright": "^1.61.1",
+ "web-ext": "^10.5.0"
}
},
"node_modules/@babel/code-frame": {
@@ -39,14 +38,11 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
- "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-8.0.0.tgz",
+ "integrity": "sha512-sL6cvO2IfkSu/iU+zs2S/w01B7A8V7suXSIKEN4hPFFdZoiPGxrj5pAG0lCaqLWiEIrjKzdznIWuaLcxPR53qw==",
"dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
+ "license": "MIT"
},
"node_modules/@devicefarmer/adbkit": {
"version": "3.3.8",
@@ -174,9 +170,9 @@
}
},
"node_modules/@eslint/eslintrc": {
- "version": "3.3.5",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
- "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz",
+ "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -186,7 +182,7 @@
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
- "js-yaml": "^4.1.1",
+ "js-yaml": "^4.3.0",
"minimatch": "^3.1.5",
"strip-json-comments": "^3.1.1"
},
@@ -198,9 +194,9 @@
}
},
"node_modules/@eslint/eslintrc/node_modules/ajv": {
- "version": "6.14.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
- "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -266,9 +262,9 @@
}
},
"node_modules/@eslint/js": {
- "version": "9.39.2",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
- "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
+ "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -324,29 +320,43 @@
}
},
"node_modules/@humanfs/core": {
- "version": "0.19.1",
- "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
- "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
"dev": true,
"license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/node": {
- "version": "0.16.7",
- "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
- "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@humanfs/core": "^0.19.1",
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
"@humanwhocodes/retry": "^0.4.0"
},
"engines": {
"node": ">=18.18.0"
}
},
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -376,9 +386,9 @@
}
},
"node_modules/@mdn/browser-compat-data": {
- "version": "7.3.6",
- "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-7.3.6.tgz",
- "integrity": "sha512-eLTmNSxv2DaDO1Hq7C9lwQbThlF5+vMMUQfIl6xRPSC2q6EcFXhim4Mc9uxHNxcPvDFCdB3qviMFDdAzQVhYcw==",
+ "version": "8.0.4",
+ "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-8.0.4.tgz",
+ "integrity": "sha512-up6DsNsaPt3jq5d2TGx4UOXIqBKU0D86dxWwcVmmOOEYqP1rFaV1XcAcwHxMvb6bbtMfY5JpjUCszzy/aVc4yA==",
"dev": true,
"license": "CC0-1.0"
},
@@ -435,9 +445,9 @@
}
},
"node_modules/@types/estree": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
- "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"dev": true,
"license": "MIT"
},
@@ -466,9 +476,9 @@
}
},
"node_modules/acorn": {
- "version": "8.16.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
- "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "version": "8.17.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
"dev": true,
"license": "MIT",
"bin": {
@@ -489,24 +499,24 @@
}
},
"node_modules/addons-linter": {
- "version": "10.1.0",
- "resolved": "https://registry.npmjs.org/addons-linter/-/addons-linter-10.1.0.tgz",
- "integrity": "sha512-Qo8QE/tGxaGMTQGiLPGfxDyrYJCKtsXFkyto3UGuVPb2V+Jc725U3Jjpwpo7cXoImCebueUVXC8KC8D7dpacTQ==",
+ "version": "10.8.0",
+ "resolved": "https://registry.npmjs.org/addons-linter/-/addons-linter-10.8.0.tgz",
+ "integrity": "sha512-AotpGWlp5FrmUIH8kjyYjQvCmthnSqPLpw7kOEl7DfoAWEndOCV/ONuekAVM8QObiei802RyyIClEaekA51n5g==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
"@fluent/syntax": "0.19.0",
"@fregante/relaxed-json": "2.0.0",
- "@mdn/browser-compat-data": "7.3.6",
+ "@mdn/browser-compat-data": "8.0.4",
"addons-moz-compare": "1.3.0",
- "addons-scanner-utils": "13.1.0",
- "ajv": "8.18.0",
- "chalk": "4.1.2",
+ "addons-scanner-utils": "15.4.0",
+ "ajv": "8.20.0",
"cheerio": "1.2.0",
"columnify": "1.6.0",
"common-tags": "1.8.2",
+ "css-tree": "3.2.1",
"deepmerge": "4.3.1",
- "eslint": "9.39.2",
+ "eslint": "9.39.4",
"eslint-plugin-no-unsanitized": "4.1.5",
"eslint-visitor-keys": "5.0.1",
"espree": "11.2.0",
@@ -515,11 +525,11 @@
"image-size": "2.0.2",
"json-merge-patch": "1.0.2",
"pino": "10.3.1",
- "semver": "7.7.4",
+ "semver": "7.8.5",
"source-map-support": "0.5.21",
- "upath": "2.0.1",
+ "upath": "3.0.7",
"yargs": "17.7.2",
- "yauzl": "3.2.1"
+ "yauzl": "3.4.0"
},
"bin": {
"addons-linter": "bin/addons-linter"
@@ -536,9 +546,9 @@
"license": "MPL-2.0"
},
"node_modules/addons-scanner-utils": {
- "version": "13.1.0",
- "resolved": "https://registry.npmjs.org/addons-scanner-utils/-/addons-scanner-utils-13.1.0.tgz",
- "integrity": "sha512-4apAnr0xrEVbIJhNPQA6XUnVAiUR0/lt8EvflrPgrmBQYwTwo7z+yv92t7F1iO2Y07lNhDCZEoL8q1H4sLJyGw==",
+ "version": "15.4.0",
+ "resolved": "https://registry.npmjs.org/addons-scanner-utils/-/addons-scanner-utils-15.4.0.tgz",
+ "integrity": "sha512-i3PnJx9YLZi96o4t4JWArP6JimC01949YVAcMJKHfyQ5qmUBaH21qNS8f12/RsloejW8IcCEljhh3Fq3gxFNsg==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
@@ -546,18 +556,14 @@
"first-chunk-stream": "3.0.0",
"jsonwebtoken": "^9.0.3",
"strip-bom-stream": "4.0.0",
- "upath": "2.0.1",
- "yauzl": "3.2.1"
+ "upath": "3.0.7",
+ "yauzl": "3.4.0"
},
"peerDependencies": {
- "body-parser": "2.2.2",
"express": "5.2.1",
"safe-compare": "1.1.4"
},
"peerDependenciesMeta": {
- "body-parser": {
- "optional": true
- },
"express": {
"optional": true
},
@@ -587,9 +593,9 @@
}
},
"node_modules/ajv": {
- "version": "8.18.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
- "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -780,9 +786,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -790,16 +796,6 @@
"concat-map": "0.0.1"
}
},
- "node_modules/buffer-crc32": {
- "version": "0.2.13",
- "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
- "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "*"
- }
- },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -1072,22 +1068,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/concat-stream": {
- "version": "1.6.2",
- "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
- "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
- "dev": true,
- "engines": [
- "node >= 0.8"
- ],
- "license": "MIT",
- "dependencies": {
- "buffer-from": "^1.0.0",
- "inherits": "^2.0.3",
- "readable-stream": "^2.2.2",
- "typedarray": "^0.0.6"
- }
- },
"node_modules/config-chain": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
@@ -1164,6 +1144,20 @@
"url": "https://github.com/sponsors/fb55"
}
},
+ "node_modules/css-tree": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.27.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
"node_modules/css-what": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
@@ -1461,25 +1455,25 @@
}
},
"node_modules/eslint": {
- "version": "9.39.2",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
- "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
+ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
- "@eslint/config-array": "^0.21.1",
+ "@eslint/config-array": "^0.21.2",
"@eslint/config-helpers": "^0.4.2",
"@eslint/core": "^0.17.0",
- "@eslint/eslintrc": "^3.3.1",
- "@eslint/js": "9.39.2",
+ "@eslint/eslintrc": "^3.3.5",
+ "@eslint/js": "9.39.4",
"@eslint/plugin-kit": "^0.4.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6",
- "ajv": "^6.12.4",
+ "ajv": "^6.14.0",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.6",
"debug": "^4.3.2",
@@ -1498,7 +1492,7 @@
"is-glob": "^4.0.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"lodash.merge": "^4.6.2",
- "minimatch": "^3.1.2",
+ "minimatch": "^3.1.5",
"natural-compare": "^1.4.0",
"optionator": "^0.9.3"
},
@@ -1561,9 +1555,9 @@
}
},
"node_modules/eslint/node_modules/ajv": {
- "version": "6.14.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
- "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1722,9 +1716,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
- "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
+ "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==",
"dev": true,
"funding": [
{
@@ -1850,17 +1844,15 @@
}
},
"node_modules/fx-runner": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/fx-runner/-/fx-runner-1.4.0.tgz",
- "integrity": "sha512-rci1g6U0rdTg6bAaBboP7XdRu01dzTAaKXxFf+PUqGuCv6Xu7o8NZdY1D5MvKGIjb6EdS1g3VlXOgksir1uGkg==",
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/fx-runner/-/fx-runner-1.5.0.tgz",
+ "integrity": "sha512-EstfdRQu04tM+SXR3pBmc0+GYKZj9FMHAvA8XAbKPzTvUeHP0v1/F3aQ10bG7EluR6phvFqsvn5Z7SvYJsEgNw==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
- "commander": "2.9.0",
- "shell-quote": "1.7.3",
- "spawn-sync": "1.0.15",
- "when": "3.7.7",
- "which": "1.2.4",
+ "commander": "^12.1.0",
+ "shell-quote": "1.8.4",
+ "which": "^4.0.0",
"winreg": "0.0.12"
},
"bin": {
@@ -1868,37 +1860,39 @@
}
},
"node_modules/fx-runner/node_modules/commander": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
- "integrity": "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==",
+ "version": "12.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
+ "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "graceful-readlink": ">= 1.0.0"
- },
"engines": {
- "node": ">= 0.6.x"
+ "node": ">=18"
}
},
"node_modules/fx-runner/node_modules/isexe": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-1.1.2.tgz",
- "integrity": "sha512-d2eJzK691yZwPHcv1LbeAOa91yMJ9QmfTgSO1oXB65ezVhXQsxBac2vEB4bMVms9cGzaA99n6V2viHMq82VLDw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz",
+ "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==",
"dev": true,
- "license": "ISC"
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
},
"node_modules/fx-runner/node_modules/which": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/which/-/which-1.2.4.tgz",
- "integrity": "sha512-zDRAqDSBudazdfM9zpiI30Fu9ve47htYXcGi3ln0wfKu2a7SmrT6F3VDoYONu//48V8Vz4TdCRNPjtvyRO3yBA==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
+ "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
"dev": true,
"license": "ISC",
"dependencies": {
- "is-absolute": "^0.1.7",
- "isexe": "^1.1.1"
+ "isexe": "^3.1.1"
},
"bin": {
- "which": "bin/which"
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^16.13.0 || >=18.0.0"
}
},
"node_modules/get-caller-file": {
@@ -1937,13 +1931,6 @@
"node": ">=10.13.0"
}
},
- "node_modules/glob-to-regexp": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
- "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
- "dev": true,
- "license": "BSD-2-Clause"
- },
"node_modules/global-directory": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz",
@@ -1990,20 +1977,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/graceful-readlink": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
- "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/growly": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
- "integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -2161,19 +2134,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
- "node_modules/is-absolute": {
- "version": "0.1.7",
- "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.1.7.tgz",
- "integrity": "sha512-Xi9/ZSn4NFapG8RP98iNPMOeaV3mXPisxKxzKtHVqr3g56j/fBn+yZmnxSVAA8lmZbl2J9b/a4kJvfU3hqQYgA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-relative": "^0.1.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
@@ -2330,15 +2290,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/is-relative": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.1.3.tgz",
- "integrity": "sha512-wBOr+rNM4gkAZqoLRJI4myw5WzzIdQosFAAbnvfXP5z1LyzgAI3ivOKehC5KfqlQJZoihVhirgtCBj378Eg8GA==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/is-utf8": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
@@ -2391,10 +2342,20 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
- "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
+ "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
"dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -2684,6 +2645,13 @@
"dev": true,
"license": "Apache-2.0"
},
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
"node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
@@ -2741,30 +2709,15 @@
"license": "MIT"
},
"node_modules/node-forge": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz",
- "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz",
+ "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==",
"dev": true,
"license": "(BSD-3-Clause OR GPL-2.0)",
"engines": {
"node": ">= 6.13.0"
}
},
- "node_modules/node-notifier": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-10.0.1.tgz",
- "integrity": "sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "growly": "^1.3.0",
- "is-wsl": "^2.2.0",
- "semver": "^7.3.5",
- "shellwords": "^0.1.1",
- "uuid": "^8.3.2",
- "which": "^2.0.2"
- }
- },
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@@ -2827,15 +2780,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/os-shim": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz",
- "integrity": "sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A==",
- "dev": true,
- "engines": {
- "node": ">= 0.4.0"
- }
- },
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -3053,13 +2997,13 @@
"license": "MIT"
},
"node_modules/playwright": {
- "version": "1.58.2",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
- "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
+ "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.58.2"
+ "playwright-core": "1.61.1"
},
"bin": {
"playwright": "cli.js"
@@ -3072,9 +3016,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.58.2",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
- "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
+ "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -3371,9 +3315,9 @@
}
},
"node_modules/semver": {
- "version": "7.7.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
- "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -3414,18 +3358,17 @@
}
},
"node_modules/shell-quote": {
- "version": "1.7.3",
- "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
- "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
+ "version": "1.8.4",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz",
+ "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==",
"dev": true,
- "license": "MIT"
- },
- "node_modules/shellwords": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
- "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
- "dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
},
"node_modules/sonic-boom": {
"version": "4.2.1",
@@ -3447,6 +3390,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
@@ -3458,18 +3411,6 @@
"source-map": "^0.6.0"
}
},
- "node_modules/spawn-sync": {
- "version": "1.0.15",
- "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz",
- "integrity": "sha512-9DWBgrgYZzNghseho0JOuh+5fg9u6QWhAWa51QC7+U5rCheZ/j1DrEZnyE0RBBRqZ9uEXGPgSSM0nky6burpVw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "dependencies": {
- "concat-stream": "^1.4.7",
- "os-shim": "^0.1.2"
- }
- },
"node_modules/split": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz",
@@ -3654,18 +3595,25 @@
}
},
"node_modules/thread-stream": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz",
- "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz",
+ "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "real-require": "^0.2.0"
+ "real-require": "^1.0.0"
},
"engines": {
"node": ">=20"
}
},
+ "node_modules/thread-stream/node_modules/real-require": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz",
+ "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@@ -3674,9 +3622,9 @@
"license": "MIT"
},
"node_modules/tmp": {
- "version": "0.2.5",
- "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
- "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
+ "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3709,17 +3657,10 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/typedarray": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
- "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/undici": {
- "version": "7.24.5",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz",
- "integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
+ "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3744,14 +3685,27 @@
}
},
"node_modules/upath": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz",
- "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==",
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/upath/-/upath-3.0.7.tgz",
+ "integrity": "sha512-VjBBquch25nUGMuVBpOb2Cj3gc8Kb7lJBqbsXR/0anZ/5uJsL14Kpth9JKfnBsckxCfgIp6hPvcvvmZ97R9X7g==",
"dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/anodynos"
+ },
+ {
+ "type": "polar",
+ "url": "https://polar.sh/anodynos"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/subscription/pkg/npm-upath"
+ }
+ ],
"license": "MIT",
"engines": {
- "node": ">=4",
- "yarn": "*"
+ "node": ">=20"
}
},
"node_modules/update-notifier": {
@@ -3809,24 +3763,13 @@
"dev": true,
"license": "MIT"
},
- "node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
"node_modules/watchpack": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
- "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==",
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz",
+ "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2"
},
"engines": {
@@ -3844,27 +3787,26 @@
}
},
"node_modules/web-ext": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/web-ext/-/web-ext-10.0.0.tgz",
- "integrity": "sha512-Cdr7GELRVOXGdV8NqZAPa9oImXWUMDbzYff+UBx5F6mz4zfIrnF3+iYUsvBfG9VJ/VsqmXKdpt1gBsXbH8Z75A==",
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/web-ext/-/web-ext-10.5.0.tgz",
+ "integrity": "sha512-tveUHhZHu5A2Qrs7u+hbh2S+7ZPuKVH/ILzqkRwpdivFOnVmgPhDLtmtGNKT3a8N9PZbW5uP6INe6Byj5qePVQ==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
- "@babel/runtime": "7.28.6",
+ "@babel/runtime": "8.0.0",
"@devicefarmer/adbkit": "3.3.8",
- "addons-linter": "10.1.0",
+ "addons-linter": "10.8.0",
"camelcase": "8.0.0",
"chrome-launcher": "1.2.0",
"debounce": "1.2.1",
"decamelize": "6.0.1",
"es6-error": "4.1.1",
"firefox-profile": "4.7.0",
- "fx-runner": "1.4.0",
+ "fx-runner": "1.5.0",
"https-proxy-agent": "^7.0.0",
"jose": "5.9.6",
"jszip": "3.10.1",
"multimatch": "6.0.0",
- "node-notifier": "10.0.1",
"open": "11.0.0",
"parse-json": "8.3.0",
"pino": "10.3.1",
@@ -3872,9 +3814,9 @@
"source-map-support": "0.5.21",
"strip-bom": "5.0.0",
"strip-json-comments": "5.0.3",
- "tmp": "0.2.5",
+ "tmp": "0.2.7",
"update-notifier": "7.3.1",
- "watchpack": "2.5.1",
+ "watchpack": "2.5.2",
"yargs": "17.7.2",
"zip-dir": "2.0.0"
},
@@ -3886,13 +3828,6 @@
"npm": ">=8.0.0"
}
},
- "node_modules/webextension-polyfill": {
- "version": "0.12.0",
- "resolved": "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.12.0.tgz",
- "integrity": "sha512-97TBmpoWJEE+3nFBQ4VocyCdLKfw54rFaJ6EVQYLBCXqCIpLSZkwGgASpv4oPt9gdKCJ80RJlcmNzNn008Ag6Q==",
- "dev": true,
- "license": "MPL-2.0"
- },
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
@@ -3917,13 +3852,6 @@
"node": ">=18"
}
},
- "node_modules/when": {
- "version": "3.7.7",
- "resolved": "https://registry.npmjs.org/when/-/when-3.7.7.tgz",
- "integrity": "sha512-9lFZp/KHoqH6bPKjbWqa+3Dg/K/r2v0X/3/G2x4DBGchVS2QX2VXL3cZV994WQVnTM1/PD71Az25nAzryEUugw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/when-exit": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz",
@@ -4172,13 +4100,12 @@
}
},
"node_modules/yauzl": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.1.tgz",
- "integrity": "sha512-k1isifdbpNSFEHFJ1ZY4YDewv0IH9FR61lDetaRMD3j2ae3bIXGV+7c+LHCqtQGofSd8PIyV4X6+dHMAnSr60A==",
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz",
+ "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "buffer-crc32": "~0.2.3",
"pend": "~1.2.0"
},
"engines": {
diff --git a/package.json b/package.json
index 7cf5d32..cb1e00d 100644
--- a/package.json
+++ b/package.json
@@ -1,19 +1,25 @@
{
"name": "cookie-quick-manager",
- "version": "0.5.0",
+ "version": "0.6.0",
"private": true,
- "description": "Chromium MV3 build tooling for Cookie Quick Manager",
+ "description": "Dual-target Chromium and Firefox MV3 Cookie Quick Manager",
"scripts": {
- "clean": "rm -rf build",
- "build": "npm run clean && mkdir -p build/static/js && cp -R src/* build/ && cp LICENSE build/ && cp node_modules/webextension-polyfill/dist/browser-polyfill.min.js build/static/js/browser-polyfill.min.js",
- "package": "npm run build && mkdir -p dist && (cd build && zip -qr ../dist/cookie_quick_manager-chromium.zip .)",
+ "clean": "rm -rf build build-firefox",
+ "build": "npm run build:chromium",
+ "build:chromium": "node scripts/build.mjs chromium build",
+ "build:firefox": "node scripts/build.mjs firefox build-firefox",
+ "build:all": "npm run build:chromium && npm run build:firefox",
+ "package": "npm run build:chromium && mkdir -p dist && rm -f dist/cookie_quick_manager-chromium.zip && (cd build && zip -qr ../dist/cookie_quick_manager-chromium.zip .)",
+ "package:firefox": "npm run build:firefox && mkdir -p dist && rm -f dist/cookie_quick_manager-firefox.zip && (cd build-firefox && zip -qr ../dist/cookie_quick_manager-firefox.zip .)",
"fixture:cert": "bash qa/generate-dev-cert.sh",
"fixture:start": "node qa/fixture-server.mjs",
- "qa:chromium": "npm run build && node qa/chromium-qa.mjs"
+ "qa:chromium": "npm run build && node qa/chromium-qa.mjs",
+ "test:unit": "node --test test/*.test.js",
+ "test": "npm run test:unit && npm run qa:chromium",
+ "check": "npm run test:unit && for file in src/*.js qa/*.mjs scripts/*.mjs; do node --check \"$file\"; done && python3 -c \"import ast, pathlib; ast.parse(pathlib.Path('qa/firefox-baseline-rdp.py').read_text())\" && npm run build:all && web-ext lint --source-dir build-firefox && node qa/chromium-qa.mjs"
},
"devDependencies": {
- "playwright": "^1.58.2",
- "web-ext": "^10.0.0",
- "webextension-polyfill": "^0.12.0"
+ "playwright": "^1.61.1",
+ "web-ext": "^10.5.0"
}
}
diff --git a/qa/PARITY_REPORT.md b/qa/PARITY_REPORT.md
index fc8105d..47ee8ee 100644
--- a/qa/PARITY_REPORT.md
+++ b/qa/PARITY_REPORT.md
@@ -1,185 +1,111 @@
-# Chromium MV3 Parity Report
+# Cookie Quick Manager 0.6.0 QA and compatibility report
-Date: 2026-03-22
+Date: 2026-07-11
-This report summarizes the parity-focused QA work completed for the Chromium/Manifest V3 port against the original Firefox extension behavior.
+This report records evidence for the current dual-target recode. It deliberately separates current-artifact verification from the historical Firefox baseline; Firefox lint alone is not treated as runtime parity.
-## Test setup
+## Target artifacts
-### Fixture environment
+- Chromium Manifest V3, minimum Chrome 130
+- Firefox Manifest V3, minimum Firefox desktop 140
+- Firefox for Android, minimum Firefox 142
+- Package and both manifests: version 0.6.0
-Deterministic fixture pages were used instead of arbitrary live sites:
+The Chromium and Firefox ZIPs were rebuilt from clean target directories and verified byte-for-byte against every file in those directories.
-* HTTP: `http://lvh.me:4173/`
-* HTTPS: `https://lvh.me:4443/`
-* subdomain variants on `sub.lvh.me`
+## Automated release gate
-These fixtures support:
-
-* host-only and domain cookies
-* page-driven cookie deletion
-* LocalStorage seeding/clearing
-* secure-cookie testing on HTTPS
-
-### Baseline Firefox extension
-
-The original Firefox artifact from `dist/cookie_quick_manager-0.5rc2.zip` was extracted and launched with `web-ext` for comparison.
-
-### Chromium target
-
-The ported extension was loaded from `build/` as an unpacked Manifest V3 Chromium extension.
-
-## Chromium verification performed
-
-### Manual GUI smoke checks
-
-Confirmed manually in Chromium:
-
-* unpacked extension loads successfully in `chrome://extensions`
-* popup opens successfully
-* site-specific popup entries appear on the fixture page
-* popup counts looked plausible for:
- * current-site cookies
- * current-store cookies
- * current-site LocalStorage
-* manager page opens and lists seeded fixture cookies
-
-### Automated Chromium QA
-
-Executed with:
+Command:
```bash
-npm run qa:chromium
+npm run check
```
-Passing checks:
-
-* fixture seeding
-* manager domain listing
-* auto-refresh when a same-domain cookie is created outside the manager
-* subdomain grouping
-* search filtering by cookie name
-* protected-cookie restoration after page-driven deletion
-* cookie editing
-* JSON export
-* direct JSON restore
-* delete cookie
-* JSON import round-trip
-* secure cookie creation on HTTPS
-* hidden Firefox-only FPI control on Chromium options page
-* representative options persistence
-
-Latest successful result set:
-
-```json
-{
- "ok": true,
- "results": [
- {"check": "fixture-seeding", "status": "passed"},
- {"check": "manager-domain-list", "status": "passed"},
- {"check": "auto-refresh", "status": "passed"},
- {"check": "group-subdomains", "status": "passed"},
- {"check": "search-filter-by-name", "status": "passed"},
- {"check": "protected-cookie-restore", "status": "passed"},
- {"check": "edit-cookie", "status": "passed"},
- {"check": "export-cookie-json", "status": "passed"},
- {"check": "direct-json-restore", "status": "passed"},
- {"check": "delete-cookie", "status": "passed"},
- {"check": "import-cookie-json", "status": "passed"},
- {"check": "create-secure-cookie", "status": "passed"},
- {"check": "options-hide-fpi", "status": "passed"},
- {"check": "options-persistence", "status": "passed"}
- ]
-}
-```
-
-## Firefox baseline verification performed
-
-The Firefox baseline was inspected through Firefox's remote debugging protocol against the temporary add-on session launched by `web-ext`.
-
-Executed with:
-
-```bash
-python3 qa/firefox-baseline-rdp.py
-```
-
-Passing checks:
-
-* fixture seeding
-* manager page opens
-* manager lists seeded `.lvh.me` and `lvh.me` domains
-* subdomain grouping collapses host/domain variants into one grouped domain
-* name-based search filtering isolates the expected cookie
-* host-only cookie can be selected from the manager
-* single-cookie protection state can be normalized to locked
-* protected host cookie survives page-driven deletion after the protection state is normalized
-
-Latest successful result set:
-
-```json
-{
- "ok": true,
- "results": [
- {"check": "fixture-seeding", "status": "passed"},
- {"check": "manager-domain-list", "status": "passed"},
- {"check": "group-subdomains", "status": "passed"},
- {"check": "search-filter-by-name", "status": "passed"},
- {"check": "select-host-cookie", "status": "passed"},
- {"check": "protect-state-normalized", "status": "passed"},
- {"check": "protected-cookie-delete-from-page-js", "status": "passed"}
- ]
-}
-```
-
-## Parity conclusions
-
-### Verified equivalent or acceptably equivalent behavior
-
-* manager page opens successfully in both Firefox baseline and Chromium port
-* seeded host-only/domain cookies are visible in the manager
-* subdomain grouping matches between Firefox baseline and Chromium
-* name-based search filtering matches between Firefox baseline and Chromium
-* single-cookie protection works and prevents page-driven deletion
-* auto-refresh works in Chromium for same-domain external cookie mutations
-* cookie edit flow works in Chromium
-* JSON export/import round-trip works in Chromium
-* secure cookie creation works in Chromium on HTTPS fixtures
-* representative options persistence works in Chromium
-
-### Intentional Chromium substitutions
-
-These are expected platform substitutions, not regressions:
-
-* **Firefox containers / contextual identities** → **Chromium cookie stores**
-* **Firefox FPI UI / setting** → hidden on Chromium
-* **Firefox panel window behavior** → tab by default, standard popup window when windowed mode is selected
-
-### Remaining QA limitations
-
-The highest-value parity scenarios are covered, but not every historical UI path was fully automated on both browsers.
-
-Not exhaustively automated yet:
-
-* Firefox popup quick-action counts
-* full Firefox export/import round-trip through UI
-* Firefox options persistence matrix
-* incognito/store comparison matrix on both browsers
-
-These are follow-up QA opportunities rather than known failures.
-
-## Overall signoff
-
-The Chromium port now has strong evidence for core parity on the most critical user-facing workflows:
-
-* cookie discovery/listing
-* search filtering
-* subdomain grouping
-* auto-refresh
-* single-cookie editing
-* single-cookie protection and restore behavior
-* JSON import/export
-* secure cookie creation
-* representative options persistence
-* Chromium-specific platform substitutions documented clearly
-
-At this point, the Chromium MV3 port is in good shape for a parity-first release candidate, with deeper modernization intentionally deferred until after this parity pass.
+The gate is self-contained. Starting without `qa/certs/` or a fixture process, it generated a development certificate, started the fixture on loopback, ran the browsers, and stopped the process it owned.
+
+Latest clean result:
+
+- 34/34 browser-independent unit, core, adapter, manifest, and locale contracts passed
+- source, QA, build-script, and Firefox RDP probe syntax checks passed
+- Chromium and Firefox builds passed
+- Firefox `web-ext lint`: 0 errors, 0 warnings, 2 known-library notices
+- 35/35 Chromium Playwright workflows passed
+- `npm audit`: 0 vulnerabilities
+- `git diff --check`: passed
+
+The final 35-scenario Chromium suite also passed in a separate repeat execution.
+
+## Chromium runtime coverage
+
+The suite loads the unpacked extension into headless Chromium and exercises production UI/adapters, not test-only reimplementations.
+
+Passing scenarios:
+
+1. Fixture cookie seeding
+2. Manager domain listing
+3. Auto-refresh on additions and website expiry tombstones
+4. Subdomain grouping
+5. Name filtering
+6. Protected host-cookie restoration after website deletion
+7. Cookie value editing
+8. Single-cookie JSON export and count rendering
+9. Exact unprotect/delete
+10. JSON import through the real file handler
+11. Domain-cookie editing
+12. Protected domain-cookie attribute restoration
+13. Fresh-value rotation winning over a stale pending restore
+14. Exact protected path identity
+15. Exact unprotected `/`, `/app`, and unusual-path deletion
+16. Host-only/domain sibling collision restore and deletion
+17. Delete-key safety inside editors
+18. Filtered bulk deletion leaving non-visible cookies untouched
+19. Domain JSON round-trip with quotes, backslashes, template tokens, and Unicode
+20. Full pre-mutation validation of invalid multi-record imports
+21. Aging backups: live nameless cookie restored, expired persistent record skipped
+22. Netscape host/domain, HttpOnly, Secure, uppercase flags, BOM, and default-store round-trip
+23. Partitioned cookie edit/export/import/delete
+24. Same-site partition identities with opposite ancestor bits shown distinctly
+25. Actual subdomain grouping
+26. Site-specific launch including applicable parent-domain cookies but not host-only parents
+27. Domain label-boundary handling
+28. Prototype-named intranet domain safety
+29. Actual popup cookie/LocalStorage counts and LocalStorage clearing
+30. Secure SameSite=None creation on HTTPS
+31. Chromium options hiding Firefox-only FPI controls
+32. Options persistence
+33. Privileged settings-tree markup rendered literally
+34. Settings restore replacing the snapshot and removing stale exact-protection keys
+35. Settings reset restoring complete defaults
+
+The runner also fails on uncaught page exceptions and extension-page/service-worker console errors.
+
+## Firefox current-artifact evidence
+
+The current Firefox artifact:
+
+- builds with the Firefox background-script manifest;
+- installs in Firefox 151 headless;
+- passed the seven strengthened runtime smoke assertions: fixture seeding, manager launch/listing, grouping, filtering, host-cookie selection, protection activation, and protected-cookie survival after page deletion;
+- preserved `partitionKey.topLevelSite` and `hasCrossSiteAncestor` in targeted Firefox 151 set/query probes;
+- handled exact nameless and host/domain tombstone deletion in targeted Firefox 151 probes.
+
+The committed RDP probe was hardened so fixture, selection, protection, and survival checks fail closed. Its asynchronous protection step now polls synchronously rather than mistaking an RDP Promise grip for a result.
+
+This is useful current-artifact smoke evidence, but it is not a full Firefox equivalent of the 35-scenario Chromium suite.
+
+## Historical baseline
+
+The original 0.5rc2 Firefox artifact remains useful for reconstructing intent. The old Chromium suite had 14 passing checks, and the historical Firefox probe had seven. Fan-out review showed that those checks contained shortcuts and false negatives: they did not expose service-worker cold-start cleanup, malformed domain URLs, incomplete cookie identity, partition blindness, unsafe repeated imports, or native `cookies.remove()` over-deletion.
+
+Those historical results are retained as behavioral context, not release signoff for 0.6.0.
+
+## Remaining matrix limitations
+
+- The full 35-scenario suite is Chromium-only; current Firefox automation is a seven-check smoke plus targeted API probes.
+- Firefox 140, Firefox Android 142, and real Android UI behavior were not run on this machine.
+- Chromium and Firefox private/incognito enablement and container/store isolation do not yet have a full automated browser-policy matrix.
+- The packaged privileged UI still contains legacy jQuery, Bootstrap 3, Moment, and context-menu libraries. Unused treeview/theme assets were removed, DOM sinks were hardened, and Firefox lint reports notices rather than errors, but dependency modernization remains worthwhile.
+
+## Conclusion
+
+Version 0.6.0 has substantially stronger evidence than the previous parity candidate for its core intent: exact cookie management, safe protection, faithful import/export, site-specific search, settings integrity, and popup LocalStorage actions. Chromium is covered by a broad reproducible release gate. Firefox has a valid, lint-clean, runtime-smoked artifact, with deeper cross-browser and private-store automation still explicitly open.
diff --git a/qa/chromium-qa.mjs b/qa/chromium-qa.mjs
index e5ab5b8..d9eb2c2 100644
--- a/qa/chromium-qa.mjs
+++ b/qa/chromium-qa.mjs
@@ -2,6 +2,7 @@ import path from 'node:path';
import process from 'node:process';
import fs from 'node:fs';
import os from 'node:os';
+import {spawn, spawnSync} from 'node:child_process';
import {fileURLToPath} from 'node:url';
import {chromium} from 'playwright';
@@ -10,7 +11,7 @@ const __dirname = path.dirname(__filename);
const workspaceRoot = path.resolve(__dirname, '..');
const extensionPath = path.join(workspaceRoot, 'build');
const fixtureUrl = 'http://lvh.me:4173/';
-const importFixturePath = path.join(workspaceRoot, 'qa', 'tmp-import-cookie.json');
+const fixtureHealthUrl = 'http://127.0.0.1:4173/api/request-info';
function assert(condition, message) {
if (!condition)
@@ -46,6 +47,121 @@ async function waitForCookieValue(page, cookieName, timeoutMs = 3000) {
return null;
}
+async function getExtensionCookies(extensionPage, details) {
+ return extensionPage.evaluate(async (query) => browser.cookies.getAll(query), details);
+}
+
+async function waitForExtensionCookie(extensionPage, details, predicate, timeoutMs = 4000) {
+ const start = Date.now();
+ while ((Date.now() - start) < timeoutMs) {
+ const cookies = await getExtensionCookies(extensionPage, details);
+ const match = cookies.find(predicate);
+ if (match)
+ return match;
+ await extensionPage.waitForTimeout(100);
+ }
+ return null;
+}
+
+async function fixtureIsReady() {
+ try {
+ const response = await fetch(fixtureHealthUrl);
+ return response.ok;
+ } catch (error) {
+ return false;
+ }
+}
+
+async function ensureFixtureServer() {
+ if (await fixtureIsReady())
+ return null;
+
+ const certPath = path.join(workspaceRoot, 'qa', 'certs', 'fixture-cert.pem');
+ const keyPath = path.join(workspaceRoot, 'qa', 'certs', 'fixture-key.pem');
+ if (!fs.existsSync(certPath) || !fs.existsSync(keyPath)) {
+ const generated = spawnSync('bash', [path.join(workspaceRoot, 'qa', 'generate-dev-cert.sh')], {
+ cwd: workspaceRoot,
+ stdio: 'inherit',
+ });
+ if (generated.status !== 0)
+ throw new Error('Unable to generate the QA fixture certificate.');
+ }
+
+ const child = spawn(process.execPath, [path.join(workspaceRoot, 'qa', 'fixture-server.mjs')], {
+ cwd: workspaceRoot,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ });
+ let diagnostics = '';
+ child.stdout.on('data', (chunk) => { diagnostics += chunk; });
+ child.stderr.on('data', (chunk) => { diagnostics += chunk; });
+ for (let attempt = 0; attempt < 100; attempt++) {
+ if (await fixtureIsReady())
+ return child;
+ if (child.exitCode !== null)
+ throw new Error(`Fixture server exited early: ${diagnostics.trim()}`);
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ }
+ child.kill('SIGTERM');
+ throw new Error(`Fixture server did not become ready: ${diagnostics.trim()}`);
+}
+
+async function stopFixtureServer(child) {
+ if (!child || child.exitCode !== null)
+ return;
+ child.kill('SIGTERM');
+ await Promise.race([
+ new Promise((resolve) => child.once('exit', resolve)),
+ new Promise((resolve) => setTimeout(resolve, 2000)),
+ ]);
+ if (child.exitCode === null)
+ child.kill('SIGKILL');
+}
+
+async function closeModal(page, selector) {
+ const modal = page.locator(selector);
+ await modal.waitFor({state: 'visible'});
+ await page.evaluate((modalSelector) => {
+ window.__cqmModalHidden = new Promise((resolve) => {
+ window.jQuery(modalSelector).one('hidden.bs.modal', resolve);
+ });
+ }, selector);
+ await modal.locator('.btn').last().click();
+ await page.evaluate(() => window.__cqmModalHidden);
+ await modal.waitFor({state: 'hidden'});
+}
+
+async function closeModalIfVisible(page, selector) {
+ try {
+ await page.locator(selector).waitFor({state: 'visible', timeout: 1500});
+ } catch (error) {
+ return;
+ }
+ await closeModal(page, selector);
+}
+
+async function waitForSelectedProtection(page, expected) {
+ await page.waitForFunction(async (isExpected) => {
+ const cookie = window.jQuery('#cookie-list li.active').data('cookie');
+ if (!cookie)
+ return false;
+ const protectedCookies = await window.vAPI.get_protected_cookies();
+ return window.CQMCore.isCookieProtected(cookie, protectedCookies) === isExpected;
+ }, expected);
+ await page.locator('#protect_button').waitFor({state: 'visible'});
+ await page.waitForFunction(() => !document.querySelector('#protect_button').disabled);
+}
+
+async function submitImport(page, file) {
+ const marker = `qa-import-pending-${Date.now()}-${Math.random()}`;
+ await page.evaluate((value) => {
+ document.getElementById('info_text').textContent = value;
+ }, marker);
+ await page.locator('#file_elem').setInputFiles(file);
+ await page.waitForFunction((value) =>
+ document.getElementById('info_text').textContent !== value, marker);
+ return page.locator('#info_text').textContent();
+}
+
async function clickDomainAndCookie(managerPage, domainText, cookieName) {
const clickedDomain = await managerPage.evaluate((targetDomainText) => {
const items = [...document.querySelectorAll('#domain-list li')];
@@ -76,21 +192,43 @@ async function clickDomainAndCookie(managerPage, domainText, cookieName) {
}
async function run() {
- const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cqm-playwright-'));
- const context = await chromium.launchPersistentContext(userDataDir, {
- headless: true,
- channel: 'chromium',
- ignoreHTTPSErrors: true,
- args: [
- `--disable-extensions-except=${extensionPath}`,
- `--load-extension=${extensionPath}`,
- ],
- });
-
- const extensionId = await getExtensionId(context);
const results = [];
-
+ let fixtureProcess = null;
+ let userDataDir = null;
+ let context = null;
try {
+ fixtureProcess = await ensureFixtureServer();
+ userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cqm-playwright-'));
+ context = await chromium.launchPersistentContext(userDataDir, {
+ headless: true,
+ channel: 'chromium',
+ ignoreHTTPSErrors: true,
+ args: [
+ `--disable-extensions-except=${extensionPath}`,
+ `--load-extension=${extensionPath}`,
+ '--host-resolver-rules=MAP lvh.me 127.0.0.1, MAP *.lvh.me 127.0.0.1',
+ ],
+ });
+
+ const extensionId = await getExtensionId(context);
+ const pageErrors = [];
+ const runtimeErrors = [];
+ const attachServiceWorkerDiagnostics = (worker) => {
+ worker.on('console', (message) => {
+ if (message.type() === 'error')
+ runtimeErrors.push(`service worker: ${message.text()}`);
+ });
+ };
+ context.serviceWorkers().forEach(attachServiceWorkerDiagnostics);
+ context.on('serviceworker', attachServiceWorkerDiagnostics);
+ context.on('page', (page) => {
+ page.on('pageerror', (error) => pageErrors.push(`${page.url()}: ${error.message}`));
+ page.on('console', (message) => {
+ if (message.type() === 'error' && page.url().startsWith(`chrome-extension://${extensionId}/`))
+ runtimeErrors.push(`${page.url()}: ${message.text()}`);
+ });
+ });
+
const fixturePage = await context.newPage();
await fixturePage.goto(fixtureUrl, {waitUntil: 'domcontentloaded'});
@@ -139,11 +277,13 @@ async function run() {
status: 'passed',
details: autoRefreshCookies,
});
- await managerPage.locator('#auto_actualize_checkbox').click();
await fixturePage.evaluate(() => {
document.cookie = 'fixture_auto_refresh=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT';
});
- await managerPage.waitForTimeout(500);
+ await managerPage.waitForFunction(() =>
+ ![...document.querySelectorAll('#cookie-list li')].some((node) =>
+ node.textContent.includes('fixture_auto_refresh')));
+ await managerPage.locator('#auto_actualize_checkbox').click();
await managerPage.locator('#query-subdomains').check();
await managerPage.locator('#actualize_button').click();
@@ -184,6 +324,7 @@ async function run() {
await clickDomainAndCookie(managerPage, 'lvh.me', 'fixture_js_host');
await managerPage.locator('#protect_button').click();
+ await waitForSelectedProtection(managerPage, true);
await fixturePage.bringToFront();
await fixturePage.getByRole('button', {name: 'Delete host cookie via JS'}).click();
@@ -216,48 +357,18 @@ async function run() {
await managerPage.locator('#modal_clipboard').waitFor();
const exportedCookieDump = await managerPage.locator('#clipboard_textarea').inputValue();
assert(exportedCookieDump.includes('fixture_js_host'), 'Single-cookie export did not include the selected fixture cookie.');
- fs.writeFileSync(importFixturePath, exportedCookieDump, 'utf8');
- await managerPage.locator('#modal_clipboard .btn').last().click();
+ assert(/1/.test(await managerPage.locator('#modal_clipboard h4.modal-title').textContent()),
+ 'Clipboard export title omitted its cookie count.');
+ await closeModal(managerPage, '#modal_clipboard');
results.push({
check: 'export-cookie-json',
status: 'passed',
details: exportedCookieDump.slice(0, 120),
});
- const directImportAttempt = await managerPage.evaluate(async (jsonDump) => {
- const [jsonCookie] = JSON.parse(jsonDump);
- const params = {
- url: jsonCookie["Host raw"],
- name: jsonCookie["Name raw"],
- value: jsonCookie["Content raw"],
- path: jsonCookie["Path raw"],
- httpOnly: (jsonCookie["HTTP only raw"] === 'true'),
- secure: (jsonCookie["Send for raw"] === 'true'),
- storeId: jsonCookie["Store raw"],
- };
-
- if (jsonCookie["SameSite raw"] !== undefined)
- params.sameSite = jsonCookie["SameSite raw"];
-
- if (jsonCookie["Expires raw"] != "0")
- params.expirationDate = parseInt(jsonCookie["Expires raw"], 10);
-
- try {
- const cookie = await browser.cookies.set(params);
- return {ok: true, params, cookie};
- } catch (error) {
- return {ok: false, params, message: error.message};
- }
- }, exportedCookieDump);
- assert(directImportAttempt.ok, `Direct cookie restore failed with params ${JSON.stringify(directImportAttempt.params)}: ${directImportAttempt.message}`);
- results.push({
- check: 'direct-json-restore',
- status: 'passed',
- details: directImportAttempt.params,
- });
-
await clickDomainAndCookie(managerPage, 'lvh.me', 'fixture_js_host');
await managerPage.locator('#protect_button').click();
+ await waitForSelectedProtection(managerPage, false);
await managerPage.locator('#delete_button').click();
await fixturePage.waitForTimeout(500);
const deletedValue = await waitForCookieValue(fixturePage, 'fixture_js_host', 1000);
@@ -269,7 +380,7 @@ async function run() {
});
await managerPage.locator('.dropup-custom .dropdown-toggle').nth(1).click();
- await managerPage.locator('#file_elem').setInputFiles({
+ await submitImport(managerPage, {
name: 'fixture-cookie.json',
mimeType: 'application/json',
buffer: Buffer.from(exportedCookieDump, 'utf8'),
@@ -285,6 +396,604 @@ async function run() {
details: importedValue,
});
+ await closeModalIfVisible(managerPage, '#modal_info');
+
+ // Domain-scoped cookies were the largest blind spot in the original
+ // parity suite. Exercise edit, exact protection, and attribute restore.
+ await managerPage.locator('#search_domain').fill('lvh.me');
+ await managerPage.locator('#actualize_button').click();
+ await managerPage.waitForTimeout(300);
+ await clickDomainAndCookie(managerPage, '.lvh.me', 'fixture_js_domain');
+ await managerPage.locator('#edit_button').click();
+ await managerPage.locator('#value').fill('domain-edited-by-qa');
+ await managerPage.locator('#save_button').click();
+ const editedDomainCookie = await waitForExtensionCookie(
+ managerPage,
+ {name: 'fixture_js_domain'},
+ (cookie) => cookie.value === 'domain-edited-by-qa',
+ );
+ assert(editedDomainCookie?.domain === '.lvh.me' && editedDomainCookie.hostOnly === false,
+ `Domain-cookie edit lost scope or failed: ${JSON.stringify(editedDomainCookie)}`);
+ results.push({
+ check: 'edit-domain-cookie',
+ status: 'passed',
+ details: {domain: editedDomainCookie.domain, hostOnly: editedDomainCookie.hostOnly},
+ });
+
+ const protectedDomainCookie = await managerPage.evaluate(async () => browser.cookies.set({
+ url: 'https://lvh.me/',
+ domain: 'lvh.me',
+ name: 'fixture_protected_domain',
+ value: 'protected-domain-value',
+ path: '/',
+ secure: true,
+ httpOnly: true,
+ sameSite: 'strict',
+ expirationDate: Math.floor(Date.now() / 1000) + 3600,
+ storeId: '0',
+ }));
+ assert(protectedDomainCookie, 'Unable to seed protected domain cookie.');
+ await managerPage.evaluate(async (cookie) => vAPI.set_cookie_protection([cookie], true), protectedDomainCookie);
+ await managerPage.evaluate(async (cookie) => browser.cookies.remove({
+ url: 'https://lvh.me/',
+ name: cookie.name,
+ storeId: cookie.storeId,
+ }), protectedDomainCookie);
+ const restoredDomainCookie = await waitForExtensionCookie(
+ managerPage,
+ {name: protectedDomainCookie.name},
+ (cookie) => cookie.value === protectedDomainCookie.value,
+ );
+ assert(restoredDomainCookie, 'Protected domain cookie was not restored.');
+ for (const field of ['domain', 'hostOnly', 'path', 'name', 'value', 'secure', 'httpOnly', 'sameSite', 'session', 'storeId'])
+ assert(restoredDomainCookie[field] === protectedDomainCookie[field],
+ `Protected domain cookie changed ${field}: ${protectedDomainCookie[field]} -> ${restoredDomainCookie[field]}`);
+ results.push({
+ check: 'protected-domain-attribute-restore',
+ status: 'passed',
+ details: {
+ domain: restoredDomainCookie.domain,
+ sameSite: restoredDomainCookie.sameSite,
+ httpOnly: restoredDomainCookie.httpOnly,
+ },
+ });
+ await managerPage.evaluate(async (cookie) => {
+ await vAPI.set_cookie_protection([cookie], false);
+ await browser.cookies.remove({url: 'https://lvh.me/', name: cookie.name, storeId: cookie.storeId});
+ }, restoredDomainCookie);
+
+ const rotatingCookie = await managerPage.evaluate(async () => browser.cookies.set({
+ url: 'http://lvh.me/',
+ name: 'fixture_protected_rotation',
+ value: 'old',
+ path: '/',
+ secure: false,
+ httpOnly: false,
+ sameSite: 'strict',
+ storeId: '0',
+ }));
+ await managerPage.evaluate(async (cookie) => {
+ await vAPI.set_cookie_protection([cookie], true);
+ await browser.cookies.remove({url: 'http://lvh.me/', name: cookie.name, storeId: cookie.storeId});
+ await browser.cookies.set(CQMCore.buildCookieSetDetails({...cookie, value: 'fresh'}));
+ }, rotatingCookie);
+ await managerPage.waitForTimeout(400);
+ const rotatedCookies = await getExtensionCookies(managerPage, {name: rotatingCookie.name});
+ assert(rotatedCookies.length === 1 && rotatedCookies[0].value === 'fresh',
+ `Stale protection restore overwrote cookie rotation: ${JSON.stringify(rotatedCookies)}`);
+ results.push({check: 'protected-cookie-rotation', status: 'passed', details: 'fresh value retained'});
+ await managerPage.evaluate(async (cookie) => {
+ await vAPI.set_cookie_protection([cookie], false);
+ await browser.cookies.remove({url: 'http://lvh.me/', name: cookie.name, storeId: cookie.storeId});
+ }, rotatedCookies[0]);
+
+ const pathCookies = await managerPage.evaluate(async () => Promise.all([
+ browser.cookies.set({url: 'http://lvh.me/', name: 'fixture_path_identity', value: 'root', path: '/', storeId: '0'}),
+ browser.cookies.set({url: 'http://lvh.me/app', name: 'fixture_path_identity', value: 'app', path: '/app', storeId: '0'}),
+ ]));
+ const pathDeletionResult = await managerPage.evaluate(async (cookies) => {
+ const rootCookie = cookies.find((cookie) => cookie.path === '/');
+ await vAPI.set_cookie_protection([rootCookie], true);
+ const protectedCount = await vAPI.delete_cookies(Promise.resolve(cookies));
+ await new Promise((resolve) => setTimeout(resolve, 250));
+ return {
+ protectedCount,
+ remaining: await browser.cookies.getAll({name: rootCookie.name}),
+ };
+ }, pathCookies);
+ assert(pathDeletionResult.protectedCount === 1 && pathDeletionResult.remaining.length === 1 &&
+ pathDeletionResult.remaining[0].path === '/',
+ `Exact path protection failed: ${JSON.stringify(pathDeletionResult)}`);
+ results.push({
+ check: 'exact-path-protection',
+ status: 'passed',
+ details: {protectedCount: pathDeletionResult.protectedCount, remainingPath: pathDeletionResult.remaining[0].path},
+ });
+ await managerPage.evaluate(async (cookie) => {
+ await vAPI.set_cookie_protection([cookie], false);
+ await vAPI.remove_cookie(cookie);
+ }, pathDeletionResult.remaining[0]);
+
+ const unprotectedPathCookies = await managerPage.evaluate(async () => Promise.all([
+ vAPI.set_cookie({url: 'http://lvh.me/', name: 'fixture_exact_path_delete', value: 'root', path: '/', storeId: '0'}),
+ vAPI.set_cookie({url: 'http://lvh.me/', name: 'fixture_exact_path_delete', value: 'app', path: '/app', storeId: '0'}),
+ vAPI.set_cookie({url: 'http://lvh.me/', name: 'fixture_exact_path_delete', value: 'odd', path: '/literal?#', storeId: '0'}),
+ ]));
+ assert(unprotectedPathCookies.every(Boolean),
+ `Exact set-result resolution failed for a path sibling: ${JSON.stringify(unprotectedPathCookies)}`);
+ await managerPage.evaluate(async (cookie) => vAPI.delete_cookies(Promise.resolve([cookie])),
+ unprotectedPathCookies.find((cookie) => cookie.path === '/app'));
+ let exactPathSurvivors = await getExtensionCookies(managerPage, {name: 'fixture_exact_path_delete'});
+ const oddPathCookie = exactPathSurvivors.find((cookie) => cookie.value === 'odd');
+ assert(exactPathSurvivors.length === 2 && exactPathSurvivors.some((cookie) => cookie.path === '/') && oddPathCookie,
+ `Deleting /app damaged a same-name path sibling: ${JSON.stringify(exactPathSurvivors)}`);
+ await managerPage.evaluate(async (cookie) => vAPI.delete_cookies(Promise.resolve([cookie])), oddPathCookie);
+ exactPathSurvivors = await getExtensionCookies(managerPage, {name: 'fixture_exact_path_delete'});
+ assert(exactPathSurvivors.length === 1 && exactPathSurvivors[0].path === '/',
+ `Deleting an unusual path damaged the root sibling: ${JSON.stringify(exactPathSurvivors)}`);
+ await managerPage.evaluate(async (cookies) => vAPI.delete_cookies(Promise.resolve(cookies)), exactPathSurvivors);
+ results.push({
+ check: 'exact-unprotected-path-deletion',
+ status: 'passed',
+ details: 'root, /app, and /literal?# remained independently addressable',
+ });
+
+ const collisionPage = await context.newPage();
+ await collisionPage.goto('http://sub.lvh.me:4173/', {waitUntil: 'domcontentloaded'});
+ const collisionCookies = await managerPage.evaluate(async () => {
+ const name = 'fixture_host_domain_collision';
+ await browser.cookies.set({
+ url: 'http://sub.lvh.me/', domain: 'lvh.me', name, value: 'domain', path: '/', storeId: '0',
+ });
+ await browser.cookies.set({
+ url: 'http://sub.lvh.me/', name, value: 'host', path: '/', storeId: '0',
+ });
+ return browser.cookies.getAll({name, storeId: '0'});
+ });
+ const collisionHost = collisionCookies.find((cookie) => cookie.hostOnly && cookie.domain === 'sub.lvh.me');
+ const collisionDomain = collisionCookies.find((cookie) => !cookie.hostOnly && cookie.domain === '.lvh.me');
+ assert(collisionHost && collisionDomain, `Unable to create host/domain collision: ${JSON.stringify(collisionCookies)}`);
+ await managerPage.evaluate(async (cookie) => vAPI.set_cookie_protection([cookie], true), collisionHost);
+ await collisionPage.evaluate((name) => {
+ document.cookie = `${name}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;
+ }, collisionHost.name);
+ const restoredCollisionHost = await waitForExtensionCookie(
+ managerPage,
+ {name: collisionHost.name, storeId: '0'},
+ (cookie) => cookie.hostOnly && cookie.domain === 'sub.lvh.me' && cookie.value === 'host',
+ );
+ const afterCollisionRestore = await getExtensionCookies(managerPage, {name: collisionHost.name, storeId: '0'});
+ assert(restoredCollisionHost && afterCollisionRestore.some((cookie) => cookie.domain === '.lvh.me'),
+ `Host restore confused a surviving domain sibling: ${JSON.stringify(afterCollisionRestore)}`);
+ await managerPage.evaluate(async (cookie) => {
+ await vAPI.set_cookie_protection([cookie], false);
+ await vAPI.delete_cookies(Promise.resolve([cookie]));
+ }, restoredCollisionHost);
+ const afterExactHostDelete = await getExtensionCookies(managerPage, {name: collisionHost.name, storeId: '0'});
+ assert(afterExactHostDelete.length === 1 && afterExactHostDelete[0].domain === '.lvh.me',
+ `Exact host deletion damaged its domain sibling: ${JSON.stringify(afterExactHostDelete)}`);
+ await managerPage.evaluate(async (cookies) => vAPI.delete_cookies(Promise.resolve(cookies)), afterExactHostDelete);
+ await collisionPage.close();
+ results.push({
+ check: 'host-domain-collision-restore-delete',
+ status: 'passed',
+ details: 'host-only and domain identities stayed independent',
+ });
+
+ const specialValue = 'quote-" backslash-\\ token-{EXPIRES} unicode-雪';
+ await managerPage.evaluate(async ({value}) => browser.cookies.set({
+ url: 'http://lvh.me/',
+ domain: 'lvh.me',
+ name: 'fixture_json_domain_special',
+ value,
+ path: '/',
+ sameSite: 'lax',
+ storeId: '0',
+ }), {value: specialValue});
+ await managerPage.locator('#actualize_button').click();
+ await managerPage.waitForTimeout(300);
+ await clickDomainAndCookie(managerPage, '.lvh.me', 'fixture_json_domain_special');
+
+ // Delete in a focused editor must edit text, never delete the cookie.
+ await managerPage.locator('#value').focus();
+ await managerPage.keyboard.press('Delete');
+ assert((await getExtensionCookies(managerPage, {name: 'fixture_json_domain_special'})).length === 1,
+ 'Delete key in the value editor deleted the selected cookie.');
+ results.push({check: 'editor-delete-key-safety', status: 'passed', details: 'cookie retained'});
+
+ await managerPage.evaluate(async () => Promise.all([
+ browser.cookies.set({url: 'http://lvh.me/', name: 'fixture_visible_delete_target', value: 'delete', path: '/'}),
+ browser.cookies.set({url: 'http://lvh.me/', name: 'fixture_visible_delete_survivor', value: 'keep', path: '/'}),
+ ]));
+ await managerPage.locator('#search_domain').fill('lvh.me :name:"fixture_visible_delete_target"');
+ await managerPage.locator('#actualize_button').click();
+ await managerPage.waitForTimeout(300);
+ assert((await managerPage.locator('#cookie-list li').count()) === 1,
+ 'Visible-delete fixture did not isolate one search result.');
+ await managerPage.locator('#ask_total_deletion_button').click();
+ await managerPage.locator('#modal_alert').waitFor({state: 'visible'});
+ await managerPage.locator('#delete_all_button').click();
+ await managerPage.waitForTimeout(300);
+ assert((await getExtensionCookies(managerPage, {name: 'fixture_visible_delete_target'})).length === 0,
+ 'Visible target cookie was not deleted.');
+ assert((await getExtensionCookies(managerPage, {name: 'fixture_visible_delete_survivor'})).length === 1,
+ 'Filtered bulk deletion removed a non-visible cookie.');
+ results.push({check: 'delete-visible-filtered-only', status: 'passed', details: 'unrelated cookie survived'});
+ await managerPage.evaluate(async () => browser.cookies.remove({
+ url: 'http://lvh.me/', name: 'fixture_visible_delete_survivor',
+ }));
+ await managerPage.locator('#search_domain').fill('lvh.me');
+ await managerPage.locator('#actualize_button').click();
+ await managerPage.waitForTimeout(300);
+ await clickDomainAndCookie(managerPage, '.lvh.me', 'fixture_json_domain_special');
+
+ await managerPage.locator('.dropup-custom .dropdown-toggle').nth(1).click();
+ await managerPage.locator('#clipboard_cookie_export').click();
+ const domainExport = await managerPage.locator('#clipboard_textarea').inputValue();
+ const [domainExportRecord] = JSON.parse(domainExport);
+ assert(domainExportRecord['Host raw'] === 'http://lvh.me/',
+ `Domain export contains malformed URL: ${domainExportRecord['Host raw']}`);
+ assert(domainExportRecord['Content raw'] === specialValue,
+ 'Structured JSON export corrupted special characters or template tokens.');
+ await closeModal(managerPage, '#modal_clipboard');
+ const specialCookieBeforeImport = (await getExtensionCookies(
+ managerPage, {name: 'fixture_json_domain_special'}))[0];
+ await managerPage.evaluate(async (cookie) => vAPI.delete_cookies(Promise.resolve([cookie])), specialCookieBeforeImport);
+ assert((await getExtensionCookies(managerPage, {name: 'fixture_json_domain_special'})).length === 0,
+ 'Domain JSON fixture was not removed before import.');
+ await submitImport(managerPage, {
+ name: 'domain-special.json',
+ mimeType: '',
+ buffer: Buffer.from(domainExport, 'utf8'),
+ });
+ const restoredSpecialCookie = await waitForExtensionCookie(
+ managerPage,
+ {name: 'fixture_json_domain_special'},
+ (cookie) => cookie.value === specialValue,
+ );
+ assert(restoredSpecialCookie?.domain === '.lvh.me' && restoredSpecialCookie.hostOnly === false,
+ `Domain JSON round-trip failed: ${JSON.stringify(restoredSpecialCookie)}`);
+ results.push({
+ check: 'domain-json-roundtrip-special-characters',
+ status: 'passed',
+ details: {domain: restoredSpecialCookie.domain, value: restoredSpecialCookie.value},
+ });
+ await closeModalIfVisible(managerPage, '#modal_info');
+
+ const invalidRecord = {...domainExportRecord, 'Name raw': 'fixture_partial_import_must_not_exist'};
+ const invalidImportInfo = await submitImport(managerPage, {
+ name: 'invalid-partial.json',
+ mimeType: 'application/json',
+ buffer: Buffer.from(JSON.stringify([invalidRecord, null]), 'utf8'),
+ });
+ assert(invalidImportInfo.length > 0, 'Invalid import did not report an error.');
+ assert((await getExtensionCookies(managerPage, {name: invalidRecord['Name raw']})).length === 0,
+ 'Invalid partial import mutated cookies before full validation.');
+ assert(!(await managerPage.locator('#info_text').innerHTML()).includes('
+
+
@@ -64,6 +65,13 @@
right: 0px;
top: 0px;
}
+ .partition-badge {
+ float: right;
+ margin-right: 4px;
+ max-width: 45%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
.cookie-expired, .cookie-expired.active, .cookie-expired.active:hover, .button-error {
color: red;
}
@@ -395,6 +403,12 @@ Details
+