feat: Chrome extension for one-click resume tailoring#821
feat: Chrome extension for one-click resume tailoring#821gingeekrishna wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a Chrome extension popup UI to detect job descriptions from common job boards and call the backend to upload jobs/tailor resumes, plus backend CORS tweaks to allow calls from the extension.
Changes:
- Introduces MV3 Chrome extension popup (HTML/CSS/JS) with settings, resume upload, job extraction, and results UI.
- Adds extension manifest with required permissions/host permissions.
- Updates backend CORS middleware to accept
chrome-extension://origins.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/chrome-extension/popup/popup.js | Implements popup state, job extraction via chrome.scripting, backend calls, and UI rendering |
| apps/chrome-extension/popup/popup.html | Adds popup layout for settings, main flow, and results |
| apps/chrome-extension/popup/popup.css | Styles popup UI and loading overlay |
| apps/chrome-extension/manifest.json | Defines MV3 extension permissions and popup entrypoint |
| apps/backend/app/base.py | Allows CORS requests originating from Chrome extensions |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| app.add_middleware( | ||
| CORSMiddleware, | ||
| allow_origins=settings.ALLOWED_ORIGINS, | ||
| allow_origin_regex=r"chrome-extension://.*", | ||
| allow_credentials=True, | ||
| allow_methods=["*"], | ||
| allow_headers=["*"], |
| "permissions": ["activeTab", "storage", "scripting"], | ||
| "host_permissions": [ | ||
| "http://localhost/*", | ||
| "http://127.0.0.1/*" | ||
| ], |
| ats.missing_keywords.forEach((kw, i) => { | ||
| const chip = document.createElement('span'); | ||
| chip.className = 'chip'; | ||
| chip.textContent = kw; | ||
| chip.setAttribute('key', `kw-${i}`); | ||
| keywordsChips.appendChild(chip); | ||
| }); |
| <label>Resume</label> | ||
| <p class="hint">Upload your master resume once, or paste an existing Resume ID.</p> | ||
| <input id="input-resume-file" type="file" accept=".pdf,.docx" /> | ||
| <button id="btn-upload-resume">Upload Resume</button> |
| <body> | ||
| <header> | ||
| <span class="logo">Resume Matcher</span> | ||
| <button id="btn-settings-toggle" aria-label="Settings">⚙</button> |
| <label>Resume</label> | ||
| <p class="hint">Upload your master resume once, or paste an existing Resume ID.</p> | ||
| <input id="input-resume-file" type="file" accept=".pdf,.docx" /> | ||
| <button id="btn-upload-resume">Upload Resume</button> |
|
|
||
| <label for="input-resume-id">Resume ID</label> | ||
| <input id="input-resume-id" type="text" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" /> | ||
| <button id="btn-save-settings">Save</button> |
| <span id="badge-resume-id" class="mono"></span> | ||
| </div> | ||
|
|
||
| <button id="btn-tailor" disabled>Tailor Resume</button> |
| <button id="btn-open-app">Open in Resume Matcher</button> | ||
| <button id="btn-tailor-again" class="secondary">Tailor Another</button> |
| app.add_middleware( | ||
| CORSMiddleware, | ||
| allow_origins=settings.ALLOWED_ORIGINS, | ||
| allow_origin_regex=r"chrome-extension://.*", |
There was a problem hiding this comment.
CRITICAL: Overly permissive CORS regex allows ANY Chrome extension to access the backend
allow_origin_regex=r"chrome-extension://.*" matches ALL Chrome extension origins. Any malicious extension installed by a user could make authenticated requests to this backend with allow_credentials=True.
Recommend using a specific extension ID pattern: r"chrome-extension://<your-extension-id>" or moving the extension ID to settings/config.
| ? job.description.slice(0, 280) + (job.description.length > 280 ? '…' : '') | ||
| : ''; | ||
| msgNoJob.hidden = Boolean(hasContent); | ||
| } |
There was a problem hiding this comment.
WARNING: No timeout on fetch requests - backend hangs will freeze the UI indefinitely
All fetch() calls in apiPost and apiUploadFile lack a timeout mechanism. If the backend becomes unresponsive, the extension popup will hang forever.
Consider wrapping fetch with AbortController and a timeout (e.g., 30 seconds).
| resultKeywords.hidden = true; | ||
| } | ||
|
|
||
| if (ats?.recommendations?.length) { |
There was a problem hiding this comment.
WARNING: No file type or size validation before upload
apiUploadFile accepts any file from the file input. Despite accept=".pdf,.docx" in the HTML (which is only a UI hint, not enforced), users could select any file type.
Validate file.type or check the file extension before sending to the backend.
| const jobId = jobRes.job_id; | ||
|
|
||
| setLoading(true, 'Tailoring resume…'); | ||
| const improveRes = await apiPost('/api/v1/resumes/improve', { |
There was a problem hiding this comment.
WARNING: Partial failure state - job created but improve fails leaves dangling data
If apiPost('/api/v1/jobs/upload') succeeds but the subsequent apiPost('/api/v1/resumes/improve') fails, a job is created on the backend but the user never sees results. Consider adding cleanup logic or at least a clearer error message indicating the job was created.
| } catch { | ||
| chrome.tabs.create({ url: 'http://localhost:3000' }); | ||
| } | ||
| }); |
There was a problem hiding this comment.
SUGGESTION: btnTailorAgain doesn't clear state.jobData - stale job info may display
Clicking "Tailor Another" returns to the main panel but state.jobData retains the previous job. The UI will show the old job title/company/description until a new job is extracted.
Consider adding state.jobData = null; renderJobData(null); here.
| if (tab?.id) { | ||
| const [result] = await chrome.scripting.executeScript({ | ||
| target: { tabId: tab.id }, | ||
| func: extractJobFromPage, |
There was a problem hiding this comment.
SUGGESTION: Silent error catch in init() - extraction failures are invisible
The catch {} block silently swallows all errors during job extraction. If chrome.scripting.executeScript fails for a non-internal-page reason (e.g., content blocked, tab closed), the user sees no feedback.
Consider logging to console or showing a subtle hint when extraction fails for unexpected reasons.
| const el = document.querySelector(sel); | ||
| if (el && el.innerText && el.innerText.trim()) return el.innerText.trim(); | ||
| } catch (_) { | ||
| // ignore invalid selectors |
There was a problem hiding this comment.
WARNING: No content size limit - very long job descriptions could exceed backend/API limits
getText() extracts ALL innerText from description containers. Job descriptions on some sites can be 10,000+ characters. Consider truncating description before storing in state.jobData or before sending to the backend.
Code Review SummaryStatus: 7 Issues Found (1 CRITICAL, 4 WARNING, 2 SUGGESTION) | Recommendation: Address before merge Overview
Issue Details (click to expand)CRITICAL
WARNING
SUGGESTION
Other Observations (not in diff)Issues found in unchanged code or structural concerns that cannot receive inline comments:
Files Reviewed (5 files)
Reviewed by qwen3.6-plus · 158,201 tokens |
There was a problem hiding this comment.
7 issues found across 5 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/backend/app/base.py">
<violation number="1" location="apps/backend/app/base.py:49">
P1: Custom agent: **Flag Security Vulnerabilities**
CORS configuration trusts every chrome-extension:// origin, creating an overly broad cross-origin access surface with credentials enabled</violation>
</file>
<file name="apps/chrome-extension/popup/popup.js">
<violation number="1" location="apps/chrome-extension/popup/popup.js:164">
P2: The empty `catch {}` block silently swallows all errors during job extraction, including unexpected failures (content blocked, tab closed, permissions issues). At minimum, log to console for debuggability; ideally show a subtle hint when extraction fails for non-internal-page reasons.</violation>
<violation number="2" location="apps/chrome-extension/popup/popup.js:233">
P3: Setting a `key` attribute on a DOM element is a React concept with no semantic meaning in vanilla JS. This is likely carried over from React patterns and will confuse maintainers. Remove it or use `chip.dataset.key = ...` if you need an identifier for testing/automation.</violation>
</file>
<file name="apps/chrome-extension/manifest.json">
<violation number="1" location="apps/chrome-extension/manifest.json:8">
P1: The extension allows users to configure a custom `backendUrl` in settings, but `host_permissions` only grants access to `localhost` and `127.0.0.1`. In Manifest V3, `fetch()` from extension pages to origins not listed in `host_permissions` will be blocked by the browser. Either restrict the settings UI to only accept localhost URLs, or document that users must modify the manifest for non-local backends.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| app.add_middleware( | ||
| CORSMiddleware, | ||
| allow_origins=settings.ALLOWED_ORIGINS, | ||
| allow_origin_regex=r"chrome-extension://.*", |
There was a problem hiding this comment.
P1: Custom agent: Flag Security Vulnerabilities
CORS configuration trusts every chrome-extension:// origin, creating an overly broad cross-origin access surface with credentials enabled
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/app/base.py, line 49:
<comment>CORS configuration trusts every chrome-extension:// origin, creating an overly broad cross-origin access surface with credentials enabled</comment>
<file context>
@@ -46,6 +46,7 @@ def create_app() -> FastAPI:
app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_ORIGINS,
+ allow_origin_regex=r"chrome-extension://.*",
allow_credentials=True,
allow_methods=["*"],
</file context>
| @@ -0,0 +1,15 @@ | |||
| { | |||
There was a problem hiding this comment.
P1: The extension allows users to configure a custom backendUrl in settings, but host_permissions only grants access to localhost and 127.0.0.1. In Manifest V3, fetch() from extension pages to origins not listed in host_permissions will be blocked by the browser. Either restrict the settings UI to only accept localhost URLs, or document that users must modify the manifest for non-local backends.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/chrome-extension/manifest.json, line 8:
<comment>The extension allows users to configure a custom `backendUrl` in settings, but `host_permissions` only grants access to `localhost` and `127.0.0.1`. In Manifest V3, `fetch()` from extension pages to origins not listed in `host_permissions` will be blocked by the browser. Either restrict the settings UI to only accept localhost URLs, or document that users must modify the manifest for non-local backends.</comment>
<file context>
@@ -0,0 +1,15 @@
+ "description": "Detect job descriptions on job boards and tailor your resume in one click.",
+ "permissions": ["activeTab", "storage", "scripting"],
+ "host_permissions": [
+ "http://localhost/*",
+ "http://127.0.0.1/*"
+ ],
</file context>
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(body), | ||
| }); | ||
| } catch { |
There was a problem hiding this comment.
P2: The empty catch {} block silently swallows all errors during job extraction, including unexpected failures (content blocked, tab closed, permissions issues). At minimum, log to console for debuggability; ideally show a subtle hint when extraction fails for non-internal-page reasons.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/chrome-extension/popup/popup.js, line 164:
<comment>The empty `catch {}` block silently swallows all errors during job extraction, including unexpected failures (content blocked, tab closed, permissions issues). At minimum, log to console for debuggability; ideally show a subtle hint when extraction fails for non-internal-page reasons.</comment>
<file context>
@@ -0,0 +1,380 @@
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ });
+ } catch {
+ throw new Error(
+ `Cannot reach backend at ${state.backendUrl}. Make sure it is running (uv run uvicorn app.main:app --port 8000).`
</file context>
| const chip = document.createElement('span'); | ||
| chip.className = 'chip'; | ||
| chip.textContent = kw; | ||
| chip.setAttribute('key', `kw-${i}`); |
There was a problem hiding this comment.
P3: Setting a key attribute on a DOM element is a React concept with no semantic meaning in vanilla JS. This is likely carried over from React patterns and will confuse maintainers. Remove it or use chip.dataset.key = ... if you need an identifier for testing/automation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/chrome-extension/popup/popup.js, line 233:
<comment>Setting a `key` attribute on a DOM element is a React concept with no semantic meaning in vanilla JS. This is likely carried over from React patterns and will confuse maintainers. Remove it or use `chip.dataset.key = ...` if you need an identifier for testing/automation.</comment>
<file context>
@@ -0,0 +1,380 @@
+ const chip = document.createElement('span');
+ chip.className = 'chip';
+ chip.textContent = kw;
+ chip.setAttribute('key', `kw-${i}`);
+ keywordsChips.appendChild(chip);
+ });
</file context>
f0ad45b to
55f56f8
Compare
| # Restrict to a specific extension ID (e.g. "chrome-extension://abcdef...") via | ||
| # EXTENSION_CORS_ORIGIN env var. Defaults to allowing any local extension for | ||
| # developer convenience; set an exact ID in shared / production environments. | ||
| extension_cors_origin: str = r"chrome-extension://.*" |
There was a problem hiding this comment.
CRITICAL: Overly permissive CORS regex allows ANY Chrome extension to access the backend with credentials
The default r"chrome-extension://.*" matches ANY Chrome extension origin, not just this specific extension. Combined with allow_credentials=True, this means any malicious Chrome extension installed by the user can make authenticated requests to your backend (including resume uploads, improvements, etc.).
The comment says to set EXTENSION_CORS_ORIGIN for production, but the default is insecure for ANY shared environment. The default should be empty/disabled ("" or None) and developers should explicitly enable it with their extension ID.
|
|
||
| const resultData = improveRes.data ?? improveRes; | ||
| panelMain.hidden = true; | ||
| panelResult.hidden = false; |
There was a problem hiding this comment.
WARNING: Partial failure state - job created but improve fails leaves dangling data
If apiPost('/api/v1/resumes/improve', ...) fails, the job (created via apiPost('/api/v1/jobs/upload', ...)) is never cleaned up. This leaves orphaned job records in the database. The jobId is captured but there's no cleanup attempt on failure.
Consider adding a delete/cleanup call in the catch block or documenting this as expected behavior.
|
|
||
| btnOpenApp.addEventListener('click', () => { | ||
| try { | ||
| const parsed = new URL(state.backendUrl); |
There was a problem hiding this comment.
WARNING: btnOpenApp hardcodes port :3000 for frontend URL - will break in production
If state.backendUrl is http://api.example.com (no port), this creates http://api.example.com:3000 which is likely incorrect. If the frontend runs on a different port (e.g., localhost:3001 for dev), this opens the wrong URL.
Consider making the frontend URL configurable in settings or inferring it from the backend URL more intelligently.
| btnTailorAgain.addEventListener('click', () => { | ||
| panelResult.hidden = true; | ||
| panelMain.hidden = false; | ||
| // Re-extract job from the active tab so stale job data doesn't linger |
There was a problem hiding this comment.
WARNING: Silent error catch in btnTailorAgain - extraction failures are invisible
The .catch(() => {}) completely swallows any errors during job re-extraction. If executeScript fails (e.g., tab navigated away, permissions issue), the user sees no feedback and state.jobData may be left in an inconsistent state.
At minimum, log the error or show a message to the user.
- Manifest V3 extension with activeTab, storage, and scripting permissions - Popup extracts job title, company, and description from LinkedIn, Indeed, Greenhouse, Lever, Glassdoor, and Workday using injected content function - One-time setup: upload master resume PDF/DOCX or paste existing resume ID; settings persisted to chrome.storage.local - Tailor flow: POST job description to /api/v1/jobs/upload, then POST to /api/v1/resumes/improve; shows ATS score, missing keywords, and recommendations from the response - "Open in Resume Matcher" button opens the frontend (localhost:3000) for PDF download - Backend: add allow_origin_regex for chrome-extension://* to CORSMiddleware so the extension popup can reach the local backend without CORS errors
55f56f8 to
23a3df3
Compare
- config.py: change extension_cors_origin default to None so shared/server deployments are unaffected; opt-in by setting EXTENSION_CORS_ORIGIN env var - main.py: only add allow_origin_regex to CORSMiddleware when the setting is non-empty, keeping the default config secure - popup.js: validate backend URL format (must be http/https) and restrict to localhost/127.0.0.1 with a clear error when a non-local URL is entered, matching the manifest host_permissions scope
Summary
Closes #772
Adds a Manifest V3 Chrome extension that lets users tailor their resume directly from any job posting page — no copy-pasting required.
What's included
apps/chrome-extension/manifest.json— MV3 extension; permissions:activeTab,storage,scripting; no broad host permissions beyondlocalhostpopup/popup.html— clean popup UI (360 px wide, Swiss International Style palette)popup/popup.css— flat design:#F0F0E8canvas, 1 px black borders, no border radius, Signal Green / Alert Red / Alert Orange for score colourspopup/popup.js— full tailor flow:extractJobFromPage()into the active tab viachrome.scripting.executeScriptto pull job title, company, and description from LinkedIn, Indeed, Greenhouse, Lever, Glassdoor, and WorkdayPOST /api/v1/resumes/upload) or paste an existing Resume ID; persisted tochrome.storage.localPOST /api/v1/jobs/upload→POST /api/v1/resumes/improve:3000for PDF downloadapps/backend/app/base.pyallow_origin_regex=r"chrome-extension://.*"toCORSMiddlewareso the extension popup can reach the local backend without CORS errors (extension IDs are dynamic, making an exact-match list impractical)How to load the extension locally
chrome://extensionsapps/chrome-extension/Test plan
localhost:3000Out of scope (follow-up)
Summary by cubic
Adds a Manifest V3 Chrome extension for one‑click resume tailoring and makes backend CORS opt‑in via
EXTENSION_CORS_ORIGIN(disabled by default). Implements #772.New Features
apps/chrome-extension/with popup UI; permissions:activeTab,storage,scripting;host_permissionslimited tohttp://localhost/*andhttp://127.0.0.1/*.POST /api/v1/resumes/upload) or paste Resume ID; backend URL and Resume ID saved inchrome.storage.local.POST /api/v1/jobs/upload→POST /api/v1/resumes/improve; shows ATS score, missing keywords, and recommendations; “Open in Resume Matcher” opens:3000.allow_origin_regexwhenEXTENSION_CORS_ORIGINis set; default isNone. Popup validates backend URL (must be http/https) and restricts to localhost/127.0.0.1 to match manifest scope.Migration
chrome://extensions→ enable Developer mode → Load unpacked → selectapps/chrome-extension/.:3000, open a supported job page, then click the extension.EXTENSION_CORS_ORIGINto your exact extension ID for shared/production; for local dev you can usechrome-extension://.*(default isNone).Written for commit ee9b8ec. Summary will update on new commits.