Skip to content

feat: Chrome extension for one-click resume tailoring#821

Open
gingeekrishna wants to merge 5 commits into
srbhr:mainfrom
gingeekrishna:feature/chrome-extension
Open

feat: Chrome extension for one-click resume tailoring#821
gingeekrishna wants to merge 5 commits into
srbhr:mainfrom
gingeekrishna:feature/chrome-extension

Conversation

@gingeekrishna

@gingeekrishna gingeekrishna commented May 31, 2026

Copy link
Copy Markdown
Contributor

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 beyond localhost
  • popup/popup.html — clean popup UI (360 px wide, Swiss International Style palette)
  • popup/popup.css — flat design: #F0F0E8 canvas, 1 px black borders, no border radius, Signal Green / Alert Red / Alert Orange for score colours
  • popup/popup.js — full tailor flow:
    1. Injects extractJobFromPage() into the active tab via chrome.scripting.executeScript to pull job title, company, and description from LinkedIn, Indeed, Greenhouse, Lever, Glassdoor, and Workday
    2. One-time setup: upload master resume PDF/DOCX (calls POST /api/v1/resumes/upload) or paste an existing Resume ID; persisted to chrome.storage.local
    3. On "Tailor Resume": POST /api/v1/jobs/uploadPOST /api/v1/resumes/improve
    4. Displays ATS score (colour-coded), missing-keywords chips, and recommendations from the response
    5. "Open in Resume Matcher" button opens the frontend on :3000 for PDF download

apps/backend/app/base.py

  • Adds allow_origin_regex=r"chrome-extension://.*" to CORSMiddleware so 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

  1. Open Chrome → chrome://extensions
  2. Enable Developer mode
  3. Click Load unpacked → select apps/chrome-extension/
  4. Navigate to any supported job posting and click the extension icon

Test plan

  • Load extension in developer mode
  • Navigate to a LinkedIn / Indeed / Greenhouse / Lever job posting — confirm title, company, and description are detected
  • First-run: upload a PDF resume from the popup — confirm resume ID is saved and displayed
  • Click Tailor Resume — confirm loading states appear and result panel shows score + keywords
  • Verify Open in Resume Matcher opens localhost:3000
  • Verify Tailor Another returns to the main panel
  • Verify existing backend routes are unaffected by the CORS change

Out of scope (follow-up)

  • Auto-filling job application forms (Workday/Greenhouse submit forms) — each ATS has a different structure; deferred
  • Exportable ATS report PDF — can be built on top once the extension is merged

Summary by cubic

Adds a Manifest V3 Chrome extension for one-click resume tailoring and makes backend CORS opt-in via EXTENSION_CORS_ORIGIN (off by default). Implements #772.

  • New Features

    • Extension at apps/chrome-extension/ with popup; permissions: activeTab, storage, scripting; host_permissions limited to http://localhost/* and http://127.0.0.1/*; buttons use type="button" and the Resume label is correctly associated.
    • Extractor supports LinkedIn, Indeed, Greenhouse, Lever, Glassdoor, and Workday.
    • Setup: upload master resume (POST /api/v1/resumes/upload, PDF/DOCX validated) or paste Resume ID; backend URL and Resume ID saved in chrome.storage.local; backend URL must be http/https on localhost/127.0.0.1.
    • Tailor: POST /api/v1/jobs/upload (JD ≤ 8,000 chars) → POST /api/v1/resumes/improve; shows ATS score, missing keywords, and recommendations; “Open in Resume Matcher” derives the web app URL from the backend host; “Tailor Another” re-extracts the tab and shows an error if it fails.
    • Reliability: all fetches use a 120s timeout; if improve fails after job upload, the error includes the created job ID.
    • Backend CORS: only adds allow_origin_regex when EXTENSION_CORS_ORIGIN is set; default is None.
  • Bug Fixes

    • Use the first job_id returned by POST /api/v1/jobs/upload when calling POST /api/v1/resumes/improve to avoid a 422 validation error.

Written for commit cbc9df0. Summary will update on new commits.

Review in cubic

