diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index c0cc156..4d3f47e 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "firefox-cli", "description": "Firefox control CLI skill for AI agents", - "version": "0.1.1", + "version": "0.2.0", "repository": "https://github.com/respawn-llc/firefox-cli" } diff --git a/AMO_SOURCE_REVIEW.md b/AMO_SOURCE_REVIEW.md deleted file mode 100644 index 33fb5e6..0000000 --- a/AMO_SOURCE_REVIEW.md +++ /dev/null @@ -1,63 +0,0 @@ -# AMO Source Review Build - -This repository contains the source used to build the `FF-CLI Bridge` WebExtension. The npm package and CLI command are named `firefox-cli`. - -Source repository: `https://github.com/respawn-llc/firefox-cli` - -`FF-CLI Bridge` is free and open-source software under the AGPL-3.0-only license. The extension is distributed with the local `firefox-cli` package and communicates with the user-local native messaging host; it does not use a hosted backend service for browser control. - -The extension manifest uses `https://opensource.respawn.pro/firefox-cli/updates.json` as its Firefox update manifest URL. - -## Build Environment - -- Operating system: macOS, Linux, or Windows. -- Bun: `1.3.14`, matching `packageManager` in `package.json`. -- Node.js: `>=22.0.0`, matching `engines.node` in `package.json`. -- Git: any recent version that can unpack the submitted source archive. - -Install Bun from `https://bun.sh/docs/installation`. Bun installs dependencies from `bun.lock`; no npm, yarn, or pnpm lockfile is used. - -## Build Steps - -From the repository root: - -```sh -bun install --frozen-lockfile -bun run extension:build -``` - -The build script executes the WebExtension build pipeline: - -- `scripts/build-extension.ts`: runs Vite/Rollup on the TypeScript entry points. -- `scripts/copy-extension-assets.ts`: copies `manifest.json`, `popup.html`, and `popup.css`; `manifest.json` receives the release version from root `package.json`. -- `scripts/build-extension-archive.ts`: writes the unsigned add-on ZIP. - -## Expected Output - -After the build, the add-on files are in `dist/extension`: - -- `background.js` -- `content.js` -- `popup.js` -- `manifest.json` -- `popup.html` -- `popup.css` - -The unsigned add-on archive is: - -```text -dist/extension-artifacts/firefox-cli-0.1.1.zip -``` - -The submitted source archive does not include `dist/` or `node_modules/`; both are generated locally from source and dependencies. - -## Source Mapping - -- `packages/extension/src/background.ts` builds to `dist/extension/background.js`. -- `packages/extension/src/content.ts` builds to `dist/extension/content.js`. -- `packages/extension/src/popup.ts` builds to `dist/extension/popup.js`. -- `packages/extension/src/manifest.json` is copied to `dist/extension/manifest.json` with the version synchronized from root `package.json`. -- `packages/extension/src/popup.html` and `packages/extension/src/popup.css` are copied to `dist/extension`. -- `docs/firefox-cli/updates.json` is the public update manifest published at `https://opensource.respawn.pro/firefox-cli/updates.json`. - -Vite/Rollup bundles the TypeScript modules and esbuild minifies the generated JavaScript. Source files in this archive are not generated, concatenated, transpiled, or minified. diff --git a/README.md b/README.md index 25ed009..bbbdf96 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Register the native messaging host: firefox-cli setup native-host ``` -Open the `firefox-cli` extension popup in Firefox and approve the native host. The approval pairs the extension with the local native host and enables CLI requests from the machine. +Run `firefox-cli connect` and respond to the approval request in Firefox. The approval pairs the extension with the local native host and enables CLI requests from the machine. Verify the installation: diff --git a/biome.json b/biome.json index 154b8ba..aec3967 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json", "vcs": { "enabled": true, "clientKind": "git", diff --git a/bun.lock b/bun.lock index 30964b9..65a4347 100644 --- a/bun.lock +++ b/bun.lock @@ -11,25 +11,25 @@ "@firefox-cli/test-support": "workspace:*", }, "devDependencies": { - "@biomejs/biome": "^2.3.8", - "@eslint/js": "^9.39.4", - "@types/bun": "^1.3.4", + "@biomejs/biome": "^2.4.16", + "@eslint/js": "^10.0.1", + "@types/bun": "^1.3.14", "@types/jsdom": "^28.0.3", - "@types/node": "^24.10.2", - "eslint": "^9.39.4", + "@types/node": "^25.9.1", + "eslint": "^10.4.1", "eslint-config-prettier": "^10.1.8", "jsdom": "^29.1.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.48.0", - "vite": "^7.2.4", - "vitest": "^4.0.14", + "typescript": "^6.0.3", + "typescript-eslint": "^8.60.1", + "vite": "^8.0.16", + "vitest": "^4.1.8", "web-ext": "^10.3.0", - "zod": "^4.1.13", + "zod": "^4.4.3", }, }, "packages/cli": { "name": "@firefox-cli/cli", - "version": "0.1.1", + "version": "0.2.0", "dependencies": { "@firefox-cli/native-host": "workspace:*", "@firefox-cli/protocol": "workspace:*", @@ -37,24 +37,27 @@ }, "packages/extension": { "name": "@firefox-cli/extension", - "version": "0.1.1", + "version": "0.2.0", "dependencies": { "@firefox-cli/protocol": "workspace:*", }, }, "packages/native-host": { "name": "@firefox-cli/native-host", - "version": "0.1.1", + "version": "0.2.0", }, "packages/protocol": { "name": "@firefox-cli/protocol", - "version": "0.1.1", + "version": "0.2.0", }, "packages/test-support": { "name": "@firefox-cli/test-support", - "version": "0.1.1", + "version": "0.2.0", }, }, + "overrides": { + "shell-quote": "1.8.4", + }, "packages": { "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="], @@ -70,23 +73,23 @@ "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], - "@biomejs/biome": ["@biomejs/biome@2.4.15", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.15", "@biomejs/cli-darwin-x64": "2.4.15", "@biomejs/cli-linux-arm64": "2.4.15", "@biomejs/cli-linux-arm64-musl": "2.4.15", "@biomejs/cli-linux-x64": "2.4.15", "@biomejs/cli-linux-x64-musl": "2.4.15", "@biomejs/cli-win32-arm64": "2.4.15", "@biomejs/cli-win32-x64": "2.4.15" }, "bin": { "biome": "bin/biome" } }, "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw=="], + "@biomejs/biome": ["@biomejs/biome@2.4.16", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.16", "@biomejs/cli-darwin-x64": "2.4.16", "@biomejs/cli-linux-arm64": "2.4.16", "@biomejs/cli-linux-arm64-musl": "2.4.16", "@biomejs/cli-linux-x64": "2.4.16", "@biomejs/cli-linux-x64-musl": "2.4.16", "@biomejs/cli-win32-arm64": "2.4.16", "@biomejs/cli-win32-x64": "2.4.16" }, "bin": { "biome": "bin/biome" } }, "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.15", "", { "os": "win32", "cpu": "x64" }, "sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw=="], "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="], @@ -108,75 +111,29 @@ "@devicefarmer/adbkit-monkey": ["@devicefarmer/adbkit-monkey@1.2.1", "", {}, "sha512-ZzZY/b66W2Jd6NHbAhLyDWOEIBWC11VizGFk7Wx7M61JZRz7HR9Cq5P+65RKWUU7u6wgsE8Lmh9nE4Mz+U2eTg=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], + "@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="], - "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + "@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="], - "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="], "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "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.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], - "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], + "@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="], - "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="], - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.2", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="], "@exodus/bytes": ["@exodus/bytes@1.15.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ=="], @@ -208,6 +165,10 @@ "@mdn/browser-compat-data": ["@mdn/browser-compat-data@7.3.16", "", {}, "sha512-JQ6SGcHeyqSYGVwWe7NzOeXfp/vZgo2yz+fsEbMOyWLyNRVP4RoG8+dqaF/VTx1zPtF4J8XvxiWXH1l2cMZTwQ=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + + "@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="], + "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], "@pnpm/config.env-replace": ["@pnpm/config.env-replace@1.1.0", "", {}, "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w=="], @@ -216,64 +177,50 @@ "@pnpm/npm-conf": ["@pnpm/npm-conf@3.0.2", "", { "dependencies": { "@pnpm/config.env-replace": "^1.1.0", "@pnpm/network.ca-file": "^1.0.1", "config-chain": "^1.1.11" } }, "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.4", "", { "os": "android", "cpu": "arm" }, "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.4", "", { "os": "android", "cpu": "arm64" }, "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.3", "", { "os": "android", "cpu": "arm64" }, "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.3", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.4", "", { "os": "none", "cpu": "arm64" }, "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/jsdom": ["@types/jsdom@28.0.3", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^8.0.0", "undici-types": "^7.21.0" } }, "sha512-/HQ2uFoetFTXuye8vzIcHw2z6Fwi7Hi/qcgC+RoS9NCyewiqxhVGqlG+ViGB6lkax481R6dmhf1I7lIGlzJStQ=="], @@ -282,43 +229,43 @@ "@types/minimatch": ["@types/minimatch@3.0.5", "", {}, "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ=="], - "@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], + "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.4", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.4", "@typescript-eslint/type-utils": "8.59.4", "@typescript-eslint/utils": "8.59.4", "@typescript-eslint/visitor-keys": "8.59.4", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.4", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.60.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/type-utils": "8.60.1", "@typescript-eslint/utils": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.60.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.4", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.4", "@typescript-eslint/types": "8.59.4", "@typescript-eslint/typescript-estree": "8.59.4", "@typescript-eslint/visitor-keys": "8.59.4", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.60.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.4", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.4", "@typescript-eslint/types": "^8.59.4", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.60.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.60.1", "@typescript-eslint/types": "^8.60.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.4", "", { "dependencies": { "@typescript-eslint/types": "8.59.4", "@typescript-eslint/visitor-keys": "8.59.4" } }, "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1" } }, "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.4", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.60.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.4", "", { "dependencies": { "@typescript-eslint/types": "8.59.4", "@typescript-eslint/typescript-estree": "8.59.4", "@typescript-eslint/utils": "8.59.4", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/utils": "8.60.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.59.4", "", {}, "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.60.1", "", {}, "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.4", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.4", "@typescript-eslint/tsconfig-utils": "8.59.4", "@typescript-eslint/types": "8.59.4", "@typescript-eslint/visitor-keys": "8.59.4", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.60.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.60.1", "@typescript-eslint/tsconfig-utils": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.4", "@typescript-eslint/types": "8.59.4", "@typescript-eslint/typescript-estree": "8.59.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.60.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.4", "", { "dependencies": { "@typescript-eslint/types": "8.59.4", "eslint-visitor-keys": "^5.0.0" } }, "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag=="], - "@vitest/expect": ["@vitest/expect@4.1.6", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.6", "@vitest/utils": "4.1.6", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg=="], + "@vitest/expect": ["@vitest/expect@4.1.8", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.8", "@vitest/utils": "4.1.8", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ=="], - "@vitest/mocker": ["@vitest/mocker@4.1.6", "", { "dependencies": { "@vitest/spy": "4.1.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ=="], + "@vitest/mocker": ["@vitest/mocker@4.1.8", "", { "dependencies": { "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw=="], - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.6", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw=="], + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.8", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA=="], - "@vitest/runner": ["@vitest/runner@4.1.6", "", { "dependencies": { "@vitest/utils": "4.1.6", "pathe": "^2.0.3" } }, "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA=="], + "@vitest/runner": ["@vitest/runner@4.1.8", "", { "dependencies": { "@vitest/utils": "4.1.8", "pathe": "^2.0.3" } }, "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg=="], - "@vitest/snapshot": ["@vitest/snapshot@4.1.6", "", { "dependencies": { "@vitest/pretty-format": "4.1.6", "@vitest/utils": "4.1.6", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw=="], + "@vitest/snapshot": ["@vitest/snapshot@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "@vitest/utils": "4.1.8", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ=="], - "@vitest/spy": ["@vitest/spy@4.1.6", "", {}, "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg=="], + "@vitest/spy": ["@vitest/spy@4.1.8", "", {}, "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA=="], - "@vitest/utils": ["@vitest/utils@4.1.6", "", { "dependencies": { "@vitest/pretty-format": "4.1.6", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ=="], + "@vitest/utils": ["@vitest/utils@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg=="], "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], @@ -356,7 +303,7 @@ "atomically": ["atomically@2.1.1", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], @@ -366,7 +313,7 @@ "boxen": ["boxen@8.0.1", "", { "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" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], - "brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], @@ -384,7 +331,7 @@ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "cheerio": ["cheerio@1.2.0", "", { "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" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="], @@ -452,6 +399,8 @@ "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -474,25 +423,23 @@ "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], - "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-goat": ["escape-goat@4.0.0", "", {}, "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.39.4", "", { "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" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], + "eslint": ["eslint@10.4.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@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": "^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", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw=="], "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], "eslint-plugin-no-unsanitized": ["eslint-plugin-no-unsanitized@4.1.5", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-MSB4hXPVFQrI8weqzs6gzl7reP2k/qSjtCoL2vUMSDejIIq9YL1ZKvq5/ORBXab/PvfBBrWO2jWviYpL+4Ghfg=="], - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], @@ -652,6 +599,30 @@ "lighthouse-logger": ["lighthouse-logger@2.0.2", "", { "dependencies": { "debug": "^4.4.1", "marky": "^1.2.2" } }, "sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], @@ -680,7 +651,7 @@ "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], - "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -778,7 +749,7 @@ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - "rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="], + "rolldown": ["rolldown@1.0.3", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="], "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], @@ -800,7 +771,7 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "shell-quote": ["shell-quote@1.7.3", "", {}, "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="], + "shell-quote": ["shell-quote@1.8.4", "", {}, "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ=="], "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], @@ -852,7 +823,7 @@ "tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], - "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], @@ -868,15 +839,17 @@ "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - "typescript-eslint": ["typescript-eslint@8.59.4", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.4", "@typescript-eslint/parser": "8.59.4", "@typescript-eslint/typescript-estree": "8.59.4", "@typescript-eslint/utils": "8.59.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ=="], + "typescript-eslint": ["typescript-eslint@8.60.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.60.1", "@typescript-eslint/parser": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/utils": "8.60.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA=="], "undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="], @@ -892,9 +865,9 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "vite": ["vite@7.3.3", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA=="], + "vite": ["vite@8.0.16", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="], - "vitest": ["vitest@4.1.6", "", { "dependencies": { "@vitest/expect": "4.1.6", "@vitest/mocker": "4.1.6", "@vitest/pretty-format": "4.1.6", "@vitest/runner": "4.1.6", "@vitest/snapshot": "4.1.6", "@vitest/spy": "4.1.6", "@vitest/utils": "4.1.6", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.6", "@vitest/browser-preview": "4.1.6", "@vitest/browser-webdriverio": "4.1.6", "@vitest/coverage-istanbul": "4.1.6", "@vitest/coverage-v8": "4.1.6", "@vitest/ui": "4.1.6", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ=="], + "vitest": ["vitest@4.1.8", "", { "dependencies": { "@vitest/expect": "4.1.8", "@vitest/mocker": "4.1.8", "@vitest/pretty-format": "4.1.8", "@vitest/runner": "4.1.8", "@vitest/snapshot": "4.1.8", "@vitest/spy": "4.1.8", "@vitest/utils": "4.1.8", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.8", "@vitest/browser-preview": "4.1.8", "@vitest/browser-webdriverio": "4.1.8", "@vitest/coverage-istanbul": "4.1.8", "@vitest/coverage-v8": "4.1.8", "@vitest/ui": "4.1.8", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig=="], "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], @@ -958,32 +931,36 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@eslint/eslintrc/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "@eslint/eslintrc/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], - "@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "@types/jsdom/@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "@typescript-eslint/typescript-estree/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "addons-linter/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - "addons-linter/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - - "addons-linter/espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], - - "boxen/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "addons-linter/eslint": ["eslint@9.39.4", "", { "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" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], "boxen/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "bun-types/@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], + "cheerio/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], "cheerio/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + "chrome-launcher/@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], @@ -1000,6 +977,8 @@ "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + "multimatch/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "parse5-htmlparser2-tree-adapter/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], "parse5-parser-stream/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], @@ -1008,11 +987,9 @@ "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "rollup/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="], - "update-notifier/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "vitest/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -1024,18 +1001,50 @@ "wsl-utils/is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + "@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + + "@types/jsdom/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "addons-linter/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "addons-linter/eslint/@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], + + "addons-linter/eslint/@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "addons-linter/eslint/@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "addons-linter/eslint/@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], + + "addons-linter/eslint/@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "addons-linter/eslint/ajv": ["ajv@6.15.0", "", { "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" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + + "addons-linter/eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "addons-linter/eslint/eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "addons-linter/eslint/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "addons-linter/eslint/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "addons-linter/eslint/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "boxen/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "boxen/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "cheerio/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "chrome-launcher/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "fx-runner/which/isexe": ["isexe@1.1.2", "", {}, "sha512-d2eJzK691yZwPHcv1LbeAOa91yMJ9QmfTgSO1oXB65ezVhXQsxBac2vEB4bMVms9cGzaA99n6V2viHMq82VLDw=="], + "multimatch/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + "parse5-htmlparser2-tree-adapter/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "parse5-parser-stream/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], @@ -1048,10 +1057,18 @@ "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "@eslint/eslintrc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "addons-linter/eslint/@eslint/config-array/@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "addons-linter/eslint/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], "boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "multimatch/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "widest-line/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "addons-linter/eslint/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], } } diff --git a/docs/all-commands-qa.md b/docs/all-commands-qa.md index a808f24..915c430 100644 --- a/docs/all-commands-qa.md +++ b/docs/all-commands-qa.md @@ -37,6 +37,7 @@ Create `UPLOAD_FILE` with any small text payload. Capture IDs from JSON output w - [ ] Run `$CLI doctor --json`; expect the disposable extension connection to be `"connected"`. - [ ] Run `$CLI doctor --fix --json`; expect the manifest status to be healthy and the connection to remain `"connected"`. - [ ] Run `$CLI capabilities --json`; expect MVP capabilities plus explicit unsupported entries. +- [ ] Run `$CLI connect`; expect an already-approved rejection that identifies the extension instance. - [ ] Run `$CLI window new "$BASE" --json`; save `WINDOW` and `TAB`. - [ ] Run `$CLI window --json`; expect `WINDOW` in the window list. - [ ] Run `$CLI window select "id:$WINDOW" --json`; expect `WINDOW` to be selected. @@ -140,7 +141,6 @@ Create `UPLOAD_FILE` with any small text payload. Capture IDs from JSON output w - [ ] Run `$CLI close`; expect `UNSUPPORTED_CAPABILITY`. - [ ] Run `$CLI quit`; expect `UNSUPPORTED_CAPABILITY`. - [ ] Run `$CLI exit`; expect `UNSUPPORTED_CAPABILITY`. -- [ ] Run `$CLI connect`; expect `UNSUPPORTED_CAPABILITY`. - [ ] Run `$CLI inspect`; expect `UNSUPPORTED_CAPABILITY`. - [ ] Run `$CLI tab close "id:$TAB3" --json`; expect `TAB3` to close. - [ ] Run `$CLI tab close "id:$TAB2" --json`; expect `TAB2` to close. diff --git a/docs/architecture.md b/docs/architecture.md index ab8e25a..068da58 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,7 +4,7 @@ - CLI: parses commands, prints terminal output, stores local configuration, and sends requests to the native host IPC endpoint. - Native host: owns Firefox native messaging, local IPC, pair state, auth tokens, native-host manifest setup, and file writes for binary outputs such as screenshots. -- Extension: owns Firefox APIs, popup approval, tab/window targeting, content-script injection, command routing, and browser permission errors. +- Extension: owns Firefox APIs, first-use approval, tab/window targeting, content-script injection, command routing, and browser permission errors. - Protocol: defines command IDs, request/response schemas, capability metadata, stable errors, and runtime validation. ## Transport @@ -15,9 +15,9 @@ Native-host stdout is reserved for Firefox native messaging frames. Human-readab ## Pairing -The first popup approval creates a pair token. The extension stores the token in extension storage; the native host stores a hash and extension identity in user-local state. CLI requests are forwarded only when the connected extension has presented a valid token. +The first approval request creates a pair token. The extension stores the token in extension storage; the native host stores a hash and extension identity in user-local state. CLI requests are forwarded only when the connected extension has presented a valid token. -`firefox-cli unpair` clears native-host pair state. The extension popup can approve again and receive a new token. +`firefox-cli unpair` clears native-host pair state. Run `firefox-cli connect` to request approval again. ## Targeting diff --git a/docs/commands.md b/docs/commands.md index 9ca25a3..17b226d 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -26,6 +26,7 @@ Private windows are listed and readable. Mutating commands against private windo | `firefox-cli setup` | Print extension setup guidance and the native-host setup command. | | `firefox-cli setup native-host [--dry-run] [--json]` | Write or print the native messaging manifest. | | `firefox-cli doctor [--fix] [--json]` | Diagnose native-host manifest and extension connection state. | +| `firefox-cli connect [--json]` | Request Firefox control approval and wait for the user's decision. | | `firefox-cli unpair` | Clear CLI/native-host pair state. | | `firefox-cli capabilities [--json]` | List supported and gated protocol capabilities. | @@ -158,6 +159,7 @@ Example: | `firefox-cli network list|clear [--url glob] [--json]` | List or clear observed web requests. | | `firefox-cli console|errors list|clear [--json]` | List or clear page console/error capture buffers. | | `firefox-cli highlight [--json]` | Outline an element. | +| `firefox-cli notify [--id id] [message...] [--json]` | Show a native Firefox notification. | | `firefox-cli set viewport <width> <height> [--json]` | Request a target browser window resize and report Firefox's observed window dimensions. Tiling/window-manager rules can prevent the requested size from taking effect. | | `firefox-cli diff url|title|snapshot <expected> [--json]` | Compare URL, title, or snapshot text with an expected value. | | `firefox-cli pdf <path> [--json]` | Returns `UNSUPPORTED_CAPABILITY`; Firefox saves PDFs through a browser dialog rather than a requested CLI path. | @@ -166,4 +168,4 @@ Network route/mock/block and HAR export are unsupported. ## Unsupported Families -The CLI returns `UNSUPPORTED_CAPABILITY` for unsupported command families and options, including `screenshot --full`, `pdf`, `connect`, `inspect`, top-level `close`, `quit`, and `exit`. +The CLI returns `UNSUPPORTED_CAPABILITY` for unsupported command families and options, including `screenshot --full`, `pdf`, `inspect`, top-level `close`, `quit`, and `exit`. diff --git a/docs/dependency-migrations.md b/docs/dependency-migrations.md new file mode 100644 index 0000000..df9c2e1 --- /dev/null +++ b/docs/dependency-migrations.md @@ -0,0 +1,39 @@ +# Dependency Migrations + +This page records major dependency migration plans required by the dependency-upgrade policy in `docs/development.md`. + +## 2026-06-11 Development Tooling + +Scope: + +- `eslint` 10 and `@eslint/js` 10. +- `@types/node` 25. +- `typescript` 6. +- `vite` 8. + +Release-age evidence as of 2026-06-11: + +- `eslint` 10.0.0 was published on 2026-02-06; installed `eslint` 10.4.1 was published on 2026-05-29. +- `@eslint/js` 10.0.1 was published on 2026-02-06. +- `@types/node` 25.0.0 was published on 2025-12-10; installed `@types/node` 25.9.1 was published on 2026-05-19. +- Installed `typescript` 6.0.3 was published on 2026-04-16. +- `vite` 8.0.0 was published on 2026-03-12; installed `vite` 8.0.16 was published on 2026-06-01. + +Migration plan: + +- Keep the existing ESLint flat config and attach caught errors as `cause` where ESLint 10 reports `preserve-caught-error`. +- Keep TypeScript path aliases and set `ignoreDeprecations` for the TypeScript 6 `baseUrl` deprecation until path aliasing is redesigned. +- Normalize Marionette socket chunks before `Buffer.concat` for the Node 25 type surface. +- Remove explicit `manualChunks: undefined` from the Vite extension build output, preserving Rollup's default chunking behavior. +- Update the Biome schema URL to match the installed Biome CLI. +- Use a Bun override for `shell-quote` 1.8.4 because latest `web-ext` pins `fx-runner` 1.4.0, which pins vulnerable `shell-quote` 1.7.3. + +Verification: + +- `bun run deps:check` +- `bun run check` +- `bun run release:check:local` + +Rollback scope: + +- Revert `package.json`, `bun.lock`, and the TypeScript, ESLint, Biome, Vite, and Marionette compatibility edits in the same change. diff --git a/docs/firefox-cli-spec.md b/docs/firefox-cli-spec.md index 79942c2..19d4663 100644 --- a/docs/firefox-cli-spec.md +++ b/docs/firefox-cli-spec.md @@ -22,7 +22,7 @@ The happy path: 1. User installs the npm package and gets one executable: `firefox-cli`. 2. User manually installs or temporarily loads the Firefox extension from the URL printed by `firefox-cli setup`. 3. User runs `firefox-cli setup native-host` or `firefox-cli doctor --fix` to register the native messaging host. -4. User opens the extension popup and approves the first connection after seeing native-host identity details. +4. User runs `firefox-cli connect` and responds to the Firefox approval request, or opens the extension popup and approves the first connection. 5. Commands control the active Firefox tab/window unless a command or flag selects another target. Example workflow: @@ -96,8 +96,8 @@ Unpaired handshake: 1. Extension connects to the native host. 2. Native host sends identity metadata: host name, executable path, package version, protocol min/max, native manifest path, extension ID, and a generated pairing nonce. -3. Extension popup shows the metadata and asks the user to approve first use. -4. Until approval, the native host rejects or queues CLI commands with `NOT_APPROVED`. +3. The extension approval UI asks the user to approve first use. +4. Until approval, the native host rejects CLI commands with `NOT_APPROVED`, except for the dedicated approval request command. 5. On approval, extension and native host persist the minimum pair state needed to reconnect. After approval, all commands are allowed without per-action confirmations, domain allowlists, or action policies. @@ -138,7 +138,7 @@ Keep the extension UI smaller than a control panel. The popup should show: - Approval/reset action for the first connection. - Copy diagnostics action. -Do not mirror CLI commands in the popup. Setup text should tell users to click the Firefox extension popup to approve; do not depend on auto-opening the popup. +Do not mirror CLI commands in the popup. Setup text can tell users to run `firefox-cli connect` or click the Firefox extension popup to approve. ## Permissions @@ -146,7 +146,7 @@ Use broad host access for the MVP full-control model after explicit first-use ap Expected manifest shape: -- `permissions`: native messaging, content scripting, tab access, local extension storage, downloads, cookies, clipboard, and web request observation. +- `permissions`: native messaging, content scripting, tab access, local extension storage, downloads, cookies, notifications, clipboard, and web request observation. - `host_permissions`: broad web access for normal web pages. - `browser_specific_settings.gecko.strict_min_version`: Firefox `150.0`. - `browser_specific_settings.gecko.data_collection_permissions`: browsing activity, website activity, and website content because command results can leave the extension through the local native host and CLI. @@ -322,7 +322,7 @@ Agent-browser family compatibility summary: | Dialogs, downloads, clipboard, cookies, storage, network list/clear | MVP with Firefox/WebExtension limits; HAR unsupported | | Debug/repro: console/errors, `highlight`, diff, trace/profiler, vitals | MVP for listed commands; deferred as listed below | | Auth/state/session/profile/security gates/content boundaries | Deferred or unsupported in MVP because this controls the existing Firefox session after pairing | -| Chrome/CDP/provider/browser-install features: `connect`, `get cdp-url`, `inspect`, `--extension`, external providers, iOS, Chrome profile import, browser install/upgrade | Unsupported unless Firefox provides an equivalent | +| Chrome/CDP/provider/browser-install features: CDP attach, `get cdp-url`, `inspect`, `--extension`, external providers, iOS, Chrome profile import, browser install/upgrade | Unsupported unless Firefox provides an equivalent | Global options: @@ -403,7 +403,7 @@ Unsupported unless Firefox provides an equivalent: - Top-level `close`, `quit`, `exit`, and `close --all`; use explicit `tab close` and `window close`. - `confirm` and `deny`, because MVP has no per-action confirmation queue. - Chrome `debugger`/CDP-specific commands. -- `connect <port|url>` and `get cdp-url`. +- CDP attach by port/URL and `get cdp-url`. - DevTools opening/inspection behavior equivalent to `agent-browser inspect`. - Chrome extension loading flags such as `--extension`. - External browser providers and iOS provider. @@ -480,8 +480,8 @@ Errors should be concise and actionable: - If extension is not installed: print the matching extension download URL. - If native host is not registered: print `firefox-cli setup native-host`. -- If Firefox is not running or extension is disconnected: tell the user to open Firefox and check the extension popup. -- If first-use approval is pending: tell the user to open the extension popup and approve. +- If Firefox is not running or extension is disconnected: tell the user to open Firefox and run `firefox-cli connect`. +- If first-use approval is pending: tell the user to run `firefox-cli connect` or open the extension popup and approve. - If a page is restricted: name the restriction and suggest trying a normal web page/tab. - If a ref is stale: tell the user to run `firefox-cli snapshot -i` again. - If a command is unsupported: name the Firefox limitation or missing implementation gate. diff --git a/docs/firefox-cli/updates.json b/docs/firefox-cli/updates.json index c942083..4f60869 100644 --- a/docs/firefox-cli/updates.json +++ b/docs/firefox-cli/updates.json @@ -10,6 +10,15 @@ "strict_min_version": "150.0" } } + }, + { + "applications": { + "gecko": { + "strict_min_version": "150.0" + } + }, + "version": "0.2.0", + "update_link": "https://github.com/respawn-llc/firefox-cli/releases/download/v0.2.0/firefox-cli-0.2.0.xpi" } ] } diff --git a/docs/setup.md b/docs/setup.md index 4e60bf3..1ebbe90 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -5,7 +5,7 @@ - the CLI/native host executable from the npm package; - the Firefox extension that connects Firefox to the native host. -The CLI cannot inspect Firefox until the extension is loaded, the native messaging manifest is installed, and the extension popup has approved the pair. +The CLI cannot inspect Firefox until the extension is loaded, the native messaging manifest is installed, and the user approves the Firefox control request. ## Install The CLI @@ -60,9 +60,9 @@ firefox-cli doctor --fix `doctor --fix` repairs missing or stale native-host manifests. -## Approve Pairing +## Connect Firefox -Open the `firefox-cli` extension popup in Firefox and approve the native host. The extension stores the pair token in Firefox extension storage; the native host stores pair state under the user-local `firefox-cli` state directory. +Run `firefox-cli connect` and respond to the approval request in Firefox, or open the `firefox-cli` extension popup in Firefox and approve the native host. The extension stores the pair token in Firefox extension storage; the native host stores pair state under the user-local `firefox-cli` state directory. Verify the connection: @@ -86,7 +86,7 @@ firefox-cli tab : Load or enable the extension and keep Firefox running. `Extension connection: not-approved` -: Open the extension popup and approve the native host. +: Run `firefox-cli connect` and respond to the approval request in Firefox. `Version mismatch` : Upgrade or rebuild the CLI, native host, and extension from the same package version. diff --git a/package.json b/package.json index cfd7320..ae9aedb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firefox-cli-workspace", - "version": "0.1.1", + "version": "0.2.0", "private": true, "type": "module", "packageManager": "bun@1.3.14", @@ -53,19 +53,22 @@ "check": "bun run ci" }, "devDependencies": { - "@biomejs/biome": "^2.3.8", - "@eslint/js": "^9.39.4", - "@types/bun": "^1.3.4", + "@biomejs/biome": "^2.4.16", + "@eslint/js": "^10.0.1", + "@types/bun": "^1.3.14", "@types/jsdom": "^28.0.3", - "@types/node": "^24.10.2", - "eslint": "^9.39.4", + "@types/node": "^25.9.1", + "eslint": "^10.4.1", "eslint-config-prettier": "^10.1.8", "jsdom": "^29.1.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.48.0", - "vite": "^7.2.4", - "vitest": "^4.0.14", + "typescript": "^6.0.3", + "typescript-eslint": "^8.60.1", + "vite": "^8.0.16", + "vitest": "^4.1.8", "web-ext": "^10.3.0", - "zod": "^4.1.13" + "zod": "^4.4.3" + }, + "overrides": { + "shell-quote": "1.8.4" } } diff --git a/packages/cli/package.json b/packages/cli/package.json index 0c2a0fe..6219265 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@firefox-cli/cli", - "version": "0.1.1", + "version": "0.2.0", "private": true, "type": "module", "exports": { diff --git a/packages/cli/src/argv-contracts.ts b/packages/cli/src/argv-contracts.ts index 6dacd1f..ecee97f 100644 --- a/packages/cli/src/argv-contracts.ts +++ b/packages/cli/src/argv-contracts.ts @@ -71,6 +71,10 @@ export const routeParserSpecs = { console: parser("console"), errors: parser("errors"), highlight: parser("highlight", { valueOptions: ["--generation", "--duration"] }), + notify: parser("notify", { + valueOptions: ["--id"], + payload: { payloadStartPositionals: 0, minPositionals: 1, variadicAfterMin: true }, + }), pdf: parser("pdf"), "set.viewport": parser("set"), diff: parser("diff", { @@ -81,6 +85,7 @@ export const routeParserSpecs = { flags: ["--bail", "--stdin"], valueOptions: ["--timeout", "--max-output"], }), + connect: parser("connect"), click: parser("click", { valueOptions: ["--generation"] }), dblclick: parser("dblclick", { valueOptions: ["--generation"] }), focus: parser("focus", { valueOptions: ["--generation"] }), diff --git a/packages/cli/src/cli-help.test.ts b/packages/cli/src/cli-help.test.ts index b231091..cdb62d6 100644 --- a/packages/cli/src/cli-help.test.ts +++ b/packages/cli/src/cli-help.test.ts @@ -3,7 +3,7 @@ import { baseDependencies } from "./cli-test-support.js"; import { runCli } from "./index.js"; describe("CLI help", () => { - it("renders workflow-oriented root help without setup approval warnings", async () => { + it("renders workflow-oriented root help without popup approval warnings", async () => { const output = await runCli(["-h"], baseDependencies()); expect(output.exitCode).toBe(0); @@ -13,9 +13,8 @@ describe("CLI help", () => { expect(output.stdout).toContain("Act on elements:"); expect(output.stdout).toContain("firefox-cli snapshot -i"); expect(output.stdout).toContain("firefox-cli <command> -h"); - expect(output.stdout).not.toContain("approve"); - expect(output.stdout).not.toContain("approval"); expect(output.stdout).not.toContain("extension popup"); + expect(output.stdout).toContain("firefox-cli connect"); }); it("renders grouped contextual help for command families", async () => { @@ -38,6 +37,17 @@ describe("CLI help", () => { expect(output.stdout).toContain("firefox-cli snapshot -i"); }); + it("renders wait examples accepted by the wait parser", async () => { + const output = await runCli(["wait", "--help"], baseDependencies()); + + expect(output.exitCode).toBe(0); + expect(output.stdout).toContain("Wait for a duration, element, text, URL, function predicate, load state, or download."); + expect(output.stdout).toContain("firefox-cli wait '#ready'"); + expect(output.stdout).not.toContain("title"); + expect(output.stdout).not.toContain("dialog"); + expect(output.stdout).not.toContain("firefox-cli wait --selector"); + }); + it("renders command help without sending a browser request", async () => { const sendRequest = vi.fn(async () => { throw new Error("help must not call transport"); diff --git a/packages/cli/src/cli-phase8.test.ts b/packages/cli/src/cli-phase8.test.ts index 48dc431..a73daf7 100644 --- a/packages/cli/src/cli-phase8.test.ts +++ b/packages/cli/src/cli-phase8.test.ts @@ -113,6 +113,10 @@ describe("runCli Phase 8 commands", () => { argv: ["highlight", "#save", "--duration", "1000", "--json"], expected: { command: "highlight", params: { selector: "#save", durationMs: 1000 } }, }, + { + argv: ["notify", "--id", "approval", "Action needed", "Open Firefox to approve control.", "--json"], + expected: { command: "notify", params: { id: "approval", title: "Action needed", message: "Open Firefox to approve control." } }, + }, { argv: ["pdf", "page.pdf", "--json"], expected: { command: "pdf", params: { path: join(cwd, "page.pdf") } }, diff --git a/packages/cli/src/cli-setup-doctor.test.ts b/packages/cli/src/cli-setup-doctor.test.ts index 7ffa529..f5ae56f 100644 --- a/packages/cli/src/cli-setup-doctor.test.ts +++ b/packages/cli/src/cli-setup-doctor.test.ts @@ -379,7 +379,7 @@ describe("runCli setup and doctor", () => { expect(output).toEqual({ exitCode: 0, - stdout: "Pair state cleared. Approve firefox-cli again from the extension popup.\n", + stdout: "Pair state cleared. Run `firefox-cli connect` to request approval again.\n", stderr: "", }); expect(unpairCalls).toEqual(["cleared"]); diff --git a/packages/cli/src/cli-test-support.ts b/packages/cli/src/cli-test-support.ts index d87efac..3a31d27 100644 --- a/packages/cli/src/cli-test-support.ts +++ b/packages/cli/src/cli-test-support.ts @@ -88,6 +88,7 @@ export function phase8CliResultFor(request: RequestEnvelope): unknown { console: { action: "list", ok: true, entries: [] }, errors: { action: "clear", ok: true }, highlight: { ok: true, element }, + notify: { ok: true, id: "approval" }, pdf: { path: "/work/page.pdf" }, "set.viewport": { window: { id: 7, index: 0, focused: true, tabCount: 1 } }, diff: { diff --git a/packages/cli/src/cli.test.ts b/packages/cli/src/cli.test.ts index 7c2a369..2fb1aca 100644 --- a/packages/cli/src/cli.test.ts +++ b/packages/cli/src/cli.test.ts @@ -68,14 +68,70 @@ describe("runCli", () => { sendRequest: async (request) => createErrorResponse(request.id, { code: "NOT_APPROVED", - message: "Approve firefox-cli in the extension popup before running CLI commands.", + message: "Run `firefox-cli connect` before running Firefox control commands.", }), }); expect(output).toEqual({ exitCode: 1, stdout: "", - stderr: "Not approved: Approve firefox-cli in the extension popup before running CLI commands.\n", + stderr: "Not approved: Run `firefox-cli connect` before running Firefox control commands.\n", + }); + }); + + it("builds approval page requests", async () => { + const output = await runCli(["connect", "--json"], { + ...baseDependencies(), + sendRequest: async (request) => { + expect(request).toMatchObject({ + command: "pair.requestApproval", + params: {}, + }); + return createOkResponse(request, { + ok: true, + url: "moz-extension://test/approval-request.html", + }); + }, + }); + + expect(output).toEqual({ + exitCode: 0, + stdout: `${JSON.stringify({ ok: true, url: "moz-extension://test/approval-request.html" }, null, 2)}\n`, + stderr: "", + }); + }); + + it("prints accepted approval requests with user-decision wording", async () => { + const output = await runCli(["connect"], { + ...baseDependencies(), + sendRequest: async (request) => + createOkResponse(request, { + ok: true, + url: "moz-extension://test/approval-request.html", + }), + }); + + expect(output).toEqual({ + exitCode: 0, + stdout: "User approved the request\n", + stderr: "", + }); + }); + + it("prints rejected approval requests without an error-code prefix", async () => { + const output = await runCli(["connect"], { + ...baseDependencies(), + sendRequest: async (request) => + createErrorResponse(request.id, { + code: "ACTION_REJECTED", + message: "User explicitly denied your request.", + }), + }); + + expect(output).toEqual({ + exitCode: 1, + stdout: "", + stderr: "User explicitly denied your request.\n", }); }); diff --git a/packages/cli/src/commands/content.ts b/packages/cli/src/commands/content.ts index 3e75da1..554f561 100644 --- a/packages/cli/src/commands/content.ts +++ b/packages/cli/src/commands/content.ts @@ -246,6 +246,26 @@ export function buildHighlightRequest(argv: readonly string[]): RequestEnvelope }); } +export function buildNotifyRequest(argv: readonly string[]): RequestEnvelope { + const parsed = parsePayloadPositionalsAndOptions( + argv.slice(1).filter((arg) => arg !== "--json"), + { + payloadStartPositionals: 0, + minPositionals: 1, + variadicAfterMin: true, + }, + ); + const [title, ...messageParts] = parsed.positionals; + if (title === undefined) { + throw new CliUsageError("Missing notification title."); + } + return createValidatedRequest("notify", { + title, + ...(messageParts.length === 0 ? {} : { message: messageParts.join(" ") }), + ...optionalStringOption(parsed.optionArgs, ["--id"], "id"), + }); +} + export function buildDiffRequest(argv: readonly string[]): RequestEnvelope { const parsed = parsePayloadPositionalsAndOptions(argv.slice(1), { payloadStartPositionals: 1, diff --git a/packages/cli/src/commands/pairing.ts b/packages/cli/src/commands/pairing.ts new file mode 100644 index 0000000..c2216db --- /dev/null +++ b/packages/cli/src/commands/pairing.ts @@ -0,0 +1,6 @@ +import type { RequestEnvelope } from "@firefox-cli/protocol"; +import { createValidatedRequest } from "../protocol-validation.js"; + +export function buildOpenApprovalRequest(): RequestEnvelope { + return createValidatedRequest("pair.requestApproval", {}); +} diff --git a/packages/cli/src/commands/setup-doctor.ts b/packages/cli/src/commands/setup-doctor.ts index ccf7ffe..1120ab1 100644 --- a/packages/cli/src/commands/setup-doctor.ts +++ b/packages/cli/src/commands/setup-doctor.ts @@ -227,7 +227,7 @@ async function checkExtensionConnection(dependencies: CliDependencies): Promise< if (response.error.code === "NOT_APPROVED") { return { status: "not-approved", - nextAction: "Open the firefox-cli extension popup and approve this native host.", + nextAction: "Run `firefox-cli connect` and respond to the approval request in Firefox.", }; } diff --git a/packages/cli/src/format.test.ts b/packages/cli/src/format.test.ts index 163309a..a5d5f24 100644 --- a/packages/cli/src/format.test.ts +++ b/packages/cli/src/format.test.ts @@ -41,7 +41,7 @@ describe("CLI response formatting", () => { exitCode: 1, stdout: "", stderr: - "Native host unavailable: Native host is offline. Run `firefox-cli setup`, install the extension, run `firefox-cli setup native-host`, then approve the extension popup.\n", + "Native host unavailable: Native host is offline. Open Firefox, run `firefox-cli setup` if setup is incomplete, then run `firefox-cli connect`.\n", }); }); }); diff --git a/packages/cli/src/format.ts b/packages/cli/src/format.ts index c0df971..2dcce70 100644 --- a/packages/cli/src/format.ts +++ b/packages/cli/src/format.ts @@ -198,6 +198,14 @@ const formatJsonOrObject: CliResponseFormatter = (response, json) => { return json ? ok(`${JSON.stringify(response.result, null, 2)}\n`) : ok(`${JSON.stringify(response.result)}\n`); }; +export const formatApprovalRequest: CliResponseFormatter<"pair.requestApproval"> = (response, json) => { + if (!response.ok) { + return error(formatProtocolError(response.error)); + } + + return json ? ok(`${JSON.stringify(response.result, null, 2)}\n`) : ok("User approved the request\n"); +}; + export const cliResponseFormatters = { capabilities: formatCapabilities, "tab-list": formatTabList, diff --git a/packages/cli/src/help.ts b/packages/cli/src/help.ts index 484ec56..166a03e 100644 --- a/packages/cli/src/help.ts +++ b/packages/cli/src/help.ts @@ -17,6 +17,7 @@ interface HelpGroup { const routeHelpSpecs = { capabilities: helpSpec("List supported command families and browser capability metadata."), + connect: helpSpec("Request Firefox control approval through a dedicated approval page."), "tab.list": helpSpec("List tabs with indexes, ids, active state, titles, and URLs.", [ "Use listed indexes with `--tab <index>` and ids with `--tab id:<id>`.", ]), @@ -42,7 +43,7 @@ const routeHelpSpecs = { "Pass a selector or `@ref` for element-scoped values.", ]), is: helpSpec("Check element/page state such as visible, enabled, checked, or selected."), - wait: helpSpec("Wait for URL, title, element, text, network-idle, download, or dialog conditions.", [ + wait: helpSpec("Wait for a duration, element, text, URL, function predicate, load state, or download.", [ "Use waits between navigation/actions and reads instead of fixed sleeps.", ]), eval: helpSpec("Evaluate JavaScript in the target page and return a serialized result.", [ @@ -65,6 +66,9 @@ const routeHelpSpecs = { console: helpSpec("List or clear captured console messages."), errors: helpSpec("List or clear captured page errors."), highlight: helpSpec("Temporarily highlight an element for visual inspection."), + notify: helpSpec("Show a native Firefox notification with a title and optional message.", [ + "Use `--id <id>` to update or replace an existing notification with the same id.", + ]), pdf: helpSpec("Report Firefox PDF export support for a target path."), "set.viewport": helpSpec("Resize the target window viewport."), diff: helpSpec("Compare URL, title, or snapshot content against an expected value."), @@ -93,7 +97,7 @@ const helpGroups: readonly HelpGroup[] = [ { title: "Setup and diagnostics", summary: "Install, repair, inspect, and reset the Firefox/native-host connection.", - routes: ["capabilities"], + routes: ["capabilities", "connect"], }, { title: "Tabs, windows, and navigation", @@ -155,7 +159,7 @@ const helpGroups: readonly HelpGroup[] = [ { title: "Browser data and files", summary: "Use browser-adjacent data and file operations.", - routes: ["screenshot", "download", "clipboard", "cookies", "storage", "pdf", "set.viewport"], + routes: ["screenshot", "download", "clipboard", "cookies", "storage", "notify", "pdf", "set.viewport"], }, { title: "Automation", @@ -196,14 +200,16 @@ const builtinHelpSpecs = new Map<string, HelpSpec>([ const commandExamples: Partial<Record<RouteBindingId, readonly string[]>> = { "tab.list": ["firefox-cli tab --json"], + connect: ["firefox-cli connect"], "tab.new": ["firefox-cli tab new https://example.com"], "tab.select": ["firefox-cli tab select 1", "firefox-cli tab select id:42"], open: ["firefox-cli open https://example.com", "firefox-cli open --new-tab https://example.com"], snapshot: ["firefox-cli snapshot -i", "firefox-cli snapshot -s main -d 3 --json"], get: ["firefox-cli get title", "firefox-cli get text '#content' --json"], - wait: ["firefox-cli wait --url '*dashboard*'", "firefox-cli wait --selector '#ready'"], + wait: ["firefox-cli wait --url '*dashboard*'", "firefox-cli wait '#ready'"], click: ["firefox-cli click 'button[type=submit]'", "firefox-cli click @e12"], fill: ["firefox-cli fill '#email' user@example.com"], + notify: ["firefox-cli notify 'Action needed' 'Open Firefox to approve control'"], batch: ['firefox-cli batch \'[["open","https://example.com"],["snapshot","-i"]]\' --json'], }; @@ -252,7 +258,7 @@ export function renderHelp(): string { ' Inspect content: firefox-cli get title --json; firefox-cli find text "Sign in"', ' Act on elements: firefox-cli click "button[type=submit]"; firefox-cli fill "#email" user@example.com', " Manage targets: firefox-cli tab; firefox-cli tab select 1; firefox-cli window", - ' Synchronize: firefox-cli wait --url "*dashboard*"; firefox-cli wait --network-idle', + ' Synchronize: firefox-cli wait --url "*dashboard*"; firefox-cli wait --load networkidle', ' Run a workflow: firefox-cli batch \'[["open","https://example.com"],["snapshot","-i"]]\'', "", "Usage:", diff --git a/packages/cli/src/parse-options.ts b/packages/cli/src/parse-options.ts index cae7b47..037a893 100644 --- a/packages/cli/src/parse-options.ts +++ b/packages/cli/src/parse-options.ts @@ -54,11 +54,12 @@ export function optionalUrl(url: string | undefined): { readonly url?: string } export function optionalStringOption(args: readonly string[], names: readonly string[], outputKey: "selector"): { readonly selector?: string }; export function optionalStringOption(args: readonly string[], names: readonly string[], outputKey: "generationId"): { readonly generationId?: string }; export function optionalStringOption(args: readonly string[], names: readonly string[], outputKey: "urlGlob"): { readonly urlGlob?: string }; +export function optionalStringOption(args: readonly string[], names: readonly string[], outputKey: "id"): { readonly id?: string }; export function optionalStringOption( args: readonly string[], names: readonly string[], - outputKey: "selector" | "generationId" | "urlGlob", -): { readonly selector?: string; readonly generationId?: string; readonly urlGlob?: string } { + outputKey: "selector" | "generationId" | "urlGlob" | "id", +): { readonly selector?: string; readonly generationId?: string; readonly urlGlob?: string; readonly id?: string } { const value = getOptionValue(args, names); if (value === undefined) { return {}; diff --git a/packages/cli/src/route-registry.ts b/packages/cli/src/route-registry.ts index eaa8733..70abcd9 100644 --- a/packages/cli/src/route-registry.ts +++ b/packages/cli/src/route-registry.ts @@ -26,17 +26,19 @@ import { buildIsRequest, buildLogRequest, buildNetworkRequest, + buildNotifyRequest, buildRefRequest, buildSnapshotRequest, buildStorageRequest, } from "./commands/content.js"; import { buildEvalRequest } from "./commands/eval.js"; import { buildCapabilitiesRequest, buildNavigationRequest, buildOpenRequest } from "./commands/navigation.js"; +import { buildOpenApprovalRequest } from "./commands/pairing.js"; import { buildPdfRequest, buildSetViewportRequest } from "./commands/phase8.js"; import { buildScreenshotRequest } from "./commands/screenshot.js"; import { buildTabsRequest, buildWindowsRequest } from "./commands/tabs-windows.js"; import { buildWaitRequest } from "./commands/wait.js"; -import { cliResponseFormatters } from "./format.js"; +import { cliResponseFormatters, formatApprovalRequest } from "./format.js"; import { getPositionals } from "./parse.js"; import type { CliRequestBuilder, CliResponseFormatter, CliResponseFormatterKind, CliRouteBinding, CliRouteParserSpec } from "./types.js"; @@ -91,10 +93,12 @@ const routeFormatterSpecs = { console: routeFormatter("console", "json-object", cliResponseFormatters["json-object"]), errors: routeFormatter("errors", "json-object", cliResponseFormatters["json-object"]), highlight: routeFormatter("highlight", "json-object", cliResponseFormatters["json-object"]), + notify: routeFormatter("notify", "json-object", cliResponseFormatters["json-object"]), pdf: routeFormatter("pdf", "json-object", cliResponseFormatters["json-object"]), "set.viewport": routeFormatter("set.viewport", "json-object", cliResponseFormatters["json-object"]), diff: routeFormatter("diff", "json-object", cliResponseFormatters["json-object"]), batch: routeFormatter("batch", "batch", cliResponseFormatters.batch), + connect: routeFormatter("pair.requestApproval", "json-object", formatApprovalRequest), click: routeFormatter("click", "action", cliResponseFormatters.action), dblclick: routeFormatter("dblclick", "action", cliResponseFormatters.action), focus: routeFormatter("focus", "action", cliResponseFormatters.action), @@ -184,10 +188,12 @@ export const cliRouteBindings = { console: bindCliRoute("console", "firefox-cli console list|clear [--json]", buildLogRequest), errors: bindCliRoute("errors", "firefox-cli errors list|clear [--json]", buildLogRequest), highlight: bindCliRoute("highlight", "firefox-cli highlight <selector|@ref> [--json]", buildHighlightRequest), + notify: bindCliRoute("notify", "firefox-cli notify [--id id] <title> [message...] [--json]", buildNotifyRequest), pdf: bindCliRoute("pdf", "firefox-cli pdf <path> [--json]", buildPdfRequest), "set.viewport": bindCliRoute("set.viewport", "firefox-cli set viewport <width> <height> [--json]", buildSetViewportRequest), diff: bindCliRoute("diff", "firefox-cli diff url|title|snapshot <expected> [--json]", buildDiffRequest), batch: bindCliRoute("batch", "firefox-cli batch <json> | --stdin [--bail] [--json]", buildBatchRequest), + connect: bindCliRoute("connect", "firefox-cli connect [--json]", buildOpenApprovalRequest), click: bindCliRoute("click", "firefox-cli click <selector|@ref> [--json]", buildElementActionRequest), dblclick: bindCliRoute("dblclick", "firefox-cli dblclick <selector|@ref> [--json]", buildElementActionRequest), focus: bindCliRoute("focus", "firefox-cli focus <selector|@ref> [--json]", buildElementActionRequest), diff --git a/packages/cli/src/runner.ts b/packages/cli/src/runner.ts index 5266794..02e2ad9 100644 --- a/packages/cli/src/runner.ts +++ b/packages/cli/src/runner.ts @@ -52,7 +52,7 @@ async function runCliOrThrow(args: readonly string[], dependencies: CliDependenc if (args[0] === "unpair") { await dependencies.clearPairState?.(); - return ok("Pair state cleared. Approve firefox-cli again from the extension popup.\n"); + return ok("Pair state cleared. Run `firefox-cli connect` to request approval again.\n"); } const routeBinding = findCliRouteBindingForArgv(args); diff --git a/packages/cli/src/transport.ts b/packages/cli/src/transport.ts index efdbcdd..bb07a93 100644 --- a/packages/cli/src/transport.ts +++ b/packages/cli/src/transport.ts @@ -43,7 +43,7 @@ export function formatProtocolError(error: ProtocolError): string { } if (error.code === "NATIVE_HOST_UNAVAILABLE") { - return `Native host unavailable: ${error.message} Run \`firefox-cli setup\`, install the extension, run \`firefox-cli setup native-host\`, then approve the extension popup.\n`; + return `Native host unavailable: ${error.message} Open Firefox, run \`firefox-cli setup\` if setup is incomplete, then run \`firefox-cli connect\`.\n`; } if (error.code === "VERSION_MISMATCH") { @@ -58,5 +58,9 @@ export function formatProtocolError(error: ProtocolError): string { return `${error.code}: ${error.message} Try a normal web page tab and reload it after updating the extension.\n`; } + if (error.code === "ACTION_REJECTED") { + return `${error.message}\n`; + } + return `${error.code}: ${error.message}\n`; } diff --git a/packages/extension/package.json b/packages/extension/package.json index dacddb9..75250c2 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -1,6 +1,6 @@ { "name": "@firefox-cli/extension", - "version": "0.1.1", + "version": "0.2.0", "private": true, "type": "module", "dependencies": { diff --git a/packages/extension/src/approval-permissions.ts b/packages/extension/src/approval-permissions.ts new file mode 100644 index 0000000..95d670f --- /dev/null +++ b/packages/extension/src/approval-permissions.ts @@ -0,0 +1,19 @@ +import { getExtensionPermissionRequirements } from "@firefox-cli/protocol"; + +export async function requestHostAccess(): Promise<boolean> { + const permissions = browser.permissions; + if (permissions === undefined) { + throw new Error("Firefox permissions API is unavailable."); + } + + const required = { origins: getExtensionPermissionRequirements().popupApprovalOrigins }; + const captureApiRequiresReload = typeof browser.tabs.captureVisibleTab !== "function"; + if (await permissions.contains(required)) { + return captureApiRequiresReload; + } + + if (!(await permissions.request(required))) { + throw new Error("Approve host access for all websites to enable browser control."); + } + return true; +} diff --git a/packages/extension/src/approval-request-service.ts b/packages/extension/src/approval-request-service.ts new file mode 100644 index 0000000..7d30f1f --- /dev/null +++ b/packages/extension/src/approval-request-service.ts @@ -0,0 +1,251 @@ +import { createErrorResponseForRequest, createOkResponse, type ProtocolError, type RequestEnvelope, type ResponseEnvelope } from "@firefox-cli/protocol"; +import type { BackgroundBrowserAdapter } from "./browser-commands.js"; + +export const USER_DENIED_APPROVAL_MESSAGE = + "User explicitly denied your request. Do not try to circumvent this decision by any means; do not try to re-request approval. If your desired usage was optional, skip it and use other tools. If denial materially affects your work (you need the CLI legitimately), ask the user how they'd like to proceed."; + +const RATE_LIMIT_MESSAGE_PREFIX = + "Request rate-limited: to prevent disturbing the user, approval auto-denied. If the user wants you to request approval again, ask them to manually open the extension popup and approve; otherwise wait "; +const RATE_LIMIT_SECONDS = [3, 27, 81] as const; +const APPROVAL_PAGE = "approval-request.html"; + +interface PendingApprovalRequest { + readonly request: RequestEnvelope<"pair.requestApproval">; + readonly resolve: (response: ResponseEnvelope<"pair.requestApproval">) => void; + readonly requestId: string; + status: "pending" | "approving"; + url: string; +} + +export interface ApprovalRequestViewState { + readonly active: boolean; + readonly close?: boolean; + readonly url?: string; +} + +export class ApprovalRequestService { + readonly #adapter: BackgroundBrowserAdapter; + readonly #nowMs: () => number; + #pending: PendingApprovalRequest | undefined; + #nextAllowedAtMs = 0; + #rateLimitIndex = 0; + + constructor(options: { readonly adapter: BackgroundBrowserAdapter; readonly nowMs?: () => number }) { + this.#adapter = options.adapter; + this.#nowMs = options.nowMs ?? Date.now; + } + + async requestApproval(request: RequestEnvelope<"pair.requestApproval">, approved: boolean): Promise<ResponseEnvelope<"pair.requestApproval">> { + if (approved) { + return createErrorResponseForRequest(request, { + code: "ACTION_REJECTED", + message: await this.#alreadyApprovedMessage(), + }); + } + + const rateLimited = this.#rateLimitError(); + if (rateLimited !== undefined) { + return createErrorResponseForRequest(request, rateLimited); + } + + if (this.#pending !== undefined) { + return createErrorResponseForRequest(request, { + code: "ACTION_REJECTED", + message: "An approval request is already open in Firefox.", + }); + } + + return new Promise<ResponseEnvelope<"pair.requestApproval">>((resolve) => { + const pagePath = `${APPROVAL_PAGE}?request=${encodeURIComponent(request.id)}`; + this.#pending = { request, resolve, requestId: request.id, status: "pending", url: pagePath }; + this.#recordApprovalRequest(); + this.#openApprovalPage(pagePath).catch((error: unknown) => { + this.#rejectRequest(request.id, { + code: "NATIVE_HOST_UNAVAILABLE", + message: error instanceof Error ? error.message : String(error), + }); + }); + }); + } + + async openApprovalPage(request: RequestEnvelope<"pair.openApproval">, approved: boolean): Promise<ResponseEnvelope<"pair.openApproval">> { + if (approved) { + return createErrorResponseForRequest(request, { + code: "ACTION_REJECTED", + message: await this.#alreadyApprovedMessage(), + }); + } + + const rateLimited = this.#rateLimitError(); + if (rateLimited !== undefined) { + return createErrorResponseForRequest(request, rateLimited); + } + if (this.#pending !== undefined) { + return createErrorResponseForRequest(request, { + code: "ACTION_REJECTED", + message: "An approval request is already open in Firefox.", + }); + } + + this.#recordApprovalRequest(); + await this.#showApprovalNotification(); + try { + return createOkResponse(request, { ok: true, url: await this.#adapter.openExtensionPage(`${APPROVAL_PAGE}?manual=1`) }); + } catch (error: unknown) { + return createErrorResponseForRequest(request, { + code: "NATIVE_HOST_UNAVAILABLE", + message: error instanceof Error ? error.message : String(error), + }); + } + } + + getViewState(requestId: string | undefined): ApprovalRequestViewState { + if (!this.#requestMatchesPending(requestId)) { + return { active: false }; + } + return this.#pending === undefined ? { active: false } : { active: true, url: this.#pending.url }; + } + + async approve(requestId: string | undefined, approvePairing: () => Promise<boolean>): Promise<ApprovalRequestViewState> { + if (!this.#requestMatchesPending(requestId)) { + return { active: false }; + } + const pending = this.#pending; + if (pending?.status !== "pending") { + return { active: false }; + } + pending.status = "approving"; + let approved: boolean; + try { + approved = await approvePairing(); + } catch (error) { + if (this.#pending === pending) { + this.#pending = undefined; + pending.resolve( + createErrorResponseForRequest(pending.request, { + code: "NATIVE_HOST_UNAVAILABLE", + message: error instanceof Error ? error.message : String(error), + }), + ); + return { active: false, close: true }; + } + return { active: false }; + } + if (approved) { + this.#pending = undefined; + this.#rateLimitIndex = 0; + this.#nextAllowedAtMs = 0; + pending.resolve(createOkResponse(pending.request, { ok: true, url: pending.url })); + return { active: false, close: true }; + } else if (this.#pending === pending) { + pending.status = "pending"; + } + return this.getViewState(requestId); + } + + deny(requestId: string | undefined): ApprovalRequestViewState { + if (!this.#requestMatchesPending(requestId)) { + return { active: false }; + } + const pending = this.#pending; + if (pending?.status === "pending") { + this.#pending = undefined; + pending.resolve( + createErrorResponseForRequest(pending.request, { + code: "ACTION_REJECTED", + message: USER_DENIED_APPROVAL_MESSAGE, + }), + ); + return { active: false, close: true }; + } + return this.getViewState(requestId); + } + + acceptExistingApproval(): void { + const pending = this.#pending; + if (pending !== undefined) { + this.#pending = undefined; + this.#rateLimitIndex = 0; + this.#nextAllowedAtMs = 0; + pending.resolve(createOkResponse(pending.request, { ok: true, url: pending.url })); + } + } + + rejectPending(error: ProtocolError): void { + const pending = this.#pending; + if (pending !== undefined) { + this.#pending = undefined; + pending.resolve(createErrorResponseForRequest(pending.request, error)); + } + } + + async #alreadyApprovedMessage(): Promise<string> { + const instance = await this.#adapter.getExtensionInstance(); + const windowSuffix = instance.focusedWindowId === undefined ? "" : `, focused window id ${String(instance.focusedWindowId)}`; + return `firefox-cli is already approved for Firefox extension instance ${instance.extensionUrl}${windowSuffix}.`; + } + + #rateLimitError(): ProtocolError | undefined { + const remainingMs = this.#nextAllowedAtMs - this.#nowMs(); + if (remainingMs <= 0) { + return undefined; + } + const retryAfterSeconds = this.#rateLimitSeconds(); + this.#rateLimitIndex += 1; + this.#nextAllowedAtMs = this.#nowMs() + retryAfterSeconds * 1000; + return { + code: "ACTION_REJECTED", + message: `${RATE_LIMIT_MESSAGE_PREFIX}${formatSeconds(retryAfterSeconds)} before trying again.`, + details: { remainingSeconds: retryAfterSeconds }, + }; + } + + #recordApprovalRequest(): void { + this.#nextAllowedAtMs = this.#nowMs() + this.#rateLimitSeconds() * 1000; + } + + #requestMatchesPending(requestId: string | undefined): boolean { + return this.#pending !== undefined && this.#pending.status === "pending" && requestId === this.#pending.requestId; + } + + async #openApprovalPage(pagePath: string): Promise<void> { + await this.#showApprovalNotification(); + const url = await this.#adapter.openExtensionPage(pagePath); + if (this.#pending !== undefined && this.#pending.url === pagePath) { + this.#pending.url = url; + } + } + + #rejectRequest(requestId: string, error: ProtocolError): void { + const pending = this.#pending; + if (pending?.requestId === requestId) { + this.#pending = undefined; + pending.resolve(createErrorResponseForRequest(pending.request, error)); + } + } + + async #showApprovalNotification(): Promise<void> { + try { + await this.#adapter.showNotification({ + id: "firefox-cli-approval", + title: "firefox-cli approval requested", + message: "A CLI client is asking for Firefox control approval right now.", + }); + } catch { + // The approval page is the authoritative prompt. Notification failures must not block it. + } + } + + #rateLimitSeconds(): number { + const configured = RATE_LIMIT_SECONDS[this.#rateLimitIndex]; + if (configured !== undefined) { + return configured; + } + const lastConfigured = RATE_LIMIT_SECONDS[2]; + return lastConfigured * 3 ** (this.#rateLimitIndex - RATE_LIMIT_SECONDS.length + 1); + } +} + +function formatSeconds(seconds: number): string { + return seconds === 1 ? "1 second" : `${String(seconds)} seconds`; +} diff --git a/packages/extension/src/approval-request.css b/packages/extension/src/approval-request.css new file mode 100644 index 0000000..2e55905 --- /dev/null +++ b/packages/extension/src/approval-request.css @@ -0,0 +1,109 @@ +:root { + color-scheme: dark; + --flow-primary: #00d46a; + --flow-primary-light: #15ff8a; + --surface: #111418; + --surface-elevated: #1e2227; + --text: #f4fff8; + --text-muted: #9facb8; + --danger: #ff6678; + --border: rgba(21, 255, 138, 0.18); + --base-font: "Comfortaa", "Montserrat", system-ui, -apple-system, blinkmacsystemfont, "Segoe UI", helvetica, arial, sans-serif; + --heading-font: "Montserrat Alternates", "Comfortaa", system-ui, -apple-system, blinkmacsystemfont, "Segoe UI", helvetica, arial, sans-serif; +} + +* { + box-sizing: border-box; +} + +body { + background: var(--surface); + color: var(--text); + font: 13px / 1.45 var(--base-font); + margin: 0; + min-height: 100vh; +} + +.approval-page { + display: grid; + min-height: 100vh; + padding: 24px; + place-items: center; +} + +.approval-dialog { + background: var(--surface-elevated); + border: 1px solid var(--border); + border-radius: 18px; + box-shadow: 0 12px 28px rgba(0, 0, 0, 0.24); + display: grid; + gap: 12px; + padding: 18px; + width: min(320px, 100%); +} + +h1, +.request-state, +.error { + margin: 0; +} + +h1 { + font: 700 18px / 1.25 var(--heading-font); +} + +.request-state { + color: var(--text-muted); +} + +.error { + background: rgba(255, 102, 120, 0.11); + border: 1px solid rgba(255, 102, 120, 0.28); + border-radius: 12px; + color: #ffd8de; + padding: 9px 10px; +} + +.actions { + display: grid; + gap: 8px; + grid-template-columns: 1fr 1fr; + margin-top: 4px; +} + +button { + border-radius: 999px; + cursor: pointer; + font: 700 13px / 1 var(--base-font); + padding: 11px 14px; +} + +button:focus-visible { + outline: 2px solid var(--flow-primary-light); + outline-offset: 3px; +} + +button:disabled { + cursor: progress; + opacity: 0.72; +} + +.primary-action { + background: var(--flow-primary); + border: 0; + color: #041008; +} + +.primary-action:hover { + filter: brightness(1.05); +} + +.deny-action { + background: transparent; + border: 1px solid var(--danger); + color: var(--text); +} + +.deny-action:hover { + color: #ffd8de; +} diff --git a/packages/extension/src/approval-request.html b/packages/extension/src/approval-request.html new file mode 100644 index 0000000..57220a1 --- /dev/null +++ b/packages/extension/src/approval-request.html @@ -0,0 +1,23 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>firefox-cli approval request + + + +
+
+

