diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e5420fa --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,50 @@ +name: CI + +on: + push: + branches: ['**'] + pull_request: + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run lint + - run: npm run test:unit + - run: npm run build + + chrome: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npx playwright install --with-deps chromium + - run: npm run test:chrome + + firefox: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - name: Firefox and geckodriver versions + run: | + firefox --version || true + geckodriver --version || true + - run: npm run test:firefox diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5a208a3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,34 @@ +name: Release + +# Push a tag equal to the manifest version, e.g.: git tag v1.1 && git push origin v1.1 +on: + push: + tags: ['v*'] + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - name: Check that tag matches manifest version + run: | + VERSION=$(node -p "require('./manifest.json').version") + echo "manifest version: $VERSION, tag: $GITHUB_REF_NAME" + if [ "v$VERSION" != "$GITHUB_REF_NAME" ]; then + echo "::error::Tag $GITHUB_REF_NAME does not match manifest version $VERSION" + exit 1 + fi + - run: npm run lint + - run: npm run build + - name: Create GitHub release with the package + env: + GH_TOKEN: ${{ github.token }} + run: gh release create "$GITHUB_REF_NAME" web-ext-artifacts/*.zip --title "$GITHUB_REF_NAME" --generate-notes diff --git a/.gitignore b/.gitignore index 3065118..f597b8d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # intellij .idea/ +.vscode/ # packed extendsion *.zip @@ -7,3 +8,8 @@ # shells *.sh + +# node +node_modules/ +web-ext-artifacts/ +test-results/ diff --git a/README.md b/README.md index 97c82a4..a3fb61b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,116 @@ -# _NCC (bitrix cookie) -Firefox addon for setting _NCC cookie +# _NCC cookie for Bitrix (BITRIX_SM_NCC) +Browser extension that turns off the 1C-Bitrix composite cache for you: it sets the `_NCC` cookie +(`BITRIX_SM_NCC=Y`) for the current site. + +[![Firefox Add-on](https://img.shields.io/amo/v/_ncc-cookie-for-bitrix?logo=firefox&label=Firefox%20Add-on)](https://addons.mozilla.org/firefox/addon/_ncc-cookie-for-bitrix/) +[![Firefox users](https://img.shields.io/amo/users/_ncc-cookie-for-bitrix?logo=firefox&label=users)](https://addons.mozilla.org/firefox/addon/_ncc-cookie-for-bitrix/) + +**Install:** [Firefox Add-ons (AMO)](https://addons.mozilla.org/firefox/addon/_ncc-cookie-for-bitrix/) (desktop and Android) · Chrome Web Store — coming soon + +Works in **Firefox 140+**, **Firefox for Android 142+** and **Chrome / Chromium 121+** (one Manifest V3 codebase). +On Android there are no keyboard shortcuts (the browser has no commands API); every browser API is +checked before use, so missing APIs only switch off the related feature. + +Interface languages: English, Russian, Kazakh (follows the browser language). + +## How it works in Bitrix + +Composite site settings are stored in `/bitrix/html_pages/.config.php`. The cookie name is +`COOKIE_NCC` there: the cookie prefix of the main module (option "cookie_name", `BITRIX_SM` by default) +plus `_NCC` (`Bitrix\Main\Composite\Helper::getOptions()`). + +- A request with the cookie `_NCC=Y` (exactly `Y`) is not served from the composite cache and + the page is not saved to it (`Bitrix\Main\Composite\Responder`): you see the page generated by PHP, + other visitors still get the cache. +- A request with the `ncc` parameter (`?ncc=1`) is not served from the cache too. +- Bitrix manages this cookie itself (`Bitrix\Main\Composite\Engine::onEpilog()`): sets it for authorized + users who must not get the cache and **deletes it for anonymous visitors** on every page. + So the extension keeps the cookie you turned on: when the site deletes it, the cookie is set again. + A cookie turned off in the extension is not restored. +- If nginx serves the composite cache directly (without PHP), its config may not check the cookie. + Then use the **"Open without composite (?ncc=1)"** button in the popup. + +![Cookie name in .config.php](screens/get_cookie_name.jpg) + +## Popup + +- switch "Disable composite cache" — sets / removes the cookie for the current site, the toolbar icon + changes its color and gets the `N` badge; +- "Set cookie for" — the current host only or a parent domain with all its subdomains + (e.g. `.example.com` for `www.example.com` and `shop.example.com`), remembered per host; +- click the cookie name — own cookie name for the current site (for a site with another cookie prefix); + an enabled cookie is moved to the new name; other sites keep the name from the settings; +- "Open without composite (?ncc=1)" — reloads the current page with `?ncc=1`. + +![Composite is on](screens/composite-on.jpg) +![Composite is off](screens/composite-off.jpg) + +## Keyboard shortcuts + +- `Alt+Shift+Y` - toggle the cookie +- open popup - no default shortcut + +Firefox: `about:addons` → gear → "Manage Extension Shortcuts". +Chrome: `chrome://extensions/shortcuts`. + +## Options + +- cookie name (`BITRIX_SM_NCC` by default); the value is always `Y`, Bitrix checks it strictly; +- icon colors for "on" and "off" states. + +After renaming the cookie in options, cookies with the old name are removed from all sites. +Settings of version 0.1 are kept (the same storage keys); the old placeholder `?????_NCC` means the default name. + +## Installation from sources + +Firefox: `about:debugging#/runtime/this-firefox` → "Load Temporary Add-on…" → select `manifest.json`. +Firefox may treat host permissions as optional: if the popup shows "Grant access", click it. + +Chrome: `chrome://extensions` → enable "Developer mode" → "Load unpacked" → select the repository folder. + +Package: `npm install && npm run build` → `web-ext-artifacts/*.zip` (works for both browsers). + +## Development + +```sh +npm install +npm run lint # web-ext lint +npm run test:unit # no browser: API helpers, Firefox for Android-like environment, cookie restore +npm run test:chrome # Playwright, Chromium (CHROMIUM_PATH can point to a custom binary) +npm run test:firefox # Selenium + geckodriver, needs Firefox + # (FIREFOX_PATH / GECKODRIVER_PATH can point to custom binaries) +``` + +The test server deletes the cookie the same way as Bitrix (`/epilog` responds with +`Set-Cookie: BITRIX_SM_NCC=deleted; expires=...1970...`). + +GitHub Actions run lint and both browser tests on every push and pull request. + +## Release + +Bump `version` in `manifest.json`, merge to `master`, then push a tag with the same version: + +```sh +git tag v1.0 && git push origin v1.0 +``` + +The Release workflow checks the version, builds the zip and creates a GitHub release with it. +Store listing texts, privacy policy and images are in [`store/`](store/README.md). + +## Debug + +All scripts log with the `[NCC]` prefix (enable "Debug"/"Verbose" level in the console): + +- Firefox: `about:debugging#/runtime/this-firefox` → this addon → "Inspect"; +- Chrome: `chrome://extensions` → this addon → "service worker". + +Popup can be opened in a regular tab for a given tab: `popup.html?tabId=N`. + +## Files + +- `common.js` - shared code (cookie, settings, keeping the cookie), used by all pages; +- `background.js` - icon, badge, shortcut, sync with cookie/settings changes, restoring the cookie; +- `popup.html/js/css` - toolbar popup; +- `options.html/js/css` - settings page; +- `_locales/` - translations (en, ru, kk); +- `tests/` - browser tests, `store/` - store listing materials, `screens/` - images for this README. diff --git a/_locales/en/messages.json b/_locales/en/messages.json new file mode 100644 index 0000000..dc793bc --- /dev/null +++ b/_locales/en/messages.json @@ -0,0 +1,105 @@ +{ + "popupUnsupported": { + "message": "Cookies can't be set on this page." + }, + "needPermission": { + "message": "The addon needs access to sites to manage cookies." + }, + "grantAccess": { + "message": "Grant access" + }, + "setCookieFor": { + "message": "Set cookie for" + }, + "scopeHostOnly": { + "message": "$1 (this host only)" + }, + "scopeWithSubdomains": { + "message": ".$1 (with subdomains)" + }, + "failed": { + "message": "Failed: $1" + }, + "scopeFailed": { + "message": "Failed to set cookie for this domain: $1" + }, + "permissionFailed": { + "message": "Permission request failed: $1" + }, + "settings": { + "message": "Settings" + }, + "setButton": { + "message": "Set" + }, + "forThisSite": { + "message": "for this site" + }, + "optOffColor": { + "message": "Off icon color" + }, + "optOnColor": { + "message": "On icon color" + }, + "optSave": { + "message": "Save" + }, + "optSaved": { + "message": "Options saved successfully." + }, + "optSaveError": { + "message": "There were some errors during save, please try again." + }, + "optInvalidName": { + "message": "Invalid cookie name: \"$1\"." + }, + "langCode": { + "message": "en", + "description": "Language code of this translation for the lang attribute" + }, + "extName": { + "message": "Bitrix _NCC Cookie: Disable Composite Cache" + }, + "extDescription": { + "message": "Toggle the Bitrix _NCC cookie (BITRIX_SM_NCC=Y) to disable the composite cache on the current site while debugging." + }, + "trigger_ncc": { + "message": "Disable composite cache" + }, + "siteCookieName": { + "message": "Cookie name for this site" + }, + "siteCookieNameHint": { + "message": "Empty — the name from the settings. The exact name is \"COOKIE_NCC\" in /bitrix/html_pages/.config.php (cookie prefix + _NCC)." + }, + "resetButton": { + "message": "Reset" + }, + "changeSiteNameTitle": { + "message": "Change the cookie name for this site" + }, + "openWithoutComposite": { + "message": "Open without composite (?ncc=1)" + }, + "openWithoutCompositeHint": { + "message": "For servers where nginx serves the composite cache itself and does not check the cookie." + }, + "titleOff": { + "message": "_NCC cookie: off" + }, + "titleOn": { + "message": "Composite cache disabled ($1)" + }, + "titleUnavailable": { + "message": "_NCC cookie (not available on this page)" + }, + "titleNoPermission": { + "message": "_NCC cookie: click to grant access to sites" + }, + "optCookieName": { + "message": "Cookie name (usually BITRIX_SM_NCC)" + }, + "optCookieNameHint": { + "message": "Cookie prefix of the site + _NCC. The exact name is \"COOKIE_NCC\" in /bitrix/html_pages/.config.php. For a site with another prefix set its own name in the popup (click the cookie name)." + } +} diff --git a/_locales/kk/messages.json b/_locales/kk/messages.json new file mode 100644 index 0000000..4813b8e --- /dev/null +++ b/_locales/kk/messages.json @@ -0,0 +1,105 @@ +{ + "popupUnsupported": { + "message": "Бұл бетте cookie орнату мүмкін емес." + }, + "needPermission": { + "message": "Cookie файлдарын басқару үшін кеңейтуге сайттарға рұқсат қажет." + }, + "grantAccess": { + "message": "Рұқсат беру" + }, + "setCookieFor": { + "message": "Cookie орнату аймағы" + }, + "scopeHostOnly": { + "message": "$1 (тек осы хост)" + }, + "scopeWithSubdomains": { + "message": ".$1 (ішкі домендерімен)" + }, + "failed": { + "message": "Қате: $1" + }, + "scopeFailed": { + "message": "Бұл доменге cookie орнату мүмкін болмады: $1" + }, + "permissionFailed": { + "message": "Рұқсат сұрау сәтсіз аяқталды: $1" + }, + "settings": { + "message": "Баптаулар" + }, + "setButton": { + "message": "Қолдану" + }, + "forThisSite": { + "message": "осы сайт үшін" + }, + "optOffColor": { + "message": "Белгіше түсі: өшірулі" + }, + "optOnColor": { + "message": "Белгіше түсі: қосулы" + }, + "optSave": { + "message": "Сақтау" + }, + "optSaved": { + "message": "Баптаулар сақталды." + }, + "optSaveError": { + "message": "Сақтау кезінде қате орын алды, қайталап көріңіз." + }, + "optInvalidName": { + "message": "Cookie атауы жарамсыз: «$1»." + }, + "langCode": { + "message": "kk", + "description": "Language code of this translation for the lang attribute" + }, + "extName": { + "message": "Bitrix _NCC: композиттік кэшті өшіру" + }, + "extDescription": { + "message": "Жөндеу кезінде ағымдағы сайтта композиттік кэшті өшіру үшін Bitrix _NCC cookie (BITRIX_SM_NCC=Y) қосады." + }, + "trigger_ncc": { + "message": "Композиттік кэшті өшіру" + }, + "siteCookieName": { + "message": "Осы сайтқа арналған cookie атауы" + }, + "siteCookieNameHint": { + "message": "Бос болса — баптаулардағы атау. Нақты атауы — /bitrix/html_pages/.config.php ішіндегі \"COOKIE_NCC\" (cookie префиксі + _NCC)." + }, + "resetButton": { + "message": "Тазарту" + }, + "changeSiteNameTitle": { + "message": "Осы сайттың cookie атауын өзгерту" + }, + "openWithoutComposite": { + "message": "Композитсіз ашу (?ncc=1)" + }, + "openWithoutCompositeHint": { + "message": "Кэшті nginx өзі беретін және cookie тексерілмейтін серверлер үшін." + }, + "titleOff": { + "message": "_NCC cookie: өшірулі" + }, + "titleOn": { + "message": "Композиттік кэш өшірілген ($1)" + }, + "titleUnavailable": { + "message": "_NCC cookie (бұл бетте қолжетімсіз)" + }, + "titleNoPermission": { + "message": "_NCC cookie: сайттарға рұқсат беру үшін басыңыз" + }, + "optCookieName": { + "message": "Cookie атауы (әдетте BITRIX_SM_NCC)" + }, + "optCookieNameHint": { + "message": "Сайттың cookie префиксі + _NCC. Нақты атауы — /bitrix/html_pages/.config.php ішіндегі \"COOKIE_NCC\". Префиксі басқа сайт үшін кеңейту терезесінде өз атауын көрсетіңіз (cookie атауын басыңыз)." + } +} diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json new file mode 100644 index 0000000..de8618b --- /dev/null +++ b/_locales/ru/messages.json @@ -0,0 +1,105 @@ +{ + "popupUnsupported": { + "message": "На этой странице нельзя установить cookie." + }, + "needPermission": { + "message": "Расширению нужен доступ к сайтам, чтобы управлять cookie." + }, + "grantAccess": { + "message": "Дать доступ" + }, + "setCookieFor": { + "message": "Ставить cookie для" + }, + "scopeHostOnly": { + "message": "$1 (только этот хост)" + }, + "scopeWithSubdomains": { + "message": ".$1 (с поддоменами)" + }, + "failed": { + "message": "Ошибка: $1" + }, + "scopeFailed": { + "message": "Не удалось установить cookie для этого домена: $1" + }, + "permissionFailed": { + "message": "Не удалось запросить доступ: $1" + }, + "settings": { + "message": "Настройки" + }, + "setButton": { + "message": "Применить" + }, + "forThisSite": { + "message": "для этого сайта" + }, + "optOffColor": { + "message": "Цвет иконки: выключено" + }, + "optOnColor": { + "message": "Цвет иконки: включено" + }, + "optSave": { + "message": "Сохранить" + }, + "optSaved": { + "message": "Настройки сохранены." + }, + "optSaveError": { + "message": "При сохранении произошла ошибка, попробуйте ещё раз." + }, + "optInvalidName": { + "message": "Недопустимое имя cookie: «$1»." + }, + "langCode": { + "message": "ru", + "description": "Language code of this translation for the lang attribute" + }, + "extName": { + "message": "Bitrix _NCC: отключение композитного кеша" + }, + "extDescription": { + "message": "Включает cookie Bitrix _NCC (BITRIX_SM_NCC=Y), чтобы отключить композитный кеш на текущем сайте при отладке." + }, + "trigger_ncc": { + "message": "Отключить композитный кеш" + }, + "siteCookieName": { + "message": "Имя cookie для этого сайта" + }, + "siteCookieNameHint": { + "message": "Пусто — имя из настроек. Точное имя — \"COOKIE_NCC\" в /bitrix/html_pages/.config.php (префикс cookie + _NCC)." + }, + "resetButton": { + "message": "Сбросить" + }, + "changeSiteNameTitle": { + "message": "Изменить имя cookie для этого сайта" + }, + "openWithoutComposite": { + "message": "Открыть без композита (?ncc=1)" + }, + "openWithoutCompositeHint": { + "message": "Для серверов, где кеш отдаёт nginx и cookie не проверяется." + }, + "titleOff": { + "message": "Cookie _NCC: выключена" + }, + "titleOn": { + "message": "Композитный кеш отключён ($1)" + }, + "titleUnavailable": { + "message": "Cookie _NCC (недоступно на этой странице)" + }, + "titleNoPermission": { + "message": "Cookie _NCC: нажмите, чтобы дать доступ к сайтам" + }, + "optCookieName": { + "message": "Имя cookie (обычно BITRIX_SM_NCC)" + }, + "optCookieNameHint": { + "message": "Префикс cookie сайта + _NCC. Точное имя — \"COOKIE_NCC\" в /bitrix/html_pages/.config.php. Для сайта с другим префиксом задайте своё имя в окне расширения (нажмите на имя cookie)." + } +} diff --git a/background.js b/background.js new file mode 100644 index 0000000..720362b --- /dev/null +++ b/background.js @@ -0,0 +1,226 @@ +// Chrome runs background as a service worker (background.service_worker), shared code is loaded here. +// Firefox runs background.scripts (common.js is listed before this file in manifest). +if (typeof importScripts === 'function' && typeof TRIGGERS === 'undefined') { + importScripts('common.js'); +} + +function getIcons(color) { + return { + "16": "icons/bug-" + color + "-16.png", + "32": "icons/bug-" + color + "-32.png", + "64": "icons/bug-" + color + "-64.png", + "128": "icons/bug-" + color + "-128.png" + }; +} + +// Calls for the same tab are serialized: otherwise a slow call with an old state +// may overwrite the icon after a newer one +const iconQueue = new Map(); + +function updateIcon(tab) { + if (!tab) { + log('updateIcon: no tab'); + return Promise.resolve(); + } + let next = (iconQueue.get(tab.id) || Promise.resolve()) + .then(() => renderIcon(tab)) + .catch(e => logError('updateIcon failed', e)); + iconQueue.set(tab.id, next); + next.then(() => { + if (iconQueue.get(tab.id) === next) { + iconQueue.delete(tab.id); + } + }); + return next; +} + +async function renderIcon(tab) { + if (!tab) { + log('updateIcon: no tab'); + return; + } + + let states = await getStates(tab); + let triggers = await getTriggerSettings(getHost(tab.url)); + let colors = await getColors(); + let enabled = triggers.filter(t => states[t.id]); + let badge = enabled.map(t => t.badge).join(''); + + let title = enabled.length + ? t('titleOn', enabled.map(trigger => trigger.name).join(', ')) + : t('titleOff'); + if (!isSupportedUrl(tab.url)) { + title = t('titleUnavailable'); + } else if (!await hasHostPermission()) { + title = t('titleNoPermission'); + } + + log('updateIcon: tab', tab.id, tab.url, 'enabled:', badge || '-'); + try { + await callApi('action.setIcon', [{ path: getIcons(enabled.length ? colors.on : colors.off), tabId: tab.id }]); + await callApi('action.setTitle', [{ title: title, tabId: tab.id }]); + await callApi('action.setBadgeText', [{ text: badge, tabId: tab.id }]); + await callApi('action.setBadgeBackgroundColor', [{ color: '#c0392b', tabId: tab.id }]); + } catch (e) { + // The tab may be closed between query and update + logError('updateIcon failed', e); + } +} + +// Service worker has no persistent state, so active tabs are always queried +async function updateActiveTabs() { + let tabs = await callApi('tabs.query', [{ active: true }], []); + for (let tab of tabs) { + await updateIcon(tab); + } +} + +// Removes cookies with the old name after it has been changed in options, +// otherwise they stay in the browser and keep the composite cache disabled. +async function removeCookiesByName(name, value) { + let stores = await callApi('cookies.getAllCookieStores', [], []); + for (let store of stores) { + let query = { name: name, storeId: store.id }; + if (IS_FIREFOX) { + query.firstPartyDomain = null; + } + let cookies = []; + try { + cookies = await requireApi('cookies.getAll', [query]); + } catch (e) { + logError('removeCookiesByName: getAll failed for store', store.id, e); + continue; + } + for (let cookie of cookies) { + if (cookie.value != value) { + continue; + } + try { + await removeCookie(cookie); + } catch (e) { + logError('removeCookiesByName: remove failed for', cookie.domain, e); + } + } + } +} + +async function onSettingsChanged(changes, area) { + log('settings changed in', area, changes); + if (area === SETTINGS_AREA) { + let triggers = await getTriggerSettings(); + for (let trigger of TRIGGERS) { + let nameChange = changes[trigger.key]; + let valueChange = changes[trigger.key + '_value']; + if (!nameChange && !valueChange) { + continue; + } + let current = triggers.find(t => t.id === trigger.id); + // Empty/missing values mean defaults + let oldName = nameChange ? nameChange.oldValue : current.globalName; + // Empty or invalid (placeholder of version 0.1) name means the default one was used + if (!oldName || !isValidCookieName(oldName)) { + oldName = trigger.defaultName; + } + let oldValue = (valueChange ? valueChange.oldValue : current.value) || trigger.defaultValue; + if (oldName !== current.globalName || oldValue !== current.value) { + log('trigger', trigger.id, 'changed from', oldName + '=' + oldValue, 'to', current.globalName + '=' + current.value); + await removeCookiesByName(oldName, oldValue); + } + } + } + await updateActiveTabs(); +} + +// Cause of a removal by Set-Cookie with a past date (how Bitrix deletes cookies). +// Chrome: "expired_overwrite". Firefox maps every single removal to "explicit" +// (toolkit/components/extensions/parent/ext-cookies.js, COOKIE_DELETED). +function isServerRemoval(cause) { + return cause === 'expired_overwrite' || (IS_FIREFOX && cause === 'explicit'); +} + +// Updates all tabs the changed cookie belongs to (not only active ones: popup may be open in another tab) +async function onCookieChanged(changeInfo) { + // Global names and per-site names ("name:" in storage.local) + let names = (await getTriggerSettings()).map(t => t.name); + let local = await callApi('storage.local.get', [null], {}); + for (let key of Object.keys(local)) { + if (key.startsWith('name:')) { + names.push(local[key]); + } + } + if (!names.includes(changeInfo.cookie.name)) { + return; + } + // The server removed a cookie the user turned on (Bitrix deletes NCC for anonymous visitors + // and composite groups): set it again. Removals by the extension clear the keep mark first. + if (changeInfo.removed && isServerRemoval(changeInfo.cause) && await isKept(changeInfo.cookie)) { + log('cookie removed by the server, restoring:', changeInfo.cookie.name, changeInfo.cookie.domain); + try { + await restoreCookie(changeInfo.cookie); + } catch (e) { + logError('restore failed', e); + } + } + log('cookie changed:', changeInfo.cookie.name, changeInfo.cookie.domain, 'removed:', changeInfo.removed, 'cause:', changeInfo.cause); + let domain = changeInfo.cookie.domain.replace(/^\./, ''); + let tabs = await callApi('tabs.query', [{}], []); + for (let tab of tabs) { + let host = getHost(tab.url); + if (host === domain || host.endsWith('.' + domain)) { + await updateIcon(tab); + } + } +} + +function onTabUpdated(tabId, changeInfo, tab) { + // React only to navigation, not to title/favicon/audio updates. + // Tab-specific badge is reset by the browser on navigation, so background tabs are updated too. + if (changeInfo.url || changeInfo.status === 'complete') { + updateIcon(tab); + } +} + +async function onTabActivated(activeInfo) { + try { + updateIcon(await requireApi('tabs.get', [activeInfo.tabId])); + } catch (e) { + logError('onTabActivated: tabs.get failed', e); + } +} + +// Keyboard shortcuts toggle triggers without opening the popup +async function onCommand(command) { + let triggerId = command.replace(/^toggle-/, ''); + let tab = await getActiveTab(); + log('command', command, 'tab', tab && tab.url); + try { + await toggleTrigger(tab, triggerId); + } catch (e) { + logError('command', command, 'failed', e); + } + await updateIcon(tab); +} + +// Firefox MV3 treats host permissions as optional: open options with "Grant access" button if needed +async function onInstalled(details) { + log('installed:', details.reason); + // On first install the options page shows the cookie name (and asks for access in Firefox) + if (details.reason === 'install' || !await hasHostPermission()) { + callApi('runtime.openOptionsPage', []); + } + await updateActiveTabs(); +} + +// Listeners must be registered synchronously at top level (service worker requirement). +// Missing events are skipped: e.g. Firefox for Android has no windows and commands (keyboard shortcuts) APIs. +addApiListener('tabs.onUpdated', onTabUpdated); +addApiListener('tabs.onActivated', onTabActivated); +addApiListener('cookies.onChanged', onCookieChanged); +addApiListener('storage.onChanged', onSettingsChanged); +addApiListener('runtime.onInstalled', onInstalled); +addApiListener('windows.onFocusChanged', updateActiveTabs); +addApiListener('commands.onCommand', onCommand); +addApiListener('permissions.onAdded', updateActiveTabs); +addApiListener('permissions.onRemoved', updateActiveTabs); + +updateActiveTabs(); diff --git a/common.js b/common.js new file mode 100644 index 0000000..43bdaba --- /dev/null +++ b/common.js @@ -0,0 +1,433 @@ +// Shared code for background script, popup and options (Firefox + Chrome, Manifest V3). +// Bitrix _NCC cookie extension (the same structure as the Xdebug cookies extension by the same author). +// Firefox exposes `browser`, Chrome exposes `chrome` (both return promises in MV3). +const api = globalThis.browser || globalThis.chrome; +const LOG_PREFIX = '[NCC]'; + +// --- Safe access to WebExtension APIs --- +// Browsers support different sets of APIs (e.g. Firefox for Android has no windows and commands), +// so every API method and event is checked before use. + +// Returns the API object or function by path ('tabs.getCurrent'), undefined if the browser has none +function getApi(path) { + let node = api; + for (let part of path.split('.')) { + if (node === undefined || node === null) { + return undefined; + } + node = node[part]; + } + return node; +} + +function hasApi(path) { + return getApi(path) !== undefined; +} + +// Calls an API method if the browser has it, otherwise returns fallback. +// The result is returned as is (usually a promise), so callers can await it in both cases. +function callApi(path, args, fallback) { + let method = getApi(path); + if (typeof method !== 'function') { + log('API is not available:', path); + return fallback; + } + return method.apply(getApi(path.split('.').slice(0, -1).join('.')), args || []); +} + +// The same for APIs the action can't work without: throws an error that is shown to the user +function requireApi(path, args) { + let method = getApi(path); + if (typeof method !== 'function') { + throw new Error('API is not available in this browser: ' + path); + } + return method.apply(getApi(path.split('.').slice(0, -1).join('.')), args || []); +} + +// Subscribes to an API event if the browser has it, returns false otherwise +function addApiListener(path, listener) { + let event = getApi(path); + if (!event || typeof event.addListener !== 'function') { + log('API event is not available:', path); + return false; + } + event.addListener(listener); + return true; +} + +// firstPartyDomain exists only in Firefox, Chrome rejects unknown properties +const IS_FIREFOX = hasApi('runtime.getBrowserInfo'); +// Settings are kept in storage.sync, or in storage.local if the browser has no sync storage +const SETTINGS_AREA = hasApi('storage.sync') ? 'sync' : 'local'; + +// Bitrix Composite: the cookie _NCC=Y ("no composite cache") disables the composite cache. +// Bitrix main module (main/lib/Composite): Helper::getOptions() - COOKIE_NCC = cookie prefix +// (option "cookie_name", default BITRIX_SM) + "_NCC", saved in /bitrix/html_pages/.config.php; +// Responder::isValidRequest() - the cache is not used when $_COOKIE[COOKIE_NCC] === "Y". +// The list form is kept to share the code structure with the Xdebug cookies extension. +const TRIGGERS = [ + { id: 'ncc', label: 'NCC', badge: 'N', key: 'cookie_ncc', defaultName: 'BITRIX_SM_NCC', defaultValue: 'Y' } +]; +const DEFAULT_ON_COLOR = 'red'; +const DEFAULT_OFF_COLOR = 'light'; +// Must match host_permissions in manifest.json +const SITE_ORIGINS = { origins: ['http://*/*', 'https://*/*'] }; +// Localized string from _locales/*/messages.json, falls back to the key itself +function t(key, ...substitutions) { + let message = callApi('i18n.getMessage', [key, substitutions.map(String)], ''); + if (!message) { + console.warn(LOG_PREFIX, 'i18n: no message for', key); + return key; + } + return message; +} + +// Translates elements with data-i18n (text), data-i18n-placeholder and data-i18n-title attributes +function localizePage(root) { + if (!root) { + root = document; + // Language of the translation actually used (stored in messages.json) + document.documentElement.lang = t('langCode'); + } + root.querySelectorAll('[data-i18n]').forEach(el => { el.textContent = t(el.dataset.i18n); }); + root.querySelectorAll('[data-i18n-placeholder]').forEach(el => { el.placeholder = t(el.dataset.i18nPlaceholder); }); + root.querySelectorAll('[data-i18n-title]').forEach(el => { el.title = t(el.dataset.i18nTitle); }); +} + +// RFC 6265: cookie-name is a token, cookie-value has no spaces, quotes, commas, semicolons, backslashes +function isValidCookieName(name) { + return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name); +} + +function isValidCookieValue(value) { + return /^[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]*$/.test(value); +} + +function log(...args) { + console.debug(LOG_PREFIX, ...args); +} + +function logError(...args) { + console.error(LOG_PREFIX, ...args); +} + +// Cookies can be managed only for regular web pages (not about:, chrome:, file:, moz-extension: etc.) +function isSupportedUrl(url) { + return /^https?:\/\//i.test(url || ''); +} + +async function hasHostPermission() { + try { + // Without the permissions API host permissions are granted at install time + return await callApi('permissions.contains', [SITE_ORIGINS], true); + } catch (e) { + logError('permissions.contains failed', e); + return false; + } +} + +// Must be called from a user action handler (click) +async function requestHostPermission() { + let granted = await requireApi('permissions.request', [SITE_ORIGINS]); + log('host permission granted:', granted); + return granted; +} + +// Returns [{...trigger, name, value, siteName}] with values from settings. +// host - apply the per-site cookie name (sites may use different cookie prefixes), siteName is true then. +async function getTriggerSettings(host) { + let keys = []; + for (let trigger of TRIGGERS) { + keys.push(trigger.key, trigger.key + '_value'); + } + let config = await callApi('storage.' + SETTINGS_AREA + '.get', [keys], {}); + let siteName = host ? await getSiteCookieName(host) : ''; + + return TRIGGERS.map(trigger => { + let name = config[trigger.key] || ''; + // Version 0.1 saved the placeholder "?????_NCC" when the name was not filled in + if (!isValidCookieName(name)) { + if (name) { + log('stored cookie name is not valid, the default is used:', name); + } + name = trigger.defaultName; + } + return Object.assign({}, trigger, { + label: t('trigger_' + trigger.id), + name: siteName || name, + globalName: name, + // Bitrix checks the value strictly: === "Y" + value: trigger.defaultValue, + siteName: !!siteName + }); + }); +} + +// Cookie name for a site, overrides the global one ('' - use global) +async function getSiteCookieName(host) { + let key = 'name:' + host; + let data = await callApi('storage.local.get', [key], {}); + return data[key] || ''; +} + +async function setSiteCookieName(host, name) { + let key = 'name:' + host; + if (name) { + await callApi('storage.local.set', [{ [key]: name }]); + } else { + await callApi('storage.local.remove', [key]); + } + log('cookie name for', host, '=', name || '(global)'); +} + +async function getColors() { + // Storage keys are the same as in version 0.1 + let config = await callApi('storage.' + SETTINGS_AREA + '.get', [['cookie_ncc_on_color', 'cookie_ncc_off_color']], {}); + + return { + on: config.cookie_ncc_on_color || DEFAULT_ON_COLOR, + off: config.cookie_ncc_off_color || DEFAULT_OFF_COLOR + }; +} + +// Firefox: tab.cookieStoreId (containers, private windows). +// Chrome: no cookieStoreId in tabs, find the store that contains the tab ("0" - normal, "1" - incognito). +async function getStoreId(tab) { + if (tab.cookieStoreId) { + return tab.cookieStoreId; + } + let stores = await callApi('cookies.getAllCookieStores', [], []); + for (let store of stores) { + if (store.tabIds && store.tabIds.includes(tab.id)) { + return store.id; + } + } + log('getStoreId: store not found for tab', tab.id, 'using default'); + return undefined; +} + +function getHost(url) { + try { + return new URL(url).hostname; + } catch (e) { + return ''; + } +} + +// Domains the cookie can be set for: the host itself and its parent domains (at least 2 labels). +// Public suffixes (like co.uk) are rejected by the browser itself, the error is shown in popup. +function getDomainCandidates(host) { + if (!host || host.indexOf('.') === -1 || /^[\d.]+$/.test(host) || host.indexOf(':') !== -1 || host.startsWith('[')) { + return []; + } + let labels = host.split('.'); + let result = []; + for (let i = 0; i <= labels.length - 2; i++) { + result.push(labels.slice(i).join('.')); + } + return result; +} + +// Scope is remembered per host: '' - host-only cookie, 'example.com' - cookie for .example.com and subdomains +async function getScope(host) { + let key = 'scope:' + host; + let data = await callApi('storage.local.get', [key], {}); + let scope = data[key] || ''; + if (scope && !getDomainCandidates(host).includes(scope)) { + return ''; + } + return scope; +} + +async function setScope(host, scope) { + let key = 'scope:' + host; + if (scope) { + await callApi('storage.local.set', [{ [key]: scope }]); + } else { + await callApi('storage.local.remove', [key]); + } + log('scope for', host, '=', scope || '(host only)'); +} + +async function getCookie(tab, trigger) { + if (!tab || !isSupportedUrl(tab.url)) { + return null; + } + + let cookie = null; + try { + cookie = await requireApi('cookies.get', [{ + url: tab.url, + name: trigger.name, + storeId: await getStoreId(tab) + }]); + } catch (e) { + logError('getCookie failed for', tab.url, trigger.name, e); + return null; + } + + if (cookie && cookie.value == trigger.value) { + return cookie; + } + if (cookie) { + log('getCookie: cookie', trigger.name, 'has unexpected value', cookie.value, 'expected', trigger.value); + } + + return null; +} + +// Returns {debug: cookie|null, profile: ..., trace: ...} +async function getStates(tab) { + let triggers = await getTriggerSettings(getHost(tab.url)); + let states = {}; + for (let trigger of triggers) { + states[trigger.id] = await getCookie(tab, trigger); + } + return states; +} + +function cookieUrl(cookie) { + return (cookie.secure ? 'https://' : 'http://') + cookie.domain.replace(/^\./, '') + cookie.path; +} + +// --- Keeping the cookie --- +// Bitrix itself deletes the NCC cookie (Engine::onEpilog(): for anonymous visitors and for users +// of composite groups), so a cookie enabled by the user is set again when the server removes it. +// The mark is stored per cookie (store, domain, name) and removed when the user turns it off. +function keepKey(cookie) { + return 'keep:' + cookie.storeId + '|' + cookie.domain + '|' + cookie.name; +} + +async function setKeep(cookie, keep) { + if (keep) { + await callApi('storage.local.set', [{ [keepKey(cookie)]: true }]); + } else { + await callApi('storage.local.remove', [keepKey(cookie)]); + } +} + +async function isKept(cookie) { + let data = await callApi('storage.local.get', [keepKey(cookie)], {}); + return !!data[keepKey(cookie)]; +} + +// Sets the removed cookie again with the same parameters +async function restoreCookie(cookie) { + let details = { + url: cookieUrl(cookie), + name: cookie.name, + value: cookie.value || 'Y', + path: cookie.path, + storeId: cookie.storeId + }; + if (!cookie.hostOnly) { + details.domain = cookie.domain; + } + if (IS_FIREFOX && cookie.firstPartyDomain) { + details.firstPartyDomain = cookie.firstPartyDomain; + } + await requireApi('cookies.set', [details]); + log('restored', cookie.name, 'for', details.url, 'store', cookie.storeId); +} + +// URL of the page with ?ncc=1: Bitrix does not use the composite cache for such requests +// (Responder::isValidRequest()), it works even when nginx serves the cache without checking cookies +function nccUrl(url) { + let result = new URL(url); + result.searchParams.set('ncc', '1'); + return result.toString(); +} + +async function removeCookie(cookie) { + let details = { + url: cookieUrl(cookie), + name: cookie.name, + storeId: cookie.storeId + }; + if (IS_FIREFOX && cookie.firstPartyDomain !== undefined) { + details.firstPartyDomain = cookie.firstPartyDomain; + } + // Turned off by the user (or by the extension), the server's removals are restored + await setKeep(cookie, false); + await requireApi('cookies.remove', [details]); + log('removed', cookie.name, 'for', details.url, 'store', cookie.storeId); +} + +// Removes all cookies with this name that are sent to the tab url (host-only and domain ones) +async function disableTrigger(tab, trigger) { + let query = { + url: tab.url, + name: trigger.name, + storeId: await getStoreId(tab) + }; + if (IS_FIREFOX) { + query.firstPartyDomain = null; + } + let cookies = await requireApi('cookies.getAll', [query]); + for (let cookie of cookies) { + await removeCookie(cookie); + } +} + +async function enableTrigger(tab, trigger) { + let host = getHost(tab.url); + let scope = await getScope(host); + let details = { + url: tab.url, + name: trigger.name, + value: trigger.value, + path: '/', + storeId: await getStoreId(tab) + }; + if (scope) { + details.domain = scope; + } + let cookie = await requireApi('cookies.set', [details]); + if (cookie) { + await setKeep(cookie, true); + } + log('set', trigger.name + '=' + trigger.value, 'for', scope ? '.' + scope : host, 'store', details.storeId); +} + +// Returns new state (true - enabled). Throws on API errors. +async function toggleTrigger(tab, triggerId) { + if (!tab || !isSupportedUrl(tab.url)) { + log('toggleTrigger: skipped, unsupported url', tab && tab.url); + return false; + } + let triggers = await getTriggerSettings(getHost(tab.url)); + let trigger = triggers.find(t => t.id === triggerId); + if (await getCookie(tab, trigger)) { + await disableTrigger(tab, trigger); + return false; + } + await enableTrigger(tab, trigger); + return true; +} + +// Re-sets enabled cookies after scope change. +// triggerIds - triggers to (re)enable, by default the currently enabled ones. +async function reapplyTriggers(tab, triggerIds) { + let triggers = await getTriggerSettings(getHost(tab.url)); + for (let trigger of triggers) { + let enabled = triggerIds ? triggerIds.includes(trigger.id) : !!await getCookie(tab, trigger); + if (enabled) { + await disableTrigger(tab, trigger); + await enableTrigger(tab, trigger); + } + } +} + +async function getActiveTab() { + let tabs = await callApi('tabs.query', [{ active: true, currentWindow: true }], []); + // Firefox for Android shows the popup as a page in its own tab: then the active tab is the + // popup itself, and the site is the most recently used other tab + let ownTab = typeof document !== 'undefined' ? await callApi('tabs.getCurrent', [], undefined) : undefined; + if (ownTab && tabs[0] && tabs[0].id === ownTab.id) { + let others = (await callApi('tabs.query', [{}], [])).filter(tab => tab.id !== ownTab.id); + others.sort((a, b) => (b.lastAccessed || 0) - (a.lastAccessed || 0)); + log('getActiveTab: popup is shown in a tab, using the last used tab', others[0] && others[0].url); + return others[0]; + } + return tabs[0]; +} diff --git a/cookie_ncc.js b/cookie_ncc.js deleted file mode 100644 index b437908..0000000 --- a/cookie_ncc.js +++ /dev/null @@ -1,102 +0,0 @@ -let cookieName = '_NCC'; -let currentTab; - -async function updateIcon() { - let cookie = await getCookie(); - let icons = await getIcons(cookie); - - browser.browserAction.setIcon({ - path: icons, - tabId: currentTab.id - }); - browser.browserAction.setTitle({ - title: cookie ? '_NCC cookie (on)' : '_NCC cookie (off)', - tabId: currentTab.id - }); -} - -async function toggleCookie() { - let cookie = await getCookie(); - let currentUrl = currentTab.url; - let cookieName = await getValueToSet(); - if (cookie) { - browser.cookies.remove({ - url: currentUrl.replace(/:{1}[0-9]{1}\d*/, ''), - name: cookieName, - storeId: currentTab.cookieStoreId - }); - updateIcon(); - return null; - } - browser.cookies.set({ - url: currentUrl.replace(/:{1}[0-9]{1}\d*/, ''), - name: cookieName, - value: "Y", - path: "/", - storeId: currentTab.cookieStoreId - }); - updateIcon(); -} - -async function getIcons(cookie) { - let state = 'cookie_ncc_off_color'; - let stateColor = 'light'; - if (cookie) { - state = 'cookie_ncc_on_color'; - stateColor = 'red'; - } - let config = await browser.storage.sync.get(state); - if (config[state]) { - stateColor = config[state]; - } - - return { - "64": "icons/bug-" + stateColor + "-64.png", - "128": "icons/bug-" + stateColor + "-128.png" - } -} - -async function getValueToSet() { - let config = await browser.storage.sync.get('cookie_ncc'); - - return config.cookie_ncc || '_NCC'; -} - -async function getCookie() { - let currentUrl = currentTab.url; - let cookieName = await getValueToSet(); - - let cookie = await browser.cookies.get({ - url: currentUrl.replace(/:{1}[0-9]{1}\d*/, ''), - name: cookieName, - storeId: currentTab.cookieStoreId - }); - - if (cookie && cookie.value == "Y") { - return cookie; - } - - return null; -} - -async function updateActiveTab() { - function updateTab(tabs) { - if (tabs[0]) { - currentTab = tabs[0]; - updateIcon(); - } - } - - let tabs = await browser.tabs.query({ - active: true, - currentWindow: true - }); - updateTab(tabs); -} - -updateActiveTab(); - -browser.browserAction.onClicked.addListener(toggleCookie); -browser.tabs.onUpdated.addListener(updateActiveTab); -browser.tabs.onActivated.addListener(updateActiveTab); -browser.windows.onFocusChanged.addListener(updateActiveTab); \ No newline at end of file diff --git a/icons/bug-dark-16.png b/icons/bug-dark-16.png new file mode 100644 index 0000000..42b2a56 Binary files /dev/null and b/icons/bug-dark-16.png differ diff --git a/icons/bug-dark-32.png b/icons/bug-dark-32.png new file mode 100644 index 0000000..c6e53f9 Binary files /dev/null and b/icons/bug-dark-32.png differ diff --git a/icons/bug-green-128-padded.png b/icons/bug-green-128-padded.png new file mode 100644 index 0000000..c04fff3 Binary files /dev/null and b/icons/bug-green-128-padded.png differ diff --git a/icons/bug-green-16.png b/icons/bug-green-16.png new file mode 100644 index 0000000..f5021c6 Binary files /dev/null and b/icons/bug-green-16.png differ diff --git a/icons/bug-green-32.png b/icons/bug-green-32.png new file mode 100644 index 0000000..a04efa7 Binary files /dev/null and b/icons/bug-green-32.png differ diff --git a/icons/bug-light-16.png b/icons/bug-light-16.png new file mode 100644 index 0000000..fa0b312 Binary files /dev/null and b/icons/bug-light-16.png differ diff --git a/icons/bug-light-32.png b/icons/bug-light-32.png new file mode 100644 index 0000000..e71eb35 Binary files /dev/null and b/icons/bug-light-32.png differ diff --git a/icons/bug-red-16.png b/icons/bug-red-16.png new file mode 100644 index 0000000..02d8fc3 Binary files /dev/null and b/icons/bug-red-16.png differ diff --git a/icons/bug-red-32.png b/icons/bug-red-32.png new file mode 100644 index 0000000..a93509e Binary files /dev/null and b/icons/bug-red-32.png differ diff --git a/manifest.json b/manifest.json index 41572fc..966ac79 100644 --- a/manifest.json +++ b/manifest.json @@ -1,43 +1,74 @@ { - "manifest_version": 2, - "name": "Manage _NCC Cookie for Bitrix", - "version": "0.1", + "manifest_version": 3, + "name": "__MSG_extName__", + "version": "1.0", + "default_locale": "en", - "description": "Manage cookie that disables Composite technology.", - "homepage_url": "https://github.com/goodprogrammist/bitrix_NCC_cookie", + "description": "__MSG_extDescription__", + "homepage_url": "https://github.com/goodprogrammist/BITRIX_SM_NCC", "icons": { + "16": "icons/bug-green-16.png", + "32": "icons/bug-green-32.png", "64": "icons/bug-green-64.png", - "128": "icons/bug-green-128.png" + "128": "icons/bug-green-128-padded.png" }, "permissions": [ - "tabs", "cookies", - "storage", - "" + "storage" ], - "browser_action": { + "host_permissions": [ + "http://*/*", + "https://*/*" + ], + + "action": { "default_icon": { + "16": "icons/bug-light-16.png", + "32": "icons/bug-light-32.png", "64": "icons/bug-light-64.png", "128": "icons/bug-light-128.png" }, - "default_title": "_NCC (off)" + "default_title": "_NCC", + "default_popup": "popup.html" + }, + + "commands": { + "_execute_action": { + "description": "Open popup" + }, + "toggle-ncc": { + "suggested_key": { + "default": "Alt+Shift+Y" + }, + "description": "Toggle _NCC cookie (disable Bitrix composite cache)" + } }, "options_ui": { "page": "options.html", - "browser_style": true + "open_in_tab": false }, - "applications": { + "browser_specific_settings": { "gecko": { - "id": "ncc_cookie_setup@goodprogrammist.ru" + "id": "ncc_cookie_setup@goodprogrammist.ru", + "strict_min_version": "140.0", + "data_collection_permissions": { + "required": ["none"] + } + }, + "gecko_android": { + "strict_min_version": "142.0" } }, + "minimum_chrome_version": "121", + "background": { - "scripts": ["cookie_ncc.js"] + "service_worker": "background.js", + "scripts": ["common.js", "background.js"] } } diff --git a/options.css b/options.css index 1003166..998229e 100644 --- a/options.css +++ b/options.css @@ -1,7 +1,9 @@ :root { + /* Default colors and form controls follow the browser light/dark theme */ + color-scheme: light dark; --gutter-size: 10px; --input-height: 2rem; - --label-width: 30rem; + --label-width: 26rem; } .options-form, @@ -51,7 +53,7 @@ input[type=radio], .message-container { position: relative; - height: 3.1rem; + min-height: 3.1rem; } .message { @@ -83,3 +85,51 @@ input[type=radio], opacity: 1; z-index: 2; } + +.triggers .form-row + .form-row, +.triggers + .form-row { + margin-top: var(--gutter-size); +} + +.triggers .form-row:has(.trigger-name):not(:first-child) { + margin-top: calc(3 * var(--gutter-size)); +} + +.form-row > input[type=text] { + flex: 1; + min-width: 8rem; +} + +.permission-row { + margin-bottom: 15px; + padding: 10px; + border: 1px solid #c0392b; + border-radius: 3px; +} + +[hidden] { + display: none !important; +} + +.welcome-row { + margin-bottom: 15px; + padding: 10px; + border: 1px solid #16a085; + border-radius: 3px; +} + +.form-row > select { + margin: 0 var(--gutter-size); + height: var(--input-height); + flex: 1; +} + +.form-row > .hint { + margin-left: calc(var(--label-width) + 2 * var(--gutter-size)); + font-size: 0.85rem; + opacity: 0.75; +} + +.invalid { + outline: 2px solid #c0392b; +} diff --git a/options.html b/options.html index 112157d..00957da 100644 --- a/options.html +++ b/options.html @@ -6,13 +6,21 @@ + +
- - + + +
+
+
- +
- +
- +
-
- Options saved successfully. -
+
-
- There ware some errors during save, please try again. -
+
+ + diff --git a/options.js b/options.js index 3b76522..ba25ce4 100644 --- a/options.js +++ b/options.js @@ -1,51 +1,97 @@ -const visiblityClass = 'is-visible'; +const visibilityClass = 'is-visible'; +const OPTIONS_LOG_PREFIX = '[NCC options]'; async function saveOptions(e) { e.preventDefault(); hideMessages(); + document.querySelectorAll(".invalid").forEach(el => el.classList.remove("invalid")); - let saved = browser.storage.sync.set({ - cookie_ncc: document.querySelector(".cookie_ncc").value, - cookie_ncc_on_color: document.querySelector(".cookie-ncc-on-color:checked").value, - cookie_ncc_off_color: document.querySelector(".cookie-ncc-off-color:checked").value - }); - saved.then(showSuccess, showError); + let onColor = document.querySelector(".cookie-ncc-on-color:checked"); + let offColor = document.querySelector(".cookie-ncc-off-color:checked"); + let nameInput = document.querySelector(".cookie_ncc"); + // Storage keys are the same as in version 0.1 + let options = { + cookie_ncc: nameInput.value.trim(), + cookie_ncc_on_color: onColor ? onColor.value : DEFAULT_ON_COLOR, + cookie_ncc_off_color: offColor ? offColor.value : DEFAULT_OFF_COLOR + }; + + let errors = []; + if (options.cookie_ncc && !isValidCookieName(options.cookie_ncc)) { + errors.push(t('optInvalidName', options.cookie_ncc)); + nameInput.classList.add("invalid"); + } + if (errors.length) { + console.debug(OPTIONS_LOG_PREFIX, 'not saved, invalid values:', errors); + showError(errors.join(' ')); + return; + } + console.debug(OPTIONS_LOG_PREFIX, 'saving', options); + + try { + await requireApi('storage.' + SETTINGS_AREA + '.set', [options]); + showSuccess(); + } catch (err) { + showError(err); + } } async function restoreOptions() { - const items = [ - 'cookie_ncc', - 'cookie_ncc_on_color', - 'cookie_ncc_off_color' - ]; - let sessionKey = await browser.storage.sync.get(items); - - document.querySelector(".cookie_ncc").value = sessionKey.cookie_ncc || '?????_NCC'; - - let onColor = sessionKey.cookie_ncc_on_color || 'red'; - let offColor = sessionKey.cookie_ncc_off_color || 'light'; - document.querySelector(".cookie-ncc-on-color[value="+ onColor +"]").checked = true; - document.querySelector(".cookie-ncc-off-color[value="+ offColor +"]").checked = true; + let trigger = (await getTriggerSettings())[0]; + let colors = await getColors(); + console.debug(OPTIONS_LOG_PREFIX, 'restored', trigger, colors); + + // The placeholder "?????_NCC" saved by version 0.1 is shown as the default name + document.querySelector(".cookie_ncc").value = trigger.globalName; + document.querySelector(".cookie-ncc-on-color[value=" + colors.on + "]").checked = true; + document.querySelector(".cookie-ncc-off-color[value=" + colors.off + "]").checked = true; + + await renderPermission(); +} + +async function renderPermission() { + let permitted = await hasHostPermission(); + console.debug(OPTIONS_LOG_PREFIX, 'host permission:', permitted); + document.querySelector(".permission-row").hidden = permitted; +} + +async function onGrantClick() { + try { + await requestHostPermission(); + } catch (err) { + console.error(OPTIONS_LOG_PREFIX, 'permission request failed', err); + } + await renderPermission(); } function showSuccess() { let messageClass = document.querySelector(".saved-successfully").classList; - messageClass.add(visiblityClass); + messageClass.add(visibilityClass); setTimeout(function() { hideMessages(); }, 2000); } -function showError() { - let messageClass = document.querySelector(".saved-error").classList; - messageClass.add('is-visible'); +// message - validation text; without it (storage error) the generic text is shown +function showError(message) { + let element = document.querySelector(".saved-error"); + if (typeof message === 'string') { + element.textContent = message; + } else { + console.error(OPTIONS_LOG_PREFIX, 'save failed', message); + element.textContent = t('optSaveError'); + } + element.classList.add(visibilityClass); } function hideMessages() { - let messages = document.querySelector(".message").classList; - messages.remove(visiblityClass); + document.querySelectorAll(".message").forEach(function(message) { + message.classList.remove(visibilityClass); + }); } +localizePage(); document.addEventListener('DOMContentLoaded', restoreOptions); document.querySelector("form").addEventListener("submit", saveOptions); +document.querySelector(".grant").addEventListener("click", onGrantClick); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..cfca026 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4170 @@ +{ + "name": "bitrix-ncc-cookie", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bitrix-ncc-cookie", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "playwright": "1.56.1", + "selenium-webdriver": "4.49.0", + "web-ext": "10.7.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-8.0.5.tgz", + "integrity": "sha512-7NK+Lz3spQ52XsUGTxIEVU4jYN2/dIaX8sTxRFAUtYmttYZnVh3aehiihv+/Gb7+duMNHl2OrFcBQRyOHScxpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@bazel/runfiles": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@bazel/runfiles/-/runfiles-6.5.0.tgz", + "integrity": "sha512-RzahvqTkfpY2jsDxo8YItPX+/iZ6hbiikw1YhE0bA9EKBR5Og8Pa6FHn9PO9M0zaXRVsr0GFQLKbB/0rzy9SzA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@devicefarmer/adbkit": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit/-/adbkit-3.3.9.tgz", + "integrity": "sha512-AYxk5G/b2BPEfubcuxVEZEOpsLf0isqrPNJnnwAlUjZAiLdFg/uvLNRDuBhsC/Ar62X+rUhVlvlL+tKp9HCjfQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@devicefarmer/adbkit-logcat": "^2.1.2", + "@devicefarmer/adbkit-monkey": "~1.2.1", + "bluebird": "~3.7", + "commander": "^9.1.0", + "debug": "~4.3.1", + "node-forge": "^1.3.1", + "split": "~1.0.1" + }, + "bin": { + "adbkit": "bin/adbkit" + }, + "engines": { + "node": ">= 0.10.4" + } + }, + "node_modules/@devicefarmer/adbkit-logcat": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit-logcat/-/adbkit-logcat-2.1.3.tgz", + "integrity": "sha512-yeaGFjNBc/6+svbDeul1tNHtNChw6h8pSHAt5D+JsedUrMTN7tla7B15WLDyekxsuS2XlZHRxpuC6m92wiwCNw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@devicefarmer/adbkit-monkey": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit-monkey/-/adbkit-monkey-1.2.1.tgz", + "integrity": "sha512-ZzZY/b66W2Jd6NHbAhLyDWOEIBWC11VizGFk7Wx7M61JZRz7HR9Cq5P+65RKWUU7u6wgsE8Lmh9nE4Mz+U2eTg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.10.4" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "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": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@fluent/syntax": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@fluent/syntax/-/syntax-0.19.0.tgz", + "integrity": "sha512-5D2qVpZrgpjtqU4eNOcWGp1gnUCgjfM+vKGE2y03kKN6z5EBhtx0qdRFbg8QuNNj8wXNoX93KJoYb+NqoxswmQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@fregante/relaxed-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@fregante/relaxed-json/-/relaxed-json-2.0.0.tgz", + "integrity": "sha512-PyUXQWB42s4jBli435TDiYuVsadwRHnMc27YaLouINktvTWsL3FcKrRMGawTayFk46X+n5bE23RjUTWQwrukWw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/@humanfs/core": { + "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.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.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", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@mdn/browser-compat-data": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-8.1.0.tgz", + "integrity": "sha512-BNlUjp+9O6gtIHPVZEGFb5rgtuWW8weR+mSKfLOevxbfoZv5DRe3N4ZTEpXiIm5wfR7+kqHu8malCCgP6USrlg==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@pnpm/npm-conf": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.3.tgz", + "integrity": "sha512-//0sR/cow/s4ICQaYoAobOl4aU8cjU6x/V24V7XkKotb9+O+3zySIYp146vpaobYHnxa4pZX8NkV54Z5AwbDKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/estree": { + "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" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/addons-linter": { + "version": "10.13.0", + "resolved": "https://registry.npmjs.org/addons-linter/-/addons-linter-10.13.0.tgz", + "integrity": "sha512-gHCI20x4umX7q1cfJgwr4k/E6+mk2xkuTji17YGdDQxUuIkoDVxO3HmeZEm3pA/3+pVgayHdDzgt+xSu7vikUQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@fluent/syntax": "0.19.0", + "@fregante/relaxed-json": "2.0.0", + "@mdn/browser-compat-data": "8.1.0", + "addons-moz-compare": "1.4.0", + "addons-scanner-utils": "15.5.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.4", + "eslint-plugin-no-unsanitized": "4.1.5", + "eslint-visitor-keys": "5.0.1", + "espree": "11.2.0", + "esprima": "4.0.1", + "fast-json-patch": "3.1.1", + "image-size": "2.0.4", + "json-merge-patch": "1.0.2", + "pino": "10.3.1", + "semver": "7.8.5", + "source-map-support": "0.5.21", + "upath": "3.0.8", + "yargs": "17.7.2", + "yauzl": "3.4.0" + }, + "bin": { + "addons-linter": "bin/addons-linter" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/addons-moz-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/addons-moz-compare/-/addons-moz-compare-1.4.0.tgz", + "integrity": "sha512-uAM48cRt09id5+A2M11f1iDYebISjrIKyFYHfBI3GzqqC5BoOLFMSAFqHRaRvChL07qYPmcVme6VMoCKo19d6Q==", + "dev": true, + "license": "MPL-2.0" + }, + "node_modules/addons-scanner-utils": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/addons-scanner-utils/-/addons-scanner-utils-15.5.0.tgz", + "integrity": "sha512-bucvaWBYzhtIVrkbzSo9SA22DLsOxFKXLlIJ234N6x8+8a3FNDGOva8x9lsM7xVQ7oUZH4RZ/WAEtR54J/iQSg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "common-tags": "1.8.2", + "first-chunk-stream": "3.0.0", + "jsonwebtoken": "^9.0.3", + "strip-bom-stream": "4.0.0", + "upath": "3.0.8", + "yauzl": "3.4.0" + }, + "peerDependencies": { + "express": "5.2.1", + "safe-compare": "1.1.4" + }, + "peerDependenciesMeta": { + "express": { + "optional": true + }, + "safe-compare": { + "optional": true + } + } + }, + "node_modules/adm-zip": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.1.tgz", + "integrity": "sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "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": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-differ": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-4.0.0.tgz", + "integrity": "sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/array-union": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", + "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/atomically": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz", + "integrity": "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "stubborn-fs": "^2.0.0", + "when-exit": "^2.1.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/boxen": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", + "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^8.0.0", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "string-width": "^7.2.0", + "type-fest": "^4.21.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz", + "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chrome-launcher": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.2.0.tgz", + "integrity": "sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^2.0.1" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.cjs" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/columnify": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.6.0.tgz", + "integrity": "sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "strip-ansi": "^6.0.1", + "wcwidth": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/configstore": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-7.1.0.tgz", + "integrity": "sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "atomically": "^2.0.3", + "dot-prop": "^9.0.0", + "graceful-fs": "^4.2.11", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "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", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", + "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-prop": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", + "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", + "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@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.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-no-unsanitized": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-4.1.5.tgz", + "integrity": "sha512-MSB4hXPVFQrI8weqzs6gzl7reP2k/qSjtCoL2vUMSDejIIq9YL1ZKvq5/ORBXab/PvfBBrWO2jWviYpL+4Ghfg==", + "dev": true, + "license": "MPL-2.0", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firefox-profile": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/firefox-profile/-/firefox-profile-4.7.1.tgz", + "integrity": "sha512-rUFGvJzXxhhOltBxf1aML9jTVismzgC3w00v2dG4Bxu5Xr6o9/D9QUT4do1ZUE9NHzmRFyQ6Bz+HBWnxZMO65w==", + "dev": true, + "license": "MIT", + "dependencies": { + "adm-zip": "~0.6.x", + "fs-extra": "^11.2.0", + "ini": "^4.1.3", + "minimist": "^1.2.8", + "xml2js": "^0.6.2" + }, + "bin": { + "firefox-profile": "lib/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/first-chunk-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-3.0.0.tgz", + "integrity": "sha512-LNRvR4hr/S8cXXkIY5pTgVP7L3tq6LlYWcg9nWBuW7o1NMxKZo6oOVa/6GIekMGI0Iw7uC+HWimMe9u/VAeKqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs-extra": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz", + "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/fx-runner": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/fx-runner/-/fx-runner-1.6.0.tgz", + "integrity": "sha512-PEEUfnQJw0/io6SspBbR4EIeXFL/qe/sugesCyfr9M3llgyxNaOH8stvO6KJOKRjeUAvEB6oSZKS3lgjSEOsXg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "commander": "^12.1.0", + "shell-quote": "1.10.0", + "which": "^4.0.0", + "winreg": "0.0.12" + }, + "bin": { + "fx-runner": "bin/fx-runner" + } + }, + "node_modules/fx-runner/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/fx-runner/node_modules/isexe": { + "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": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/fx-runner/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-directory/node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.4.tgz", + "integrity": "sha512-QRUkFFsRV/6fuESxb9Vkq+a0LkSrgKXuc2NEqfikiXxxN/G3tjWt5EVUlMaImRBZRZK/jRBEbYvpPYZL8t08Zw==", + "dev": true, + "license": "MIT", + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-in-ci": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz", + "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==", + "dev": true, + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-installed-globally": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.1.tgz", + "integrity": "sha512-li88H4QGRoNMX27jEmY8nR515oJVspAgC/WFMlhHfFV8RJ4QyGpHhXnwAe9gH5RIgKmdYRUmrOhQobcgEjIdSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-npm": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.1.0.tgz", + "integrity": "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jose": { + "version": "5.9.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", + "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "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" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-merge-patch": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-merge-patch/-/json-merge-patch-1.0.2.tgz", + "integrity": "sha512-M6Vp2GN9L7cfuMXiWOmHj9bEFbeC250iVtcKQbqVgEsDVYnIsrNsbU+h/Y/PkbBQCtEa4Bez+Ebv0zfbC8ObLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jszip": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz", + "integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/ky": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/ky/-/ky-1.14.3.tgz", + "integrity": "sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/latest-version": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-9.0.0.tgz", + "integrity": "sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-json": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lighthouse-logger": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.2.tgz", + "integrity": "sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.1", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/marky": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", + "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", + "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", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/multimatch": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-6.0.0.tgz", + "integrity": "sha512-I7tSVxHGPlmPN/enE3mS1aOSo6bWBfls+3HmuEeCUBCE7gWnm3cBXCBkpurzFjVRwC6Kld8lLaZ1Iv5vOcjvcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "^3.0.5", + "array-differ": "^4.0.0", + "array-union": "^3.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-forge": { + "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/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/open": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.3.tgz", + "integrity": "sha512-b3souUgU0VaAvMmzA4+9cRgrOgMdE0vc9H9VhQ0FYnjjcyZ8R/f9DiwMU9V8rl+vjf05On9UJhbgb06fsTs87Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.5.1", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.2.1", + "wsl-utils": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz", + "integrity": "sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ky": "^1.2.0", + "registry-auth-token": "^5.0.2", + "registry-url": "^6.0.1", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "dev": true, + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/powershell-utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz", + "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.1.tgz", + "integrity": "sha512-oCkNVj4OKnKEcocVBcDfiIugN1/cE6rgfT3nCtzeqZPA8+rIxMaQ0xXzIkGP8Gj2qUaP1UR7uhJ1VD8lUCpzCg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/promise-toolbox": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/promise-toolbox/-/promise-toolbox-0.21.0.tgz", + "integrity": "sha512-NV8aTmpwrZv+Iys54sSFOBx3tuVaOBvvrft5PNppnxy9xpU/akHbaWIril22AB22zaPgrgwKdD0KsrM0ptUtpg==", + "dev": true, + "license": "ISC", + "dependencies": { + "make-error": "^1.3.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pupa": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz", + "integrity": "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-goat": "^4.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "dev": true, + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/registry-auth-token": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz", + "integrity": "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pnpm/npm-conf": "^3.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/registry-url": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", + "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "1.2.8" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/selenium-webdriver": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.49.0.tgz", + "integrity": "sha512-16XqeOEMj+4+p+pzLLyvNWmF5jCY05EKIl3e1anZBgv4+zq7PUTtiNjKtQhMpwosZ0wIMKDFAQ2xdyogOHD4DA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/SeleniumHQ" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/selenium" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@bazel/runfiles": "^6.5.0", + "jszip": "^3.10.1", + "tmp": "^0.2.7", + "ws": "^8.21.3" + }, + "engines": { + "node": ">= 22.0.0" + } + }, + "node_modules/semver": { + "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": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "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", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-5.0.0.tgz", + "integrity": "sha512-p+byADHF7SzEcVnLvc/r3uognM1hUhObuHXxJcgLCfD194XAkaLbjq3Wzb0N5G2tgIjH0dgT708Z51QxMeu60A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-bom-buf": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-buf/-/strip-bom-buf-2.0.0.tgz", + "integrity": "sha512-gLFNHucd6gzb8jMsl5QmZ3QgnUJmp7qn4uUSHNwEXumAp7YizoGYw19ZUVfuq4aBOQUtyn2k8X/CwzWB73W2lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-utf8": "^0.2.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-4.0.0.tgz", + "integrity": "sha512-0ApK3iAkHv6WbgLICw/J4nhwHeDZsBxIIsOD+gHgZICL6SeJ0S9f/WZqemka9cjkTyMN5geId6e8U5WGFAn3cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "first-chunk-stream": "^3.0.0", + "strip-bom-buf": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stubborn-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", + "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "stubborn-utils": "^1.0.1" + } + }, + "node_modules/stubborn-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", + "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/thread-stream": { + "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": "^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", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "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": { + "node": ">=14.14" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/upath": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/upath/-/upath-3.0.8.tgz", + "integrity": "sha512-YAsrLMIlhfSCm9rga5TZsJ1mXgahs7N0qOTokzU8mFz35hUrYMvVkaEPefI3rEb/vkAWAOj1Km/qdije9RC1kQ==", + "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": ">=20" + } + }, + "node_modules/update-notifier": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-7.3.1.tgz", + "integrity": "sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boxen": "^8.0.1", + "chalk": "^5.3.0", + "configstore": "^7.0.0", + "is-in-ci": "^1.0.0", + "is-installed-globally": "^1.0.0", + "is-npm": "^6.0.0", + "latest-version": "^9.0.0", + "pupa": "^3.1.0", + "semver": "^7.6.3", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/update-notifier/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/watchpack": { + "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": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-ext": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/web-ext/-/web-ext-10.7.0.tgz", + "integrity": "sha512-G1CJMzQx8rWzD5WhhlUupKkMXXI+oVKUP44aY7H8Qte30xaozZ592kdK7V24U4HMNgayEFsMWxrYfX+/ccqB1w==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@babel/runtime": "8.0.5", + "@devicefarmer/adbkit": "3.3.9", + "addons-linter": "10.13.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.1", + "fx-runner": "1.6.0", + "https-proxy-agent": "^7.0.0", + "jose": "5.9.6", + "jszip": "3.10.2", + "multimatch": "6.0.0", + "open": "11.0.3", + "parse-json": "8.3.0", + "pino": "10.3.1", + "promise-toolbox": "0.21.0", + "source-map-support": "0.5.21", + "strip-bom": "5.0.0", + "strip-json-comments": "5.0.3", + "tmp": "0.2.7", + "update-notifier": "7.3.1", + "watchpack": "2.5.2", + "yargs": "17.7.2", + "zip-dir": "2.0.0" + }, + "bin": { + "web-ext": "bin/web-ext.js" + }, + "engines": { + "node": ">=20.0.0", + "npm": ">=8.0.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/when-exit": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", + "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/winreg": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/winreg/-/winreg-0.0.12.tgz", + "integrity": "sha512-typ/+JRmi7RqP1NanzFULK36vczznSNN8kWVA9vIqXyv8GhghUlwhGp1Xj3Nms1FsPcNnsQrJOR10N58/nQ9hQ==", + "dev": true, + "license": "BSD" + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.1.tgz", + "integrity": "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wsl-utils/node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xdg-basedir": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", + "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yauzl": { + "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": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/zip-dir/-/zip-dir-2.0.0.tgz", + "integrity": "sha512-uhlsJZWz26FLYXOD6WVuq+fIcZ3aBPGo/cFdiLlv3KNwpa52IF3ISV8fLhQLiqVu5No3VhlqlgthN6gehil1Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.0", + "jszip": "^3.2.2" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1ff4035 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "bitrix-ncc-cookie", + "version": "1.0.0", + "private": true, + "description": "Firefox/Chrome extension: toggle the Bitrix _NCC cookie (disable Composite cache) for the current site", + "license": "MIT", + "scripts": { + "lint": "web-ext lint", + "build": "web-ext build --overwrite-dest", + "test": "npm run test:unit && npm run test:chrome", + "test:chrome": "node --test --test-concurrency=1 tests/chrome.test.js", + "test:firefox": "npm run build && node --test --test-concurrency=1 tests/firefox.test.js", + "test:unit": "node --test tests/unit.test.js" + }, + "devDependencies": { + "playwright": "1.56.1", + "selenium-webdriver": "4.49.0", + "web-ext": "10.7.0" + } +} diff --git a/popup.css b/popup.css new file mode 100644 index 0000000..cc2931d --- /dev/null +++ b/popup.css @@ -0,0 +1,158 @@ +:root { + color-scheme: light dark; + --text: #15141a; + --muted: #6b6a70; + --bg: #ffffff; + --border: #d7d7db; + --accent: #c0392b; +} + +@media (prefers-color-scheme: dark) { + :root { + --text: #fbfbfe; + --muted: #a8a8b0; + --bg: #2b2a33; + --border: #52525e; + } +} + +body { + margin: 0; + padding: 10px 12px; + width: 300px; + box-sizing: border-box; + overflow-x: hidden; + font: 13px/1.4 system-ui, sans-serif; + color: var(--text); + background: var(--bg); +} + +.host { + font-weight: 600; + margin-bottom: 8px; + word-break: break-all; +} + +.trigger { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 0; + cursor: pointer; +} + +.trigger-label { + font-weight: 600; + min-width: 52px; +} + +.trigger-info { + display: flex; + flex-direction: column; + min-width: 0; +} + +.trigger-ide { + color: var(--text); + font-size: 12px; +} + +.trigger-cookie { + color: var(--muted); + font-family: monospace; + font-size: 11px; +} + +.scope-row { + display: block; + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--border); +} + +.scope, +.site-ide { + display: block; + width: 100%; + margin-top: 4px; +} + +.site-ide-row { + margin-top: 8px; +} + +.site-ide-custom-row { + display: flex; + gap: 6px; + margin-top: 4px; +} + +.site-ide-custom { + flex: 1; + min-width: 0; +} + +.notice, +.error { + margin: 6px 0; +} + +.error { + color: var(--accent); +} + +.grant { + display: block; + margin-top: 6px; +} + +.footer { + margin-top: 8px; + text-align: right; +} + +.footer a { + color: var(--muted); +} + +[hidden] { + display: none !important; +} + +.trigger-cookie { + color: var(--muted); +} + +.site-name-row { + margin-top: 8px; +} + +.site-name { + display: block; + width: 100%; + box-sizing: border-box; + margin-top: 4px; +} + +.hint { + display: block; + margin-top: 4px; + color: var(--muted); + font-size: 11px; +} + +.site-name-buttons { + display: flex; + gap: 6px; + margin-top: 6px; +} + +.ncc-row { + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--border); +} + +.open-ncc { + width: 100%; +} diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..124552b --- /dev/null +++ b/popup.html @@ -0,0 +1,62 @@ + + + + + + + +
+ + + + + + + + + + + + + + + + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..dae19a1 --- /dev/null +++ b/popup.js @@ -0,0 +1,189 @@ +let popupTab; + +function showError(message) { + let el = document.querySelector('.error'); + el.textContent = message; + el.hidden = !message; +} + +async function render() { + // popup.html?tabId=N opens the popup for a given tab (for debugging in a regular tab) + let debugTabId = new URLSearchParams(location.search).get('tabId'); + popupTab = debugTabId ? await callApi('tabs.get', [Number(debugTabId)], undefined) : await getActiveTab(); + let host = popupTab ? getHost(popupTab.url) : ''; + log('popup: render for', popupTab && popupTab.url); + + document.querySelector('.host').textContent = host || (popupTab && popupTab.url) || ''; + + let supported = popupTab && isSupportedUrl(popupTab.url); + let permitted = supported && await hasHostPermission(); + document.querySelector('.unsupported').hidden = !!supported; + document.querySelector('.no-permission').hidden = !supported || permitted; + document.querySelector('.controls').hidden = !permitted; + if (!permitted) { + return; + } + + let triggers = await getTriggerSettings(host); + let states = await getStates(popupTab); + let container = document.querySelector('.triggers'); + let template = document.querySelector('.trigger-template'); + container.textContent = ''; + for (let trigger of triggers) { + let row = template.content.cloneNode(true); + let checkbox = row.querySelector('input'); + checkbox.checked = !!states[trigger.id]; + checkbox.dataset.trigger = trigger.id; + checkbox.addEventListener('change', onTriggerChange); + row.querySelector('.trigger-label').textContent = trigger.label; + let cookie = row.querySelector('.trigger-cookie'); + cookie.textContent = trigger.name + '=' + trigger.value + (trigger.siteName ? ' - ' + t('forThisSite') : ''); + cookie.title = t('changeSiteNameTitle'); + cookie.addEventListener('click', toggleSiteNameRow); + container.appendChild(row); + } + + let select = document.querySelector('.scope'); + let scope = await getScope(host); + select.textContent = ''; + select.appendChild(new Option(t('scopeHostOnly', host), '')); + for (let domain of getDomainCandidates(host)) { + select.appendChild(new Option(t('scopeWithSubdomains', domain), domain)); + } + select.value = scope; + select.disabled = select.options.length < 2; + + await renderSiteName(host, triggers[0]); +} + +// Per-site cookie name: input with the current name, the global one is the placeholder +async function renderSiteName(host, trigger) { + let siteName = await getSiteCookieName(host); + let input = document.querySelector('.site-name'); + input.value = siteName; + input.placeholder = trigger.globalName; + // The row is opened by click on the cookie name, stays open when a site name is set + if (siteName) { + document.querySelector('.site-name-row').hidden = false; + } + log('popup: cookie name for', host, '=', trigger.name, trigger.siteName ? '(site)' : '(global)'); +} + +function toggleSiteNameRow(e) { + e.preventDefault(); + let row = document.querySelector('.site-name-row'); + row.hidden = !row.hidden; + if (!row.hidden) { + document.querySelector('.site-name').focus(); + } +} + +// Changes the cookie name for the site. An enabled cookie is moved to the new name, +// otherwise the cookie with the old name would stay and keep the composite cache disabled. +async function applySiteCookieName(name) { + showError(''); + let host = getHost(popupTab.url); + let before = (await getTriggerSettings(host))[0]; + let enabled = !!await getCookie(popupTab, before); + try { + if (enabled) { + await disableTrigger(popupTab, before); + } + await setSiteCookieName(host, name); + if (enabled) { + await enableTrigger(popupTab, (await getTriggerSettings(host))[0]); + } + } catch (err) { + logError('popup: cookie name change failed', err); + showError(t('failed', err && err.message || err)); + } + await render(); +} + +async function onSiteNameSubmit(e) { + e.preventDefault(); + let name = document.querySelector('.site-name').value.trim(); + if (name && !isValidCookieName(name)) { + showError(t('optInvalidName', name)); + return; + } + await applySiteCookieName(name); +} + +async function onSiteNameReset() { + await applySiteCookieName(''); +} + +async function onTriggerChange(e) { + showError(''); + let triggerId = e.target.dataset.trigger; + try { + let enabled = await toggleTrigger(popupTab, triggerId); + log('popup: trigger', triggerId, enabled ? 'enabled' : 'disabled'); + } catch (err) { + logError('popup: toggle failed', triggerId, err); + showError(t('failed', err && err.message || err)); + } + await render(); +} + +async function onScopeChange(e) { + showError(''); + let host = getHost(popupTab.url); + let oldScope = await getScope(host); + let states = await getStates(popupTab); + let enabledIds = Object.keys(states).filter(id => states[id]); + await setScope(host, e.target.value); + try { + await reapplyTriggers(popupTab, enabledIds); + } catch (err) { + // For example, the browser refuses to set a cookie for a public suffix (co.uk) + logError('popup: scope change failed', err); + showError(t('scopeFailed', err && err.message || err)); + await setScope(host, oldScope); + await reapplyTriggers(popupTab, enabledIds).catch(err2 => logError('popup: rollback failed', err2)); + } + await render(); +} + +async function onGrantClick() { + try { + await requestHostPermission(); + } catch (err) { + logError('popup: permission request failed', err); + showError(t('permissionFailed', err && err.message || err)); + } + await render(); +} + +// Reloads the page with ?ncc=1: works even when nginx serves the composite cache without PHP +async function onOpenWithoutComposite() { + let url = nccUrl(popupTab.url); + log('popup: open without composite', url); + try { + await requireApi('tabs.update', [popupTab.id, { url: url }]); + window.close(); + } catch (err) { + logError('popup: tabs.update failed', err); + showError(t('failed', err && err.message || err)); + } +} + +function openOptions(e) { + e.preventDefault(); + callApi('runtime.openOptionsPage', []); + window.close(); +} + +document.querySelector('.scope').addEventListener('change', onScopeChange); +document.querySelector('.site-name-row').addEventListener('submit', onSiteNameSubmit); +document.querySelector('.site-name-reset').addEventListener('click', onSiteNameReset); +document.querySelector('.grant').addEventListener('click', onGrantClick); +document.querySelector('.open-options').addEventListener('click', openOptions); +document.querySelector('.open-ncc').addEventListener('click', onOpenWithoutComposite); + +localizePage(); +render().catch(err => { + logError('popup: render failed', err); + showError(String(err && err.message || err)); +}); diff --git a/store/README.md b/store/README.md new file mode 100644 index 0000000..9f50020 --- /dev/null +++ b/store/README.md @@ -0,0 +1,44 @@ +# Публикация в магазинах + +Все файлы для публикации лежат в этой папке: + +| Файл | Что это | +|---|---| +| `listing-en.md`, `listing-ru.md` | Название, краткое и полное описание, категория | +| `privacy-policy.md` | Политика приватности (можно указать ссылку на этот файл на GitHub) | +| `permissions.md` | Обоснования разрешений и «single purpose» для форм проверки | +| `images/screenshot-*-en.png`, `-ru.png` | Скриншоты 1280×800 | +| `images/promo-small-440x280.png` | Малый промо-баннер 440×280 (обязателен в Chrome Web Store) | +| `make-images.js` | Генератор картинок из настоящего интерфейса: `node store/make-images.js` | + +## Пакет + +Архив собирается командой `npm run build` (папка `web-ext-artifacts/`) или автоматически +GitHub Actions при создании тега версии, например `v1.0` — тогда zip прикрепляется к GitHub Release. +Один и тот же zip подходит для обоих магазинов. Перед каждой новой публикацией увеличьте `version` в `manifest.json`. + +## Firefox — addons.mozilla.org (AMO) + +Расширение уже опубликовано: https://addons.mozilla.org/firefox/addon/_ncc-cookie-for-bitrix/ +(ID `ncc_cookie_setup@goodprogrammist.ru` в манифесте совпадает), поэтому загружается **новая версия**: + +1. https://addons.mozilla.org/developers/addons → дополнение → «Upload New Version». +2. Загрузите zip. Галочка «Android» ставится автоматически: в манифесте есть `gecko_android`. +3. На вопрос о исходном коде ответьте «No»: код не минифицирован и не обфусцирован. +4. «Notes for Reviewers» — текст из `reviewer-notes.md`. +5. На странице дополнения («Edit Product Page») обновите описание из `listing-en.md` + (и `listing-ru.md` для локали ru), скриншоты и ссылку на политику приватности. +6. Сведения о сборе данных уже указаны в манифесте: `data_collection_permissions: none`. + +## Chrome Web Store + +1. Зарегистрируйтесь как разработчик в Chrome Web Store Developer Dashboard + (нужен Google-аккаунт и разовый регистрационный взнос). +2. «New item» → загрузите zip. +3. Store listing: описание из `listing-en.md` / `listing-ru.md`, категория Developer Tools, + скриншоты из `images/` (от 1 до 5, 1280×800), малый промо-баннер 440×280. + Иконка 128×128 берётся из пакета (`icons/bug-green-128-padded.png`, рисунок 96×96 с полями по 16 px), + она же — `images/store-icon-128.png`. +4. Раздел о приватности: «single purpose» и обоснования разрешений из `permissions.md`, + отметьте, что данные не собираются, укажите ссылку на `privacy-policy.md`. +5. Отправьте на проверку. diff --git a/store/images/promo-small-440x280.png b/store/images/promo-small-440x280.png new file mode 100644 index 0000000..d047be3 Binary files /dev/null and b/store/images/promo-small-440x280.png differ diff --git a/store/images/screenshot-1-popup-en.png b/store/images/screenshot-1-popup-en.png new file mode 100644 index 0000000..65e03b7 Binary files /dev/null and b/store/images/screenshot-1-popup-en.png differ diff --git a/store/images/screenshot-1-popup-ru.png b/store/images/screenshot-1-popup-ru.png new file mode 100644 index 0000000..259581a Binary files /dev/null and b/store/images/screenshot-1-popup-ru.png differ diff --git a/store/images/screenshot-2-site-en.png b/store/images/screenshot-2-site-en.png new file mode 100644 index 0000000..d546226 Binary files /dev/null and b/store/images/screenshot-2-site-en.png differ diff --git a/store/images/screenshot-2-site-ru.png b/store/images/screenshot-2-site-ru.png new file mode 100644 index 0000000..1acd456 Binary files /dev/null and b/store/images/screenshot-2-site-ru.png differ diff --git a/store/images/screenshot-3-options-en.png b/store/images/screenshot-3-options-en.png new file mode 100644 index 0000000..e3ea83e Binary files /dev/null and b/store/images/screenshot-3-options-en.png differ diff --git a/store/images/screenshot-3-options-ru.png b/store/images/screenshot-3-options-ru.png new file mode 100644 index 0000000..5a713aa Binary files /dev/null and b/store/images/screenshot-3-options-ru.png differ diff --git a/store/images/store-icon-128.png b/store/images/store-icon-128.png new file mode 100644 index 0000000..c04fff3 Binary files /dev/null and b/store/images/store-icon-128.png differ diff --git a/store/listing-en.md b/store/listing-en.md new file mode 100644 index 0000000..7c4b360 --- /dev/null +++ b/store/listing-en.md @@ -0,0 +1,33 @@ +# Store listing (English) + +## Name +Bitrix _NCC Cookie: Disable Composite Cache + +## Summary (short description) +Toggle the Bitrix _NCC cookie (BITRIX_SM_NCC=Y) to disable the composite cache on the current site while debugging. + +## Description +Turn off the 1C-Bitrix composite cache for yourself in one click, without changing the site settings. + +Bitrix does not use the composite cache for a request with the cookie _NCC=Y +(BITRIX_SM_NCC=Y with the default prefix): pages are generated by PHP, so you see your latest changes. +Other visitors still get the cached pages. + +Features: +• The toolbar popup sets and removes BITRIX_SM_NCC=Y for the current site; the "N" badge shows it is on +• The cookie is kept: Bitrix deletes the _NCC cookie for anonymous visitors on every page, the extension sets it again until you turn it off +• Cookie for the current host only or for a parent domain with all subdomains +• Own cookie name for a site with another cookie prefix (the exact name is COOKIE_NCC in /bitrix/html_pages/.config.php) +• "Open without composite (?ncc=1)" button — for servers where nginx serves the composite cache itself and does not check the cookie +• Keyboard shortcut Alt+Shift+Y +• Works with Firefox containers, private windows and Chrome incognito (after you allow the extension there in the browser add-on settings) +• Firefox for Android is supported (without keyboard shortcuts) +• English, Russian and Kazakh interface + +The extension does not collect or send any data. Source code: https://github.com/goodprogrammist/BITRIX_SM_NCC + +## Category +Developer Tools + +## Homepage / support +https://github.com/goodprogrammist/BITRIX_SM_NCC diff --git a/store/listing-ru.md b/store/listing-ru.md new file mode 100644 index 0000000..8c64ea0 --- /dev/null +++ b/store/listing-ru.md @@ -0,0 +1,33 @@ +# Описание для магазинов (русский) + +## Название +Bitrix _NCC: отключение композитного кеша + +## Краткое описание +Включает cookie Bitrix _NCC (BITRIX_SM_NCC=Y), чтобы отключить композитный кеш на текущем сайте при отладке. + +## Описание +Отключите композитный кеш 1С-Битрикс для себя одним кликом, не меняя настройки сайта. + +Битрикс не использует композитный кеш для запроса с cookie <префикс>_NCC=Y +(BITRIX_SM_NCC=Y при стандартном префиксе): страницы генерирует PHP, и вы видите свои последние изменения. +Остальные посетители по-прежнему получают закешированные страницы. + +Возможности: +• Кнопка на панели ставит и убирает BITRIX_SM_NCC=Y для текущего сайта; бейдж «N» показывает, что cookie включена +• Cookie сохраняется: Битрикс удаляет cookie _NCC у неавторизованных посетителей на каждой странице, расширение ставит её снова, пока вы её не выключите +• Cookie только для текущего хоста или для родительского домена со всеми поддоменами +• Своё имя cookie для сайта с другим префиксом (точное имя — COOKIE_NCC в /bitrix/html_pages/.config.php) +• Кнопка «Открыть без композита (?ncc=1)» — для серверов, где композитный кеш отдаёт nginx и cookie не проверяется +• Горячая клавиша Alt+Shift+Y +• Работает с контейнерами Firefox, приватными окнами и режимом инкогнито Chrome (после разрешения в настройках дополнений браузера) +• Поддерживается Firefox для Android (без горячих клавиш) +• Интерфейс на английском, русском и казахском + +Расширение не собирает и не отправляет никаких данных. Исходный код: https://github.com/goodprogrammist/BITRIX_SM_NCC + +## Категория +Developer Tools (Инструменты разработчика) + +## Сайт / поддержка +https://github.com/goodprogrammist/BITRIX_SM_NCC diff --git a/store/make-images.js b/store/make-images.js new file mode 100644 index 0000000..4137145 --- /dev/null +++ b/store/make-images.js @@ -0,0 +1,136 @@ +// Generates store images from the real addon UI: node store/make-images.js +// Screenshots 1280x800 (en, ru) and small promo tile 440x280 (Chrome Web Store requirements). +const path = require('node:path'); +const fs = require('node:fs'); +const { chromium } = require('playwright'); +const { SITE, startServer, eventually } = require('../tests/helpers'); + +const EXT_DIR = path.resolve(__dirname, '..'); +const OUT_DIR = path.join(__dirname, 'images'); + +const CAPTIONS = { + en: { + popup: ['Bitrix without composite cache', 'One click sets BITRIX_SM_NCC=Y for the current site: pages are generated by PHP, the badge shows it is on.'], + site: ['Per-site settings', 'Cookie for the host or the whole domain, own cookie name for a site with another prefix.'], + options: ['Cookie name and icons', 'The name is the cookie prefix + _NCC, as in /bitrix/html_pages/.config.php.'] + }, + ru: { + popup: ['Битрикс без композитного кеша', 'Один клик ставит BITRIX_SM_NCC=Y для текущего сайта: страницы генерирует PHP, бейдж показывает, что cookie включена.'], + site: ['Настройки для сайта', 'Cookie для хоста или всего домена, своё имя cookie для сайта с другим префиксом.'], + options: ['Имя cookie и иконки', 'Имя — это префикс cookie + _NCC, как в /bitrix/html_pages/.config.php.'] + } +}; + +async function launch(lang) { + const context = await chromium.launchPersistentContext('', { + executablePath: process.env.CHROMIUM_PATH || undefined, + headless: false, + ignoreDefaultArgs: ['--disable-extensions'], + args: [`--disable-extensions-except=${EXT_DIR}`, `--load-extension=${EXT_DIR}`, + '--host-resolver-rules=MAP * 127.0.0.1', '--headless=new', '--lang=' + lang], + locale: lang, + deviceScaleFactor: 2 + }); + const worker = context.serviceWorkers()[0] || await context.waitForEvent('serviceworker'); + await eventually(() => { + if (!context.pages().some(p => p.url().includes('options'))) { + throw new Error('options page is not opened yet'); + } + }, 10000); + return { context, worker, extensionId: worker.url().split('/')[2] }; +} + +const dataUrl = buffer => 'data:image/png;base64,' + buffer.toString('base64'); + +function frameHtml(title, text, image, imageWidth, host, badge, full) { + return ` +

${title}

${text}

+
+
${host}
+
${badge ? `${badge}` : ''}
+
+
+ `; +} + +async function render(context, html, file, width, height) { + const page = await context.newPage(); + await page.setViewportSize({ width, height }); + await page.setContent(html); + // deviceScaleFactor 2 is used for crisp UI shots, store images must have exact size + const shot = await page.screenshot({ clip: { x: 0, y: 0, width, height }, scale: 'css' }); + fs.writeFileSync(path.join(OUT_DIR, file), shot); + console.log('[images] written', file); + await page.close(); +} + +async function makeForLanguage(lang) { + const { context, worker, extensionId } = await launch(lang); + const site = await context.newPage(); + await site.goto(SITE); + const tabId = await worker.evaluate(async () => (await api.tabs.query({ url: 'http://app.example.co.uk/*' }))[0].id); + const popup = await context.newPage(); + await popup.setViewportSize({ width: 300, height: 400 }); + await popup.goto(`chrome-extension://${extensionId}/popup.html?tabId=${tabId}`); + await popup.waitForSelector('.trigger input'); + await popup.check('.trigger input[data-trigger=ncc]'); + await popup.waitForTimeout(500); + const popupShot = await popup.locator('body').screenshot(); + + await popup.selectOption('.scope', 'example.co.uk'); + await popup.waitForTimeout(300); + await popup.click('.trigger-cookie'); + await popup.waitForTimeout(300); + const siteShot = await popup.locator('body').screenshot(); + + const options = await context.newPage(); + await options.setViewportSize({ width: 1000, height: 800 }); + await options.goto(`chrome-extension://${extensionId}/options.html`); + await options.waitForSelector('.cookie_ncc'); + const optionsShot = await options.screenshot({ clip: { x: 0, y: 0, width: 1000, height: 800 } }); + + const c = CAPTIONS[lang]; + await render(context, frameHtml(c.popup[0], c.popup[1], dataUrl(popupShot), 300, 'app.example.co.uk', 'N'), `screenshot-1-popup-${lang}.png`, 1280, 800); + await render(context, frameHtml(c.site[0], c.site[1], dataUrl(siteShot), 300, 'app.example.co.uk', 'N'), `screenshot-2-site-${lang}.png`, 1280, 800); + await render(context, frameHtml(c.options[0], c.options[1], dataUrl(optionsShot), 700, 'Bitrix _NCC', '', true), `screenshot-3-options-${lang}.png`, 1280, 800); + + if (lang === 'en') { + const icon = dataUrl(fs.readFileSync(path.join(EXT_DIR, 'icons', 'bug-red-128.png'))); + await render(context, `

Bitrix _NCC

Composite cache off
in one click
Chrome & Firefox

`, + 'promo-small-440x280.png', 440, 280); + } + await context.close(); +} + +(async () => { + fs.mkdirSync(OUT_DIR, { recursive: true }); + const server = await startServer(); + for (const lang of ['en', 'ru']) { + await makeForLanguage(lang); + } + server.close(); +})().catch(e => { + console.error('[images] failed', e); + process.exit(1); +}); diff --git a/store/permissions.md b/store/permissions.md new file mode 100644 index 0000000..f5e12df --- /dev/null +++ b/store/permissions.md @@ -0,0 +1,12 @@ +# Permission justifications (for store review forms) + +**Single purpose:** toggle the 1C-Bitrix _NCC cookie (BITRIX_SM_NCC=Y) for the current site +to disable the Bitrix composite cache for the developer's own requests. + +| Permission | Why it is needed | +|---|---| +| `cookies` | Set and remove the _NCC cookie for the current site; watch cookie changes to show the actual state on the toolbar icon and to set the cookie again when the site deletes it (Bitrix deletes it for anonymous visitors) while the user keeps it turned on. | +| `storage` | Save extension settings (cookie name, icon colors) and per-site choices (cookie domain, cookie name for the site). | +| Host permissions `http://*/*`, `https://*/*` | The cookies API works only for sites the extension has host permission for; the same permission gives the URL of the current tab (to know which site the cookie is for, to show the state on the toolbar icon and to reload the page with ?ncc=1). Developers work with arbitrary Bitrix sites (local, test and production domains), so the list of sites cannot be fixed in advance. Only http and https are requested. | +| Remote code | Not used. All code is in the package. | +| Data collection | None. | diff --git a/store/privacy-policy.md b/store/privacy-policy.md new file mode 100644 index 0000000..2b277bc --- /dev/null +++ b/store/privacy-policy.md @@ -0,0 +1,21 @@ +# Privacy Policy — Bitrix _NCC Cookie + +Last updated: 2026-10-11 + +The extension does not collect, transmit, sell or share any personal or usage data. + +What the extension stores, only in your browser: +- settings (cookie name, icon colors) in the browser extension storage + (`storage.sync`; the browser may synchronize it between your devices if you enabled browser sync); +- per-site choices (cookie domain, cookie name for the site) and marks of the cookies you turned on + in the local extension storage. + +What the extension does: +- sets and removes the _NCC cookie (BITRIX_SM_NCC or the name you configured) for the site you choose, + and sets it again when the site deletes it while you keep it turned on; +- reads the address of the current tab only to know which site the cookie belongs to; +- reloads the current tab with the `?ncc=1` parameter when you press the corresponding button. + +The extension makes no network requests of its own and contains no analytics or remote code. + +Contact: https://github.com/goodprogrammist/BITRIX_SM_NCC/issues diff --git a/store/reviewer-notes.md b/store/reviewer-notes.md new file mode 100644 index 0000000..addf2e6 --- /dev/null +++ b/store/reviewer-notes.md @@ -0,0 +1,20 @@ +# Notes for reviewers (AMO "Notes for Reviewers") + +No account, server or build step is needed. The package contains plain, non-minified source files +(identical to https://github.com/goodprogrammist/BITRIX_SM_NCC). + +How to test: +1. Open any http(s) site, e.g. https://example.com/. +2. Click the toolbar icon: the popup shows the "Disable composite cache" switch. +3. Turn it on: the cookie BITRIX_SM_NCC=Y is set for the site + (visible in DevTools → Storage → Cookies), the icon gets the "N" badge. +4. Turn it off: the cookie is removed. +5. "Open without composite (?ncc=1)" reloads the current tab with the ?ncc=1 parameter. + +1C-Bitrix sites do not use their composite (full page) cache for requests with this cookie. +Bitrix deletes the cookie for anonymous visitors, so while the user keeps the switch on, the extension +sets the cookie again when the site deletes it (cookies.onChanged). + +Permissions: "cookies" and host permissions for http/https are needed to set and remove this cookie +for the current site; "storage" keeps the settings. The extension makes no network requests +and collects no data. diff --git a/tests/chrome.test.js b/tests/chrome.test.js new file mode 100644 index 0000000..cb65d8c --- /dev/null +++ b/tests/chrome.test.js @@ -0,0 +1,377 @@ +// End-to-end tests in Chromium: the unpacked addon is loaded into a real browser. +// Run: npm run test:chrome (CHROMIUM_PATH can point to a custom Chromium binary) +const { describe, it, before, after } = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('node:path'); +const { chromium } = require('playwright'); +const { SITE, OTHER_SITE, startServer, eventually } = require('./helpers'); + +const EXT_DIR = path.resolve(__dirname, '..'); + +async function launch(lang, extraArgs) { + const args = [ + `--disable-extensions-except=${EXT_DIR}`, + `--load-extension=${EXT_DIR}`, + '--host-resolver-rules=MAP * 127.0.0.1', + // Extensions work only in the new headless mode + '--headless=new' + ]; + if (lang) { + args.push('--lang=' + lang); + } + args.push(...(extraArgs || [])); + const context = await chromium.launchPersistentContext('', { + executablePath: process.env.CHROMIUM_PATH || undefined, + headless: false, + ignoreDefaultArgs: ['--disable-extensions'], + args: args, + locale: lang || 'en-US' + }); + const worker = context.serviceWorkers()[0] || await context.waitForEvent('serviceworker'); + const extensionId = worker.url().split('/')[2]; + console.log('[test] extension id', extensionId, 'lang', lang || 'default'); + // The addon opens options page after install: Chrome may reuse a fresh blank tab for it, + // so tests open their pages only after that + await eventually(() => assert.ok(context.pages().some(p => p.url().includes('options'))), 10000); + return { context, worker, extensionId }; +} + +describe('Chromium', () => { + let server, context, worker, extensionId, site, popup, siteTabId; + const errors = []; + + const pageText = async (page = site) => { + await page.reload(); + return page.textContent('body'); + }; + const badge = () => worker.evaluate(tabId => api.action.getBadgeText({ tabId }), siteTabId); + const title = () => worker.evaluate(tabId => api.action.getTitle({ tabId }), siteTabId); + const cookieDomains = name => worker.evaluate(n => api.cookies.getAll({ name: n }), name) + .then(cookies => cookies.map(c => c.domain + '=' + c.value)); + const openPopup = async () => { + await popup.goto(`chrome-extension://${extensionId}/popup.html?tabId=${siteTabId}`); + await popup.waitForSelector('.trigger input'); + }; + + before(async () => { + server = await startServer(); + ({ context, worker, extensionId } = await launch()); + site = await context.newPage(); + await site.goto(SITE); + siteTabId = await worker.evaluate(async () => (await api.tabs.query({ url: 'http://app.example.co.uk/*' }))[0].id); + popup = await context.newPage(); + popup.on('console', m => m.type() === 'error' && errors.push(m.text())); + await openPopup(); + }); + + after(async () => { + console.log('[test] console errors:', errors); + await context.close(); + server.close(); + }); + + it('opens options page after install', async () => { + const urls = context.pages().map(p => p.url()); + assert.ok(urls.some(u => u.includes('options')), 'pages: ' + urls.join(', ')); + }); + + it('keyboard shortcut is assigned (not reserved by Chrome)', async () => { + const commands = await worker.evaluate(() => api.commands.getAll()); + const shortcuts = Object.fromEntries(commands.map(c => [c.name, c.shortcut])); + console.log('[test] shortcuts', shortcuts); + assert.equal(shortcuts['toggle-ncc'], 'Alt+Shift+Y'); + assert.equal(shortcuts._execute_action, ''); + }); + + it('has host permission and shows the cookie', async () => { + assert.ok(await popup.isHidden('.no-permission')); + const rows = await popup.$$eval('.trigger', els => els.map(e => e.textContent.replace(/\s+/g, ' ').trim())); + assert.deepEqual(rows, ['Disable composite cache BITRIX_SM_NCC=Y']); + assert.equal(await title(), '_NCC cookie: off'); + }); + + it('popup opened as a tab (as on Firefox for Android) works for the last used site tab', async () => { + await site.bringToFront(); + const asTab = await context.newPage(); + await asTab.goto(`chrome-extension://${extensionId}/popup.html`); + await eventually(async () => assert.equal(await asTab.textContent('.host'), 'app.example.co.uk')); + await asTab.waitForSelector('.trigger input'); + assert.ok(await asTab.isHidden('.unsupported')); + await asTab.close(); + }); + + it('toggles the cookie, badge and title', async () => { + await popup.check('.trigger input[data-trigger=ncc]'); + // Popup sets the cookie asynchronously after the click + await eventually(async () => assert.match(await pageText(), /BITRIX_SM_NCC=Y/)); + await eventually(async () => assert.equal(await badge(), 'N')); + assert.equal(await title(), 'Composite cache disabled (BITRIX_SM_NCC)'); + }); + + it('restores the cookie deleted by the server (as Bitrix does for anonymous visitors)', async () => { + await site.goto(SITE + 'epilog'); + await eventually(async () => assert.deepEqual(await cookieDomains('BITRIX_SM_NCC'), ['app.example.co.uk=Y'])); + await site.goto(SITE); + assert.match(await site.textContent('body'), /BITRIX_SM_NCC=Y/); + await eventually(async () => assert.equal(await badge(), 'N')); + }); + + it('sets cookie for parent domain and subdomains', async () => { + await openPopup(); + await popup.selectOption('.scope', 'example.co.uk'); + await eventually(async () => assert.deepEqual(await cookieDomains('BITRIX_SM_NCC'), ['.example.co.uk=Y'])); + const other = await context.newPage(); + await other.goto(OTHER_SITE); + assert.match(await other.textContent('body'), /BITRIX_SM_NCC=Y/); + await other.close(); + }); + + it('rolls back when the browser rejects public suffix domain', async () => { + await popup.selectOption('.scope', 'co.uk'); + await eventually(async () => assert.notEqual(await popup.textContent('.error'), '')); + assert.deepEqual(await cookieDomains('BITRIX_SM_NCC'), ['.example.co.uk=Y']); + assert.equal(await popup.inputValue('.scope'), 'example.co.uk'); + await popup.selectOption('.scope', ''); + await eventually(async () => assert.deepEqual(await cookieDomains('BITRIX_SM_NCC'), ['app.example.co.uk=Y'])); + }); + + it('turned off by the user: the server deletion is not restored', async () => { + await openPopup(); + await popup.uncheck('.trigger input[data-trigger=ncc]'); + await eventually(async () => assert.deepEqual(await cookieDomains('BITRIX_SM_NCC'), [])); + await eventually(async () => assert.equal(await badge(), '')); + assert.deepEqual(await worker.evaluate(() => api.storage.local.get(null)), {}, 'no keep marks left'); + // Control: a cookie not turned on by the extension is really deleted by the server response + await worker.evaluate(() => api.cookies.set({ url: 'http://app.example.co.uk/', name: 'BITRIX_SM_NCC', value: 'Y', path: '/' })); + await site.goto(SITE); + assert.match(await site.textContent('body'), /BITRIX_SM_NCC=Y/); + await site.goto(SITE + 'epilog'); + await site.goto(SITE); + assert.doesNotMatch(await site.textContent('body'), /BITRIX_SM_NCC/); + await eventually(async () => assert.deepEqual(await cookieDomains('BITRIX_SM_NCC'), [])); + }); + + it('toggles the cookie by keyboard command', async () => { + await site.bringToFront(); + await worker.evaluate(() => onCommand('toggle-ncc')); + assert.match(await pageText(), /BITRIX_SM_NCC=Y/); + await eventually(async () => assert.equal(await badge(), 'N')); + await worker.evaluate(() => onCommand('toggle-ncc')); + assert.doesNotMatch(await pageText(), /BITRIX_SM_NCC/); + }); + + it('per-site cookie name moves the enabled cookie', async () => { + await openPopup(); + await popup.check('.trigger input[data-trigger=ncc]'); + await eventually(async () => assert.match(await pageText(), /BITRIX_SM_NCC=Y/)); + assert.ok(await popup.isHidden('.site-name-row')); + await popup.click('.trigger-cookie'); + assert.ok(await popup.isVisible('.site-name-row')); + assert.equal(await popup.getAttribute('.site-name', 'placeholder'), 'BITRIX_SM_NCC'); + + await popup.fill('.site-name', 'bad name'); + await popup.click('.site-name-row button[type=submit]'); + await eventually(async () => assert.match(await popup.textContent('.error'), /"bad name"/)); + assert.equal(await worker.evaluate(() => getSiteCookieName('app.example.co.uk')), ''); + + await popup.fill('.site-name', 'SHOP_NCC'); + await popup.click('.site-name-row button[type=submit]'); + await eventually(async () => { + const text = await pageText(); + assert.match(text, /SHOP_NCC=Y/); + assert.doesNotMatch(text, /BITRIX_SM_NCC/); + }); + await popup.waitForSelector('.trigger-cookie'); + assert.equal(await popup.textContent('.trigger-cookie'), 'SHOP_NCC=Y - for this site'); + await eventually(async () => assert.equal(await title(), 'Composite cache disabled (SHOP_NCC)')); + + // The kept cookie with the site name is restored too + await site.goto(SITE + 'epilog'); + await site.goto(SITE); + await eventually(async () => assert.deepEqual(await cookieDomains('SHOP_NCC'), ['app.example.co.uk=Y'])); + + // Other sites use the global name + const other = await context.newPage(); + await other.goto(OTHER_SITE); + const otherTabId = await worker.evaluate(async () => (await api.tabs.query({ url: 'http://other.example.co.uk/*' }))[0].id); + await worker.evaluate(id => api.tabs.update(id, { active: true }), otherTabId); + await worker.evaluate(() => onCommand('toggle-ncc')); + await other.reload(); + assert.match(await other.textContent('body'), /BITRIX_SM_NCC=Y/); + assert.doesNotMatch(await other.textContent('body'), /SHOP_NCC/); + await worker.evaluate(() => onCommand('toggle-ncc')); + await other.close(); + + await openPopup(); + assert.ok(await popup.isVisible('.site-name-row'), 'stays open when the site name is set'); + await popup.click('.site-name-reset'); + await eventually(async () => { + const text = await pageText(); + assert.match(text, /BITRIX_SM_NCC=Y/); + assert.doesNotMatch(text, /SHOP_NCC/); + }); + await popup.waitForSelector('.trigger input'); + await popup.uncheck('.trigger input[data-trigger=ncc]'); + await eventually(async () => assert.doesNotMatch(await pageText(), /NCC/)); + }); + + it('options: name of version 0.1, invalid name, rename removes the old cookie', async () => { + // Version 0.1 saved the placeholder "?????_NCC" when the name was left as is + await worker.evaluate(() => api.storage.sync.set({ cookie_ncc: '?????_NCC', cookie_ncc_on_color: 'green', cookie_ncc_off_color: 'dark' })); + const options = await context.newPage(); + options.on('console', m => m.type() === 'error' && errors.push('options: ' + m.text())); + await options.goto(`chrome-extension://${extensionId}/options.html`); + await eventually(async () => assert.equal(await options.inputValue('.cookie_ncc'), 'BITRIX_SM_NCC')); + assert.ok(await options.isHidden('.permission-row')); + assert.ok(await options.isChecked('.cookie-ncc-on-color[value=green]')); + assert.ok(await options.isChecked('.cookie-ncc-off-color[value=dark]')); + + await openPopup(); + await popup.check('.trigger input[data-trigger=ncc]'); + await eventually(async () => assert.match(await pageText(), /BITRIX_SM_NCC=Y/)); + + await options.fill('.cookie_ncc', 'BAD NAME'); + await options.click('button[type=submit]'); + await eventually(async () => assert.match(await options.textContent('.saved-error'), /"BAD NAME"/)); + assert.equal((await worker.evaluate(() => api.storage.sync.get('cookie_ncc'))).cookie_ncc, '?????_NCC'); + + await options.fill('.cookie_ncc', 'MYPREFIX_NCC'); + await options.click('button[type=submit]'); + await eventually(async () => assert.ok(await options.isVisible('.saved-successfully'))); + // The old cookie is removed, otherwise it would keep the composite cache disabled + await eventually(async () => assert.doesNotMatch(await pageText(), /BITRIX_SM_NCC/)); + await eventually(async () => assert.equal(await badge(), '')); + + await options.fill('.cookie_ncc', ''); + await options.click('button[type=submit]'); + await eventually(async () => assert.equal((await worker.evaluate(() => api.storage.sync.get('cookie_ncc'))).cookie_ncc, '')); + await options.reload(); + await eventually(async () => assert.equal(await options.inputValue('.cookie_ncc'), 'BITRIX_SM_NCC')); + await options.close(); + }); + + it('opens the page with ?ncc=1', async () => { + await openPopup(); + const closed = popup.waitForEvent('close').then(() => true); + await popup.click('.open-ncc'); + await eventually(async () => assert.equal(site.url(), SITE + '?ncc=1')); + await eventually(async () => assert.match(await site.textContent('body'), /url=\/\?ncc=1/)); + assert.equal(await closed, true, 'popup is closed'); + popup = await context.newPage(); + popup.on('console', m => m.type() === 'error' && errors.push(m.text())); + }); + + it('shows unsupported message on browser pages', async () => { + // Without the "tabs" permission non-http tabs can't be found by URL, so the site tab is reused + await site.goto('about:blank'); + await popup.goto(`chrome-extension://${extensionId}/popup.html?tabId=${siteTabId}`); + await eventually(async () => assert.ok(await popup.isVisible('.unsupported'))); + assert.ok(await popup.isHidden('.controls')); + }); + + it('has no unexpected console errors', () => { + // The public suffix rejection is expected and logged by popup + const unexpected = errors.filter(e => !e.includes('scope change failed')); + assert.deepEqual(unexpected, []); + }); +}); + +for (const [lang, label, settingsLabel, titleStart] of [ + ['ru', 'Отключить композитный кеш', 'Настройки', 'Cookie _NCC: '], + ['kk', 'Композиттік кэшті өшіру', 'Баптаулар', '_NCC cookie: '] +]) { + describe('Chromium UI language: ' + lang, () => { + let server, context, extensionId; + + before(async () => { + server = await startServer(); + ({ context, extensionId } = await launch(lang)); + }); + + after(async () => { + await context.close(); + server.close(); + }); + + it('popup and options are translated', async () => { + const site = await context.newPage(); + await site.goto(SITE); + const page = await context.newPage(); + const worker = context.serviceWorkers()[0]; + const tabId = await worker.evaluate(async () => (await api.tabs.query({ url: 'http://app.example.co.uk/*' }))[0].id); + await page.goto(`chrome-extension://${extensionId}/popup.html?tabId=${tabId}`); + await page.waitForSelector('.trigger input'); + assert.equal(await page.textContent('.trigger-label'), label); + assert.equal(await page.textContent('.open-options'), settingsLabel); + assert.equal(await page.getAttribute('html', 'lang'), lang); + await eventually(async () => { + const title = await worker.evaluate(id => api.action.getTitle({ tabId: id }), tabId); + console.log('[test]', lang, 'action title:', title); + assert.ok(title.startsWith(titleStart), title); + }); + await page.goto(`chrome-extension://${extensionId}/options.html`); + assert.match(await page.textContent('.hint'), /\.config\.php/); + }); + }); +} + +describe('Chromium incognito', () => { + let server, context, extensionId; + + before(async () => { + server = await startServer(); + // Incognito tries https:// first (HTTPS-First mode) and the http-only test server makes + // the page load wait for that attempt; Playwright's own HttpsUpgrades switch is kept + ({ context, extensionId } = await launch(undefined, ['--disable-features=HttpsUpgrades,HttpsFirstModeIncognito'])); + }); + + after(async () => { + await context.close(); + server.close(); + }); + + it('works in incognito after the user allows it (own cookie store)', async () => { + // Same switches as on chrome://extensions: developer mode (unpacked addon) and "Allow in Incognito" + const page = await context.newPage(); + await page.goto('chrome://extensions/'); + const info = id => page.evaluate(i => new Promise(r => chrome.developerPrivate.getExtensionInfo(i, r)), id); + assert.equal((await info(extensionId)).incognitoAccess.isActive, false, 'not allowed in incognito by default'); + await page.evaluate(() => new Promise(r => chrome.developerPrivate.updateProfileConfiguration({ inDeveloperMode: true }, r))); + await page.evaluate(id => new Promise(r => chrome.developerPrivate.updateExtensionConfiguration( + { extensionId: id, incognitoAccess: true }, r)), extensionId); + await eventually(async () => assert.equal((await info(extensionId)).state, 'ENABLED'), 10000); + + // The addon is reloaded, its functions are called from an extension page + const extPage = await context.newPage(); + await eventually(() => extPage.goto(`chrome-extension://${extensionId}/options.html`), 10000); + const result = await extPage.evaluate(async site => { + const win = await api.windows.create({ incognito: true, url: site }); + let tab; + for (let i = 0; i < 50; i++) { + tab = (await api.tabs.query({ windowId: win.id }))[0]; + // A new window first reports "complete" for about:blank, so wait for the site itself + if (tab.status === 'complete' && tab.url === site) { + break; + } + await new Promise(r => setTimeout(r, 200)); + } + const count = async storeId => (await api.cookies.getAll({ name: 'BITRIX_SM_NCC', storeId: storeId })).length; + const out = { incognito: tab.incognito, store: await getStoreId(tab) }; + out.enabled = await toggleTrigger(tab, 'ncc'); + out.inIncognito = await count('1'); + out.inNormal = await count('0'); + // The server deletes the cookie in the incognito store: it is restored there + await api.tabs.update(tab.id, { url: site + 'epilog' }); + for (let i = 0; i < 50 && (await api.tabs.get(tab.id)).url !== site + 'epilog'; i++) { + await new Promise(r => setTimeout(r, 100)); + } + await new Promise(r => setTimeout(r, 1000)); + out.restored = await count('1'); + out.disabled = await toggleTrigger(tab, 'ncc'); + out.afterOff = await count('1'); + await api.windows.remove(win.id); + return out; + }, SITE); + assert.deepEqual(result, { incognito: true, store: '1', enabled: true, inIncognito: 1, inNormal: 0, restored: 1, disabled: false, afterOff: 0 }); + }); +}); diff --git a/tests/firefox.test.js b/tests/firefox.test.js new file mode 100644 index 0000000..15a15c0 --- /dev/null +++ b/tests/firefox.test.js @@ -0,0 +1,429 @@ +// End-to-end tests in Firefox via WebDriver (geckodriver): the built addon is installed as temporary. +// Run: npm run test:firefox (needs Firefox; geckodriver is found or downloaded by Selenium Manager) +const { describe, it, before, after } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { Builder, By } = require('selenium-webdriver'); +const firefox = require('selenium-webdriver/firefox'); +const { Select } = require('selenium-webdriver/lib/select'); +const { SITE, OTHER_SITE, startServer, eventually } = require('./helpers'); + +const ADDON_ID = 'ncc_cookie_setup@goodprogrammist.ru'; +// Fixed internal UUID, so extension pages have a known moz-extension:// URL +const ADDON_UUID = '3e0c1f0a-6b1d-4a52-9d6c-7a5b0f1c2d3e'; +const BASE = `moz-extension://${ADDON_UUID}/`; + +function findPackage() { + const dir = path.resolve(__dirname, '..', 'web-ext-artifacts'); + const zip = fs.readdirSync(dir).find(f => f.endsWith('.zip')); + assert.ok(zip, 'run `npm run build` first'); + return path.join(dir, zip); +} + +describe('Firefox', () => { + let server, driver, siteHandle, popupHandle, siteTabId; + + // Calls a function of the extension page by its path (e.g. 'browser.tabs.query') with JSON arguments. + // The current tab must be an extension page. CSP of extension pages forbids eval/new Function, + // so only existing functions are called. + async function inExtension(fnPath, ...args) { + const json = await driver.executeAsyncScript(` + const done = arguments[arguments.length - 1]; + const win = window.wrappedJSObject || window; + const parts = arguments[0].split('.'); + let context = win; + for (const part of parts.slice(0, -1)) { + context = context[part]; + } + const fn = context[parts[parts.length - 1]]; + Promise.resolve(fn.apply(context, win.JSON.parse(arguments[1]))).then( + r => done(win.JSON.stringify(r === undefined ? null : r)), + e => done(JSON.stringify({ __error: String(e) })) + ); + `, fnPath, JSON.stringify(args)); + const result = JSON.parse(json); + if (result && result.__error) { + throw new Error(fnPath + ': ' + result.__error); + } + return result; + } + + async function sitePageText() { + await driver.switchTo().window(siteHandle); + await driver.navigate().refresh(); + const text = await driver.findElement(By.css('body')).getText(); + await driver.switchTo().window(popupHandle); + return text; + } + + async function openPopup(tabId = siteTabId) { + await driver.switchTo().window(popupHandle); + await openExtensionPage(`${BASE}popup.html?tabId=${tabId}`); + await eventually(async () => { + const rows = await driver.findElements(By.css('.trigger input')); + assert.ok(rows.length > 0 || await isVisible('.unsupported')); + }); + } + + // WebDriver can't navigate content to moz-extension:// URLs, so the page is loaded + // from the browser (chrome) context with the system principal + async function openExtensionPage(url) { + await driver.setContext(firefox.Context.CHROME); + // In the chrome context `window` is the browser window of the current WebDriver tab + await driver.executeScript(` + window.gBrowser.selectedBrowser.fixupAndLoadURIString(arguments[0], { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal() + });`, url); + await driver.setContext(firefox.Context.CONTENT); + await eventually(async () => assert.equal(await driver.getCurrentUrl(), url)); + await eventually(async () => assert.equal(await driver.executeScript('return document.readyState'), 'complete')); + } + + async function isVisible(selector) { + const elements = await driver.findElements(By.css(selector)); + return elements.length > 0 && await elements[0].isDisplayed(); + } + + const badge = () => inExtension('browser.action.getBadgeText', { tabId: siteTabId }); + const cookies = async name => (await inExtension('browser.cookies.getAll', { name: name, firstPartyDomain: null })) + .map(c => c.domain + '=' + c.value); + const select = async (selector, value) => { + await new Select(await driver.findElement(By.css(selector))).selectByValue(value); + }; + + before(async () => { + server = await startServer(); + const options = new firefox.Options() + .addArguments('-headless') + .setPreference('extensions.webextensions.uuids', JSON.stringify({ [ADDON_ID]: ADDON_UUID })) + // Test hosts are resolved to 127.0.0.1 + .setPreference('network.dns.localDomains', 'app.example.co.uk,other.example.co.uk') + .setPreference('dom.security.https_first', false) + .setPreference('dom.security.https_first_schemeless', false) + // Direct connection: test hosts must not go through a system proxy + .setPreference('network.proxy.type', 0) + // Containers for the container tab test + .setPreference('privacy.userContext.enabled', true); + // Custom Firefox / geckodriver, otherwise they are found by Selenium Manager + if (process.env.FIREFOX_PATH) { + options.setBinary(process.env.FIREFOX_PATH); + } + // --allow-system-access: needed for the chrome context used by openExtensionPage() + const service = new firefox.ServiceBuilder(process.env.GECKODRIVER_PATH || undefined) + .addArguments('--allow-system-access'); + driver = await new Builder().forBrowser('firefox') + .setFirefoxOptions(options) + .setFirefoxService(service) + .build(); + const caps = await driver.getCapabilities(); + console.log('[test] Firefox', caps.get('browserVersion')); + + await driver.installAddon(findPackage(), true); + // The addon opens its options (about:addons) after install and Firefox may reuse + // the current blank tab for it, so the site is opened in a new tab after that + await eventually(async () => { + let found = false; + for (const handle of await driver.getAllWindowHandles()) { + await driver.switchTo().window(handle); + found = found || (await driver.getCurrentUrl()).startsWith('about:addons'); + } + assert.ok(found, 'options page is not opened yet'); + }, 10000); + await driver.switchTo().newWindow('tab'); + siteHandle = await driver.getWindowHandle(); + await driver.get(SITE); + await driver.switchTo().newWindow('tab'); + popupHandle = await driver.getWindowHandle(); + await openExtensionPage(BASE + 'options.html'); + siteTabId = await eventually(async () => { + const tabs = await inExtension('browser.tabs.query', { url: 'http://app.example.co.uk/*' }); + assert.equal(tabs.length, 1); + return tabs[0].id; + }); + console.log('[test] site tab id', siteTabId); + await openPopup(); + }); + + after(async () => { + if (driver) { + await driver.quit(); + } + server.close(); + }); + + const title = () => inExtension('browser.action.getTitle', { tabId: siteTabId }); + + async function goSite(url) { + await driver.switchTo().window(siteHandle); + await driver.get(url); + const text = await driver.findElement(By.css('body')).getText(); + await driver.switchTo().window(popupHandle); + return text; + } + + it('runs as Firefox and has host permission', async () => { + assert.equal((await inExtension('browser.runtime.getBrowserInfo')).name, 'Firefox'); + assert.equal(await inExtension('hasHostPermission'), true); + assert.ok(!await isVisible('.no-permission')); + }); + + it('keyboard shortcut is assigned', async () => { + const commands = await inExtension('browser.commands.getAll'); + const shortcuts = Object.fromEntries(commands.map(c => [c.name, c.shortcut])); + console.log('[test] shortcuts', shortcuts); + assert.equal(shortcuts['toggle-ncc'], 'Alt+Shift+Y'); + }); + + it('shows the cookie', async () => { + const rows = []; + for (const el of await driver.findElements(By.css('.trigger'))) { + rows.push((await el.getText()).replace(/\s+/g, ' ').trim()); + } + assert.deepEqual(rows, ['Disable composite cache BITRIX_SM_NCC=Y']); + }); + + it('popup opened as a tab (as on Firefox for Android) works for the last used site tab', async () => { + await driver.switchTo().window(siteHandle); + await driver.switchTo().window(popupHandle); + await openExtensionPage(BASE + 'popup.html'); + await eventually(async () => assert.equal(await driver.findElement(By.css('.host')).getText(), 'app.example.co.uk')); + assert.ok(!await isVisible('.unsupported')); + assert.equal((await driver.findElements(By.css('.trigger input'))).length, 1); + await openPopup(); + }); + + it('toggles the cookie, badge and title', async () => { + await driver.findElement(By.css('.trigger input[data-trigger=ncc]')).click(); + await eventually(async () => assert.match(await sitePageText(), /BITRIX_SM_NCC=Y/)); + await eventually(async () => assert.equal(await badge(), 'N')); + assert.equal(await title(), 'Composite cache disabled (BITRIX_SM_NCC)'); + }); + + it('restores the cookie deleted by the server (as Bitrix does for anonymous visitors)', async () => { + await goSite(SITE + 'epilog'); + await eventually(async () => assert.deepEqual(await cookies('BITRIX_SM_NCC'), ['app.example.co.uk=Y'])); + assert.match(await goSite(SITE), /BITRIX_SM_NCC=Y/); + }); + + it('sets cookie for parent domain', async () => { + await openPopup(); + await select('.scope', 'example.co.uk'); + await eventually(async () => assert.deepEqual(await cookies('BITRIX_SM_NCC'), ['.example.co.uk=Y'])); + assert.match(await goSite(OTHER_SITE), /BITRIX_SM_NCC=Y/); + // The domain cookie is restored too + await goSite(OTHER_SITE + 'epilog'); + await eventually(async () => assert.deepEqual(await cookies('BITRIX_SM_NCC'), ['.example.co.uk=Y'])); + await goSite(SITE); + await openPopup(); + }); + + it('rolls back when the browser rejects public suffix domain', async () => { + await select('.scope', 'co.uk'); + await eventually(async () => assert.ok(await isVisible('.error'))); + console.log('[test] error:', await driver.findElement(By.css('.error')).getText()); + assert.deepEqual(await cookies('BITRIX_SM_NCC'), ['.example.co.uk=Y']); + await select('.scope', ''); + await eventually(async () => assert.deepEqual(await cookies('BITRIX_SM_NCC'), ['app.example.co.uk=Y'])); + }); + + it('turned off by the user: not restored, the server deletion really works', async () => { + await openPopup(); + await driver.findElement(By.css('.trigger input[data-trigger=ncc]')).click(); + await eventually(async () => assert.deepEqual(await cookies('BITRIX_SM_NCC'), [])); + await eventually(async () => assert.equal(await badge(), '')); + assert.deepEqual(await inExtension('browser.storage.local.get', null), {}, 'no keep marks left'); + // Control: a cookie not turned on by the extension is deleted by the server response + await inExtension('browser.cookies.set', { url: SITE, name: 'BITRIX_SM_NCC', value: 'Y', path: '/' }); + assert.match(await goSite(SITE), /BITRIX_SM_NCC=Y/); + await goSite(SITE + 'epilog'); + assert.doesNotMatch(await goSite(SITE), /BITRIX_SM_NCC/); + await eventually(async () => assert.deepEqual(await cookies('BITRIX_SM_NCC'), [])); + }); + + it('per-site cookie name moves the enabled cookie', async () => { + await openPopup(); + await driver.findElement(By.css('.trigger input[data-trigger=ncc]')).click(); + await eventually(async () => assert.match(await sitePageText(), /BITRIX_SM_NCC=Y/)); + await openPopup(); + await driver.findElement(By.css('.trigger-cookie')).click(); + assert.ok(await isVisible('.site-name')); + const input = await driver.findElement(By.css('.site-name')); + await input.sendKeys('SHOP_NCC'); + await driver.findElement(By.css('.site-name-row button[type=submit]')).click(); + await eventually(async () => { + const text = await sitePageText(); + assert.match(text, /SHOP_NCC=Y/); + assert.doesNotMatch(text, /BITRIX_SM_NCC/); + }); + await eventually(async () => assert.equal(await title(), 'Composite cache disabled (SHOP_NCC)')); + await goSite(SITE + 'epilog'); + await eventually(async () => assert.deepEqual(await cookies('SHOP_NCC'), ['app.example.co.uk=Y'])); + + await openPopup(); + await eventually(async () => assert.ok(await isVisible('.site-name-reset'))); + await driver.findElement(By.css('.site-name-reset')).click(); + await eventually(async () => { + const text = await sitePageText(); + assert.match(text, /BITRIX_SM_NCC=Y/); + assert.doesNotMatch(text, /SHOP_NCC/); + }); + await openPopup(); + await driver.findElement(By.css('.trigger input[data-trigger=ncc]')).click(); + await eventually(async () => assert.doesNotMatch(await sitePageText(), /NCC/)); + }); + + it('options page: name of version 0.1 is shown as the default, a new name is saved', async () => { + await inExtension('browser.storage.sync.set', { cookie_ncc: '?????_NCC' }); + await openExtensionPage(BASE + 'options.html'); + await eventually(async () => assert.equal(await driver.findElement(By.css('.cookie_ncc')).getAttribute('value'), 'BITRIX_SM_NCC')); + const input = await driver.findElement(By.css('.cookie_ncc')); + await input.clear(); + await input.sendKeys('MYPREFIX_NCC'); + await driver.findElement(By.css('button[type=submit]')).click(); + await eventually(async () => assert.equal((await inExtension('browser.storage.sync.get', 'cookie_ncc')).cookie_ncc, 'MYPREFIX_NCC')); + await inExtension('browser.storage.sync.set', { cookie_ncc: '' }); + }); + + it('opens the page with ?ncc=1', async () => { + await goSite(SITE); + await openPopup(); + await driver.findElement(By.css('.open-ncc')).click(); + await driver.switchTo().window(siteHandle); + try { + await eventually(async () => assert.equal(await driver.getCurrentUrl(), SITE + '?ncc=1')); + await eventually(async () => assert.match(await driver.findElement(By.css('body')).getText(), /url=\/\?ncc=1/)); + } finally { + // Next tests load extension pages into the current tab, it must not be the site tab + await driver.switchTo().window(popupHandle); + } + }); + + // Opens a tab from the browser (chrome) context: in a container or in a new private window + async function openSpecialTab(url, kind) { + await driver.setContext(firefox.Context.CHROME); + await driver.executeScript(` + const principal = Services.scriptSecurityManager.getSystemPrincipal(); + if (arguments[1] === 'private') { + const privateWin = window.OpenBrowserWindow({ private: true }); + // The window is ready for navigation only after its delayed startup, + // otherwise the initial about:privatebrowsing page may replace the site + privateWin.addEventListener('load', () => { + privateWin.delayedStartupPromise.then(() => { + privateWin.gBrowser.selectedBrowser.fixupAndLoadURIString(arguments[0], { triggeringPrincipal: principal }); + }); + }, { once: true }); + } else { + window.gBrowser.addTab(arguments[0], { userContextId: 1, triggeringPrincipal: principal }); + }`, url, kind); + await driver.setContext(firefox.Context.CONTENT); + } + + it('works in a container tab (own cookie store), restore stays in the container', async () => { + await openSpecialTab(SITE, 'container'); + await openExtensionPage(BASE + 'options.html'); + const tab = await eventually(async () => { + const tabs = await inExtension('browser.tabs.query', { cookieStoreId: 'firefox-container-1' }); + assert.equal(tabs.length, 1); + assert.equal(tabs[0].status, 'complete'); + return tabs[0]; + }, 10000); + await openPopup(tab.id); + await driver.findElement(By.css('.trigger input[data-trigger=ncc]')).click(); + const inStore = async storeId => (await inExtension('browser.cookies.getAll', + { name: 'BITRIX_SM_NCC', storeId: storeId, firstPartyDomain: null })).length; + await eventually(async () => assert.equal(await inStore('firefox-container-1'), 1)); + assert.equal(await inStore('firefox-default'), 0, 'cookie must not leak to the default store'); + await inExtension('browser.tabs.update', tab.id, { url: SITE + 'epilog' }); + await eventually(async () => assert.equal((await inExtension('browser.tabs.get', tab.id)).url, SITE + 'epilog')); + await eventually(async () => assert.equal(await inStore('firefox-container-1'), 1)); + assert.equal(await inStore('firefox-default'), 0, 'restored cookie must not leak to the default store'); + await openPopup(tab.id); + await driver.findElement(By.css('.trigger input[data-trigger=ncc]')).click(); + await eventually(async () => assert.equal(await inStore('firefox-container-1'), 0)); + await inExtension('browser.tabs.remove', tab.id); + }); + + // Same as the user switch "Run in Private Windows" in about:addons: permission + addon reload + async function allowInPrivateWindows() { + await driver.setContext(firefox.Context.CHROME); + await driver.executeAsyncScript(` + const done = arguments[arguments.length - 1]; + const { ExtensionPermissions } = ChromeUtils.importESModule('resource://gre/modules/ExtensionPermissions.sys.mjs'); + const { AddonManager } = ChromeUtils.importESModule('resource://gre/modules/AddonManager.sys.mjs'); + ExtensionPermissions.add(arguments[0], { permissions: ['internal:privateBrowsingAllowed'], origins: [] }) + .then(() => AddonManager.getAddonByID(arguments[0])) + .then(addon => addon.reload()) + .then(() => done('ok'), e => done(String(e)));`, ADDON_ID); + await driver.setContext(firefox.Context.CONTENT); + } + + it('is not active in private windows until the user allows it', async () => { + await openSpecialTab(SITE, 'private'); + await openExtensionPage(BASE + 'options.html'); + // Wait until the private tab exists for the browser, then make sure the addon does not see it + await new Promise(resolve => setTimeout(resolve, 2000)); + assert.equal((await inExtension('browser.tabs.query', { cookieStoreId: 'firefox-private' })).length, 0); + assert.equal(await inExtension('browser.extension.isAllowedIncognitoAccess'), false); + }); + + it('works in a private window when allowed', async () => { + await allowInPrivateWindows(); + // The reload closes extension pages, including the tab used for them + await driver.switchTo().window(siteHandle); + await driver.switchTo().newWindow('tab'); + popupHandle = await driver.getWindowHandle(); + await eventually(async () => { + await openExtensionPage(BASE + 'options.html'); + assert.equal(await inExtension('hasHostPermission'), true); + }, 10000); + assert.equal(await inExtension('browser.extension.isAllowedIncognitoAccess'), true); + await openExtensionPage(BASE + 'options.html'); + // Right after the addon reload the page may still be initializing, so the call is retried + const before = await eventually(async () => { + const tabs = await inExtension('browser.tabs.query', { cookieStoreId: 'firefox-private' }); + assert.ok(Array.isArray(tabs)); + return tabs; + }, 10000); + console.log('[test] private tabs opened before the permission, url:', before.map(t => t.url)); + // A private window opened after the permission is granted (the usual user flow) + await openSpecialTab(SITE, 'private'); + await openExtensionPage(BASE + 'options.html'); + const tab = await eventually(async () => { + const tabs = (await inExtension('browser.tabs.query', { cookieStoreId: 'firefox-private' })) + .filter(t => t.url === SITE && t.status === 'complete'); + // Both private windows (opened before and after the permission) show the site + assert.equal(tabs.length, 2); + return tabs[1]; + }, 10000); + assert.equal(tab.incognito, true); + await openPopup(tab.id); + await driver.findElement(By.css('.trigger input[data-trigger=ncc]')).click(); + const inStore = async storeId => (await inExtension('browser.cookies.getAll', + { name: 'BITRIX_SM_NCC', storeId: storeId, firstPartyDomain: null })).length; + await eventually(async () => assert.equal(await inStore('firefox-private'), 1)); + assert.equal(await inStore('firefox-default'), 0, 'cookie must not leak to the default store'); + await inExtension('browser.windows.remove', tab.windowId); + }); + + it('sets and removes cookie of an IPv6 host', async () => { + await openExtensionPage(BASE + 'options.html'); + // Cookie API does not need a loaded page, the tab object is enough + const fakeTab = { id: -1, url: 'http://[::1]:8765/' }; + assert.equal(await inExtension('toggleTrigger', fakeTab, 'ncc'), true); + const list = async () => (await inExtension('browser.cookies.getAll', + { name: 'BITRIX_SM_NCC', firstPartyDomain: null })).map(c => c.domain); + console.log('[test] IPv6 cookie domain:', await list()); + assert.equal((await list()).length, 1); + assert.equal(await inExtension('toggleTrigger', fakeTab, 'ncc'), false); + assert.deepEqual(await list(), []); + }); + + it('shows unsupported message on browser pages', async () => { + await driver.switchTo().window(siteHandle); + await driver.get('about:blank'); + // The site tab now shows about:blank + await openPopup(siteTabId); + assert.ok(await isVisible('.unsupported')); + }); +}); diff --git a/tests/helpers.js b/tests/helpers.js new file mode 100644 index 0000000..90966c3 --- /dev/null +++ b/tests/helpers.js @@ -0,0 +1,44 @@ +// Shared helpers for browser tests +const http = require('node:http'); + +// Test hosts are resolved to 127.0.0.1 by browser settings (see tests) +const PORT = Number(process.env.TEST_PORT || 8765); +const SITE = `http://app.example.co.uk:${PORT}/`; +const OTHER_SITE = `http://other.example.co.uk:${PORT}/`; + +// Echo server: the page body shows the request url and cookies sent by the browser. +// /epilog deletes the cookie as Bitrix does for anonymous visitors (Engine::deleteCompositeCookies(): +// $APPLICATION->set_cookie("NCC", "", 0) - PHP setcookie() with an empty value sends "deleted" with a past date) +const DELETE_NCC = 'BITRIX_SM_NCC=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/'; + +function startServer() { + return new Promise(resolve => { + const server = http.createServer((req, res) => { + res.setHeader('content-type', 'text/plain; charset=utf-8'); + if (req.url.startsWith('/epilog')) { + res.setHeader('set-cookie', DELETE_NCC); + } + res.end('url=' + req.url + ' cookie=' + (req.headers.cookie || '')); + }); + server.listen(PORT, () => resolve(server)); + }); +} + +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + +// Retries the check until it passes or timeout expires (async browser events) +async function eventually(check, timeout = 5000) { + const start = Date.now(); + let lastError; + while (Date.now() - start < timeout) { + try { + return await check(); + } catch (e) { + lastError = e; + await sleep(100); + } + } + throw lastError; +} + +module.exports = { PORT, SITE, OTHER_SITE, startServer, sleep, eventually }; diff --git a/tests/unit.test.js b/tests/unit.test.js new file mode 100644 index 0000000..1415695 --- /dev/null +++ b/tests/unit.test.js @@ -0,0 +1,219 @@ +// Background script and shared code without a browser: reduced environments (Firefox for Android), +// API helpers, settings migration and restoring the cookie removed by the server. +// Firefox for Android has no windows and commands APIs (@mdn/browser-compat-data: +// webextensions.api.windows / commands - firefox_android: false), other browsers may lack other APIs. +// Run: node --test tests/unit.test.js +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); + +function event(name, registered) { + return { addListener: () => registered.push(name) }; +} + +// Loads common.js (+ background.js) into a sandbox with the given `browser` object +function load(browser, files) { + const errors = []; + const context = vm.createContext({ + browser: browser, + console: { debug: () => {}, warn: () => {}, error: (...args) => errors.push(args.map(String).join(' ')) }, + URL: URL + }); + context.globalThis = context; + for (const file of files) { + vm.runInContext(fs.readFileSync(path.join(__dirname, '..', file), 'utf8'), context, { filename: file }); + } + return { context, errors }; +} + +// Simple storage area: get(null | key | [keys]), set, remove +function storageArea(data) { + return { + data: data, + get: async keys => { + if (keys === null) { + return Object.assign({}, data); + } + const result = {}; + for (const key of [].concat(keys)) { + if (key in data) { + result[key] = data[key]; + } + } + return result; + }, + set: async items => { Object.assign(data, items); }, + remove: async keys => { for (const key of [].concat(keys)) delete data[key]; } + }; +} + +// Firefox for Android: everything except windows and commands +function androidBrowser(registered, calls) { + return { + runtime: { getBrowserInfo: async () => ({ name: 'Firefox' }), onInstalled: event('runtime.onInstalled', registered), openOptionsPage: async () => {} }, + i18n: { getMessage: key => key }, + tabs: { query: async () => [], get: async () => ({}), onUpdated: event('tabs.onUpdated', registered), onActivated: event('tabs.onActivated', registered) }, + cookies: { onChanged: event('cookies.onChanged', registered), getAllCookieStores: async () => [], get: async () => null }, + storage: { onChanged: event('storage.onChanged', registered), sync: storageArea({}), local: storageArea({}) }, + permissions: { contains: async () => true, onAdded: event('permissions.onAdded', registered), onRemoved: event('permissions.onRemoved', registered) }, + action: { + setIcon: async () => calls.push('setIcon'), + setTitle: async () => calls.push('setTitle'), + setBadgeText: async () => calls.push('setBadgeText'), + setBadgeBackgroundColor: async () => calls.push('setBadgeBackgroundColor') + } + }; +} + +test('background script starts without windows and commands APIs (Firefox for Android)', async () => { + const registered = []; + const { errors } = load(androidBrowser(registered, []), ['common.js', 'background.js']); + await new Promise(resolve => setTimeout(resolve, 50)); + assert.deepEqual(registered.sort(), [ + 'cookies.onChanged', 'permissions.onAdded', 'permissions.onRemoved', 'runtime.onInstalled', + 'storage.onChanged', 'tabs.onActivated', 'tabs.onUpdated' + ]); + assert.deepEqual(errors, []); +}); + +test('icon is updated with a partial action API and without storage.sync and permissions', async () => { + const registered = []; + const calls = []; + const browser = androidBrowser(registered, calls); + delete browser.action.setBadgeBackgroundColor; + delete browser.permissions; + delete browser.storage.sync; + let localGets = 0; + const get = browser.storage.local.get; + browser.storage.local.get = async keys => { + localGets++; + return get(keys); + }; + const { context, errors } = load(browser, ['common.js', 'background.js']); + // Top-level const is not a property of the context, so it is read by an expression + assert.equal(vm.runInContext('SETTINGS_AREA', context), 'local', 'settings fall back to storage.local'); + await context.updateIcon({ id: 1, url: 'http://app.example.com/' }); + assert.deepEqual(calls, ['setIcon', 'setTitle', 'setBadgeText']); + assert.ok(localGets > 0, 'settings are read from storage.local'); + assert.deepEqual(errors, []); +}); + +test('API helpers: getApi, hasApi, callApi, requireApi, addApiListener', () => { + const registered = []; + const browser = { + runtime: {}, + tabs: { query: function (info) { return { self: this === browser.tabs, info: info }; }, onUpdated: event('tabs.onUpdated', registered) } + }; + const { context } = load(browser, ['common.js']); + assert.equal(context.hasApi('tabs.query'), true); + assert.equal(context.hasApi('tabs.getCurrent'), false); + assert.equal(context.hasApi('windows.onFocusChanged'), false); + assert.equal(context.getApi('nothing.at.all'), undefined); + // Method is called with its namespace as `this` + assert.deepEqual(JSON.parse(JSON.stringify(context.callApi('tabs.query', [{ active: true }], 'fallback'))), { self: true, info: { active: true } }); + assert.equal(context.callApi('tabs.getCurrent', [], 'fallback'), 'fallback'); + assert.throws(() => context.requireApi('cookies.set', [{}]), /API is not available in this browser: cookies\.set/); + assert.equal(context.addApiListener('tabs.onUpdated', () => {}), true); + assert.equal(context.addApiListener('commands.onCommand', () => {}), false); + assert.deepEqual(registered, ['tabs.onUpdated']); + assert.equal(vm.runInContext('IS_FIREFOX', context), false); +}); + +test('toggle shows a clear error when the cookies API is missing', async () => { + const browser = androidBrowser([], []); + delete browser.cookies.set; + const { context } = load(browser, ['common.js']); + await assert.rejects(context.toggleTrigger({ id: 1, url: 'http://app.example.com/' }, 'ncc'), /API is not available in this browser: cookies\.set/); +}); + +test('settings: default name, placeholder of version 0.1, global and per-site names, value is always Y', async () => { + const browser = androidBrowser([], []); + const { context } = load(browser, ['common.js']); + const settings = async host => JSON.parse(JSON.stringify((await context.getTriggerSettings(host))[0])); + + let s = await settings(); + assert.equal(s.name, 'BITRIX_SM_NCC'); + assert.equal(s.value, 'Y'); + assert.equal(s.siteName, false); + + // Version 0.1 saved "?????_NCC" when the name was left as is + browser.storage.sync.data.cookie_ncc = '?????_NCC'; + browser.storage.sync.data.cookie_ncc_value = 'N'; + s = await settings(); + assert.equal(s.name, 'BITRIX_SM_NCC'); + assert.equal(s.globalName, 'BITRIX_SM_NCC'); + assert.equal(s.value, 'Y', 'Bitrix checks === "Y", other values are ignored'); + + browser.storage.sync.data.cookie_ncc = 'MYPREFIX_NCC'; + await context.setSiteCookieName('shop.example.com', 'SHOP_NCC'); + assert.equal((await settings('app.example.com')).name, 'MYPREFIX_NCC'); + s = await settings('shop.example.com'); + assert.equal(s.name, 'SHOP_NCC'); + assert.equal(s.globalName, 'MYPREFIX_NCC'); + assert.equal(s.siteName, true); + await context.setSiteCookieName('shop.example.com', ''); + assert.equal((await settings('shop.example.com')).name, 'MYPREFIX_NCC'); + assert.deepEqual(browser.storage.local.data, {}); +}); + +test('nccUrl adds ncc=1 and keeps the rest of the url', () => { + const { context } = load(androidBrowser([], []), ['common.js']); + assert.equal(context.nccUrl('https://shop.example.com/catalog/'), 'https://shop.example.com/catalog/?ncc=1'); + assert.equal(context.nccUrl('https://shop.example.com/?a=1&ncc=0#top'), 'https://shop.example.com/?a=1&ncc=1#top'); +}); + +// Background with cookies.set recorded; Firefox (getBrowserInfo) or Chrome +function restoreEnv(isFirefox) { + const sets = []; + const browser = androidBrowser([], []); + if (!isFirefox) { + delete browser.runtime.getBrowserInfo; + } + browser.cookies.set = async details => { sets.push(details); return details; }; + const { context, errors } = load(browser, ['common.js', 'background.js']); + const cookie = { name: 'BITRIX_SM_NCC', value: 'Y', domain: 'app.example.com', path: '/', hostOnly: true, secure: false, storeId: '0' }; + return { browser, context, errors, sets, cookie }; +} + +for (const [browserName, isFirefox, serverCause] of [['Chrome', false, 'expired_overwrite'], ['Firefox', true, 'explicit']]) { + test(browserName + ': cookie removed by the server is restored only when turned on by the user', async () => { + const { context, errors, sets, cookie } = restoreEnv(isFirefox); + // Not turned on by the extension: not restored + await context.onCookieChanged({ removed: true, cause: serverCause, cookie: cookie }); + assert.equal(sets.length, 0); + + await context.setKeep(cookie, true); + // Overwrite and eviction are not server removals + await context.onCookieChanged({ removed: true, cause: 'overwrite', cookie: cookie }); + await context.onCookieChanged({ removed: true, cause: 'evicted', cookie: cookie }); + assert.equal(sets.length, 0); + + await context.onCookieChanged({ removed: true, cause: serverCause, cookie: cookie }); + assert.deepEqual(JSON.parse(JSON.stringify(sets)), [{ url: 'http://app.example.com/', name: 'BITRIX_SM_NCC', value: 'Y', path: '/', storeId: '0' }]); + + // Other cookies are ignored + await context.onCookieChanged({ removed: true, cause: serverCause, cookie: Object.assign({}, cookie, { name: 'PHPSESSID' }) }); + assert.equal(sets.length, 1); + assert.deepEqual(errors, []); + }); +} + +test('Chrome: removal by the user (devtools, site data) is not restored', async () => { + const { context, sets, cookie } = restoreEnv(false); + await context.setKeep(cookie, true); + await context.onCookieChanged({ removed: true, cause: 'explicit', cookie: cookie }); + assert.equal(sets.length, 0); +}); + +test('domain cookie is restored with its domain, per-site name is recognized', async () => { + const { browser, context, sets, cookie } = restoreEnv(false); + const domainCookie = Object.assign({}, cookie, { name: 'SHOP_NCC', domain: '.example.com', hostOnly: false, secure: true }); + browser.storage.local.data['name:shop.example.com'] = 'SHOP_NCC'; + await context.setKeep(domainCookie, true); + await context.onCookieChanged({ removed: true, cause: 'expired_overwrite', cookie: domainCookie }); + assert.deepEqual(JSON.parse(JSON.stringify(sets)), [{ + url: 'https://example.com/', name: 'SHOP_NCC', value: 'Y', path: '/', storeId: '0', domain: '.example.com' + }]); +}); diff --git a/web-ext-config.mjs b/web-ext-config.mjs new file mode 100644 index 0000000..301c20d --- /dev/null +++ b/web-ext-config.mjs @@ -0,0 +1,18 @@ +// Settings for `web-ext lint` / `web-ext build`: only the addon files go into the package +export default { + ignoreFiles: [ + '.github', + 'node_modules', + 'package.json', + 'package-lock.json', + 'web-ext-config.mjs', + 'web-ext-artifacts', + 'tests', + 'screens', + 'store', + 'docs', + // Not used by the addon (picture for the repository) + 'icons/xdebug.png', + '*.md' + ] +};