Copilot AI review requested due to automatic review settings May 31, 2026 08:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread apps/backend/app/base.py Outdated
Comment on lines 46 to 52
app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_ORIGINS,
allow_origin_regex=r"chrome-extension://.*",
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
Comment on lines +6 to +10
"permissions": ["activeTab", "storage", "scripting"],
"host_permissions": [
"http://localhost/*",
"http://127.0.0.1/*"
],
Comment thread apps/chrome-extension/popup/popup.js Outdated
Comment on lines +229 to +235
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);
});
Comment thread apps/chrome-extension/popup/popup.html Outdated
Comment on lines +24 to +27
<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>
Comment thread apps/chrome-extension/popup/popup.html Outdated
<body>
<header>
<span class="logo">Resume Matcher</span>
<button id="btn-settings-toggle" aria-label="Settings">&#9881;</button>
Comment thread apps/chrome-extension/popup/popup.html Outdated
<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>
Comment thread apps/chrome-extension/popup/popup.html Outdated

<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>
Comment thread apps/chrome-extension/popup/popup.html Outdated
<span id="badge-resume-id" class="mono"></span>
</div>

<button id="btn-tailor" disabled>Tailor Resume</button>
Comment thread apps/chrome-extension/popup/popup.html Outdated
Comment on lines +76 to +77
<button id="btn-open-app">Open in Resume Matcher</button>
<button id="btn-tailor-again" class="secondary">Tailor Another</button>
Comment thread apps/backend/app/base.py Outdated
app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_ORIGINS,
allow_origin_regex=r"chrome-extension://.*",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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', {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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' });
}
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@kilo-code-bot

kilo-code-bot Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Incremental Changes

Since the previous review (commit 0cbef793), 1 commit was added:

  • Added safety guard before jobRes.job_id[0] access — throws clear error if backend returns empty/null job ID
  • Added user-visible error message when btnTailorAgain re-extraction fails (previously only console.warn)

Resolved Since Last Review

File Previous Line Issue Resolution
apps/chrome-extension/popup/popup.js 352 WARNING: No safety guard on jobRes.job_id[0] — empty/null array access could cause TypeError Guard added: if (!jobRes.job_id?.length) throw new Error('Backend returned no job ID.')
apps/chrome-extension/popup/popup.js 399 SUGGESTION: Re-extraction failure provides no user-visible feedback showMsg(msgMain, ...) added in catch block

Previously Resolved (from earlier commits)

Issue Resolution
CORS regex overly permissive Now opt-in via EXTENSION_CORS_ORIGIN env var (defaults to None)
No timeout on fetch requests All fetches wrapped with fetchWithTimeout() (120s AbortController)
No file type validation before upload apiUploadFile() validates against ALLOWED_TYPES allowlist
No content size limit JD truncated to MAX_JD_CHARS = 8000 before upload
Silent error catch in init() Now uses console.warn + distinguishes expected vs unexpected errors
btnTailorAgain stale job data Now re-extracts job from active tab
Silent error catch in btnTailorAgain Now logs via console.warn + shows user-visible error
Files Reviewed (1 file)
  • apps/chrome-extension/popup/popup.js — No issues
Previous Review Summaries (3 snapshots, latest commit 0cbef79)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 0cbef79)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0

Incremental Changes

Since the previous review, 1 commit was added:

  • Extract first element from job_id array (jobRes.job_id[0]) to fix a 422 Pydantic validation error. No safety guard was added for this array access.

Resolved Since Last Review

File Previous Line Issue Resolution
apps/backend/app/base.py / config.py / main.py N/A Overly permissive CORS regex allows any Chrome extension CORS regex is now opt-in via EXTENSION_CORS_ORIGIN env var (defaults to None); only applied when explicitly set
apps/chrome-extension/popup/popup.js 229 No timeout on fetch requests All fetches wrapped with fetchWithTimeout() (120s AbortController)
apps/chrome-extension/popup/popup.js 256 No file type validation before upload apiUploadFile() validates against ALLOWED_TYPES allowlist
apps/chrome-extension/popup/popup.js 86 No content size limit JD truncated to MAX_JD_CHARS = 8000 before upload
apps/chrome-extension/popup/popup.js 423 Silent error catch in init() Now uses console.warn + distinguishes expected vs unexpected errors
apps/chrome-extension/popup/popup.js 382 btnTailorAgain stale job data Now re-extracts job from active tab
apps/chrome-extension/popup/popup.js 387 Silent error catch in btnTailorAgain Now logs via console.warn
Issue Details (click to expand)

WARNING

File Line Issue
apps/chrome-extension/popup/popup.js 352 No safety guard on jobRes.job_id[0] — empty/null array access could cause TypeError or confusing 422 error
Files Reviewed (6 files)
  • apps/backend/app/config.py - No issues in incremental change
  • apps/backend/app/main.py - No issues in incremental change
  • apps/chrome-extension/manifest.json - No issues in incremental change
  • apps/chrome-extension/popup/popup.css - No issues in incremental change
  • apps/chrome-extension/popup/popup.html - No issues in incremental change
  • apps/chrome-extension/popup/popup.js - 1 WARNING (new issue)

