From e6e58e4be96f4cd5e21c71ef0d54b7985349ce2f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 19:28:12 +0530 Subject: [PATCH 1/6] docs: production readiness baseline audit --- docs/PRODUCTION_READINESS.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/PRODUCTION_READINESS.md diff --git a/docs/PRODUCTION_READINESS.md b/docs/PRODUCTION_READINESS.md new file mode 100644 index 0000000..3e0daea --- /dev/null +++ b/docs/PRODUCTION_READINESS.md @@ -0,0 +1,26 @@ +# Production readiness + +Audit date: 2026-10-12. Scores are 0-10; 10 only if verifiably complete. Nothing here has been +submitted to the Chrome Web Store; the Web Store listing, screenshots and publishing remain owner actions. + +## Baseline (before this pass) + +| # | Dimension | Score | Evidence / gap | +|---|-----------|-------|----------------| +| 1 | Build and run reproducible | 8 | No build step; `package-lock.json`, deterministic zip script. No single `check` command. | +| 2 | Tests | 8 | 361 node tests (drift, heuristics, privacy, lifecycle, storage) plus Playwright journeys. No store/manifest consistency test. | +| 3 | Static quality | 3 | No ESLint, Prettier or type checking. | +| 4 | Security | 8 | Session-only API key, tight CSP, sender-validated messaging, 0 audit findings, no secrets found. Gap: no SECURITY.md, audit not in CI. | +| 5 | Reliability | 8 | Epoch-guarded storage, alarms, retention pruning, malformed-storage tests. Browser lifecycle tests exist but were not run in this audit. | +| 6 | Config and packaging | 7 | Explicit-allowlist zip, version validation, CHANGELOG, listing text. Screenshots missing (human capture required); Unreleased changelog not versioned. | +| 7 | CI | 6 | test.yml (Node 18/20/22 + browser), release.yml, pages.yml. No lint, no audit. Actions currently failing account-wide (billing), so unverified remotely. | +| 8 | Docs | 7 | README, AGENTS.md, docs/architecture.md, privacy policy and page. Missing SECURITY.md, CONTRIBUTING.md, root ARCHITECTURE.md. | +| 9 | UX / accessibility / performance | 7 | ARIA roles, focus outlines, labelled controls present. No documented contrast or idle-CPU measurement. | +| 10 | Operability | 8 | Local redacted error log, diagnostics page with copy/clear. No documented diagnostics privacy guarantees. | + +Other findings: personal-data scan of tracked files found none (only fake test keys); stray untracked +macOS duplicates ("name 2.ext") existed in the local clone and are now ignored via `.gitignore`. + +## After (this pass) + +See the table below, updated at the end of the pass. From c3eed9d3970a47a36ce1fa9918af2eef5897217f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 19:28:12 +0530 Subject: [PATCH 2/6] chore: add ESLint with clean baseline, lint/audit/check scripts --- background.js | 5 +- eslint.config.mjs | 29 + package-lock.json | 995 +++++++++++++++++++++++++++- package.json | 13 +- tests/browser/fixtures.mjs | 1 + tests/heuristic-policy.test.mjs | 1 - tests/intervention-overlay.test.mjs | 3 +- tests/manifest-runtime.test.mjs | 2 +- tests/static-smoke.test.mjs | 2 +- 9 files changed, 1040 insertions(+), 11 deletions(-) create mode 100644 eslint.config.mjs diff --git a/background.js b/background.js index 9819bb3..4495789 100644 --- a/background.js +++ b/background.js @@ -11,7 +11,7 @@ import { checkDriftLLM } from './llm.js'; import { clearDriftCache } from './drift-cache.js'; import { logError, registerErrorLogAuthority } from './error-log.js'; import { getEffectiveDistractionSites, DEFAULT_DISTRACTION_SITES } from './distraction-sites.js'; -import { clearLlmBackoff, getQuotaBackoffUntil, registerBackoffCallback, setQuotaBackoff } from './llm-backoff.js'; +import { clearLlmBackoff, registerBackoffCallback, setQuotaBackoff } from './llm-backoff.js'; import { applyDwellDelta, computeOnIntentRatio, @@ -26,7 +26,6 @@ import { import { sanitizeSessionHistory, sanitizeUrl, - hostnameFromUrl, pruneByRetention, redactSecrets, SESSION_RETENTION_MS, @@ -1480,7 +1479,7 @@ function triggerIntervention(reason, tabId = null) { const session = result.activeSession; if (!session?.isActive) return null; - let targetTab = null; + let targetTab; if (Number.isInteger(tabId)) { targetTab = await getTab(tabId); } else { diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..1f00c5a --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,29 @@ +import js from '@eslint/js'; +import globals from 'globals'; + +// Extension sources run as browser/service-worker modules or classic content +// scripts; tests, scripts and config run in Node. +export default [ + { ignores: ['node_modules/**', 'dist/**', 'legacy-extension-source/**', 'test-results/**', 'playwright-report/**'] }, + js.configs.recommended, + { + languageOptions: { + ecmaVersion: 2024, + sourceType: 'module', + globals: { ...globals.browser, ...globals.serviceworker, chrome: 'readonly' }, + }, + rules: { + 'no-unused-vars': ['warn', { args: 'none', caughtErrors: 'none' }], + 'no-var': 'error', + eqeqeq: ['error', 'always', { null: 'ignore' }], + }, + }, + { + files: ['content.js', 'page-tracker.js', 'intervention-overlay.js'], + languageOptions: { sourceType: 'script' }, + }, + { + files: ['tests/**/*.mjs', 'scripts/**/*.mjs', '*.mjs'], + languageOptions: { globals: { ...globals.node } }, + }, +]; diff --git a/package-lock.json b/package-lock.json index ec2bc73..3e67417 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,9 +6,254 @@ "": { "name": "intentlock", "devDependencies": { - "@playwright/test": "1.60.0" + "@eslint/js": "10.0.1", + "@playwright/test": "1.60.0", + "eslint": "10.12.0", + "globals": "17.13.0" } }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "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.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "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/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@playwright/test": { "version": "1.60.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", @@ -25,6 +270,379 @@ "node": ">=18" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "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/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/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/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "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/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/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/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": "10.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.12.0.tgz", + "integrity": "sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@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", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "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", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "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/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/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-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/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "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/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "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/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -40,6 +658,256 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "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/globals": { + "version": "17.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz", + "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "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/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/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-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/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/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/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/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "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/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/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/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/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/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/playwright": { "version": "1.60.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", @@ -71,6 +939,131 @@ "engines": { "node": ">=18" } + }, + "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/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/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "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/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/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/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/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/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" + } } } } diff --git a/package.json b/package.json index 8896b35..dd3634c 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,18 @@ "test": "node --test tests/*.test.mjs", "verify:static": "node --test tests/static-smoke.test.mjs tests/manifest-runtime.test.mjs tests/content-bootstrap.test.mjs", "package": "node scripts/package-release.mjs", - "validate:version": "node scripts/validate-version.mjs" + "validate:version": "node scripts/validate-version.mjs", + "lint": "eslint .", + "audit:prod": "npm audit --audit-level=moderate", + "check": "npm run lint && npm test && npm run verify:static && npm run validate:version -- \"v$(node -p \"require('./manifest.json').version\")\"" }, "devDependencies": { - "@playwright/test": "1.60.0" + "@eslint/js": "10.0.1", + "@playwright/test": "1.60.0", + "eslint": "10.12.0", + "globals": "17.13.0" + }, + "engines": { + "node": ">=18" } } diff --git a/tests/browser/fixtures.mjs b/tests/browser/fixtures.mjs index 4db32d6..b8473c4 100644 --- a/tests/browser/fixtures.mjs +++ b/tests/browser/fixtures.mjs @@ -9,6 +9,7 @@ const extensionPath = fileURLToPath(new URL('../../', import.meta.url)); const hosts = new Set(['work.localhost', 'distraction.localhost', 'other.localhost']); export const test = base.extend({ + // eslint-disable-next-line no-empty-pattern -- Playwright requires a destructured first argument journey: async ({}, use, testInfo) => { try { await access(chromium.executablePath()); diff --git a/tests/heuristic-policy.test.mjs b/tests/heuristic-policy.test.mjs index d7676a3..0720c8a 100644 --- a/tests/heuristic-policy.test.mjs +++ b/tests/heuristic-policy.test.mjs @@ -12,7 +12,6 @@ import { getEffectiveBlockList, evaluatePolicyDrift, isUrlAligned, - intentTerms, } from '../heuristic-policy.js'; test('INTENT_CATEGORIES has at least 12 entries', () => { diff --git a/tests/intervention-overlay.test.mjs b/tests/intervention-overlay.test.mjs index 115dac7..e05b993 100644 --- a/tests/intervention-overlay.test.mjs +++ b/tests/intervention-overlay.test.mjs @@ -49,10 +49,9 @@ test('classic overlay script exposes its factory through the narrow global API', }); test('createInterventionOverlay accepts onEndSession callback', () => { - let called = false; // The factory function signature should accept onEndSession without throwing assert.doesNotThrow(() => { - createInterventionOverlay({ onEndSession: () => { called = true; } }); + createInterventionOverlay({ onEndSession: () => {} }); }); }); diff --git a/tests/manifest-runtime.test.mjs b/tests/manifest-runtime.test.mjs index 751a91a..9a2e141 100644 --- a/tests/manifest-runtime.test.mjs +++ b/tests/manifest-runtime.test.mjs @@ -253,7 +253,7 @@ test('packaging validates regular in-repository sources and rejects symlinks or /symbolic link|symlink/i, ); await assert.rejects( - validateRuntimeSources(tempDir, ['..\/outside.js']), + validateRuntimeSources(tempDir, ['../outside.js']), /repository root/i, ); await assert.rejects( diff --git a/tests/static-smoke.test.mjs b/tests/static-smoke.test.mjs index 132de00..5fb45a1 100644 --- a/tests/static-smoke.test.mjs +++ b/tests/static-smoke.test.mjs @@ -258,7 +258,7 @@ test('declare form is intent plus optional minutes', async () => { const start = code.indexOf('function showNewSessionForm'); assert.ok(start >= 0, 'showNewSessionForm must exist'); const rest = code.slice(start + 'function showNewSessionForm'.length); - const nextFn = rest.search(/\n function /); + const nextFn = rest.search(/\n {2}function /); const formFn = nextFn === -1 ? code.slice(start) : code.slice(start, start + 'function showNewSessionForm'.length + nextFn); assert.ok(formFn.length > 50, 'showNewSessionForm body must be extracted'); assert.match(formFn, /Lock in/); From af7461fb0985452c8dc77e6bfb955b3d33e05020 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 19:28:13 +0530 Subject: [PATCH 3/6] test: keep manifest, store listing, package contents and docs in sync --- tests/store-readiness.test.mjs | 63 ++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/store-readiness.test.mjs diff --git a/tests/store-readiness.test.mjs b/tests/store-readiness.test.mjs new file mode 100644 index 0000000..1565c44 --- /dev/null +++ b/tests/store-readiness.test.mjs @@ -0,0 +1,63 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile, access } from 'node:fs/promises'; +import { RUNTIME_FILES } from '../scripts/package-release.mjs'; + +const read = (path) => readFile(new URL(`../${path}`, import.meta.url), 'utf8'); +const manifest = JSON.parse(await read('manifest.json')); + +test('every manifest permission is justified in the store listing', async () => { + const listing = await read('store/LISTING.md'); + for (const permission of manifest.permissions) { + assert.match(listing, new RegExp(`\\*\\*${permission}\\*\\*`), `listing lacks justification for ${permission}`); + } + for (const host of manifest.host_permissions) { + assert.ok(listing.includes(host), `listing lacks justification for host permission ${host}`); + } +}); + +test('manifest requests no permissions beyond the reviewed set', () => { + assert.deepEqual([...manifest.permissions].sort(), ['alarms', 'idle', 'storage', 'tabGroups', 'tabs']); + assert.deepEqual([...manifest.host_permissions].sort(), ['http://*/*', 'https://*/*']); +}); + +test('extension page CSP forbids remote and inline script', () => { + const csp = manifest.content_security_policy.extension_pages; + assert.match(csp, /script-src 'self'/); + assert.doesNotMatch(csp, /unsafe-inline|unsafe-eval|https?:/); +}); + +test('store listing links the published privacy policy and fits store limits', async () => { + const listing = await read('store/LISTING.md'); + assert.match(listing, /privacy\.html/); + const short = listing.split('## Short description')[1].split('##')[0].trim(); + assert.ok(short.length <= 132, `short description is ${short.length} chars (limit 132)`); +}); + +test('packaged files exist and exclude legacy, docs, tests and dev files', async () => { + for (const file of RUNTIME_FILES) await access(new URL(`../${file}`, import.meta.url)); + for (const file of RUNTIME_FILES) { + assert.doesNotMatch(file, /^(legacy-extension-source|docs|tests|scripts|store|node_modules)\//); + assert.doesNotMatch(file, /\.(md|mjs|py|map)$/); + assert.doesNotMatch(file, /(^|\/)\.env/); + } +}); + +test('every script and page the manifest references is packaged', () => { + const referenced = new Set([ + manifest.background.service_worker, + manifest.chrome_url_overrides.newtab, + manifest.options_ui.page, + manifest.action.default_popup, + ...Object.values(manifest.icons), + ...manifest.content_scripts.flatMap((entry) => entry.js), + ...manifest.web_accessible_resources.flatMap((entry) => entry.resources), + ]); + for (const file of referenced) assert.ok(RUNTIME_FILES.includes(file), `${file} is not packaged`); +}); + +test('required public documentation exists', async () => { + for (const file of ['README.md', 'AGENTS.md', 'ARCHITECTURE.md', 'CONTRIBUTING.md', 'SECURITY.md', 'CHANGELOG.md', 'docs/privacy-policy.md', 'docs/site/privacy.html']) { + await access(new URL(`../${file}`, import.meta.url)); + } +}); From e52c0265fc4d957e188d2cab8c36fb39d2ce0a29 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 19:28:13 +0530 Subject: [PATCH 4/6] ci: add lint and audit job, verify package contents; ignore macOS duplicates --- .github/workflows/test.yml | 24 ++++++++++++++++++++++++ .gitignore | 3 +++ 2 files changed, 27 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4e6d0fd..0eb1e4a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,6 +8,26 @@ permissions: contents: read jobs: + # Lint and dependency audit run once on a current Node (ESLint 10 needs >=20.19). + quality: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: false + - name: Set up Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 22.x + - name: Install dependencies + run: npm ci + - name: Lint + run: npm run lint + - name: Dependency audit + run: npm run audit:prod + test: runs-on: ubuntu-latest timeout-minutes: 15 @@ -31,6 +51,10 @@ jobs: run: npm run validate:version -- "v$(node -p "require('./manifest.json').version")" - name: Build release package run: npm run package -- --output "$RUNNER_TEMP/IntentLock-ci.zip" + - name: Check release package contents + run: | + unzip -l "$RUNNER_TEMP/IntentLock-ci.zip" | grep -q manifest.json + ! unzip -l "$RUNNER_TEMP/IntentLock-ci.zip" | grep -E 'legacy-extension-source|tests/|docs/|node_modules|\.env' # Real Chromium journeys load the unpacked MV3 extension against synthetic # localhost fixtures only; no secrets, no network beyond installation. diff --git a/.gitignore b/.gitignore index 7f0bf61..9ce63da 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ xcuserdata/ data/app-state.json data/app-state.json.lock storage/logs/ +# macOS Finder / sync-conflict duplicates such as "background 2.js" +* [0-9].* +* [0-9] From 612e2b46b2c7f60404a671aa35c7613d4d34d33e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 19:28:13 +0530 Subject: [PATCH 5/6] docs: add SECURITY, CONTRIBUTING, ARCHITECTURE; document checks --- ARCHITECTURE.md | 25 +++++++++++++++++++++++++ CHANGELOG.md | 2 ++ CONTRIBUTING.md | 40 ++++++++++++++++++++++++++++++++++++++++ README.md | 4 ++++ SECURITY.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ docs/development.md | 2 +- 6 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 ARCHITECTURE.md create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..2a26658 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,25 @@ +# Architecture (summary) + +Full detail lives in [`docs/architecture.md`](docs/architecture.md); storage layout in +[`docs/storage-schema.md`](docs/storage-schema.md); heuristics in [`docs/heuristic-policy.md`](docs/heuristic-policy.md). + +## Components + +| Piece | Files | Role | +| --- | --- | --- | +| Service worker | `background.js` | Owns session state, drift pipeline, alarms, history retention. Terminated when idle; state is restored from `storage.local` and alarms on wake. | +| Content scripts | `page-tracker.js`, `intervention-overlay.js`, `content.js` | Report dwell, render the lock overlay. Untrusted page context: they can only send an allowlisted set of messages. | +| Extension pages | `newtab`, `popup`, `options`, `history`, `analytics`, `diagnostics`, `intervention` | UI. Only these may mutate session or settings state. | +| Drift logic | `heuristic-policy.js`, `drift.js`, `drift-cache.js` | Local rules decide locks; optional LLM verdict is a second opinion. | +| LLM | `providers.js`, `llm.js`, `llm-backoff.js` | Calls only the user-selected provider with minimized context. | +| Storage | `storage-authority.js`, `storage-client.js`, `storage-queue.js`, `privacy-utils.js` | Epoch-guarded serialized writes, sanitization, redaction. | +| Diagnostics | `error-log.js`, `diagnostics.*` | Local, redacted, bounded error log with manual copy/export. | + +## Trust boundaries + +page -> content script -> (allowlisted messages, sender.tab required) -> service worker -> `storage.local` (state) / `storage.session` (API key) -> optional HTTPS call to the chosen LLM provider. + +## Legacy snapshot + +`legacy-extension-source/` is an old v1.4.1 copy of the extension kept for history only. It is not +loaded, packaged, linted or tested. See `docs/PRODUCTION_READINESS.md` (Blocked on owner) for the removal proposal. diff --git a/CHANGELOG.md b/CHANGELOG.md index 503f742..2c355b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ - Sessions not ended within 24 hours end automatically; history and diagnostics are pruned in storage at startup and hourly. ### Added +- ESLint (`npm run lint`), `npm run audit:prod`, `npm run check`, and a CI quality job (lint + dependency audit); package contents are checked in CI. +- `SECURITY.md`, `CONTRIBUTING.md`, root `ARCHITECTURE.md`, and `tests/store-readiness.test.mjs` (manifest permissions, CSP, listing justifications and packaged files stay in sync). - Pause timer / Resume timer on the new-tab session view. Paused time does not count toward dwell or the time budget, no dwell lock fires while paused, and the state survives worker restarts. ### Changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c70a78b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,40 @@ +# Contributing + +IntentLock is a no-build Chrome MV3 extension. See `docs/development.md` for loading it unpacked +and `ARCHITECTURE.md` for the process model. + +## Setup + +```bash +npm ci +``` + +## Checks (run before opening a PR) + +```bash +npm run lint # ESLint +npm test # unit + integration (node:test) +npm run verify:static # manifest, CSP, runtime references +npm run audit:prod # npm audit +npm run package -- --output /tmp/IntentLock-check.zip # deterministic release zip +npm run test:browser # optional: real Chromium journeys (needs npx playwright install chromium) +``` + +`npm run check` runs lint, tests, static verification and version validation together. + +## Rules + +- Keep behavior backward compatible; do not add telemetry or new outbound network calls. +- A new permission or host pattern needs a justification in `store/LISTING.md`, + `docs/privacy-policy.md` and the README Permissions section. +- Never store the API key outside `chrome.storage.session`, and never log it. +- New files that must ship go in `RUNTIME_FILES` in `scripts/package-release.mjs`. +- No personal paths, emails or secrets in committed files. + +## Releasing (maintainer only) + +1. Bump `version` in `manifest.json` (and `package.json` if desired); move `CHANGELOG.md` + `[Unreleased]` entries under the new version heading. +2. `npm run check` and `npm run package`. +3. Merge to `main`, then the maintainer tags `vX.Y.Z`; the Release workflow validates the tag against + the manifest and attaches the zip. Chrome Web Store submission is a manual step using `store/LISTING.md`. diff --git a/README.md b/README.md index 0b0c493..89e1a50 100644 --- a/README.md +++ b/README.md @@ -303,6 +303,10 @@ Quick start: # After content.js change: also reload the affected tab ``` +### Contributing and security + +See [CONTRIBUTING.md](CONTRIBUTING.md) (checks, release steps), [SECURITY.md](SECURITY.md) (reporting and security model) and [ARCHITECTURE.md](ARCHITECTURE.md). `npm run check` runs lint, tests, static verification and version validation. + ### Version history See [CHANGELOG.md](CHANGELOG.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..76fc8e0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,44 @@ +# Security Policy + +## Reporting a vulnerability + +Please report suspected vulnerabilities privately through GitHub's +**Security > Report a vulnerability** (private advisory) on this repository. +Do not open a public issue with exploit details, and never include a real API key in a report. + +You can expect an acknowledgement, a fix or mitigation plan, and credit if you want it. +This is a single-maintainer project, so response times are best effort. + +## Supported versions + +Only the latest release on `main` is supported. + +## Security model + +- **Permissions (least privilege):** `tabs`, `storage`, `idle`, `tabGroups`, `alarms`, plus + `http://*/*` and `https://*/*` host access. Each is justified in `store/LISTING.md` and a test + (`tests/store-readiness.test.mjs`) fails if the manifest and the listing drift apart. + Host access is required because the drift overlay is injected into whatever page the user is on. +- **CSP:** extension pages use `script-src 'self'; object-src 'self'`. No remote code, no inline + script, no `eval`. +- **API key:** stored only in `chrome.storage.session` (cleared when the browser closes, never + written to `storage.local`, never synced). If session storage is unavailable the key cannot be + saved and AI checks stay off. The key is removed when the provider or endpoint changes, so it is + never sent to a destination the user did not choose. It is only sent to the provider the user selected. +- **Logging:** error logs and the diagnostics export pass through `redactSecrets`; page titles, + paths, queries and fragments are never stored. Logs stay on the device. +- **Message passing:** the service worker accepts only known message types, rejects senders with + another extension id, restricts content scripts to a small allowlist + (`CONTENT_EVENT`, `GET_INTERVENTION_STATE`, `INTERVENTION_TRANSITION`, `LOG_ERROR`), + and requires a storage epoch on every mutation so stale writers cannot resurrect deleted data. +- **No telemetry:** the only outbound requests are LLM calls to the provider the user configured. + +## Dependencies + +The extension has no runtime dependencies. Dev dependencies (Playwright, ESLint) are pinned +exactly; `npm run audit:prod` runs `npm audit` and CI runs it on every push. + +## Secrets hygiene + +Never commit keys, tokens, `.env` files, personal paths or browser profiles. +Tests use obviously fake keys only. diff --git a/docs/development.md b/docs/development.md index caca820..6e4bf06 100644 --- a/docs/development.md +++ b/docs/development.md @@ -67,7 +67,7 @@ npm run package The Node suites use built-in `node:test` + `assert/strict`; `npm test` needs no dependency install and does not discover browser tests. CI (`.github/workflows/test.yml`) -runs `npm test`, `npm run verify:static`, version validation and a package build on +runs lint and `npm audit` (quality job), `npm test`, `npm run verify:static`, version validation and a package build on Node 18/20/22, plus a separate `browser` job that installs the pinned Playwright and Chromium and runs the real journeys with the browser sandbox enabled. Locally, run the real browser suite separately: From 461c3f8679a8b9bcce4110f8b2f4b04b14ed54c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 19:29:05 +0530 Subject: [PATCH 6/6] docs: production readiness after-scores and blocked-on-owner list --- docs/PRODUCTION_READINESS.md | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/docs/PRODUCTION_READINESS.md b/docs/PRODUCTION_READINESS.md index 3e0daea..3151417 100644 --- a/docs/PRODUCTION_READINESS.md +++ b/docs/PRODUCTION_READINESS.md @@ -23,4 +23,37 @@ macOS duplicates ("name 2.ext") existed in the local clone and are now ignored v ## After (this pass) -See the table below, updated at the end of the pass. +| # | Dimension | Before | After | What changed / what still caps it | +|---|-----------|--------|-------|-----------------------------------| +| 1 | Build and run | 8 | 9 | `npm run check` (lint, tests, static, version). Not verified on Node 18 locally. | +| 2 | Tests | 8 | 8 | +7 store-readiness tests (368 pass). Playwright journeys not run in this pass; no coverage measurement. | +| 3 | Static quality | 3 | 6 | ESLint 10 clean (0 errors, 0 warnings), CI-enforced. No formatter or type checking (would require a repo-wide reformat or TS migration). | +| 4 | Security | 8 | 9 | SECURITY.md, audit in CI, permission/CSP tests tied to the listing. Capped: no third-party review. | +| 5 | Reliability | 8 | 8 | Unchanged; already strong. Lifecycle browser tests need a run on the owner's machine/CI. | +| 6 | Config and packaging | 7 | 8 | Zip contents checked in CI and tests. Capped: no screenshots, changelog still Unreleased. | +| 7 | CI | 6 | 7 | Lint and audit job added. Cannot be verified remotely (Actions billing failure). | +| 8 | Docs | 7 | 9 | SECURITY, CONTRIBUTING, ARCHITECTURE, release steps. | +| 9 | UX / a11y / perf | 7 | 7 | Not changed; no measured contrast or idle-CPU results. | +| 10 | Operability | 8 | 8 | Unchanged. | + +## Verification (local) + +`npm run lint`, `npm test` (368 pass), `npm run verify:static` (74 pass), `npm audit` (0 vulnerabilities), +`npm run package` (44-file zip, no legacy/docs/tests/env entries), `npm run validate:version`. + +## Branches and legacy code (not deleted) + +- `feat/post-merge-remediation` (32 unmerged commits) and `gh-pages` are untouched; review before any cleanup. +- `legacy-extension-source/` is an old v1.4.1 copy of the extension. Nothing references it, it is not + packaged, linted or tested (ESLint ignores it), and the current source has superseded it. It is dead code. + +## Blocked on owner + +1. Capture the five 1280x800 Web Store screenshots listed in `store/screenshots/README.md`. +2. Submit to the Chrome Web Store, tag `vX.Y.Z` and cut the release (intentionally not done). +3. Fix GitHub Actions billing so the new quality job and existing workflows can run; re-verify. +4. Decide on removing `legacy-extension-source/` (proposal: `git rm -r`; history keeps it) and on the + 32-commit `feat/post-merge-remediation` and `gh-pages` branches. +5. Run `npm run test:browser` (needs `npx playwright install chromium`) and the manual acceptance checklist. +6. Optional: adopt Prettier/TypeScript checking (large diff), enable GitHub private vulnerability reporting + (SECURITY.md assumes it), measure contrast and idle CPU.