Skip to content

Commit 36eb03b

Browse files
Merge pull request #28 from New1Direction/redesign/bright-inspector
feat(site): bright "Inspector" website redesign + v3.0.1 audit hardening
2 parents 499881e + d07e520 commit 36eb03b

17 files changed

Lines changed: 2845 additions & 35 deletions

.github/workflows/ci.yml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ jobs:
1414
with:
1515
node-version: '20'
1616
cache: 'npm'
17-
# `npm install` (not `npm ci`) so the lockfile can pick up the newly added
18-
# dev tooling without a separate commit.
19-
- run: npm install
17+
# Lockfile is in sync, so `npm ci` enforces it (reproducible installs).
18+
- run: npm ci
2019
- name: Unit tests
2120
run: npm test
22-
- name: Lint (advisory)
21+
- name: Lint
2322
run: npm run lint
24-
continue-on-error: true
2523
- name: Format check (advisory)
2624
run: npm run format:check
2725
continue-on-error: true
26+
- name: Dependency audit (advisory)
27+
run: npm audit --audit-level=high
28+
continue-on-error: true

CHANGELOG.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,43 @@ This project follows [Semantic Versioning](https://semver.org/) and groups chang
77
by theme. Dates are when the release landed on `main` — 1.1.0 through 1.6.0 shipped
88
the same day, as a rapid burst of improvements, so they share a date.
99

10+
## [3.0.1] — 2026-06-15 · _Audit hardening_
11+
12+
A focused correctness, security, and tooling pass from a full code audit — no
13+
behavioural changes to features, just fixes and guardrails.
14+
15+
### Fixed
16+
17+
- **Batch scanner XSS.** The Batch view rendered provider error messages and the
18+
URLs you paste straight into the DOM; both are now HTML-escaped like everywhere
19+
else in the app.
20+
- **"Compare" modal crash.** The multi-repo compare table threw a `ReferenceError`
21+
on the _Fit delta_ cell whenever a compared repo had a fit change — a constant was
22+
scoped to the wrong function. Hoisted to one shared definition.
23+
- **Drift alert never fired.** The daily "repos went stale" check read a field
24+
(`savedAt`) the store never writes (`saved_at`), so the count was always zero. The
25+
field names now match and stale repos surface again.
26+
- **Reduced-motion leaks.** The Batch and Stack loading dots kept pulsing for users
27+
who asked for reduced motion; both now honour `prefers-reduced-motion`.
28+
- **Light-theme contrast.** Faint label text on the light themes (paper, cream,
29+
apple, latte, solarized) now clears WCAG AA.
30+
31+
### Changed
32+
33+
- **One version of the truth.** `package.json` and the manifest now agree (3.0.1),
34+
resolving the long-standing drift between them.
35+
- **Explicit Content-Security-Policy** in the manifest (matches the MV3 default, now
36+
auditable).
37+
- **Stronger CI.** Reproducible installs via `npm ci`, lint promoted to a blocking
38+
gate (it was advisory), and a dependency-audit step added.
39+
- The shared `esc()` helper now escapes single quotes too, matching the canonical
40+
`safe-html` escaper.
41+
42+
### Notes
43+
44+
- Still 100% client-side — fixes and hardening only, no new permissions and no new
45+
data collected.
46+
1047
## [1.7.0] — 2026-06-13 · _Boards, Vee, and a motion pass_
1148

1249
### Added

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
![Zero build](https://img.shields.io/badge/build-none-0e1722)
1111
![Vanilla ES modules](https://img.shields.io/badge/vanilla-ES_modules-f7df1e?logo=javascript&logoColor=black)
1212
![Tests](https://img.shields.io/badge/tests-730%2B_passing-2f7d34)
13-
![Version](https://img.shields.io/badge/version-3.0.0-c2691c)
13+
![Version](https://img.shields.io/badge/version-3.0.1-c2691c)
1414
![Storage](https://img.shields.io/badge/storage-in--browser_IndexedDB-38bdf8)
1515

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

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

49+
### v3.0.1 — Audit hardening
50+
51+
A correctness, security, and tooling pass from a full code audit — fixes only, no feature changes.
52+
53+
- 🔒 **Hardened.** Batch-scan output is now HTML-escaped (XSS), the manifest declares an explicit CSP, and the shared escaper covers single quotes too.
54+
- 🐞 **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.
55+
-**Contrast.** Faint label text on the light themes now meets WCAG AA.
56+
- 🧰 **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.
57+
4958
### v1.7.0 — Boards, Vee & a motion pass
5059

5160
- 🗂️ **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.

background.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ chrome.alarms.onAlarm.addListener(async (alarm) => {
152152
try {
153153
const points = await scrollPoints({ limit: 2000 });
154154
const STALE_MS = 14 * 24 * 60 * 60 * 1000;
155-
const staleCount = points.filter(p => p.payload?.savedAt && (Date.now() - Date.parse(p.payload.savedAt)) > STALE_MS).length;
155+
const staleCount = points.filter(p => p.payload?.saved_at && (Date.now() - Date.parse(p.payload.saved_at)) > STALE_MS).length;
156156
await chrome.storage.local.set({ repolens_drift: { staleCount, checkedAt: new Date().toISOString() } });
157157
} catch { /* offline or IDB unavailable */ }
158158
});
@@ -200,7 +200,8 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
200200
.then(() => {
201201
sendResponse({ ok: true });
202202
runAnalysis(msg.sessionKey, detected); // fire and forget; tab polls the session
203-
});
203+
})
204+
.catch((err) => sendResponse({ ok: false, error: err?.message || 'Could not start the scan' }));
204205
return true; // keep the message channel open for the async sendResponse
205206
}
206207

@@ -444,8 +445,6 @@ const _handledOAuthCodes = new Set();
444445
async function handleOpenAIOAuthCallback(rawUrl, tabId) {
445446
if (!rawUrl || !isOpenAIOAuthCallbackUrl(rawUrl)) return;
446447

447-
console.log('[RepoLens OAuth] OpenAI callback detected:', rawUrl.split('?')[0]); // strip ?code=…
448-
449448
let url;
450449
try {
451450
url = new URL(rawUrl);
@@ -495,7 +494,6 @@ async function handleOpenAIOAuthCallback(rawUrl, tabId) {
495494
// Mint a usable API key so scans run through the ordinary OpenAI engine.
496495
const apiKey = await mintOpenAIApiKey(creds.id_token);
497496
await chrome.storage.local.set({ openaiKey: apiKey });
498-
console.log('[RepoLens OAuth] OpenAI success — signed in via ChatGPT');
499497
await cleanupFlowMarkers();
500498
if (tabId) chrome.tabs.remove(tabId).catch(() => {});
501499
} catch (err) {
@@ -1133,13 +1131,15 @@ async function runCombinator(sessionKey, detected, { mode = 'repo', wildness = 0
11331131
const cur = (await chrome.storage.session.get(sessionKey))[sessionKey] || {};
11341132
await setC({ status: 'running', mode, wildness, results: [] });
11351133

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

11451145
// Library/studio mode mines the whole library seed-free; pairs only, to bound the candidate count.

batch.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
/* Row loading dot */
8888
.row-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); flex-shrink: 0; animation: dot-pulse 1.2s ease-in-out infinite; }
8989
@keyframes dot-pulse { 0%,100%{opacity:.4;transform:scale(.9)} 50%{opacity:1;transform:scale(1.1)} }
90+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; } }
9091

9192
/* Done summary */
9293
#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; }

batch.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Batch Scan page — paste URLs, watch them scan one by one.
22
import { initTheme } from './theme.js';
3+
import { esc } from './format.js';
34

45
initTheme();
56

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

0 commit comments

Comments
 (0)