Fix these issues in Kilo Cloud

Previous review (commit 78c8c1a)

Status: 7 Issues Found (1 CRITICAL, 4 WARNING, 2 SUGGESTION) | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 4
SUGGESTION 2
Issue Details (click to expand)

CRITICAL

File Line Issue
apps/backend/app/base.py 49 Overly permissive CORS regex chrome-extension://.* allows ANY Chrome extension to access the backend with credentials

WARNING

File Line Issue
apps/chrome-extension/popup/popup.js 229 No timeout on fetch requests - backend hangs will freeze UI indefinitely (timeout constant exists but not applied to all calls)
apps/chrome-extension/popup/popup.js 256 No file type or size validation before upload - accept attribute is UI-only
apps/chrome-extension/popup/popup.js 355 Partial failure state - job created but improve fails leaves dangling data
apps/chrome-extension/popup/popup.js 86 No content size limit - very long job descriptions could exceed backend limits

SUGGESTION

File Line Issue
apps/chrome-extension/popup/popup.js 382 btnTailorAgain doesn't clear state.jobData on failure - stale job info may display
apps/chrome-extension/popup/popup.js 398 Re-extraction failure provides console-only feedback - no user-visible error when job re-extraction fails
Resolved Since Last Review
File Previous Line Issue Resolution
apps/chrome-extension/popup/popup.js 423 Silent error catch in init() - extraction failures invisible Fixed: init() now uses console.warn to log extraction errors
Other Observations (not in diff)
File Line Issue
apps/chrome-extension/popup/popup.js 377 btnOpenApp hardcodes port :3000 for frontend URL - will break if frontend runs on different port or in production
apps/chrome-extension/manifest.json 7-9 Host permissions limited to localhost only - no production host permissions configured
apps/chrome-extension/popup/popup.html 52 No loading overlay element in HTML - created dynamically in JS, could cause flicker
apps/chrome-extension/popup/popup.js 100-160 Job extraction selectors are brittle - will break with any platform UI redesign
Files Reviewed (5 files)
  • apps/backend/app/base.py - 1 CRITICAL issue (CORS regex)
  • apps/chrome-extension/manifest.json - 1 observation (localhost-only hosts)
  • apps/chrome-extension/popup/popup.html - 1 observation (loading overlay)
  • apps/chrome-extension/popup/popup.css - No issues
  • apps/chrome-extension/popup/popup.js - 6 issues (timeouts, validation, partial failure, stale state, user feedback, content size)

Incremental Changes

Since the last review, 1 commit was added:

  • Added console.warn logging to btnTailorAgain re-extraction error handler (previously silent catch). This resolves the 'Silent error catch in init()' suggestion but still lacks user-visible feedback. A new suggestion was added for the missing user-facing error message.

Fix these issues in Kilo Cloud

Previous review (commit ee9b8ec)

Status: 7 Issues Found (1 CRITICAL, 4 WARNING, 2 SUGGESTION) | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 4
SUGGESTION 2
Issue Details (click to expand)

CRITICAL

File Line Issue
apps/backend/app/base.py 49 Overly permissive CORS regex chrome-extension://.* allows ANY Chrome extension to access the backend with credentials

WARNING

File Line Issue
apps/chrome-extension/popup/popup.js 213 No timeout on fetch requests - backend hangs will freeze UI indefinitely
apps/chrome-extension/popup/popup.js 241 No file type or size validation before upload - accept attribute is UI-only
apps/chrome-extension/popup/popup.js 317 Partial failure state - job created but improve fails leaves dangling data
apps/chrome-extension/popup/popup.js 86 No content size limit - very long job descriptions could exceed backend limits

SUGGESTION

File Line Issue
apps/chrome-extension/popup/popup.js 341 btnTailorAgain doesn't clear state.jobData - stale job info may display
apps/chrome-extension/popup/popup.js 368 Silent error catch in init() - extraction failures are invisible
Other Observations (not in diff)

Issues found in unchanged code or structural concerns that cannot receive inline comments:

File Line Issue
apps/chrome-extension/popup/popup.js 350 btnOpenApp hardcodes port :3000 for frontend URL - will break if frontend runs on different port or in production
apps/chrome-extension/manifest.json 7-9 Host permissions limited to localhost only - no production host permissions configured
apps/chrome-extension/popup/popup.html 52 No loading overlay element in HTML - it's created dynamically in JS, which could cause flicker
apps/chrome-extension/popup/popup.js 100-160 Job extraction selectors are brittle and will break with any platform UI redesign - no fallback retry mechanism
apps/chrome-extension/popup/popup.js 100-160 getText() uses innerText which triggers reflow and may miss content in shadow DOMs or lazy-loaded sections
Files Reviewed (5 files)
  • apps/backend/app/base.py - 1 CRITICAL issue (CORS regex)
  • apps/chrome-extension/manifest.json - 1 observation (localhost-only hosts)
  • apps/chrome-extension/popup/popup.html - 1 observation (loading overlay)
  • apps/chrome-extension/popup/popup.css - No issues
  • apps/chrome-extension/popup/popup.js - 6 issues (timeouts, validation, partial failure, stale state, silent errors, content size)

Reviewed by qwen3.7-plus-20260602 · Input: 4.8K · Output: 4K · Cached: 239.7K

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/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

Comment thread apps/backend/app/base.py Outdated
app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_ORIGINS,
allow_origin_regex=r"chrome-extension://.*",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 @@
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread apps/chrome-extension/popup/popup.js Outdated
Comment thread apps/chrome-extension/popup/popup.js Outdated
Comment thread apps/chrome-extension/popup/popup.js
Comment thread apps/chrome-extension/popup/popup.js Outdated
Comment thread apps/chrome-extension/popup/popup.js Outdated
@gingeekrishna gingeekrishna force-pushed the feature/chrome-extension branch 2 times, most recently from f0ad45b to 55f56f8 Compare May 31, 2026 08:24
Comment thread apps/backend/app/config.py Outdated
# 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://.*"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@gingeekrishna gingeekrishna force-pushed the feature/chrome-extension branch from 55f56f8 to 23a3df3 Compare May 31, 2026 08:27
Adds a Manifest V3 Chrome extension that detects job descriptions on job
boards and tailors the user's resume in one click, with no copy-pasting.

Extension (apps/chrome-extension/):
- manifest.json: MV3; permissions: activeTab, storage, scripting;
  host_permissions limited to http://localhost/* and http://127.0.0.1/*
- popup.html: all buttons carry type="button"; <label for> properly
  associates the Resume label with the file input for accessibility
- popup.css: flat Swiss International Style palette (#F0F0E8 canvas,
  1 px black borders, Signal Green / Alert Red / Alert Orange)
