From ace5c7a4580ba7ff42a2d95a1049df4359b17d83 Mon Sep 17 00:00:00 2001 From: Andriy Polanski Date: Mon, 13 Jul 2026 06:49:39 +0000 Subject: [PATCH] feat(miner-extension): grant loopback host permissions for local miner-ui access (#4860) --- apps/gittensory-miner-extension/README.md | 8 ++++++++ apps/gittensory-miner-extension/manifest.json | 2 +- test/unit/miner-extension-content.test.ts | 13 +++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/gittensory-miner-extension/README.md b/apps/gittensory-miner-extension/README.md index 5071648260..02506e3968 100644 --- a/apps/gittensory-miner-extension/README.md +++ b/apps/gittensory-miner-extension/README.md @@ -27,3 +27,11 @@ omitted entirely for a cache saved before this field existed. The extension does not request the `unlimitedStorage` permission, so a paste is rejected with a clear error before being parsed or saved once it exceeds a conservative size bound well under `chrome.storage.local`'s default 10 MiB quota, instead of silently failing to save or leaving storage partially written. + +## Host permissions + +`manifest.json` grants `https://github.com/*` (for the issue-page content script) plus loopback host permissions — +`http://localhost/*` and `http://127.0.0.1/*` — so the extension can reach the operator's own local miner-ui API +(#4860). Chrome match patterns cannot pin a port, so `http://localhost/*` is the narrowest grant the platform +allows; `https` is intentionally omitted because the local miner-ui dev server is plain HTTP. This is the enabling +permission for live-fetching ranked candidates from the local miner-ui instead of pasting them. diff --git a/apps/gittensory-miner-extension/manifest.json b/apps/gittensory-miner-extension/manifest.json index 2e3be765f1..f27558bd76 100644 --- a/apps/gittensory-miner-extension/manifest.json +++ b/apps/gittensory-miner-extension/manifest.json @@ -4,7 +4,7 @@ "description": "Contributor-facing GitHub issue opportunity signals from a locally configured miner plane.", "version": "0.1.0", "permissions": ["storage"], - "host_permissions": ["https://github.com/*"], + "host_permissions": ["https://github.com/*", "http://localhost/*", "http://127.0.0.1/*"], "background": { "service_worker": "background.js", "type": "module" diff --git a/test/unit/miner-extension-content.test.ts b/test/unit/miner-extension-content.test.ts index 8b83ba7033..1dd31a51b4 100644 --- a/test/unit/miner-extension-content.test.ts +++ b/test/unit/miner-extension-content.test.ts @@ -46,6 +46,19 @@ describe("miner extension opportunity badge", () => { expect(manifest.content_scripts[0].css).toEqual(["styles.css"]); }); + it("grants loopback host permissions so the extension can reach the local miner-ui, scoped to localhost only (#4860)", () => { + // Chrome match patterns cannot pin a port, so http://localhost/* + http://127.0.0.1/* is the narrowest the + // platform allows; https is intentionally omitted (the local miner-ui dev server is plain HTTP). + expect(manifest.host_permissions).toContain("http://localhost/*"); + expect(manifest.host_permissions).toContain("http://127.0.0.1/*"); + // github.com stays; the loopback grant is additive, not a replacement. + expect(manifest.host_permissions).toContain("https://github.com/*"); + // No broad or non-loopback host is granted alongside it. + for (const pattern of manifest.host_permissions) { + expect(pattern).toMatch(/^https:\/\/github\.com\/\*$|^http:\/\/(?:localhost|127\.0\.0\.1)\/\*$/); + } + }); + it("detects GitHub issue routes without matching pull requests", () => { const internals = loadContentInternals(); expect(internals.matchGitHubIssueTarget("/JSONbored/gittensory/issues/145")).toEqual({