From 8209ed604e946566a4d738c55396f37e47101a09 Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Mon, 21 Sep 2026 10:58:30 +0100 Subject: [PATCH 1/5] Package releases with the manifest at the zip root The release script zipped ./dist, so every entry was nested under dist/ and the Chrome Web Store rejects an upload whose manifest.json is not at the root. It now zips the contents of dist instead, and builds first so an artifact cannot be cut from a stale dist. Co-Authored-By: Claude Opus 5 (1M context) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a38b8e4..e0dab78 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "watch": "node scripts/build.mjs --dev --watch", "build": "node scripts/build.mjs", "build:dev": "node scripts/build.mjs --dev", - "release": "zip -r ./releases/search_and_replace_$npm_package_version.zip ./dist", + "release": "npm run build && mkdir -p releases && cd dist && zip -qr ../releases/search_and_replace_$npm_package_version.zip .", "start": "light-server --no-reload -s . -p 9000 -w \"**/*.html,**/*.css,**/*.js,**/*.gz\"", "typecheck": "tsc --noEmit && tsc --noEmit --project tsconfig.test.json && tsc --noEmit --project src/cypress/tsconfig.json", "lint": "eslint 'src/**/*.ts' 'scripts/*.mjs'", From a43bf09e3314073cd5bde01f44e1290b9523269e Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Mon, 21 Sep 2026 11:05:05 +0100 Subject: [PATCH 2/5] Record the Chrome Web Store permission justifications The 2.4.0 submission adds the tabs and scripting permissions, so the store asks for a justification for each. Keeping them in the repo next to the code they describe means they can be checked against it and reused on the next submission. Co-Authored-By: Claude Opus 5 (1M context) --- docs/permission-justifications.md | 49 +++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/permission-justifications.md diff --git a/docs/permission-justifications.md b/docs/permission-justifications.md new file mode 100644 index 0000000..4628381 --- /dev/null +++ b/docs/permission-justifications.md @@ -0,0 +1,49 @@ +# Chrome Web Store permission justifications — Search and Replace 2.4.0 + +## tabs + +The extension lets a user save a search-and-replace rule and have it reapplied automatically on +later visits to the same page. To do that it must read the URL of the active tab, which is a +privileged tab property and so requires the "tabs" permission. + +It is used in exactly three places: + +- When a rule is saved, the current tab's URL is read so the rule can be stored against it + (`src/popup.ts`, `getInstanceId(..., url: tab.url)`). +- On each page load, the active tab's URL is read and matched against the saved rules, so the + right rule — if any — is applied (`src/background/saved.ts`, `matchSavedInstances`). +- On install or update, tabs are queried by URL pattern so the content script can be injected + into pages that are already open (`src/background/install.ts`). + +URLs are compared against the user's own saved rules and stored only as part of a rule the user +explicitly chose to save. They are held in local extension storage, are never transmitted +anywhere, and no browsing history is read or retained. + +## scripting + +Chrome only injects a content script into pages loaded after an extension is installed or +updated. Without this permission, every tab a user already had open would do nothing until +manually reloaded, which was a frequent complaint (issue #102). + +On install and on update the extension injects its single content script, `searchreplace.js`, +into the tabs that are already open, so it works immediately (`src/background/install.ts`, +`injectContentScriptIntoOpenTabs`). + +The injection is limited to that one bundled file — never remote or generated code — and only +into the http, https and file URLs already covered by the extension's host permissions. + +## host permissions (http://*/*, https://*/*, file:///*) + +Searching and replacing text requires reading and modifying the content of whichever page the +user is working on, and the user chooses that page rather than the extension. All work happens +locally in the page; no page content is sent off the device. + +## Single purpose + +Find text on the page the user is viewing and replace it with different text, including in form +fields and rich-text editors. + +## Remote code + +No. All code is bundled in the package. The extension executes no remote or dynamically +generated code. From ed3480fa1b35687c6b100fc9ce37a43e1fd60a3f Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Mon, 21 Sep 2026 11:08:59 +0100 Subject: [PATCH 3/5] Trim the permission justifications to dashboard length Co-Authored-By: Claude Opus 5 (1M context) --- docs/permission-justifications.md | 65 ++++++++++++++++--------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/docs/permission-justifications.md b/docs/permission-justifications.md index 4628381..a68553e 100644 --- a/docs/permission-justifications.md +++ b/docs/permission-justifications.md @@ -1,49 +1,52 @@ -# Chrome Web Store permission justifications — Search and Replace 2.4.0 +# Chrome Web Store permission justifications + +Paste-ready text for the Developer Dashboard. The dashboard asks for a justification for every +permission requested, not only the new ones. Code references are for our own use — they are not +needed in the field, but they are where to check each claim. ## tabs -The extension lets a user save a search-and-replace rule and have it reapplied automatically on -later visits to the same page. To do that it must read the URL of the active tab, which is a -privileged tab property and so requires the "tabs" permission. +> Reads the URL of the active tab so that saved find-and-replace rules can be matched to the page +> they were saved on and reapplied on later visits. URLs are kept in local extension storage as +> part of the user's own saved rules and are never transmitted. + +`src/background/saved.ts` (matching), `src/popup.ts` (saving), `src/background/install.ts` (query). -It is used in exactly three places: +## scripting -- When a rule is saved, the current tab's URL is read so the rule can be stored against it - (`src/popup.ts`, `getInstanceId(..., url: tab.url)`). -- On each page load, the active tab's URL is read and matched against the saved rules, so the - right rule — if any — is applied (`src/background/saved.ts`, `matchSavedInstances`). -- On install or update, tabs are queried by URL pattern so the content script can be injected - into pages that are already open (`src/background/install.ts`). +> Injects the extension's own bundled content script into tabs that are already open when the +> extension is installed or updated, so the user does not have to reload each page first. No +> remote or generated code is executed. -URLs are compared against the user's own saved rules and stored only as part of a rule the user -explicitly chose to save. They are held in local extension storage, are never transmitted -anywhere, and no browsing history is read or retained. +`src/background/install.ts`, `injectContentScriptIntoOpenTabs`. -## scripting +## activeTab + +> Lets the user run a find and replace on the page they are currently viewing, when they open the +> extension and ask for it. + +## storage + +> Stores the user's search terms, recent history, saved rules and chosen language locally. -Chrome only injects a content script into pages loaded after an extension is installed or -updated. Without this permission, every tab a user already had open would do nothing until -manually reloaded, which was a frequent complaint (issue #102). +## notifications -On install and on update the extension injects its single content script, `searchreplace.js`, -into the tabs that are already open, so it works immediately (`src/background/install.ts`, -`injectContentScriptIntoOpenTabs`). +> Shows a single notification after installation explaining that a page open before installing +> needs reloading. -The injection is limited to that one bundled file — never remote or generated code — and only -into the http, https and file URLs already covered by the extension's host permissions. +`src/background/install.ts`. -## host permissions (http://*/*, https://*/*, file:///*) +## Host permissions — `http://*/*`, `https://*/*`, `file:///*` -Searching and replacing text requires reading and modifying the content of whichever page the -user is working on, and the user chooses that page rather than the extension. All work happens -locally in the page; no page content is sent off the device. +> Finding and replacing text requires reading and changing the content of whichever page the user +> is working on. The user chooses that page; nothing is read in the background and no page content +> leaves the device. ## Single purpose -Find text on the page the user is viewing and replace it with different text, including in form -fields and rich-text editors. +> Find text on the page the user is viewing and replace it with different text, including in form +> fields and rich-text editors. ## Remote code -No. All code is bundled in the package. The extension executes no remote or dynamically -generated code. +> No. All code is bundled in the package. From d717d9276a9dd92320c0f9349e1b56336bd4a1b7 Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Mon, 21 Sep 2026 11:19:08 +0100 Subject: [PATCH 4/5] Drop the tabs permission; host permissions already cover tab URLs The tabs permission was added in the unreleased 2.1.0. It is not needed: in manifest v3 a tab's url is exposed to an extension holding either the tabs permission or a host permission matching that tab, and this extension already declares http://*/* and https://*/*. The published 2.0.9 build is the proof. It declares only activeTab, storage and notifications, yet its background script already reads tab.url to match saved rules against the current page, and has been doing so in production. Keeping tabs would have added the "Read your browsing history" warning, which Chrome shows on update and which disables the extension until the user approves it. Relative to the live build, 2.4.0 now adds only `scripting`, which carries no warning at all. The injection query now uses the host permission patterns rather than the content script's `matches`, which also include file:///*. Without a host permission for file URLs those tabs cannot be queried or injected, so file:// pages still need a manual reload. Co-Authored-By: Claude Opus 5 (1M context) --- docs/permission-justifications.md | 12 ++++-------- manifest.json | 2 +- src/background/install.ts | 14 +++++++++++--- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/permission-justifications.md b/docs/permission-justifications.md index a68553e..c71810e 100644 --- a/docs/permission-justifications.md +++ b/docs/permission-justifications.md @@ -4,14 +4,6 @@ Paste-ready text for the Developer Dashboard. The dashboard asks for a justifica permission requested, not only the new ones. Code references are for our own use — they are not needed in the field, but they are where to check each claim. -## tabs - -> Reads the URL of the active tab so that saved find-and-replace rules can be matched to the page -> they were saved on and reapplied on later visits. URLs are kept in local extension storage as -> part of the user's own saved rules and are never transmitted. - -`src/background/saved.ts` (matching), `src/popup.ts` (saving), `src/background/install.ts` (query). - ## scripting > Injects the extension's own bundled content script into tabs that are already open when the @@ -20,6 +12,10 @@ needed in the field, but they are where to check each claim. `src/background/install.ts`, `injectContentScriptIntoOpenTabs`. +> **Note:** the `tabs` permission is deliberately not requested. Tab URLs, needed to match saved +> rules, are available through the host permissions below, so `tabs` would add the "Read your +> browsing history" warning for nothing. The published 2.0.9 build works this way already. + ## activeTab > Lets the user run a find and replace on the page they are currently viewing, when they open the diff --git a/manifest.json b/manifest.json index affa17b..ab9ba2f 100644 --- a/manifest.json +++ b/manifest.json @@ -28,7 +28,7 @@ "name": "Search and Replace", "description": "__MSG_ext_description__", "manifest_version": 3, - "permissions": ["activeTab", "storage", "notifications", "tabs", "scripting"], + "permissions": ["activeTab", "storage", "notifications", "scripting"], "host_permissions": ["http://*/*", "https://*/*"], "update_url": "http://clients2.google.com/service/update2/crx", "version": "2.4.0", diff --git a/src/background/install.ts b/src/background/install.ts index 42a0df1..f963f81 100644 --- a/src/background/install.ts +++ b/src/background/install.ts @@ -10,8 +10,16 @@ const DEFAULT_LANGUAGE = 'en' /** The bundle listed under content_scripts in manifest.json */ const CONTENT_SCRIPT = 'searchreplace.js' -/** Kept in step with the `matches` patterns of the content script in manifest.json */ -const CONTENT_SCRIPT_MATCHES = ['http://*/*', 'https://*/*', 'file:///*'] +/** + * Tabs to inject into on install and update. + * + * These are the `host_permissions` from manifest.json, not the content script's `matches`, which + * also include `file:///*`. Without the `tabs` permission, querying by a URL pattern — and + * reading a tab's URL at all — works only where a host permission covers it, so file:// pages + * still need a manual reload. That is a fair trade for not asking every user to approve the + * "Read your browsing history" warning that `tabs` carries. + */ +const INJECTABLE_TABS = ['http://*/*', 'https://*/*'] /** * The first browser UI language we also ship a translation for, else English. @@ -38,7 +46,7 @@ export async function injectContentScriptIntoOpenTabs(): Promise { const injected: number[] = [] let tabs: chrome.tabs.Tab[] = [] try { - tabs = await chrome.tabs.query({ url: CONTENT_SCRIPT_MATCHES }) + tabs = await chrome.tabs.query({ url: INJECTABLE_TABS }) } catch (error) { console.error('BACKGROUND: Could not list open tabs to inject into', error) return injected From 8d49984deb801a9ee03bb4f205d79e1e72af7392 Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Mon, 21 Sep 2026 11:51:22 +0100 Subject: [PATCH 5/5] Bump to 2.4.1 master was merged carrying 2.4.0 with the tabs permission still in the manifest, so that version number would describe two different builds. 2.4.1 is what actually ships: no tabs permission, and a release script that produces a zip the Chrome Web Store will accept. Co-Authored-By: Claude Opus 5 (1M context) --- manifest.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/manifest.json b/manifest.json index ab9ba2f..4d8554b 100644 --- a/manifest.json +++ b/manifest.json @@ -31,7 +31,7 @@ "permissions": ["activeTab", "storage", "notifications", "scripting"], "host_permissions": ["http://*/*", "https://*/*"], "update_url": "http://clients2.google.com/service/update2/crx", - "version": "2.4.0", + "version": "2.4.1", "icons": { "16": "assets/icon-16.png", "32": "assets/icon-32.png", diff --git a/package.json b/package.json index e0dab78..495714c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "search_and_replace", - "version": "2.4.0", + "version": "2.4.1", "resolutions": { "author": "Chris Taylor " },