- popup.js:
  - fetchWithTimeout() wraps every fetch with AbortController (120 s)
    so LLM-backed calls don't hang the popup indefinitely; AbortError
    surfaces as a clear 'Request timed out' message
  - apiUploadFile() validates file.type against the PDF/DOCX allowlist
    before sending, enforcing what accept='.pdf,.docx' only hints at
  - Settings: backend URL is validated (must be http/https and
    localhost/127.0.0.1) matching manifest host_permissions scope
  - Job description truncated to MAX_JD_CHARS=8000 before upload
  - Partial-failure messaging: if the job upload succeeds but the
    improve call fails, the error includes the created job ID so it
    can be referenced or cleaned up manually
  - btnOpenApp derives the frontend URL from backendUrl hostname
    instead of hardcoding ':3000' on an arbitrary base
  - btnTailorAgain re-extracts the active tab instead of reusing
    stale job data; catch block logs via console.warn instead of
    swallowing the error silently
  - init() catch distinguishes expected 'Cannot access' errors (e.g.
    chrome:// pages) from unexpected ones and warns for the latter

Backend (apps/backend/):
- config.py: extension_cors_origin defaults to None; opt-in by setting
  EXTENSION_CORS_ORIGIN env var to the extension ID (or a regex for
  local dev); shared/server deployments are unaffected by default
- main.py: allow_origin_regex is only added to CORSMiddleware when
  extension_cors_origin is explicitly set and non-empty
@gingeekrishna gingeekrishna force-pushed the feature/chrome-extension branch from ee9b8ec to 3545c71 Compare June 19, 2026 16:35
@gingeekrishna

gingeekrishna commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Here is a summary of every issue raised before and how it was addressed:

CORS / Security (Copilot, kilo-code-bot, cubic P1)

  • extension_cors_origin now defaults to None in config.py — no allow_origin_regex is added unless the operator explicitly sets the EXTENSION_CORS_ORIGIN env var. Shared/server deployments are unaffected.
  • main.py gates the allow_origin_regex kwarg behind if settings.extension_cors_origin so the default is fully secure.

host_permissions vs custom backend URL (Copilot, cubic P1)

  • Settings save now validates the backend URL with new URL(), checks for http:/https: scheme, and rejects any non-localhost hostname with a clear error message. This matches the manifest's host_permissions scope and prevents silent network failures at fetch time.

Fetch timeout (kilo-code-bot, cubic P2)

  • fetchWithTimeout(url, options) wraps every fetch() with an AbortController (120 s — generous for LLM-backed calls). An AbortError surfaces as "Request timed out. The backend may be busy."; other network failures get the existing backend-unreachable message.

File type validation (kilo-code-bot, cubic P2)

  • apiUploadFile now checks file.type against ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'] before sending. Files that bypass the accept hint get a clear "Only PDF and DOCX files are supported." error.

Job description size limit (kilo-code-bot)

  • MAX_JD_CHARS = 8000 constant; the description is sliced before the /jobs/upload call.

Partial failure state (kilo-code-bot)

  • When the job upload succeeds but the improve call fails, the error message now includes the created job ID so the user can reference or clean it up manually.

btnTailorAgain stale job data / silent catch (kilo-code-bot)

  • The handler now re-extracts the active tab on click (instead of keeping stale state.jobData). The previously empty .catch(() => {}) now logs via console.warn('[Resume Matcher] Re-extraction failed: ...') so failures are visible in DevTools.

init() silent extraction catch (kilo-code-bot, cubic P2)

  • The catch distinguishes expected browser-internal-page errors ('Cannot access') from unexpected ones; unexpected errors are logged with console.warn.

btnOpenApp hardcoded port (kilo-code-bot)

  • The frontend URL is now derived from new URL(state.backendUrl).hostname plus :3000, falling back to http://localhost:3000 if the URL can't be parsed.

key attribute on DOM elements (Copilot, cubic P3)

  • chip.setAttribute('key', ...) was removed; the chips render without the React-specific attribute.

Buttons missing type="button" (Copilot)

  • All buttons in popup.html carry type="button" explicitly.

Label not associated with file input (Copilot)

  • <label for="input-resume-file">Resume</label> is present and correctly linked to <input id="input-resume-file">.

Commit history

  • Squashed to a single clean commit (3545c71) authored by Radhakrishnan Pachyappan <gingeekrishna@gmail.com>.

updateTailorBtn();
})
.catch((e) => {
console.warn('[Resume Matcher] Re-extraction failed:', e.message);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Re-extraction failure still provides no user-visible feedback

The console.warn is a good improvement over the previous silent catch, but when re-extraction fails the user sees the main panel with stale job data and no indication anything went wrong. Consider adding a showMsg call so the user knows their job data may be outdated:

.catch((e) => {
  console.warn('[Resume Matcher] Re-extraction failed:', e.message);
  showMsg(msgMain, 'Could not re-extract job details. Showing previous data.', 'error');
});

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

updateTailorBtn();
})
.catch((e) => {
console.warn('[Resume Matcher] Re-extraction failed:', e.message);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Re-extraction failure still provides no user-visible feedback

The console.warn is a good improvement over the previous silent catch, but when re-extraction fails the user sees the main panel with stale job data and no indication anything went wrong. Consider adding a showMsg call so the user knows their job data may be outdated:

.catch((e) => {
  console.warn('[Resume Matcher] Re-extraction failed:', e.message);
  showMsg(msgMain, 'Could not re-extract job details. Showing previous data.', 'error');
});

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread apps/chrome-extension/popup/popup.js Outdated
Comment thread apps/chrome-extension/popup/popup.js Outdated
Comment thread apps/chrome-extension/popup/popup.js Outdated
…smatch

- POST /api/v1/jobs/upload returns job_id as list[str]; extract [0]
  before passing to improve endpoint which expects a single string
- Add guard before index: throw if backend returns empty/null job_id
- Show user-visible error via showMsg when btnTailorAgain re-extraction
  fails (previously only logged to console.warn)
@gingeekrishna gingeekrishna force-pushed the feature/chrome-extension branch from 0cbef79 to eed518a Compare July 1, 2026 11:04
@srbhr

srbhr commented Jul 2, 2026

Copy link
Copy Markdown
Owner

@gingeekrishna I think this project can go into the Resume Matcher organization's projects.

@gingeekrishna

gingeekrishna commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

That's exciting to hear, @srbhr! Happy to help with whatever form works best for the org.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Chrome extension for tailoring and uploading resume

3 participants