Approve CLI control?

+

A CLI client is requesting control of this Firefox instance.

+ +
+ + +
+
+
+ + + diff --git a/packages/extension/src/approval-request.test.ts b/packages/extension/src/approval-request.test.ts new file mode 100644 index 0000000..e1a857d --- /dev/null +++ b/packages/extension/src/approval-request.test.ts @@ -0,0 +1,137 @@ +import { readFile } from "node:fs/promises"; +import { JSDOM } from "jsdom"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +describe("approval request page", () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it("renders a dedicated centered approval dialog with approve and deny actions", async () => { + const { document } = await renderApprovalRequestPage(); + const css = await readFile(new URL("./approval-request.css", import.meta.url), "utf8"); + + expect(document.title).toBe("firefox-cli approval request"); + expect(document.querySelector(".approval-page")).not.toBeNull(); + expect(document.querySelector(".approval-dialog")).not.toBeNull(); + expect(document.querySelector(".approval-dialog")?.textContent).toContain("A CLI client is requesting control of this Firefox instance."); + expect(document.querySelector("#approve")?.textContent).toBe("Approve"); + expect(document.querySelector("#deny")?.textContent).toBe("Deny"); + expect(css).toContain("width: min(320px, 100%)"); + expect(css).toContain("border: 1px solid var(--danger)"); + expect(css).toContain("--danger: #ff6678"); + }); + + it("requests Firefox host access before approving the pending CLI request", async () => { + const sendMessage = vi.fn().mockResolvedValueOnce({ active: true }).mockResolvedValueOnce({ active: false, close: true }); + const contains = vi.fn(async () => false); + const request = vi.fn(async () => true); + const events: string[] = []; + + const { document } = await renderApprovalRequestPage({ + sendMessage, + contains, + request, + reload: () => { + events.push("reload"); + }, + removeTab: async () => { + events.push("close"); + }, + }); + document.querySelector("#approve")?.click(); + + await vi.waitFor(() => { + expect(sendMessage).toHaveBeenLastCalledWith({ type: "firefox-cli:approve-request", requestId: "approval-1" }); + }); + + expect(contains).toHaveBeenCalledWith({ origins: [""] }); + expect(request).toHaveBeenCalledWith({ origins: [""] }); + await vi.waitFor(() => { + expect(events).toEqual(["reload", "close"]); + }); + }); + + it("denies the pending CLI request without asking for Firefox host access", async () => { + const sendMessage = vi.fn().mockResolvedValueOnce({ active: true }).mockResolvedValueOnce({ active: false, close: true }); + const request = vi.fn(async () => true); + const removeTab = vi.fn(async () => undefined); + + const { document } = await renderApprovalRequestPage({ sendMessage, request, removeTab }); + document.querySelector("#deny")?.click(); + + await vi.waitFor(() => { + expect(sendMessage).toHaveBeenLastCalledWith({ type: "firefox-cli:deny-approval-request", requestId: "approval-1" }); + }); + expect(request).not.toHaveBeenCalled(); + await vi.waitFor(() => { + expect(removeTab).toHaveBeenCalledWith(99); + }); + }); + + it("reloads after manual approval grants Firefox host access without closing the tab", async () => { + const sendMessage = vi.fn().mockResolvedValueOnce({ approved: true }); + const contains = vi.fn(async () => false); + const request = vi.fn(async () => true); + const reload = vi.fn(); + const removeTab = vi.fn(async () => undefined); + + const { document } = await renderApprovalRequestPage({ + search: "?manual=1", + sendMessage, + contains, + request, + reload, + removeTab, + }); + document.querySelector("#approve")?.click(); + + await vi.waitFor(() => { + expect(sendMessage).toHaveBeenLastCalledWith({ type: "firefox-cli:approve" }); + }); + + expect(reload).toHaveBeenCalledTimes(1); + expect(removeTab).not.toHaveBeenCalled(); + }); +}); + +async function renderApprovalRequestPage( + options: { + readonly search?: string; + readonly sendMessage?: (message: unknown) => Promise; + readonly contains?: (permissions: { readonly origins: readonly string[] }) => Promise; + readonly request?: (permissions: { readonly origins: readonly string[] }) => Promise; + readonly reload?: () => void; + readonly removeTab?: (tabId: number) => Promise; + } = {}, +): Promise<{ readonly document: Document }> { + vi.resetModules(); + const dom = new JSDOM(await readFile(new URL("./approval-request.html", import.meta.url), "utf8"), { + url: `moz-extension://test/approval-request.html${options.search ?? "?request=approval-1"}`, + }); + + vi.stubGlobal("window", dom.window); + vi.stubGlobal("document", dom.window.document); + vi.stubGlobal("browser", { + runtime: { + sendMessage: options.sendMessage ?? vi.fn(async () => ({ active: true })), + reload: options.reload ?? vi.fn(), + }, + permissions: { + contains: options.contains ?? vi.fn(async () => true), + request: options.request ?? vi.fn(async () => true), + }, + tabs: { + captureVisibleTab: vi.fn(), + getCurrent: vi.fn(async () => ({ id: 99, index: 0, active: true, windowId: 1 })), + remove: options.removeTab ?? vi.fn(async () => undefined), + }, + }); + + await import("./approval-request.js"); + await vi.waitFor(() => { + expect(dom.window.document.querySelector("#request-state")?.textContent).not.toBe("Checking approval..."); + }); + return { document: dom.window.document }; +} diff --git a/packages/extension/src/approval-request.ts b/packages/extension/src/approval-request.ts new file mode 100644 index 0000000..6bbe3cf --- /dev/null +++ b/packages/extension/src/approval-request.ts @@ -0,0 +1,112 @@ +import { requestHostAccess } from "./approval-permissions.js"; + +interface ApprovalRequestState { + readonly active: boolean; + readonly close?: boolean; +} + +interface ExtensionStatus { + readonly approved: boolean; + readonly lastError?: string; +} + +const stateElement = document.querySelector("#request-state"); +const errorElement = document.querySelector("#error"); +const approveButton = document.querySelector("#approve"); +const denyButton = document.querySelector("#deny"); +const query = new URLSearchParams(window.location.search); +const manualApproval = query.get("manual") === "1"; +const requestId = manualApproval ? undefined : (query.get("request") ?? undefined); + +approveButton?.addEventListener("click", () => { + approve().catch(renderError); +}); + +denyButton?.addEventListener("click", () => { + deny().catch(renderError); +}); + +loadRequest().catch(renderError); + +async function loadRequest(): Promise { + renderState(manualApproval ? { active: true } : await sendMessage("firefox-cli:get-approval-request")); +} + +async function approve(): Promise { + setBusy(true); + const reloadAfterApproval = await requestHostAccess(); + const state = manualApproval + ? statusToState(await sendMessage("firefox-cli:approve")) + : await sendMessage("firefox-cli:approve-request"); + renderState(state); + await finishTerminalRequest(state, reloadAfterApproval); +} + +async function deny(): Promise { + setBusy(true); + const state = manualApproval ? { active: false } : await sendMessage("firefox-cli:deny-approval-request"); + renderState(state); + await finishTerminalRequest(state, false); +} + +async function sendMessage(type: string): Promise { + const response: T = await browser.runtime.sendMessage({ type, requestId }); + return response; +} + +function renderState(state: ApprovalRequestState): void { + setBusy(false); + if (!state.active) { + if (stateElement) { + stateElement.textContent = "There is no active CLI approval request."; + } + if (approveButton) { + approveButton.disabled = true; + } + if (denyButton) { + denyButton.disabled = true; + } + } +} + +async function finishTerminalRequest(state: ApprovalRequestState, reloadAfterApproval: boolean): Promise { + if (state.close !== true) { + if (reloadAfterApproval) { + browser.runtime.reload(); + } + return; + } + const tab = await browser.tabs.getCurrent(); + if (reloadAfterApproval) { + browser.runtime.reload(); + } + if (tab?.id !== undefined) { + await browser.tabs.remove(tab.id); + } else { + window.close(); + } +} + +function statusToState(status: ExtensionStatus): ApprovalRequestState { + if (status.lastError !== undefined) { + renderError(status.lastError); + } + return { active: !status.approved }; +} + +function renderError(error: unknown): void { + setBusy(false); + if (errorElement) { + errorElement.hidden = false; + errorElement.textContent = error instanceof Error ? error.message : String(error); + } +} + +function setBusy(busy: boolean): void { + if (approveButton) { + approveButton.disabled = busy; + } + if (denyButton) { + denyButton.disabled = busy; + } +} diff --git a/packages/extension/src/background-bootstrap-storage.test.ts b/packages/extension/src/background-bootstrap-storage.test.ts new file mode 100644 index 0000000..3c33f94 --- /dev/null +++ b/packages/extension/src/background-bootstrap-storage.test.ts @@ -0,0 +1,199 @@ +import { createOkResponse, parseBoundaryRequest, type RequestEnvelope } from "@firefox-cli/protocol"; +import { describe, expect, it } from "vitest"; +import { type BackgroundBrowserApi, startBackground } from "./background-bootstrap.js"; +import type { NativePortLike } from "./background-controller.js"; + +const PAIR_TOKEN_STORAGE_KEY = "firefoxCliPairToken"; +interface NotificationOptions { + readonly type: "basic"; + readonly title: string; + readonly message: string; +} + +describe("background bootstrap storage", () => { + it("persists popup approval tokens in extension storage", async () => { + const port = new FakeNativePort(); + const browser = createFakeBrowserApi(port); + const lifecycle = startBackground({ + browser, + manifest: { version: "0.0.0" }, + controllerOptions: { reconnectDelaysMs: [] }, + }); + await completeNativeHello(port); + + const approval = lifecycle.controller.handleRuntimeMessage({ type: "firefox-cli:approve" }); + const approve = latestHostRequest(port); + expect(approve.command).toBe("pair.approve"); + port.emitMessage( + createOkResponse(approve, { + hostId: "host-1", + extensionId: "ff-cli-bridge@respawn.pro", + token: "paired-token", + generation: 1, + approvedAt: "2026-01-02T03:04:05.000Z", + }), + ); + + await approval; + + await expect(browser.storage.local.get(PAIR_TOKEN_STORAGE_KEY)).resolves.toEqual({ + [PAIR_TOKEN_STORAGE_KEY]: "paired-token", + }); + }); + + it("restores stored approval tokens from extension storage on startup", async () => { + const port = new FakeNativePort(); + const browser = createFakeBrowserApi(port, { [PAIR_TOKEN_STORAGE_KEY]: "stored-token" }); + + startBackground({ + browser, + manifest: { version: "0.0.0" }, + controllerOptions: { reconnectDelaysMs: [] }, + }); + await flushPromises(); + + expect( + port.messages.some((message) => { + const parsed = parseBoundaryRequest("host-to-extension", message, { protocolVersion: 1 }); + return parsed.ok && parsed.value.command === "hello" && parsed.value.params.pairToken === "stored-token"; + }), + ).toBe(true); + }); +}); + +class FakeNativePort implements NativePortLike { + readonly messages: unknown[] = []; + readonly onMessage = createEvent(); + readonly onDisconnect = createEvent<{ readonly message?: string } | undefined>(); + + postMessage(message: unknown): void { + this.messages.push(message); + } + + emitMessage(message: unknown): void { + this.onMessage.emit(message); + } +} + +function createFakeBrowserApi(port: NativePortLike, initialStorage: Record = {}): BackgroundBrowserApi { + const runtimeOnMessage = createEvent<{ readonly type?: string }, unknown>(); + const storageValues: Record = { ...initialStorage }; + + return { + runtime: { + onMessage: runtimeOnMessage, + connectNative: () => port, + getURL: (path) => `moz-extension://fake/${path}`, + sendMessage: async (): Promise => { + throw new Error("runtime.sendMessage is not implemented in this fake."); + }, + reload: () => undefined, + }, + windows: { + getAll: async () => [], + create: async () => ({ id: 1 }), + update: async (id: number) => ({ id, focused: true }), + remove: async () => undefined, + }, + tabs: { + create: async () => ({ id: 1, index: 0, active: true, windowId: 1 }), + update: async (id: number) => ({ id, index: 0, active: true, windowId: 1 }), + getCurrent: async () => ({ id: 1, index: 0, active: true, windowId: 1 }), + get: async (id: number) => ({ id, index: 0, active: true, windowId: 1 }), + remove: async () => undefined, + goBack: async () => undefined, + goForward: async () => undefined, + reload: async () => undefined, + sendMessage: async () => ({}), + captureVisibleTab: async () => "data:image/png;base64,", + onRemoved: createEvent(), + }, + permissions: { + contains: async () => true, + request: async () => true, + }, + scripting: { + executeScript: async () => [], + }, + storage: { + local: { + get: async (key: string) => ({ [key]: storageValues[key] }), + set: async (values: Record) => { + Object.assign(storageValues, values); + }, + }, + }, + downloads: { + download: async () => 1, + search: async () => [], + }, + cookies: { + getAll: async () => [], + set: async (cookie) => cookie, + remove: async () => undefined, + }, + notifications: { + create: createNotification, + }, + }; +} + +async function createNotification(options: NotificationOptions): Promise; +async function createNotification(id: string, options: NotificationOptions): Promise; +async function createNotification(idOrOptions: string | NotificationOptions): Promise { + return typeof idOrOptions === "string" ? idOrOptions : "notification-1"; +} + +async function completeNativeHello(port: FakeNativePort): Promise { + const hello = latestHostRequest(port); + expect(hello.command).toBe("hello"); + port.emitMessage( + createOkResponse(hello, { + accepted: true, + negotiatedProtocolVersion: 1, + peer: { + component: "native-host", + productName: "firefox-cli", + productVersion: "0.0.0", + protocolMin: 1, + protocolMax: 1, + features: [], + }, + }), + ); + await flushPromises(); +} + +function latestHostRequest(port: FakeNativePort): RequestEnvelope { + const raw = port.messages.at(-1); + if (raw === undefined) { + throw new Error("Expected native-host request."); + } + const parsed = parseBoundaryRequest("host-to-extension", raw, { protocolVersion: 1 }); + if (!parsed.ok) { + throw new Error(parsed.error.message); + } + return parsed.value; +} + +function createEvent() { + const listeners: ((value: T) => TResult)[] = []; + return { + addListener(listener: (value: T) => TResult): void { + listeners.push(listener); + }, + removeListener(listener: (value: T) => TResult): void { + const index = listeners.indexOf(listener); + if (index >= 0) { + listeners.splice(index, 1); + } + }, + emit(value: T): readonly TResult[] { + return listeners.map((listener) => listener(value)); + }, + }; +} + +async function flushPromises(): Promise { + await new Promise((resolve) => setTimeout(resolve, 0)); +} diff --git a/packages/extension/src/background-bootstrap-test-cases.ts b/packages/extension/src/background-bootstrap-test-cases.ts index 0a8a044..046fddd 100644 --- a/packages/extension/src/background-bootstrap-test-cases.ts +++ b/packages/extension/src/background-bootstrap-test-cases.ts @@ -6,6 +6,12 @@ import type { NativePortLike } from "./background-controller.js"; import { NetworkObservationService } from "./network-observation-service.js"; import { NetworkRequestTracker } from "./network-tracker.js"; +interface NotificationOptions { + readonly type: "basic"; + readonly title: string; + readonly message: string; +} + export async function runCase01() { const port = new FakeNativePort(); const browser = createFakeBrowserApi(port); @@ -114,6 +120,31 @@ export async function runCase04() { expect(browser.scripting.calls).toEqual([]); } +export async function runCase05() { + const port = new FakeNativePort(); + const browser = createFakeBrowserApi(port); + const networkTracker = new NetworkRequestTracker(); + const networkObservation = new NetworkObservationService({ browser, tracker: networkTracker }); + const adapter = createBackgroundBrowserAdapter({ browser, networkObservation }); + + await expect(adapter.showNotification({ id: "approval", title: "Action needed", message: "Open Firefox." })).resolves.toEqual({ + ok: true, + id: "approval", + }); + await expect(adapter.openExtensionPage("popup.html")).resolves.toBe("moz-extension://fake/popup.html"); + expect(browser.notifications.calls).toEqual([ + { + id: "approval", + options: { + type: "basic", + title: "Action needed", + message: "Open Firefox.", + }, + }, + ]); + expect(browser.tabs.created).toEqual([{ active: true, url: "moz-extension://fake/popup.html" }]); +} + class FakeNativePort implements NativePortLike { readonly onMessage = createEvent(); readonly onDisconnect = createEvent<{ readonly message?: string } | undefined>(); @@ -131,11 +162,30 @@ function createFakeBrowserApi(port: NativePortLike): FakeBackgroundBrowserApi { const onRemoved = createEvent(); let sendMessageCalls = 0; const scriptingCalls: unknown[] = []; + const notificationCalls: { + readonly id: string | undefined; + readonly options: NotificationOptions; + }[] = []; + + function createNotification(options: NotificationOptions): Promise; + function createNotification(id: string, options: NotificationOptions): Promise; + async function createNotification(idOrOptions: string | NotificationOptions, options?: NotificationOptions): Promise { + if (typeof idOrOptions !== "string") { + notificationCalls.push({ id: undefined, options: idOrOptions }); + return "generated-notification"; + } + if (options === undefined) { + throw new Error("Missing notification options."); + } + notificationCalls.push({ id: idOrOptions, options }); + return idOrOptions; + } return { runtime: { onMessage: runtimeOnMessage, connectNative: () => port, + getURL: (path) => `moz-extension://fake/${path}`, sendMessage: async (): Promise => { throw new Error("runtime.sendMessage is not implemented in this fake."); }, @@ -150,14 +200,19 @@ function createFakeBrowserApi(port: NativePortLike): FakeBackgroundBrowserApi { tabs: { failNextSendMessage: false, sendMessageFailure: undefined, - create: async () => ({ id: 1, index: 0, active: true, windowId: 1 }), + create: async function create(this: { readonly created: unknown[] }, options: unknown) { + this.created.push(options); + return { id: 1, index: 0, active: true, windowId: 1 }; + }, update: async (id: number) => ({ id, index: 0, active: true, windowId: 1 }), + getCurrent: async () => ({ id: 1, index: 0, active: true, windowId: 1 }), get: async (id: number) => ({ id, index: 0, active: true, windowId: 1 }), remove: async () => undefined, goBack: async () => undefined, goForward: async () => undefined, reload: async () => undefined, response: undefined, + created: [], sendMessage: async function sendMessage(this: { failNextSendMessage: boolean; response: unknown; @@ -202,6 +257,10 @@ function createFakeBrowserApi(port: NativePortLike): FakeBackgroundBrowserApi { set: async (cookie: BrowserCookie) => cookie, remove: async () => undefined, }, + notifications: { + calls: notificationCalls, + create: createNotification, + }, webRequest: { onBeforeRequest, onCompleted, @@ -232,6 +291,7 @@ type FakeBackgroundBrowserApi = Omit { it("registers runtime eagerly and webRequest listeners lazily by target tab", runCase01); it("preserves content injection and eval execution product contracts", runCase02); it("does not refresh content scripts when an existing script returns a structured mismatch", runCase03); it("does not inject content scripts for classified non-recoverable send failures", runCase04); + it("creates native notifications through the browser adapter", runCase05); }); diff --git a/packages/extension/src/background-bootstrap.ts b/packages/extension/src/background-bootstrap.ts index 00bb359..84feb3e 100644 --- a/packages/extension/src/background-bootstrap.ts +++ b/packages/extension/src/background-bootstrap.ts @@ -11,6 +11,8 @@ type RuntimeMessageListener = (message: RuntimeMessage) => Promise; export type BackgroundBrowserApi = typeof browser; +const PAIR_TOKEN_STORAGE_KEY = "firefoxCliPairToken"; + export interface BackgroundLifecycle { readonly controller: FirefoxCliBackgroundController; dispose(): void; @@ -45,6 +47,7 @@ export function startBackground(options: { }), connectNative: (name) => options.browser.runtime.connectNative(name), productVersion: options.manifest.version, + storageAdapter: createBackgroundStorageAdapter(options.browser), ...createControllerOptions(options.controllerOptions), }); @@ -69,6 +72,19 @@ export function startBackground(options: { }; } +function createBackgroundStorageAdapter(browser: BackgroundBrowserApi): BackgroundStorageAdapter { + return { + getPairToken: async () => { + const values = await browser.storage.local.get(PAIR_TOKEN_STORAGE_KEY); + const value = values[PAIR_TOKEN_STORAGE_KEY]; + return typeof value === "string" && value.length > 0 ? value : null; + }, + setPairToken: async (token) => { + await browser.storage.local.set({ [PAIR_TOKEN_STORAGE_KEY]: token }); + }, + }; +} + function createControllerOptions( options: | { diff --git a/packages/extension/src/background-browser-adapter.ts b/packages/extension/src/background-browser-adapter.ts index fbf1a29..e4b88c9 100644 --- a/packages/extension/src/background-browser-adapter.ts +++ b/packages/extension/src/background-browser-adapter.ts @@ -1,5 +1,5 @@ import { getExtensionPermissionRequirements } from "@firefox-cli/protocol"; -import type { BackgroundBrowserAdapter } from "./background-controller.js"; +import type { BackgroundBrowserAdapter } from "./browser-commands.js"; import { createBrowserCommandDeadline } from "./browser-command/deadline.js"; import { createContentScriptInjectionState, deliverContentScriptRequest, type ContentScriptInjectionState } from "./content-script-delivery.js"; import { executeEvalInPage } from "./eval-executor.js"; @@ -169,6 +169,33 @@ export function createBackgroundBrowserAdapter(options: { } }); }, + showNotification: async (notificationOptions) => { + const payload = { + type: "basic", + title: notificationOptions.title, + message: notificationOptions.message ?? "", + } as const; + return { + ok: true, + id: + notificationOptions.id === undefined + ? await options.browser.notifications.create(payload) + : await options.browser.notifications.create(notificationOptions.id, payload), + }; + }, + getExtensionInstance: async () => { + const windows = await options.browser.windows.getAll({ populate: false }); + const focused = windows.find((window) => window.focused === true); + return { + extensionUrl: options.browser.runtime.getURL(""), + ...(focused?.id === undefined ? {} : { focusedWindowId: focused.id }), + }; + }, + openExtensionPage: async (path) => { + const url = options.browser.runtime.getURL(path); + await options.browser.tabs.create({ active: true, url }); + return url; + }, resizeWindow: async (windowId, size) => { await options.browser.windows.update(windowId, size); const windows = await options.browser.windows.getAll({ populate: true }); diff --git a/packages/extension/src/background-controller-approval-race-test-cases.ts b/packages/extension/src/background-controller-approval-race-test-cases.ts new file mode 100644 index 0000000..f5815bb --- /dev/null +++ b/packages/extension/src/background-controller-approval-race-test-cases.ts @@ -0,0 +1,166 @@ +import { createOkResponse, createRequest } from "@firefox-cli/protocol"; +import { expect } from "vitest"; +import { FirefoxCliBackgroundController } from "./background-controller.js"; +import { + completeNativeHello, + createTestBrowserAdapter, + FakeNativePort, + flushPromises, + latestPairApproveRequest, +} from "./background-controller-test-support.js"; + +export async function runCase09() { + let resolveOpenPage: ((url: string) => void) | undefined; + const openPage = new Promise((resolve) => { + resolveOpenPage = resolve; + }); + const port = new FakeNativePort(); + const controller = new FirefoxCliBackgroundController({ + browserAdapter: createTestBrowserAdapter([], { + openExtensionPage: async () => openPage, + }), + connectNative: () => port, + productVersion: "0.0.0", + }); + controller.start(); + await completeNativeHello(port); + + port.emitMessage(createRequest("pair.requestApproval", {}, "approval-race-1")); + await flushPromises(); + + await expect(controller.handleRuntimeMessage({ type: "firefox-cli:get-approval-request", requestId: "approval-race-1" })).resolves.toEqual({ + active: true, + url: "approval-request.html?request=approval-race-1", + }); + resolveOpenPage?.("moz-extension://test/approval-request.html?request=approval-race-1"); + await controller.handleRuntimeMessage({ type: "firefox-cli:deny-approval-request", requestId: "approval-race-1" }); +} + +export async function runCase10() { + const port = new FakeNativePort(); + const controller = new FirefoxCliBackgroundController({ + browserAdapter: createTestBrowserAdapter([], { + openExtensionPage: async (path) => `moz-extension://test/${path}`, + }), + connectNative: () => port, + productVersion: "0.0.0", + }); + controller.start(); + await completeNativeHello(port); + + const request = createRequest("pair.requestApproval", {}, "approval-atomic-1"); + port.emitMessage(request); + await flushPromises(); + const approval = controller.handleRuntimeMessage({ type: "firefox-cli:approve-request", requestId: "approval-atomic-1" }); + await flushPromises(); + + await expect(controller.handleRuntimeMessage({ type: "firefox-cli:deny-approval-request", requestId: "approval-atomic-1" })).resolves.toEqual({ + active: false, + }); + expect(port.messages).toHaveLength(2); + + const approve = latestPairApproveRequest(port); + port.emitMessage( + createOkResponse(approve, { + hostId: "host-1", + extensionId: "ff-cli-bridge@respawn.pro", + token: "paired-token", + generation: 1, + approvedAt: "2026-01-02T03:04:05.000Z", + }), + ); + await approval; + await flushPromises(); + + expect(port.messages[2]).toEqual({ + protocolVersion: request.protocolVersion, + id: "approval-atomic-1", + ok: true, + result: { + ok: true, + url: "moz-extension://test/approval-request.html?request=approval-atomic-1", + }, + }); +} + +export async function runCase11() { + const port = new FakeNativePort(); + const controller = new FirefoxCliBackgroundController({ + browserAdapter: createTestBrowserAdapter([], { + openExtensionPage: async (path) => `moz-extension://test/${path}`, + }), + connectNative: () => port, + productVersion: "0.0.0", + }); + controller.start(); + await completeNativeHello(port); + + const request = createRequest("pair.openApproval", {}, "legacy-approval-1"); + port.emitMessage(request); + await flushPromises(); + + expect(port.messages[1]).toEqual({ + protocolVersion: request.protocolVersion, + id: "legacy-approval-1", + ok: true, + result: { + ok: true, + url: "moz-extension://test/approval-request.html?manual=1", + }, + }); +} + +export async function runCase12() { + const port = new FakeNativePort(); + const controller = new FirefoxCliBackgroundController({ + browserAdapter: createTestBrowserAdapter([], { + openExtensionPage: async (path) => `moz-extension://test/${path}`, + }), + connectNative: () => port, + productVersion: "0.0.0", + storageAdapter: { + getPairToken: async () => null, + setPairToken: async () => { + throw new Error("Could not persist pair token."); + }, + }, + }); + controller.start(); + await completeNativeHello(port); + + const request = createRequest("pair.requestApproval", {}, "approval-throw-1"); + port.emitMessage(request); + await flushPromises(); + const approval = controller.handleRuntimeMessage({ type: "firefox-cli:approve-request", requestId: "approval-throw-1" }); + await flushPromises(); + + const approve = latestPairApproveRequest(port); + port.emitMessage( + createOkResponse(approve, { + hostId: "host-1", + extensionId: "ff-cli-bridge@respawn.pro", + token: "paired-token", + generation: 1, + approvedAt: "2026-01-02T03:04:05.000Z", + }), + ); + + await expect(approval).resolves.toEqual({ + active: false, + close: true, + }); + await flushPromises(); + + expect(port.messages[2]).toEqual({ + protocolVersion: request.protocolVersion, + id: "approval-throw-1", + ok: false, + error: { + code: "NATIVE_HOST_UNAVAILABLE", + message: "Could not persist pair token.", + }, + }); + await expect(controller.handleRuntimeMessage({ type: "firefox-cli:get-status" })).resolves.toMatchObject({ + approved: false, + }); +} diff --git a/packages/extension/src/background-controller-approval-test-cases.ts b/packages/extension/src/background-controller-approval-test-cases.ts index 8715305..29488c4 100644 --- a/packages/extension/src/background-controller-approval-test-cases.ts +++ b/packages/extension/src/background-controller-approval-test-cases.ts @@ -1,15 +1,17 @@ -import { createOkResponse } from "@firefox-cli/protocol"; +import { createRequest, createOkResponse } from "@firefox-cli/protocol"; import { expect } from "vitest"; import { FirefoxCliBackgroundController } from "./background-controller.js"; import { completeNativeHello, + createTestBrowserAdapter, FakeNativePort, flushPromises, latestHelloRequest, latestPairApproveRequest, sleep, } from "./background-controller-test-support.js"; +import { USER_DENIED_APPROVAL_MESSAGE } from "./approval-request-service.js"; export async function runCase01() { const port = new FakeNativePort(); @@ -229,3 +231,130 @@ export function runCase05() { expect(secondPort.messages).toEqual([]); expect(controller.getStatus().connected).toBe(false); } + +export async function runCase06() { + const port = new FakeNativePort(); + const controller = new FirefoxCliBackgroundController({ + browserAdapter: createTestBrowserAdapter([], { + openExtensionPage: async (path) => `moz-extension://test/${path}`, + }), + connectNative: () => port, + productVersion: "0.0.0", + }); + controller.start(); + await completeNativeHello(port); + + const request = createRequest("pair.requestApproval", {}, "approval-deny-1"); + port.emitMessage(request); + await flushPromises(); + + await expect(controller.handleRuntimeMessage({ type: "firefox-cli:deny-approval-request", requestId: "approval-deny-1" })).resolves.toEqual({ + active: false, + close: true, + }); + await flushPromises(); + + expect(port.messages[1]).toEqual({ + protocolVersion: request.protocolVersion, + id: "approval-deny-1", + ok: false, + error: { + code: "ACTION_REJECTED", + message: USER_DENIED_APPROVAL_MESSAGE, + }, + }); +} + +export async function runCase07() { + let nowMs = 1000; + const port = new FakeNativePort(); + const controller = new FirefoxCliBackgroundController({ + browserAdapter: createTestBrowserAdapter([], { + openExtensionPage: async (path) => `moz-extension://test/${path}`, + }), + connectNative: () => port, + productVersion: "0.0.0", + nowMs: () => nowMs, + }); + controller.start(); + await completeNativeHello(port); + + const first = createRequest("pair.requestApproval", {}, "approval-rate-1"); + port.emitMessage(first); + await flushPromises(); + await controller.handleRuntimeMessage({ type: "firefox-cli:deny-approval-request", requestId: "approval-rate-1" }); + await flushPromises(); + + nowMs = 2000; + const second = createRequest("pair.requestApproval", {}, "approval-rate-2"); + port.emitMessage(second); + await flushPromises(); + + expect(port.messages[2]).toEqual({ + protocolVersion: second.protocolVersion, + id: "approval-rate-2", + ok: false, + error: { + code: "ACTION_REJECTED", + message: + "Request rate-limited: to prevent disturbing the user, approval auto-denied. If the user wants you to request approval again, ask them to manually open the extension popup and approve; otherwise wait 3 seconds before trying again.", + details: { remainingSeconds: 3 }, + }, + }); + + nowMs = 3000; + const third = createRequest("pair.requestApproval", {}, "approval-rate-3"); + port.emitMessage(third); + await flushPromises(); + + expect(port.messages[3]).toEqual({ + protocolVersion: third.protocolVersion, + id: "approval-rate-3", + ok: false, + error: { + code: "ACTION_REJECTED", + message: + "Request rate-limited: to prevent disturbing the user, approval auto-denied. If the user wants you to request approval again, ask them to manually open the extension popup and approve; otherwise wait 27 seconds before trying again.", + details: { remainingSeconds: 27 }, + }, + }); +} + +export async function runCase08() { + const port = new FakeNativePort(); + const controller = new FirefoxCliBackgroundController({ + browserAdapter: createTestBrowserAdapter([], { + getExtensionInstance: async () => ({ extensionUrl: "moz-extension://test/", focusedWindowId: 17 }), + }), + connectNative: () => port, + productVersion: "0.0.0", + }); + controller.start(); + await completeNativeHello(port); + const approval = controller.handleRuntimeMessage({ type: "firefox-cli:approve" }); + const approve = latestPairApproveRequest(port); + port.emitMessage( + createOkResponse(approve, { + hostId: "host-1", + extensionId: "ff-cli-bridge@respawn.pro", + token: "paired-token", + generation: 1, + approvedAt: "2026-01-02T03:04:05.000Z", + }), + ); + await approval; + + const request = createRequest("pair.requestApproval", {}, "approval-approved-1"); + port.emitMessage(request); + await flushPromises(); + + expect(port.messages[2]).toEqual({ + protocolVersion: request.protocolVersion, + id: "approval-approved-1", + ok: false, + error: { + code: "ACTION_REJECTED", + message: "firefox-cli is already approved for Firefox extension instance moz-extension://test/, focused window id 17.", + }, + }); +} diff --git a/packages/extension/src/background-controller-approval.test.ts b/packages/extension/src/background-controller-approval.test.ts index d2572b7..e6e49dc 100644 --- a/packages/extension/src/background-controller-approval.test.ts +++ b/packages/extension/src/background-controller-approval.test.ts @@ -1,5 +1,6 @@ import { describe, it } from "vitest"; -import { runCase01, runCase02, runCase03, runCase04, runCase05 } from "./background-controller-approval-test-cases.js"; +import { runCase01, runCase02, runCase03, runCase04, runCase05, runCase06, runCase07, runCase08 } from "./background-controller-approval-test-cases.js"; +import { runCase09, runCase10, runCase11, runCase12 } from "./background-controller-approval-race-test-cases.js"; describe("FirefoxCliBackgroundController", () => { it("ignores responses that arrive after request timeout", runCase01); @@ -7,4 +8,11 @@ describe("FirefoxCliBackgroundController", () => { it("clears incompatible protocol state on reconnect", runCase03); it("stops controller effects, drains pending requests, and ignores stale native messages", runCase04); it("suppresses reconnect callbacks after stop", runCase05); + it("settles CLI approval requests when the user denies the dedicated page", runCase06); + it("auto-denies repeated approval requests inside the extension rate limit", runCase07); + it("rejects approval requests after approval with Firefox instance diagnostics", runCase08); + it("exposes pending approval state before the approval tab finishes opening", runCase09); + it("ignores deny events while native approval is in flight", runCase10); + it("keeps legacy open-approval requests compatible with the dedicated page", runCase11); + it("settles pending approval requests when native approval throws", runCase12); }); diff --git a/packages/extension/src/background-controller-test-cases.ts b/packages/extension/src/background-controller-test-cases.ts index 16d983c..0a0f05f 100644 --- a/packages/extension/src/background-controller-test-cases.ts +++ b/packages/extension/src/background-controller-test-cases.ts @@ -9,6 +9,7 @@ import { FakeNativePort, flushPromises, latestHelloRequest, + latestPairApproveRequest, } from "./background-controller-test-support.js"; export function runCase01() { @@ -171,12 +172,75 @@ export async function runCase05() { ok: false, error: { code: "NOT_APPROVED", - message: "Approve firefox-cli in the extension popup before running CLI commands.", + message: "Run `firefox-cli connect` before running Firefox control commands.", }, }); } export async function runCase06() { + const port = new FakeNativePort(); + const notifications: unknown[] = []; + const pages: string[] = []; + const controller = new FirefoxCliBackgroundController({ + browserAdapter: createTestBrowserAdapter([], { + showNotification: async (options) => { + notifications.push(options); + return { ok: true, id: options.id ?? "notification-1" }; + }, + openExtensionPage: async (path) => { + pages.push(path); + return `moz-extension://test/${path}`; + }, + }), + connectNative: () => port, + productVersion: "0.0.0", + }); + controller.start(); + await completeNativeHello(port); + + const request = createRequest("pair.requestApproval", {}, "approval-request-1"); + port.emitMessage(request); + await flushPromises(); + + expect(notifications).toEqual([ + { id: "firefox-cli-approval", title: "firefox-cli approval requested", message: "A CLI client is asking for Firefox control approval right now." }, + ]); + expect(pages).toEqual(["approval-request.html?request=approval-request-1"]); + await expect(controller.handleRuntimeMessage({ type: "firefox-cli:get-approval-request", requestId: "approval-request-1" })).resolves.toEqual({ + active: true, + url: "moz-extension://test/approval-request.html?request=approval-request-1", + }); + + const approval = controller.handleRuntimeMessage({ type: "firefox-cli:approve-request", requestId: "approval-request-1" }); + await flushPromises(); + const approve = latestPairApproveRequest(port); + port.emitMessage( + createOkResponse(approve, { + hostId: "host-1", + extensionId: "ff-cli-bridge@respawn.pro", + token: "paired-token", + generation: 1, + approvedAt: "2026-01-02T03:04:05.000Z", + }), + ); + await expect(approval).resolves.toEqual({ + active: false, + close: true, + }); + await flushPromises(); + + expect(port.messages[2]).toEqual({ + protocolVersion: request.protocolVersion, + id: "approval-request-1", + ok: true, + result: { + ok: true, + url: "moz-extension://test/approval-request.html?request=approval-request-1", + }, + }); +} + +export async function runCase07() { const port = new FakeNativePort(); const browserCalls: string[] = []; const controller = new FirefoxCliBackgroundController({ @@ -231,13 +295,13 @@ export async function runCase06() { ok: false, error: { code: "NOT_APPROVED", - message: "Approve firefox-cli in the extension popup before running CLI commands.", + message: "Run `firefox-cli connect` before running Firefox control commands.", }, }); expect(browserCalls).toEqual([]); } -export async function runCase07() { +export async function runCase08() { const port = new FakeNativePort(); const browserCalls: string[] = []; const controller = new FirefoxCliBackgroundController({ diff --git a/packages/extension/src/background-controller-test-support.ts b/packages/extension/src/background-controller-test-support.ts index 1592575..55033ba 100644 --- a/packages/extension/src/background-controller-test-support.ts +++ b/packages/extension/src/background-controller-test-support.ts @@ -1,6 +1,7 @@ import { createOkResponse, createRequest, PROTOCOL_VERSION, parseBoundaryRequest, type RequestEnvelope } from "@firefox-cli/protocol"; import { expect } from "vitest"; -import type { BackgroundBrowserAdapter, BrowserWindowSnapshot, FirefoxCliBackgroundController, NativePortLike } from "./background-controller.js"; +import type { FirefoxCliBackgroundController, NativePortLike } from "./background-controller.js"; +import type { BackgroundBrowserAdapter, BrowserWindowSnapshot } from "./browser-commands.js"; export class FakeNativePort implements NativePortLike { readonly messages: unknown[] = []; @@ -108,6 +109,12 @@ export function createTestBrowserAdapter( listNetworkRequests: async () => [], clearNetworkRequests: async () => undefined, waitForNetworkIdle: async () => undefined, + showNotification: async (options) => ({ + ok: true, + id: options.id ?? "notification-1", + }), + getExtensionInstance: async () => ({ extensionUrl: "moz-extension://test/" }), + openExtensionPage: async (path) => `moz-extension://test/${path}`, resizeWindow: async () => { throw new Error("not implemented"); }, diff --git a/packages/extension/src/background-controller.test.ts b/packages/extension/src/background-controller.test.ts index eaeb0a9..716eb20 100644 --- a/packages/extension/src/background-controller.test.ts +++ b/packages/extension/src/background-controller.test.ts @@ -1,12 +1,13 @@ import { describe, it } from "vitest"; -import { runCase01, runCase02, runCase03, runCase04, runCase05, runCase06, runCase07 } from "./background-controller-test-cases.js"; +import { runCase01, runCase02, runCase03, runCase04, runCase05, runCase06, runCase07, runCase08 } from "./background-controller-test-cases.js"; describe("FirefoxCliBackgroundController", () => { it("connects to the native host and sends hello", runCase01); it("accepts valid hello responses regardless of request ID shape", runCase02); it("answers native-host capability and no-op requests", runCase03); it("lists tabs through the injected Firefox browser adapter", runCase04); - it("rejects native-host requests before popup approval", runCase05); - it("gates unapproved privilege-sensitive native-host requests before browser handlers", runCase06); - it("rejects malformed sensitive native-host requests before browser handlers", runCase07); + it("rejects native-host requests before first-use approval", runCase05); + it("opens the dedicated approval UI before first-use approval", runCase06); + it("gates unapproved privilege-sensitive native-host requests before browser handlers", runCase07); + it("rejects malformed sensitive native-host requests before browser handlers", runCase08); }); diff --git a/packages/extension/src/background-controller.ts b/packages/extension/src/background-controller.ts index 3d146cd..998f67d 100644 --- a/packages/extension/src/background-controller.ts +++ b/packages/extension/src/background-controller.ts @@ -4,6 +4,7 @@ import { createErrorResponseForRequest, createLocalComponentIdentity, createRequest, + isRequestCommand, localProtocolVersionRange, NATIVE_HOST_NAME, PendingRequestTracker, @@ -15,26 +16,20 @@ import type { BackgroundRuntimeAdapter, BackgroundStorageAdapter, ExtensionStatu import { createUnconfiguredBrowserAdapter } from "./background-default-browser-adapter.js"; import { NativeConnectionManager } from "./background-native-connection.js"; import { isResponseLike } from "./background-native-protocol-state.js"; -import { NativeSessionService } from "./background-native-session.js"; +import { isHelloResponse, NativeSessionService } from "./background-native-session.js"; import { PairingStateService } from "./background-pairing-service.js"; import { BackgroundRequestForwarder } from "./background-request-forwarder.js"; -import type { BackgroundBrowserAdapter, BrowserWindowSnapshot } from "./browser-commands.js"; +import { ApprovalRequestService } from "./approval-request-service.js"; +import type { BackgroundBrowserAdapter } from "./browser-commands.js"; -export type { - BackgroundRuntimeAdapter, - BackgroundStorageAdapter, - ExtensionStatus, - NativePortLike, -} from "./background-controller-types.js"; -export type { BackgroundBrowserAdapter, BrowserWindowSnapshot }; - -const DEFAULT_PENDING_REQUEST_TIMEOUT_MS = 660_000; +export type { BackgroundRuntimeAdapter, BackgroundStorageAdapter, ExtensionStatus, NativePortLike } from "./background-controller-types.js"; export class FirefoxCliBackgroundController { readonly #connection: NativeConnectionManager; readonly #pairing: PairingStateService; readonly #nativeSession = new NativeSessionService(); readonly #requestForwarder: BackgroundRequestForwarder; + readonly #approvalRequests: ApprovalRequestService; readonly #productVersion: string; readonly #pendingCommands: PendingRequestTracker; #lastError: string | undefined; @@ -47,6 +42,7 @@ export class FirefoxCliBackgroundController { readonly reconnectDelaysMs?: readonly number[]; readonly scheduleTimer?: (callback: () => void, delayMs: number) => void; readonly requestTimeoutMs?: number; + readonly nowMs?: () => number; }) { const browserAdapter = options.browserAdapter ?? createUnconfiguredBrowserAdapter(); const storageAdapter = options.storageAdapter ?? { @@ -55,9 +51,19 @@ export class FirefoxCliBackgroundController { }; this.#productVersion = options.productVersion; this.#pairing = new PairingStateService(storageAdapter); + this.#approvalRequests = new ApprovalRequestService({ + adapter: browserAdapter, + ...(options.nowMs === undefined ? {} : { nowMs: options.nowMs }), + }); this.#requestForwarder = new BackgroundRequestForwarder({ browserAdapter, productVersion: this.#productVersion, + intercept: (request, approved): Promise | undefined => + isRequestCommand(request, "pair.requestApproval") + ? this.#approvalRequests.requestApproval(request, approved) + : isRequestCommand(request, "pair.openApproval") + ? this.#approvalRequests.openApprovalPage(request, approved) + : undefined, }); this.#connection = new NativeConnectionManager({ connectNative: options.connectNative, @@ -80,6 +86,10 @@ export class FirefoxCliBackgroundController { this.#nativeSession.markDisconnected(); this.#lastError = message; this.#drainPendingOnDisconnect(); + this.#approvalRequests.rejectPending({ + code: "NATIVE_HOST_UNAVAILABLE", + message: "Native host disconnected before the user responded to the approval request.", + }); }, onConnectError: (message) => { this.#nativeSession.markDisconnected(); @@ -88,7 +98,7 @@ export class FirefoxCliBackgroundController { }, }); - const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_PENDING_REQUEST_TIMEOUT_MS; + const requestTimeoutMs = options.requestTimeoutMs ?? 660_000; this.#pendingCommands = new PendingRequestTracker({ timeoutMs: requestTimeoutMs, onDuplicate: (request) => @@ -150,21 +160,26 @@ export class FirefoxCliBackgroundController { }; } - async handleRuntimeMessage(message: { readonly type?: string }): Promise { + async handleRuntimeMessage(message: { readonly type?: string; readonly requestId?: string }): Promise { if (message.type === "firefox-cli:get-status") { return this.getStatus(); } + if (message.type === "firefox-cli:get-approval-request") { + return this.#approvalRequests.getViewState(message.requestId); + } + + if (message.type === "firefox-cli:deny-approval-request") { + return this.#approvalRequests.deny(message.requestId); + } + + if (message.type === "firefox-cli:approve-request") { + return this.#approvalRequests.approve(message.requestId, async () => this.#approveWithNativeHost()); + } + if (message.type === "firefox-cli:approve") { - this.#pairing.beginMutation(); - const request = createRequest("pair.approve", {}); - const response = await this.#sendNativeRequest(request); - if (response.ok) { - await this.#pairing.approve(response.result.token); - this.#lastError = undefined; - } else { - this.#pairing.markRejected(); - this.#lastError = response.error.message; + if (await this.#approveWithNativeHost()) { + this.#approvalRequests.acceptExistingApproval(); } return this.getStatus(); } @@ -204,6 +219,29 @@ export class FirefoxCliBackgroundController { request.protocolVersion ?? localProtocolVersionRange.protocolMax, ), ); + this.#approvalRequests.rejectPending({ + code: "NATIVE_HOST_UNAVAILABLE", + message: "Extension background stopped before the user responded to the approval request.", + }); + } + + async #approveWithNativeHost(): Promise { + this.#pairing.beginMutation(); + const request = createRequest("pair.approve", {}); + const response = await this.#sendNativeRequest(request); + if (!response.ok) { + this.#pairing.markRejected(); + this.#lastError = response.error.message; + return false; + } + if (!("token" in response.result)) { + this.#pairing.markRejected(); + this.#lastError = "Native host returned an invalid approval response."; + return false; + } + await this.#pairing.approve(response.result.token); + this.#lastError = undefined; + return true; } #postHello(): void { @@ -231,9 +269,6 @@ export class FirefoxCliBackgroundController { }); } - async #sendNativeRequest(request: RequestEnvelope<"hello">): Promise>; - async #sendNativeRequest(request: RequestEnvelope<"pair.approve">): Promise>; - async #sendNativeRequest(request: RequestEnvelope<"pair.reset">): Promise>; async #sendNativeRequest(request: RequestEnvelope): Promise { if (this.#connection.stopped) { return createErrorResponseForRequest(request, { @@ -303,12 +338,10 @@ export class FirefoxCliBackgroundController { } let helloPairingError: string | undefined; - if (command === "hello") { - if (isHelloResponse(command, response.value)) { - this.#nativeSession.applyHelloResponse(response.value); - if (response.value.ok) { - helloPairingError = await this.#pairing.applyHelloPairing(response.value.result.pairing); - } + if (command === "hello" && isHelloResponse(command, response.value)) { + this.#nativeSession.applyHelloResponse(response.value); + if (response.value.ok) { + helloPairingError = await this.#pairing.applyHelloPairing(response.value.result.pairing); } } this.#pendingCommands.settle(message.id, response.value); @@ -346,7 +379,3 @@ export class FirefoxCliBackgroundController { ); } } - -function isHelloResponse(command: CommandId, response: ResponseEnvelope): response is ResponseEnvelope<"hello"> { - return command === "hello" && (!response.ok || ("accepted" in response.result && "peer" in response.result)); -} diff --git a/packages/extension/src/background-default-browser-adapter.ts b/packages/extension/src/background-default-browser-adapter.ts index 7192542..5590fd9 100644 --- a/packages/extension/src/background-default-browser-adapter.ts +++ b/packages/extension/src/background-default-browser-adapter.ts @@ -67,6 +67,15 @@ export function createUnconfiguredBrowserAdapter(): BackgroundBrowserAdapter { listNetworkRequests: async () => [], clearNetworkRequests: async () => undefined, waitForNetworkIdle: async () => undefined, + showNotification: async () => { + throw new Error("Browser adapter is not configured."); + }, + getExtensionInstance: async () => { + throw new Error("Browser adapter is not configured."); + }, + openExtensionPage: async () => { + throw new Error("Browser adapter is not configured."); + }, resizeWindow: async () => { throw new Error("Browser adapter is not configured."); }, diff --git a/packages/extension/src/background-native-session.ts b/packages/extension/src/background-native-session.ts index 40dfbf7..65e9847 100644 --- a/packages/extension/src/background-native-session.ts +++ b/packages/extension/src/background-native-session.ts @@ -121,3 +121,7 @@ export class NativeSessionService { return getMessageProtocolVersion(message); } } + +export function isHelloResponse(command: CommandId, response: ResponseEnvelope): response is ResponseEnvelope<"hello"> { + return command === "hello" && (!response.ok || ("accepted" in response.result && "peer" in response.result)); +} diff --git a/packages/extension/src/background-pairing-service.ts b/packages/extension/src/background-pairing-service.ts index 076a8d8..5d18d37 100644 --- a/packages/extension/src/background-pairing-service.ts +++ b/packages/extension/src/background-pairing-service.ts @@ -40,9 +40,9 @@ export class PairingStateService { } async approve(pairToken: string): Promise { + await this.#storageAdapter.setPairToken(pairToken); this.#pairToken = pairToken; this.#approved = true; - await this.#storageAdapter.setPairToken(pairToken); } markRejected(): void { diff --git a/packages/extension/src/background-request-forwarder.ts b/packages/extension/src/background-request-forwarder.ts index ebf5bf5..06c7502 100644 --- a/packages/extension/src/background-request-forwarder.ts +++ b/packages/extension/src/background-request-forwarder.ts @@ -5,16 +5,23 @@ import { handleRequest } from "./background-request-handler.js"; export class BackgroundRequestForwarder { readonly #browserAdapter: BackgroundBrowserAdapter; readonly #productVersion: string; + readonly #intercept: ((request: RequestEnvelope, approved: boolean) => Promise | ResponseEnvelope | undefined) | undefined; constructor(options: { readonly browserAdapter: BackgroundBrowserAdapter; readonly productVersion: string; + readonly intercept?: (request: RequestEnvelope, approved: boolean) => Promise | ResponseEnvelope | undefined; }) { this.#browserAdapter = options.browserAdapter; this.#productVersion = options.productVersion; + this.#intercept = options.intercept; } forward(request: RequestEnvelope, approved: boolean, protocolSession: ProtocolSession): Promise | ResponseEnvelope { + const intercepted = this.#intercept?.(request, approved); + if (intercepted !== undefined) { + return intercepted; + } return handleRequest({ request, productVersion: this.#productVersion, diff --git a/packages/extension/src/background-request-handler.ts b/packages/extension/src/background-request-handler.ts index dc27063..2da82af 100644 --- a/packages/extension/src/background-request-handler.ts +++ b/packages/extension/src/background-request-handler.ts @@ -1,4 +1,5 @@ import { + commandAllowedBeforeApproval, createLocalComponentIdentity, kernelCapabilities, localProtocolVersionRange, @@ -35,10 +36,10 @@ export function handleRequest(options: { }); } - if (!approved) { + if (!approved && !commandAllowedBeforeApproval(request.command)) { return protocolSession.createErrorResponse(request.id, { code: "NOT_APPROVED", - message: "Approve firefox-cli in the extension popup before running CLI commands.", + message: "Run `firefox-cli connect` before running Firefox control commands.", }); } diff --git a/packages/extension/src/browser-command/types.ts b/packages/extension/src/browser-command/types.ts index 496d0ad..dc8ea1a 100644 --- a/packages/extension/src/browser-command/types.ts +++ b/packages/extension/src/browser-command/types.ts @@ -1,4 +1,4 @@ -import type { DownloadResult, NetworkResult, CookieResult, RequestEnvelope, ResolvedTarget, TabSummary } from "@firefox-cli/protocol"; +import type { CookieResult, DownloadResult, NetworkResult, NotifyResult, RequestEnvelope, ResolvedTarget, TabSummary } from "@firefox-cli/protocol"; import type { EvalExecutorPayload, EvalExecutorResult } from "../eval-executor.js"; export interface BrowserWindowSnapshot { @@ -49,6 +49,9 @@ export interface BackgroundBrowserAdapter { listNetworkRequests(options: { readonly tabId: number; readonly urlGlob?: string }): Promise>; clearNetworkRequests(options: { readonly tabId: number; readonly urlGlob?: string }): Promise; waitForNetworkIdle(options: { readonly tabId: number; readonly timeoutMs: number; readonly idleMs: number }): Promise; + showNotification(options: { readonly id?: string; readonly title: string; readonly message?: string }): Promise; + getExtensionInstance(): Promise<{ readonly extensionUrl: string; readonly focusedWindowId?: number }>; + openExtensionPage(path: string): Promise; resizeWindow(windowId: number, size: { readonly width: number; readonly height: number }): Promise; } diff --git a/packages/extension/src/browser-commands-content.test.ts b/packages/extension/src/browser-commands-content.test.ts index 62836a6..a7d886e 100644 --- a/packages/extension/src/browser-commands-content.test.ts +++ b/packages/extension/src/browser-commands-content.test.ts @@ -179,7 +179,16 @@ describe("browser command handling", () => { }); expect(adapter.networkClearRequests).toEqual([{ tabId: 101 }]); expect(adapter.networkRequests).toEqual([]); - + await expect( + handleBrowserRequest( + createRequest("notify", { id: "approval", title: "Action needed", message: "Open Firefox to approve control." }, "notify-1"), + adapter, + ), + ).resolves.toMatchObject({ + ok: true, + result: { id: "approval" }, + }); + expect(adapter.notifications).toEqual([{ id: "approval", title: "Action needed", message: "Open Firefox to approve control." }]); await expect(handleBrowserRequest(createRequest("set.viewport", { width: 1200, height: 800 }, "viewport-1"), adapter)).resolves.toMatchObject({ ok: true, result: { window: { id: 10, width: 1200, height: 800 } }, diff --git a/packages/extension/src/browser-commands-test-cases.ts b/packages/extension/src/browser-commands-test-cases.ts index 5932480..1bdff06 100644 --- a/packages/extension/src/browser-commands-test-cases.ts +++ b/packages/extension/src/browser-commands-test-cases.ts @@ -8,7 +8,14 @@ import { browserSmokeRequests } from "./browser-commands-test-smoke.js"; export async function runCase01() { const expectedCommands = Object.keys(commandSchemas) .filter(isCommandId) - .filter((command) => commandSchemas[command].owner === "extension" && command !== "capabilities" && command !== "noop"); + .filter( + (command) => + commandSchemas[command].owner === "extension" && + command !== "capabilities" && + command !== "noop" && + command !== "pair.requestApproval" && + command !== "pair.openApproval", + ); const unsupportedPdfMessage: unknown = expect.stringContaining("PDF export is unsupported"); expect([...browserSmokeRequests.keys()].sort()).toEqual(expectedCommands.sort()); diff --git a/packages/extension/src/browser-commands-test-smoke.ts b/packages/extension/src/browser-commands-test-smoke.ts index b86f9de..a4ad7e9 100644 --- a/packages/extension/src/browser-commands-test-smoke.ts +++ b/packages/extension/src/browser-commands-test-smoke.ts @@ -37,6 +37,7 @@ export const browserSmokeRequests = new Map([ ["console", { action: "list" }], ["errors", { action: "list" }], ["highlight", { selector: "button" }], + ["notify", { title: "Action needed" }], ["pdf", { path: "/tmp/page.pdf" }], ["set.viewport", { width: 1200, height: 800 }], ["diff", { kind: "title", expected: "Expected title" }], diff --git a/packages/extension/src/browser-commands-test-utils.ts b/packages/extension/src/browser-commands-test-utils.ts index 2fa412d..a482570 100644 --- a/packages/extension/src/browser-commands-test-utils.ts +++ b/packages/extension/src/browser-commands-test-utils.ts @@ -50,6 +50,8 @@ export class FakeBrowserAdapter implements BackgroundBrowserAdapter { readonly timeoutMs: number; readonly idleMs: number; }[] = []; + readonly notifications: { readonly id?: string; readonly title: string; readonly message?: string }[] = []; + readonly extensionPages: string[] = []; clipboardText = ""; networkRequests: { readonly id: string; readonly tabId: number; readonly url: string }[] = []; listWindowCalls = 0; @@ -271,6 +273,28 @@ export class FakeBrowserAdapter implements BackgroundBrowserAdapter { this.networkIdleWaits.push(options); } + async showNotification(options: { + readonly id?: string; + readonly title: string; + readonly message?: string; + }): Promise<{ readonly ok: true; readonly id: string }> { + this.notifications.push(options); + return { ok: true, id: options.id ?? `notification-${String(this.notifications.length)}` }; + } + + async getExtensionInstance(): Promise<{ readonly extensionUrl: string; readonly focusedWindowId?: number }> { + const focused = this.#windows.find((window) => window.focused); + return { + extensionUrl: "moz-extension://test/", + ...(focused === undefined ? {} : { focusedWindowId: focused.id }), + }; + } + + async openExtensionPage(path: string): Promise { + this.extensionPages.push(path); + return `moz-extension://test/${path}`; + } + async resizeWindow(windowId: number, size: { readonly width: number; readonly height: number }): Promise { const window = this.#windows.find((candidate) => candidate.id === windowId); if (window === undefined) { diff --git a/packages/extension/src/browser-handlers/phase8-browser.ts b/packages/extension/src/browser-handlers/phase8-browser.ts index cd2c30c..83cc2e8 100644 --- a/packages/extension/src/browser-handlers/phase8-browser.ts +++ b/packages/extension/src/browser-handlers/phase8-browser.ts @@ -11,7 +11,7 @@ import { BrowserCommandError } from "../browser-command/errors.js"; import { toOrderedWindows, toWindowSummary } from "../browser-command/targets.js"; import type { BrowserHandlerMap } from "./types.js"; -type Phase8BrowserCommand = "download" | "clipboard" | "cookies" | "network" | "pdf" | "set.viewport"; +type Phase8BrowserCommand = "download" | "clipboard" | "cookies" | "network" | "notify" | "pdf" | "set.viewport"; export const phase8BrowserHandlers: BrowserHandlerMap = { download: async (request, adapter) => { @@ -102,6 +102,14 @@ export const phase8BrowserHandlers: BrowserHandlerMap = { }), }); }, + notify: async (request, adapter) => { + const result = await adapter.showNotification({ + ...(request.params.id === undefined ? {} : { id: request.params.id }), + title: request.params.title, + ...(request.params.message === undefined ? {} : { message: request.params.message }), + }); + return createOkResponse(request, result); + }, pdf: async (request) => { return createErrorResponseForRequest(request, { code: "UNSUPPORTED_CAPABILITY", diff --git a/packages/extension/src/manifest.json b/packages/extension/src/manifest.json index 256022b..52717ec 100644 --- a/packages/extension/src/manifest.json +++ b/packages/extension/src/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "FF-CLI Bridge", - "version": "0.1.1", + "version": "0.2.0", "description": "Browser extension bridge for CLI control.", "browser_specific_settings": { "gecko": { @@ -14,7 +14,7 @@ } } }, - "permissions": ["nativeMessaging", "scripting", "tabs", "storage", "downloads", "cookies", "clipboardRead", "clipboardWrite", "webRequest"], + "permissions": ["nativeMessaging", "scripting", "tabs", "storage", "downloads", "cookies", "notifications", "clipboardRead", "clipboardWrite", "webRequest"], "host_permissions": [""], "background": { "scripts": ["background.js"] diff --git a/packages/extension/src/popup.ts b/packages/extension/src/popup.ts index f5d33b9..986cc78 100644 --- a/packages/extension/src/popup.ts +++ b/packages/extension/src/popup.ts @@ -1,4 +1,4 @@ -import { getExtensionPermissionRequirements } from "@firefox-cli/protocol"; +import { requestHostAccess } from "./approval-permissions.js"; interface Status { readonly connected: boolean; @@ -68,21 +68,3 @@ async function approve(): Promise { browser.runtime.reload(); } } - -async function requestHostAccess(): Promise { - const permissions = browser.permissions; - if (permissions === undefined) { - throw new Error("Firefox permissions API is unavailable."); - } - - const required = { origins: getExtensionPermissionRequirements().popupApprovalOrigins }; - const captureApiRequiresReload = typeof browser.tabs.captureVisibleTab !== "function"; - if (await permissions.contains(required)) { - return captureApiRequiresReload; - } - - if (!(await permissions.request(required))) { - throw new Error("Approve host access for all websites to enable browser control."); - } - return true; -} diff --git a/packages/extension/src/webextension.d.ts b/packages/extension/src/webextension.d.ts index cd75275..9b92749 100644 --- a/packages/extension/src/webextension.d.ts +++ b/packages/extension/src/webextension.d.ts @@ -35,11 +35,12 @@ declare const browser: { }; postMessage(message: unknown): void; }; + getURL(path: string): string; sendMessage(message: unknown): Promise; reload(): void; }; readonly windows: { - getAll(options: { readonly populate: true }): Promise; + getAll(options: { readonly populate: boolean }): Promise; create(options: { readonly url?: string }): Promise; update(windowId: number, options: { readonly focused?: boolean; readonly width?: number; readonly height?: number }): Promise; remove(windowId: number): Promise; @@ -47,6 +48,7 @@ declare const browser: { readonly tabs: { create(options: { readonly active?: boolean; readonly url?: string; readonly windowId?: number }): Promise; update(tabId: number, options: { readonly active?: boolean; readonly url?: string }): Promise; + getCurrent(): Promise; get(tabId: number): Promise; remove(tabId: number): Promise; goBack(tabId: number): Promise; @@ -107,6 +109,17 @@ declare const browser: { }): Promise; remove(options: { readonly url: string; readonly name: string }): Promise; }; + readonly notifications: { + create(options: { readonly type: "basic"; readonly title: string; readonly message: string }): Promise; + create( + notificationId: string, + options: { + readonly type: "basic"; + readonly title: string; + readonly message: string; + }, + ): Promise; + }; readonly webRequest?: { readonly onBeforeRequest?: BrowserWebRequestEvent; readonly onCompleted?: BrowserWebRequestEvent; diff --git a/packages/extension/vite.config.ts b/packages/extension/vite.config.ts index e8574b3..230a2de 100644 --- a/packages/extension/vite.config.ts +++ b/packages/extension/vite.config.ts @@ -17,7 +17,6 @@ export default defineConfig({ output: { entryFileNames: "[name].js", assetFileNames: "assets/[name][extname]", - manualChunks: undefined, }, }, }, diff --git a/packages/native-host/package.json b/packages/native-host/package.json index 77263de..698f086 100644 --- a/packages/native-host/package.json +++ b/packages/native-host/package.json @@ -1,6 +1,6 @@ { "name": "@firefox-cli/native-host", - "version": "0.1.1", + "version": "0.2.0", "private": true, "type": "module", "exports": { diff --git a/packages/native-host/src/host-broker-helpers.ts b/packages/native-host/src/host-broker-helpers.ts index e78172b..2ebe409 100644 --- a/packages/native-host/src/host-broker-helpers.ts +++ b/packages/native-host/src/host-broker-helpers.ts @@ -14,7 +14,7 @@ export function pairVerificationToProtocolError(verification: PairTokenVerificat if (verification.ok) { return { code: "NOT_APPROVED", - message: "Approve firefox-cli in the extension popup before running CLI commands.", + message: "Run `firefox-cli connect` before running Firefox control commands.", }; } diff --git a/packages/native-host/src/host-broker.test.ts b/packages/native-host/src/host-broker.test.ts index 47509ab..dcd5fe7 100644 --- a/packages/native-host/src/host-broker.test.ts +++ b/packages/native-host/src/host-broker.test.ts @@ -1,5 +1,13 @@ import { describe, expect, it } from "vitest"; -import { PROTOCOL_VERSION, createProtocolSession, createOkResponse, createRequest, kernelCapabilities, parseBoundaryResponse } from "@firefox-cli/protocol"; +import { + PROTOCOL_VERSION, + createProtocolSession, + createOkResponse, + createRequest, + kernelCapabilities, + parseBoundaryResponse, + type RequestEnvelope, +} from "@firefox-cli/protocol"; import { FIREFOX_CLI_EXTENSION_ID } from "./host-launch.js"; import { createHostIdentity } from "./pair-state.js"; import { NativeHostBroker } from "./host-broker.js"; @@ -150,11 +158,33 @@ describe("NativeHostBroker", () => { ok: false, error: { code: "NOT_APPROVED", - message: "Approve firefox-cli in the extension popup before running CLI commands.", + message: "Run `firefox-cli connect` before running Firefox control commands.", }, }); }); + it("allows requesting approval before extension approval", async () => { + const request = createRequest("pair.requestApproval", {}, "approval-1"); + let forwardedRequest: RequestEnvelope | undefined; + const broker = new NativeHostBroker({ + hostIdentity: createHostIdentity({ + extensionId: FIREFOX_CLI_EXTENSION_ID, + generateId: () => "host-1", + }), + }); + broker.connectExtension({ + approved: false, + token: undefined, + send: async (forwarded) => { + forwardedRequest = forwarded; + return createOkResponse(forwarded, { ok: true, url: "moz-extension://test/approval-request.html" }); + }, + }); + + await expect(broker.handleCliRequest(request)).resolves.toEqual(createOkResponse(request, { ok: true, url: "moz-extension://test/approval-request.html" })); + expect(forwardedRequest).toMatchObject({ command: "pair.requestApproval" }); + }); + it("rejects CLI requests when the extension pair token is invalid", async () => { const request = createRequest("noop", {}, "request-1"); const broker = new NativeHostBroker({ diff --git a/packages/native-host/src/host-broker.ts b/packages/native-host/src/host-broker.ts index fab393b..79a8156 100644 --- a/packages/native-host/src/host-broker.ts +++ b/packages/native-host/src/host-broker.ts @@ -2,6 +2,7 @@ import { Buffer } from "node:buffer"; import { writeFile } from "node:fs/promises"; import { MAX_SCREENSHOT_BYTES, + commandAllowedBeforeApproval, createLocalComponentIdentity, createProtocolSession, createRequestProtocolMismatchError, @@ -150,9 +151,11 @@ export class NativeHostBroker { return { ok: false, response: cliSession.createErrorResponseForRequest(request, extensionSession.error) }; } - const approval = await this.#verifyExtensionApproval(connection); - if (!approval.ok) { - return { ok: false, response: cliSession.createErrorResponseForRequest(request, approval.error) }; + if (!commandAllowedBeforeApproval(request.command)) { + const approval = await this.#verifyExtensionApproval(connection); + if (!approval.ok) { + return { ok: false, response: cliSession.createErrorResponseForRequest(request, approval.error) }; + } } if (!getRequestProtocolCompatibility(request, extensionSession.value.protocolVersion).compatible) { @@ -176,7 +179,7 @@ export class NativeHostBroker { ok: false, error: { code: "NOT_APPROVED", - message: "Approve firefox-cli in the extension popup before running CLI commands.", + message: "Run `firefox-cli connect` before running Firefox control commands.", }, }; } diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 1165f1c..056a28e 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -1,6 +1,6 @@ { "name": "@firefox-cli/protocol", - "version": "0.1.1", + "version": "0.2.0", "private": true, "type": "module", "exports": { diff --git a/packages/protocol/src/browser/output.ts b/packages/protocol/src/browser/output.ts index 82370df..53ad30e 100644 --- a/packages/protocol/src/browser/output.ts +++ b/packages/protocol/src/browser/output.ts @@ -53,6 +53,21 @@ export const highlightResultSchema = z .strict(); export type HighlightResult = z.infer; +export const notifyParamsSchema = z + .object({ + id: z.string().min(1).max(256).optional(), + title: z.string().min(1).max(256), + message: z.string().max(1024).optional(), + }) + .strict(); +export const notifyResultSchema = z + .object({ + ok: z.literal(true), + id: z.string().min(1), + }) + .strict(); +export type NotifyResult = z.infer; + export const pdfParamsSchema = z .object({ target: targetSelectorSchema.optional(), diff --git a/packages/protocol/src/capabilities.ts b/packages/protocol/src/capabilities.ts index 7be736c..19b516d 100644 --- a/packages/protocol/src/capabilities.ts +++ b/packages/protocol/src/capabilities.ts @@ -29,12 +29,6 @@ export const gatedCapabilities: readonly GatedCapabilitySummary[] = [ reason: "exit is unsupported because firefox-cli must not terminate the user's Firefox process.", cliCommands: ["exit"], }, - { - command: "connect", - status: "unsupported", - reason: "connect is unsupported because Firefox does not provide Chrome CDP attach semantics.", - cliCommands: ["connect"], - }, { command: "inspect", status: "unsupported", diff --git a/packages/protocol/src/constants.ts b/packages/protocol/src/constants.ts index 04c7fca..2497ccb 100644 --- a/packages/protocol/src/constants.ts +++ b/packages/protocol/src/constants.ts @@ -2,7 +2,7 @@ export const PRODUCT_NAME = "firefox-cli"; export const NATIVE_HOST_NAME = "firefox_cli"; export const FIREFOX_CLI_EXTENSION_ID = "ff-cli-bridge@respawn.pro"; export const FIREFOX_CLI_EXTENSION_UPDATE_URL = "https://opensource.respawn.pro/firefox-cli/updates.json"; -export const PROTOCOL_VERSION = 3; +export const PROTOCOL_VERSION = 4; export const PROTOCOL_MIN_VERSION = 1; export const PROTOCOL_MAX_VERSION = PROTOCOL_VERSION; export const MAX_EVAL_SCRIPT_BYTES = 100_000; diff --git a/packages/protocol/src/extension-requirements.ts b/packages/protocol/src/extension-requirements.ts index 8885ca5..b271e4c 100644 --- a/packages/protocol/src/extension-requirements.ts +++ b/packages/protocol/src/extension-requirements.ts @@ -8,6 +8,7 @@ export type FirefoxManifestPermission = | "storage" | "downloads" | "cookies" + | "notifications" | "clipboardRead" | "clipboardWrite" | "webRequest"; @@ -69,6 +70,7 @@ const privilegeManifestPermissions = { downloads: ["downloads"], cookies: ["cookies"], "network-observation": ["webRequest"], + notifications: ["notifications"], } as const satisfies Record; const pageAccessReasons = new Set([ diff --git a/packages/protocol/src/metadata.ts b/packages/protocol/src/metadata.ts index 4f74103..2295b91 100644 --- a/packages/protocol/src/metadata.ts +++ b/packages/protocol/src/metadata.ts @@ -19,7 +19,8 @@ export type CommandPrivilegeReason = | "clipboard" | "downloads" | "cookies" - | "network-observation"; + | "network-observation" + | "notifications"; export type CommandSecurityMetadata = | { readonly level: "normal"; diff --git a/packages/protocol/src/pairing.ts b/packages/protocol/src/pairing.ts index bfd7427..8a38d33 100644 --- a/packages/protocol/src/pairing.ts +++ b/packages/protocol/src/pairing.ts @@ -15,3 +15,19 @@ export const pairResetParamsSchema = z.object({}).strict(); export const pairResetResultSchema = z.object({ ok: z.literal(true), }); + +export const pairOpenApprovalParamsSchema = z.object({}).strict(); +export const pairOpenApprovalResultSchema = z + .object({ + ok: z.literal(true), + url: z.string().min(1), + }) + .strict(); + +export const pairRequestApprovalParamsSchema = z.object({}).strict(); +export const pairRequestApprovalResultSchema = z + .object({ + ok: z.literal(true), + url: z.string().min(1), + }) + .strict(); diff --git a/packages/protocol/src/protocol-compatibility.ts b/packages/protocol/src/protocol-compatibility.ts index cc39b9d..8d4f80a 100644 --- a/packages/protocol/src/protocol-compatibility.ts +++ b/packages/protocol/src/protocol-compatibility.ts @@ -66,7 +66,7 @@ function getRequestProtocolRequirementForSubject( ? undefined : { minProtocolVersion: childRequirement.minProtocolVersion, - reason: "Batch contains scoped network command semantics.", + reason: childRequirement.reason, }; } diff --git a/packages/protocol/src/protocol-metadata-behavior.test.ts b/packages/protocol/src/protocol-metadata-behavior.test.ts index 05f65da..6313fcb 100644 --- a/packages/protocol/src/protocol-metadata-behavior.test.ts +++ b/packages/protocol/src/protocol-metadata-behavior.test.ts @@ -68,7 +68,7 @@ describe("protocol command metadata", () => { } const nonBatchableCommands = commandIds().filter((command) => !isBatchableCommandId(command)); - expect(nonBatchableCommands).toEqual(["hello", "capabilities", "noop", "batch", "pair.approve", "pair.reset"]); + expect(nonBatchableCommands).toEqual(["hello", "capabilities", "noop", "batch", "pair.approve", "pair.reset", "pair.requestApproval", "pair.openApproval"]); }); it("marks only required tab/window selectors for protocol batch default targets", () => { @@ -169,6 +169,7 @@ describe("protocol command metadata", () => { "clipboard", "cookies", "network", + "notify", "click", "dblclick", "focus", @@ -225,6 +226,14 @@ describe("request protocol compatibility", () => { }, }); expect(getRequestProtocolRequirement(createRequest("wait", { kind: "load-state", state: "complete" }))).toBeUndefined(); + expect(getCommandCompatibilityMetadata("notify")).toEqual({ + requirements: [ + { + minProtocolVersion: 4, + reason: "Native notifications were added in protocol v4.", + }, + ], + }); }); it("requires protocol v2 for scoped network semantics", () => { @@ -262,6 +271,64 @@ describe("request protocol compatibility", () => { } }); + it("requires protocol v4 for CLI approval requests", () => { + const request = createRequest("pair.requestApproval", {}, "approval-v4"); + + expect(getRequestProtocolCompatibility(request, 3)).toMatchObject({ + compatible: false, + requiredProtocolVersion: 4, + }); + expect(parseBoundaryRequest("host-to-extension", { ...request, protocolVersion: 3 }, { protocolVersion: 3 })).toMatchObject({ + ok: false, + error: { + code: "VERSION_MISMATCH", + details: { + requiredProtocolVersion: 4, + negotiatedProtocolVersion: 3, + }, + }, + }); + expect(parseBoundaryRequest("host-to-extension", { ...request, protocolVersion: 4 }, { protocolVersion: 4 })).toMatchObject({ + ok: true, + }); + }); + + it("requires protocol v4 for native notifications, including batch steps", () => { + const notify = createRequest("notify", { title: "Action needed" }, "notify-v4"); + const batch = createRequest( + "batch", + { + steps: [ + { command: "snapshot", params: {} }, + { command: "notify", params: { title: "Action needed" } }, + ], + }, + "notify-batch-v4", + ); + + for (const request of [notify, batch]) { + expect(getRequestProtocolCompatibility(request, 3)).toMatchObject({ + compatible: false, + requiredProtocolVersion: 4, + reason: "Native notifications were added in protocol v4.", + }); + expect(parseBoundaryRequest("host-to-extension", { ...request, protocolVersion: 3 }, { protocolVersion: 3 })).toMatchObject({ + ok: false, + error: { + code: "VERSION_MISMATCH", + details: { + requiredProtocolVersion: 4, + negotiatedProtocolVersion: 3, + reason: "Native notifications were added in protocol v4.", + }, + }, + }); + expect(parseBoundaryRequest("host-to-extension", { ...request, protocolVersion: 4 }, { protocolVersion: 4 })).toMatchObject({ + ok: true, + }); + } + }); + it("keeps non-network commands compatible with protocol v1 sessions", () => { const request = createRequest("capabilities", {}, "capabilities-v1", 1); diff --git a/packages/protocol/src/protocol-metadata.test.ts b/packages/protocol/src/protocol-metadata.test.ts index 989ef65..9085674 100644 --- a/packages/protocol/src/protocol-metadata.test.ts +++ b/packages/protocol/src/protocol-metadata.test.ts @@ -39,6 +39,7 @@ describe("protocol command metadata", () => { "cookies", "downloads", "nativeMessaging", + "notifications", "scripting", "storage", "tabs", @@ -58,6 +59,12 @@ describe("protocol command metadata", () => { }); expect(commandRequiresExtensionHostAccess("click")).toBe(true); expect(commandRequiresExtensionHostAccess("download")).toBe(false); + expect(commandRequiresExtensionHostAccess("notify")).toBe(false); + expect(requirements.commands.find((requirement) => requirement.command === "notify")).toMatchObject({ + securityReasons: ["notifications"], + manifestPermissions: ["notifications"], + networkObservation: false, + }); expect(requirements.commands.find((requirement) => requirement.command === "network")).toMatchObject({ securityReasons: ["network-observation"], manifestPermissions: ["webRequest"], diff --git a/packages/protocol/src/protocol-test-support.ts b/packages/protocol/src/protocol-test-support.ts index 2414a20..adf9559 100644 --- a/packages/protocol/src/protocol-test-support.ts +++ b/packages/protocol/src/protocol-test-support.ts @@ -61,6 +61,7 @@ export const expectedCliRoutesByCommand: Partial = { readonly [C in CommandId]: (typeof commandSchemas)[C]["content"] extends P ? C : never; }[CommandId]; diff --git a/packages/protocol/src/registry/pairing.ts b/packages/protocol/src/registry/pairing.ts index 7a3283c..53e80c1 100644 --- a/packages/protocol/src/registry/pairing.ts +++ b/packages/protocol/src/registry/pairing.ts @@ -1,4 +1,13 @@ -import { pairApproveParamsSchema, pairApproveResultSchema, pairResetParamsSchema, pairResetResultSchema } from "../pairing.js"; +import { + pairApproveParamsSchema, + pairApproveResultSchema, + pairOpenApprovalParamsSchema, + pairOpenApprovalResultSchema, + pairRequestApprovalParamsSchema, + pairRequestApprovalResultSchema, + pairResetParamsSchema, + pairResetResultSchema, +} from "../pairing.js"; import { defineCommandEntries } from "./define.js"; export const pairingCommandEntries = defineCommandEntries({ @@ -26,4 +35,36 @@ export const pairingCommandEntries = defineCommandEntries({ batch: { allowed: false }, cliRoutes: [], }, + "pair.requestApproval": { + params: pairRequestApprovalParamsSchema, + result: pairRequestApprovalResultSchema, + status: "mvp", + owner: "extension", + target: "none", + content: "never", + action: false, + timeout: "none", + compatibility: { + requirements: [ + { + minProtocolVersion: 4, + reason: "CLI approval requests use a dedicated decision page and wait for explicit user approval or denial.", + }, + ], + }, + batch: { allowed: false }, + cliRoutes: [{ id: "connect", path: ["connect"], batch: false }], + }, + "pair.openApproval": { + params: pairOpenApprovalParamsSchema, + result: pairOpenApprovalResultSchema, + status: "mvp", + owner: "extension", + target: "none", + content: "never", + action: false, + timeout: "none", + batch: { allowed: false }, + cliRoutes: [], + }, }); diff --git a/packages/protocol/src/registry/phase8.ts b/packages/protocol/src/registry/phase8.ts index 2fa7d6e..1836a91 100644 --- a/packages/protocol/src/registry/phase8.ts +++ b/packages/protocol/src/registry/phase8.ts @@ -27,6 +27,8 @@ import { mouseParamsSchema, networkParamsSchema, networkResultSchema, + notifyParamsSchema, + notifyResultSchema, pdfParamsSchema, pdfResultSchema, screenshotParamsSchema, @@ -279,6 +281,27 @@ export const phase8CommandEntries = defineCommandEntries({ batch: { allowed: true, extensionDefaultTarget: true }, cliRoutes: [{ id: "highlight", path: ["highlight"], batch: true }], }, + notify: { + params: notifyParamsSchema, + result: notifyResultSchema, + status: "mvp", + owner: "extension", + target: "none", + content: "never", + action: false, + timeout: "none", + security: { level: "sensitive", reasons: ["notifications"] }, + compatibility: { + requirements: [ + { + minProtocolVersion: 4, + reason: "Native notifications were added in protocol v4.", + }, + ], + }, + batch: { allowed: true }, + cliRoutes: [{ id: "notify", path: ["notify"], batch: true }], + }, pdf: { params: pdfParamsSchema, result: pdfResultSchema, diff --git a/packages/protocol/src/registry/registry.test.ts b/packages/protocol/src/registry/registry.test.ts index 428b0e3..6611c11 100644 --- a/packages/protocol/src/registry/registry.test.ts +++ b/packages/protocol/src/registry/registry.test.ts @@ -54,6 +54,7 @@ const expectedCommandIds = [ "console", "errors", "highlight", + "notify", "pdf", "set.viewport", "diff", @@ -75,6 +76,8 @@ const expectedCommandIds = [ "swipe", "pair.approve", "pair.reset", + "pair.requestApproval", + "pair.openApproval", ] as const satisfies readonly CommandId[]; type Assert = T; diff --git a/packages/test-support/package.json b/packages/test-support/package.json index 5bc72e4..fc2679f 100644 --- a/packages/test-support/package.json +++ b/packages/test-support/package.json @@ -1,6 +1,6 @@ { "name": "@firefox-cli/test-support", - "version": "0.1.1", + "version": "0.2.0", "private": true, "type": "module", "exports": { diff --git a/scripts/build-extension.ts b/scripts/build-extension.ts index 21a3bd6..2919e22 100644 --- a/scripts/build-extension.ts +++ b/scripts/build-extension.ts @@ -9,6 +9,7 @@ const entries = [ { name: "background", path: resolve(extensionRoot, "src/background.ts") }, { name: "content", path: resolve(extensionRoot, "src/content.ts") }, { name: "popup", path: resolve(extensionRoot, "src/popup.ts") }, + { name: "approval-request", path: resolve(extensionRoot, "src/approval-request.ts") }, ] as const; for (const [index, entry] of entries.entries()) { diff --git a/scripts/copy-extension-assets.ts b/scripts/copy-extension-assets.ts index 67ef8ae..be8e521 100644 --- a/scripts/copy-extension-assets.ts +++ b/scripts/copy-extension-assets.ts @@ -15,6 +15,8 @@ export async function copyExtensionAssets(options: { readonly sourceDir: string; await writeFile(resolve(options.outputDir, "manifest.json"), JSON.stringify(manifest, null, 2)); await cp(resolve(options.sourceDir, "popup.html"), resolve(options.outputDir, "popup.html")); await cp(resolve(options.sourceDir, "popup.css"), resolve(options.outputDir, "popup.css")); + await cp(resolve(options.sourceDir, "approval-request.html"), resolve(options.outputDir, "approval-request.html")); + await cp(resolve(options.sourceDir, "approval-request.css"), resolve(options.outputDir, "approval-request.css")); } if (import.meta.main) { diff --git a/scripts/extension-payload-check.ts b/scripts/extension-payload-check.ts index fdf3a28..4b07a9f 100644 --- a/scripts/extension-payload-check.ts +++ b/scripts/extension-payload-check.ts @@ -3,20 +3,32 @@ import rootPackage from "../package.json" with { type: "json" }; import { listRegularFilesUnder, readRegularFileUnder } from "./safe-extension-files.js"; export async function verifyExtensionBundlePayload(payload: ReadonlyMap): Promise { - const requiredFiles = ["manifest.json", "background.js", "content.js", "popup.js", "popup.html", "popup.css"] as const; + const requiredFiles = [ + "manifest.json", + "background.js", + "content.js", + "popup.js", + "popup.html", + "popup.css", + "approval-request.js", + "approval-request.html", + "approval-request.css", + ] as const; for (const artifact of requiredFiles) { if (!payload.has(artifact)) { throw new Error(`Expected extension artifact: ${artifact}`); } } - const unexpectedJs = [...payload.keys()].filter((file) => file.endsWith(".js")).filter((file) => !["background.js", "content.js", "popup.js"].includes(file)); + const unexpectedJs = [...payload.keys()] + .filter((file) => file.endsWith(".js")) + .filter((file) => !["background.js", "content.js", "popup.js", "approval-request.js"].includes(file)); if (unexpectedJs.length > 0) { throw new Error(`Unexpected extension JavaScript artifacts: ${unexpectedJs.join(", ")}`); } await Promise.all( - ["background.js", "content.js", "popup.js"].map(async (artifact) => { + ["background.js", "content.js", "popup.js", "approval-request.js"].map(async (artifact) => { const source = payload.get(artifact)?.toString("utf8") ?? ""; if ( source.includes('from"./') || diff --git a/scripts/manifest-validation.ts b/scripts/manifest-validation.ts index 3394b72..0cb5a65 100644 --- a/scripts/manifest-validation.ts +++ b/scripts/manifest-validation.ts @@ -56,7 +56,7 @@ export function parseJsonWithSchema(content: string, label: string, location: try { raw = JSON.parse(content); } catch (error) { - throw new Error(`Invalid ${label} JSON at ${location}: ${error instanceof Error ? error.message : String(error)}`); + throw new Error(`Invalid ${label} JSON at ${location}: ${error instanceof Error ? error.message : String(error)}`, { cause: error }); } const parsed = schema.safeParse(raw); diff --git a/scripts/marionette-client.ts b/scripts/marionette-client.ts index e09d67b..da9960a 100644 --- a/scripts/marionette-client.ts +++ b/scripts/marionette-client.ts @@ -114,7 +114,8 @@ export class MarionetteClient { this.#commandTimeoutMs = options.commandTimeoutMs ?? timeoutPolicies.marionetteCommand.timeoutMs; this.#maxFrameBytes = options.maxFrameBytes ?? timeoutPolicies.marionetteFrame.maxBytes; this.#socket.on("data", (chunk) => { - this.#buffer = Buffer.concat([this.#buffer, chunk]); + const data = typeof chunk === "string" ? Buffer.from(chunk) : chunk; + this.#buffer = Buffer.concat([this.#buffer, data]); try { this.#parse(); } catch (error) { diff --git a/scripts/signed-extension-signature.ts b/scripts/signed-extension-signature.ts index 313d3a6..8a5737d 100644 --- a/scripts/signed-extension-signature.ts +++ b/scripts/signed-extension-signature.ts @@ -65,7 +65,7 @@ export async function verifySignedExtensionSignature(input: SignedExtensionSigna ); } catch (error) { const message = error instanceof Error ? error.message : String(error); - throw new Error(`Signed extension PKCS7 verification failed: ${message}`); + throw new Error(`Signed extension PKCS7 verification failed: ${message}`, { cause: error }); } if (result.exitCode !== 0) { diff --git a/scripts/test/copy-extension-assets.test.ts b/scripts/test/copy-extension-assets.test.ts index 6f58396..5a655fd 100644 --- a/scripts/test/copy-extension-assets.test.ts +++ b/scripts/test/copy-extension-assets.test.ts @@ -63,5 +63,7 @@ async function createExtensionAssetFixture(): Promise<{ ); await writeFile(join(sourceDir, "popup.html"), "\n"); await writeFile(join(sourceDir, "popup.css"), "body {}\n"); + await writeFile(join(sourceDir, "approval-request.html"), "\n"); + await writeFile(join(sourceDir, "approval-request.css"), "body {}\n"); return { sourceDir, outputDir }; } diff --git a/scripts/test/package-check-test-utils.ts b/scripts/test/package-check-test-utils.ts index d910979..2b8751b 100644 --- a/scripts/test/package-check-test-utils.ts +++ b/scripts/test/package-check-test-utils.ts @@ -90,8 +90,11 @@ export async function createPackageRoot( await writeFile(join(packageRoot, "extension/development/background.js"), "console.log('bg');\n"); await writeFile(join(packageRoot, "extension/development/content.js"), "console.log('cs');\n"); await writeFile(join(packageRoot, "extension/development/popup.js"), "console.log('popup');\n"); + await writeFile(join(packageRoot, "extension/development/approval-request.js"), "console.log('approval');\n"); await writeFile(join(packageRoot, "extension/development/popup.html"), "\n"); await writeFile(join(packageRoot, "extension/development/popup.css"), "body {}\n"); + await writeFile(join(packageRoot, "extension/development/approval-request.html"), "\n"); + await writeFile(join(packageRoot, "extension/development/approval-request.css"), "body {}\n"); } if (options.includeBinary !== false) { diff --git a/skills/firefox-cli/SKILL.md b/skills/firefox-cli/SKILL.md index bd2ea25..1a3c930 100644 --- a/skills/firefox-cli/SKILL.md +++ b/skills/firefox-cli/SKILL.md @@ -1,13 +1,9 @@ --- name: firefox-cli -description: Control the user's normal Firefox session from a terminal. Use when a task needs using the browser to perform work, user's browser context or authenticated session, navigation, tab/window control, screenshots, DOM reads, waits, or page interactions in Firefox. +description: Control the user's Firefox from a terminal. Use when your task needs the user's browser or authenticated session, page navigation, tab/window control, screenshots, DOM reads, waits, or page interactions in Firefox. Do not use it as a web search replacement. --- -## Purpose - -`firefox-cli` gives agents terminal access to the user's real Firefox session through the installed Firefox extension and native host. - -Use it when browser state matters and the task benefits from the user's normal Firefox profile, signed-in websites, active tabs, or real page behavior. Prefer it over starting a separate automation browser when the user asks to inspect, navigate, test, read, or manipulate pages in Firefox. +`firefox-cli` gives agents terminal access to the user's real authenticated Firefox session through the installed Firefox extension. ## Command Discovery @@ -30,21 +26,6 @@ firefox-cli wait -h Use `--json` when another program or agent consumes the output. -## When To Use - -Good fits: -- Read the active page or a URL into agent context. -- Inspect page title, URL, text, element state, frames, console logs, errors, or network observations. -- Navigate Firefox, open pages, reload, move through history, or manage tabs/windows. -- Click, fill, type, press keys, scroll, upload files, or run a multi-step browser workflow. -- Capture screenshots or other browser-adjacent artifacts. -- Synchronize on page state with waits instead of fixed sleeps. - -Poor fits: -- Tasks that only need HTTP fetching, static source code inspection, or public web search. -- Work that must run in an isolated disposable browser profile. -- Browser-internal or privileged Firefox pages that WebExtensions cannot script. - ## Startup Pattern For a page-reading task: @@ -80,7 +61,9 @@ firefox-cli setup -h firefox-cli doctor -h ``` -## Boundaries +## Usage Rules - Element refs from `snapshot -i` are useful handles for follow-up actions, but page navigation or reload can make them stale. -- Respect CLI errors as the authority for unsupported pages, stale refs, setup gaps, and version mismatches. +- Be careful with the user's data: the Firefox instance you're using contains real cookies, auth credentials, logins, tabs, and PII. Under no circumstances should you perform actions that may harm the user or exfiltrate their data. Under no circumstances should you perform payments, cash transfers, other dangerous, destructive, or irreversible operations, or leak PII without asking for the user's explicit approval before each such action. +- If you need to work with Firefox, but you see interference, such as tabs changing or real user tabs being open, or the user is unhappy, prefer opening a new Firefox window for yourself. Every action you take is visibly reflected in the browser window and can disrupt the user's work. The upside is that you can demonstrate something to the user in their real browser, such as running demos, opening pages, or showing your work. +- If the CLI denies requests with approval and asks you to run `firefox-cli connect`, invoke that **once** to show the user a permission prompt. The command will exit once approval is granted. Do not attempt to circumvent denials in any way. diff --git a/tsconfig.base.json b/tsconfig.base.json index afa6c7e..e7f2059 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -14,6 +14,7 @@ "isolatedModules": true, "skipLibCheck": false, "baseUrl": ".", + "ignoreDeprecations": "6.0", "paths": { "@firefox-cli/cli": ["packages/cli/src/index.ts"], "@firefox-cli/native-host": ["packages/native-host/src/index.ts"],