Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ jobs:
with:
node-version: '20'
cache: 'npm'
# `npm install` (not `npm ci`) so the lockfile can pick up the newly added
# dev tooling without a separate commit.
- run: npm install
# Lockfile is in sync, so `npm ci` enforces it (reproducible installs).
- run: npm ci
- name: Unit tests
run: npm test
- name: Lint (advisory)
- name: Lint
run: npm run lint
continue-on-error: true
- name: Format check (advisory)
run: npm run format:check
continue-on-error: true
- name: Dependency audit (advisory)
run: npm audit --audit-level=high
continue-on-error: true
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,43 @@ This project follows [Semantic Versioning](https://semver.org/) and groups chang
by theme. Dates are when the release landed on `main` — 1.1.0 through 1.6.0 shipped
the same day, as a rapid burst of improvements, so they share a date.

## [3.0.1] — 2026-06-15 · _Audit hardening_

A focused correctness, security, and tooling pass from a full code audit — no
behavioural changes to features, just fixes and guardrails.

### Fixed

- **Batch scanner XSS.** The Batch view rendered provider error messages and the
URLs you paste straight into the DOM; both are now HTML-escaped like everywhere
else in the app.
- **"Compare" modal crash.** The multi-repo compare table threw a `ReferenceError`
on the _Fit delta_ cell whenever a compared repo had a fit change — a constant was
scoped to the wrong function. Hoisted to one shared definition.
- **Drift alert never fired.** The daily "repos went stale" check read a field
(`savedAt`) the store never writes (`saved_at`), so the count was always zero. The
field names now match and stale repos surface again.
- **Reduced-motion leaks.** The Batch and Stack loading dots kept pulsing for users
who asked for reduced motion; both now honour `prefers-reduced-motion`.
- **Light-theme contrast.** Faint label text on the light themes (paper, cream,
apple, latte, solarized) now clears WCAG AA.

### Changed

- **One version of the truth.** `package.json` and the manifest now agree (3.0.1),
resolving the long-standing drift between them.
- **Explicit Content-Security-Policy** in the manifest (matches the MV3 default, now
auditable).
- **Stronger CI.** Reproducible installs via `npm ci`, lint promoted to a blocking
gate (it was advisory), and a dependency-audit step added.
- The shared `esc()` helper now escapes single quotes too, matching the canonical
`safe-html` escaper.

### Notes

- Still 100% client-side — fixes and hardening only, no new permissions and no new
data collected.

## [1.7.0] — 2026-06-13 · _Boards, Vee, and a motion pass_

### Added
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
![Zero build](https://img.shields.io/badge/build-none-0e1722)
![Vanilla ES modules](https://img.shields.io/badge/vanilla-ES_modules-f7df1e?logo=javascript&logoColor=black)
![Tests](https://img.shields.io/badge/tests-730%2B_passing-2f7d34)
![Version](https://img.shields.io/badge/version-3.0.0-c2691c)
![Version](https://img.shields.io/badge/version-3.0.1-c2691c)
![Storage](https://img.shields.io/badge/storage-in--browser_IndexedDB-38bdf8)

</div>
Expand Down Expand Up @@ -46,6 +46,15 @@ Plus **SKTPG** (a one-tap State / Known-pitfalls / Trajectory / Proof / Growth r

Newest first — the highlights. Full, detailed notes live in the **[changelog](CHANGELOG.md)**.

### v3.0.1 — Audit hardening

A correctness, security, and tooling pass from a full code audit — fixes only, no feature changes.

- 🔒 **Hardened.** Batch-scan output is now HTML-escaped (XSS), the manifest declares an explicit CSP, and the shared escaper covers single quotes too.
- 🐞 **Squashed.** Fixed a "Compare" modal crash, made the daily **stale-repos drift alert** actually fire (it was reading the wrong field), and stopped two loaders from pulsing under reduced-motion.
- ♿ **Contrast.** Faint label text on the light themes now meets WCAG AA.
- 🧰 **Tooling.** Reconciled the version across manifest / package.json, and switched CI to `npm ci` with a blocking lint gate + a dependency audit. 733 tests green.

### v1.7.0 — Boards, Vee & a motion pass

- 🗂️ **Collections ("Boards").** Group the repos you're evaluating together and filter the Library by board — with live counts, per-card membership dots, and a one-click assignment popover. Boards travel in your library export/import.
Expand Down
16 changes: 8 additions & 8 deletions background.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
try {
const points = await scrollPoints({ limit: 2000 });
const STALE_MS = 14 * 24 * 60 * 60 * 1000;
const staleCount = points.filter(p => p.payload?.savedAt && (Date.now() - Date.parse(p.payload.savedAt)) > STALE_MS).length;
const staleCount = points.filter(p => p.payload?.saved_at && (Date.now() - Date.parse(p.payload.saved_at)) > STALE_MS).length;
await chrome.storage.local.set({ repolens_drift: { staleCount, checkedAt: new Date().toISOString() } });
} catch { /* offline or IDB unavailable */ }
});
Expand Down Expand Up @@ -200,7 +200,8 @@
.then(() => {
sendResponse({ ok: true });
runAnalysis(msg.sessionKey, detected); // fire and forget; tab polls the session
});
})
.catch((err) => sendResponse({ ok: false, error: err?.message || 'Could not start the scan' }));
return true; // keep the message channel open for the async sendResponse
}

Expand Down Expand Up @@ -260,7 +261,7 @@
if (msg.type === 'PIN_IDEA' && msg.sessionKey && msg.idea && Array.isArray(msg.idea.sources)) {
sendResponse({ ok: true });
(async () => {
const cur = (await chrome.storage.session.get(msg.sessionKey))[msg.sessionKey] || {};

Check warning on line 264 in background.js

View workflow job for this annotation

GitHub Actions / test

'cur' is assigned a value but never used. Allowed unused vars must match /^_/u
await pinIdea({ ...msg.idea, createdIso: new Date().toISOString() });
})();
return true;
Expand Down Expand Up @@ -318,7 +319,7 @@
try {
const persisted = await chrome.storage.local.get(`repolens_ask_${cur.repoId}`);
sessionHistory = persisted[`repolens_ask_${cur.repoId}`] || [];
} catch (_) {}

Check warning on line 322 in background.js

View workflow job for this annotation

GitHub Actions / test

'_' is defined but never used
}
const history = sessionHistory.slice(-4); // keep last 4 completed pairs for AI context
await setAsk({ pending: { status: 'thinking', question: msg.question }, history });
Expand Down Expand Up @@ -444,8 +445,6 @@
async function handleOpenAIOAuthCallback(rawUrl, tabId) {
if (!rawUrl || !isOpenAIOAuthCallbackUrl(rawUrl)) return;

console.log('[RepoLens OAuth] OpenAI callback detected:', rawUrl.split('?')[0]); // strip ?code=…

let url;
try {
url = new URL(rawUrl);
Expand All @@ -467,7 +466,7 @@

if (error) {
const msg = errorDesc || error;
console.warn('[RepoLens OAuth] OpenAI provider returned error:', msg);

Check warning on line 469 in background.js

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement
await chrome.storage.local.set({ [OPENAI_OAUTH_ERROR_KEY]: `ChatGPT sign-in error: ${msg}` });
await cleanupFlowMarkers();
if (tabId) chrome.tabs.remove(tabId).catch(() => {});
Expand All @@ -484,7 +483,7 @@
const storedState = stored[OPENAI_OAUTH_STATE_KEY];

if (!verifier) {
console.warn('[RepoLens OAuth] No stored OpenAI verifier — flow interrupted or for another extension');

Check warning on line 486 in background.js

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement
await cleanupFlowMarkers(); // clear any stale state marker from an interrupted flow
if (tabId) chrome.tabs.remove(tabId).catch(() => {});
return;
Expand All @@ -495,11 +494,10 @@
// Mint a usable API key so scans run through the ordinary OpenAI engine.
const apiKey = await mintOpenAIApiKey(creds.id_token);
await chrome.storage.local.set({ openaiKey: apiKey });
console.log('[RepoLens OAuth] OpenAI success — signed in via ChatGPT');
await cleanupFlowMarkers();
if (tabId) chrome.tabs.remove(tabId).catch(() => {});
} catch (err) {
console.error('[RepoLens OAuth] OpenAI exchange error:', err.message);

Check warning on line 500 in background.js

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement
// No usable key ⇒ don't leave half-finished OAuth state that reads as "connected".
await clearOpenAICredentials().catch(() => {});
await chrome.storage.local.set({ [OPENAI_OAUTH_ERROR_KEY]: err.message });
Expand Down Expand Up @@ -1133,13 +1131,15 @@
const cur = (await chrome.storage.session.get(sessionKey))[sessionKey] || {};
await setC({ status: 'running', mode, wildness, results: [] });

const rows = await scrollLibrary();
let rows = await scrollLibrary();
if (mode === 'repo') {
// Repo-anchored: ensure the current repo (the seed) is represented with its capabilities.
const seedCaps = (Array.isArray(cur.capabilities) && cur.capabilities.length) ? cur.capabilities : deriveCapabilities(cur);
const seedRow = { repoId: detected.repoId, name: detected.repoId.split('/').pop() || detected.repoId, capabilities: seedCaps, eli5: cur.eli5 || '' };
const existing = rows.find(r => r.repoId === detected.repoId);
if (existing) existing.capabilities = seedRow.capabilities; else rows.push(seedRow);
// Immutable: rebuild rather than mutate the objects scrollLibrary returned.
rows = rows.some(r => r.repoId === detected.repoId)
? rows.map(r => (r.repoId === detected.repoId ? { ...r, capabilities: seedRow.capabilities } : r))
: [...rows, seedRow];
}

// Library/studio mode mines the whole library seed-free; pairs only, to bound the candidate count.
Expand Down Expand Up @@ -1572,7 +1572,7 @@
throw new Error('xAI returned no text content');
}
const err = await res.json().catch(() => ({}));
console.warn('[RepoLens xAI]', endpoint, res.status, JSON.stringify(err));

Check warning on line 1575 in background.js

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement
lastErr = err.error?.message || ('xAI API error ' + res.status + ' at ' + endpoint);
if (res.status === 401 && isOAuth) {
await chrome.storage.local.remove(['xaiKey', 'xaiRefresh', 'xaiExpiry', 'xaiCredentials']);
Expand Down
1 change: 1 addition & 0 deletions batch.html
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
/* Row loading dot */
.row-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); flex-shrink: 0; animation: dot-pulse 1.2s ease-in-out infinite; }
@keyframes dot-pulse { 0%,100%{opacity:.4;transform:scale(.9)} 50%{opacity:1;transform:scale(1.1)} }
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; } }

/* Done summary */
#done-bar { display: none; background: var(--ok-bg); border: 1px solid var(--ok-edge); border-radius: 10px; padding: 14px 18px; margin-top: 20px; font: 600 13px var(--font); color: var(--ok-ink); display: none; align-items: center; gap: 14px; }
Expand Down
5 changes: 3 additions & 2 deletions batch.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Batch Scan page — paste URLs, watch them scan one by one.
import { initTheme } from './theme.js';
import { esc } from './format.js';

initTheme();

Expand Down Expand Up @@ -69,11 +70,11 @@ function rowHtml(item, idx) {
: `<span class="row-icon">${icon}</span>`;
const label = STATUS_LABEL[item.status] ?? item.status;
const fitHtml = item.fit ? `<span class="row-fit fit-${item.fit}">${FIT_LABELS[item.fit] ?? item.fit}</span>` : '';
const errHtml = item.error ? `<span class="row-status" style="color:var(--bad-ink)">${item.error.slice(0, 60)}</span>` : '';
const errHtml = item.error ? `<span class="row-status" style="color:var(--bad-ink)">${esc(item.error.slice(0, 60))}</span>` : '';
const displayId = item.repoId || item.url?.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '') || `#${idx + 1}`;
return `<div class="batch-row ${item.status}" data-idx="${idx}">
${iconHtml}
<span class="row-id" title="${displayId}">${displayId}</span>
<span class="row-id" title="${esc(displayId)}">${esc(displayId)}</span>
${fitHtml || errHtml || `<span class="row-status">${label}</span>`}
</div>`;
}
Expand Down
Loading
Loading