From f87e6809cb352168d25b195a00fb04c476eb4561 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 2 Jun 2026 20:50:26 +0300 Subject: [PATCH 1/9] fix(extension): unify JWT decode into single decodeJwtPayload helper --- extension/src/background/index.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/extension/src/background/index.ts b/extension/src/background/index.ts index 941f714..8cc5070 100644 --- a/extension/src/background/index.ts +++ b/extension/src/background/index.ts @@ -48,17 +48,20 @@ function isExpired(expiresAt: number): boolean { return Date.now() >= expiresAt - EXPIRY_BUFFER_MS; } -function decodeEmail(jwt: string): string | null { +function decodeJwtPayload(jwt: string): { email?: string; exp?: number } | null { try { const payloadB64 = jwt.split(".")[1]; if (!payloadB64) return null; - const payload = JSON.parse(atob(payloadB64)) as { email?: string }; - return payload.email ?? null; + return JSON.parse(atob(payloadB64)) as { email?: string; exp?: number }; } catch { return null; } } +function decodeEmail(jwt: string): string | null { + return decodeJwtPayload(jwt)?.email ?? null; +} + // ─── Google OAuth ────────────────────────────────────────────────────────────── async function launchGoogleOAuth(): Promise { @@ -101,9 +104,9 @@ async function exchangeForJWT(googleIdToken: string): Promise { } const { token } = await res.json() as { token: string }; - const payloadB64 = token.split(".")[1] ?? ""; - const { exp } = JSON.parse(atob(payloadB64)) as { exp: number }; - return { jwt: token, jwtExpiresAt: exp * 1000 }; + const payload = decodeJwtPayload(token); + if (!payload?.exp) throw new Error("JWT missing exp claim"); + return { jwt: token, jwtExpiresAt: payload.exp * 1000 }; } // ─── Core ────────────────────────────────────────────────────────────────────── From a61ad894ae03b2564cb84c5d3ba27df062dbb2eb Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 2 Jun 2026 20:50:45 +0300 Subject: [PATCH 2/9] fix(extension): log ensureDbSession failure instead of silent void --- extension/src/background/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/extension/src/background/index.ts b/extension/src/background/index.ts index 8cc5070..a424b64 100644 --- a/extension/src/background/index.ts +++ b/extension/src/background/index.ts @@ -358,7 +358,11 @@ chrome.runtime.onMessage.addListener( session: { ...session, elapsedMs: getElapsedMs(session) }, }); // Fire-and-forget DB session creation — retried by sync alarm if it fails - if (!session.dbSessionId) void ensureDbSession(); + if (!session.dbSessionId) { + ensureDbSession().catch((err) => + console.warn("[worktrace] ensureDbSession failed:", err), + ); + } }) .catch((err: unknown) => sendResponse({ From 5cac6973b5d927804063e8db765243e9363c153e Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 2 Jun 2026 20:51:28 +0300 Subject: [PATCH 3/9] fix(extension): extract isTrackBlocked helper, remove blocklist check duplication --- extension/src/background/index.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/extension/src/background/index.ts b/extension/src/background/index.ts index a424b64..720a25a 100644 --- a/extension/src/background/index.ts +++ b/extension/src/background/index.ts @@ -166,6 +166,17 @@ async function tryEndDbSession(dbSessionId: string): Promise { } } +// ─── Music helpers ──────────────────────────────────────────────────────────── + +const TRACK_SOURCE_DOMAINS: Record = { + "youtube-music": "music.youtube.com", + "soundcloud": "soundcloud.com", +}; + +async function isTrackBlocked(track: TrackInfo): Promise { + return isDomainBlocked(`https://${TRACK_SOURCE_DOMAINS[track.source]}`); +} + // ─── Music: pull track from active tab ──────────────────────────────────────── // Returns current playbackTime from the music tab without any side effects. @@ -207,10 +218,7 @@ async function queryActiveTabForTrack(): Promise { const track = await chrome.tabs.sendMessage(tab.id, { type: "TRACK_REQUEST" }) as TrackInfo | null; if (track) { // Blocklist check before storing or persisting to DB - const sourceUrl = `https://${ - track.source === "youtube-music" ? "music.youtube.com" : "soundcloud.com" - }`; - const blocked = await isDomainBlocked(sourceUrl); + const blocked = await isTrackBlocked(track); if (blocked) return null; await chrome.storage.local.set({ currentTrack: track }); @@ -501,13 +509,8 @@ chrome.runtime.onMessage.addListener( const track = message.payload; // Background-level blocklist check: second line of defence after content script - // Derive the domain from the known source → hostname mapping - const trackSourceDomain: Record = { - "youtube-music": "music.youtube.com", - "soundcloud": "soundcloud.com", - }; void (async () => { - const blocked = await isDomainBlocked(`https://${trackSourceDomain[track.source]}`); + const blocked = await isTrackBlocked(track); if (blocked) return; void chrome.storage.local.set({ currentTrack: track }); From 71e8d74d5917cf20a7d090fa55659993ae94b515 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 2 Jun 2026 20:51:45 +0300 Subject: [PATCH 4/9] fix(extension): exhaustiveness guard on message type dispatcher --- extension/src/background/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extension/src/background/index.ts b/extension/src/background/index.ts index 720a25a..5c20838 100644 --- a/extension/src/background/index.ts +++ b/extension/src/background/index.ts @@ -630,6 +630,9 @@ chrome.runtime.onMessage.addListener( return true; } + // Exhaustiveness guard — TypeScript compile error if a new message type is added without a handler + const _exhaustive: never = message; + void _exhaustive; return false; }, ); From 5477cd7a234fd4c1cae37754a64b846335fc1d9c Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 2 Jun 2026 20:52:29 +0300 Subject: [PATCH 5/9] feat(extension): make WORKTRACE title a link to the dashboard --- extension/src/popup/index.html | 2 +- extension/src/popup/popup.css | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/extension/src/popup/index.html b/extension/src/popup/index.html index 9477633..28db512 100644 --- a/extension/src/popup/index.html +++ b/extension/src/popup/index.html @@ -14,7 +14,7 @@ diff --git a/extension/src/popup/popup.css b/extension/src/popup/popup.css index 8b438c5..16591e4 100644 --- a/extension/src/popup/popup.css +++ b/extension/src/popup/popup.css @@ -67,6 +67,7 @@ body { font-weight: 700; color: var(--c-cyan); text-shadow: 0 0 6px color-mix(in srgb, var(--c-cyan) 60%, transparent); + text-decoration: none; } .popup__user-email { From 11044376f32434cb825f1fc5778174457f80408a Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 2 Jun 2026 20:52:56 +0300 Subject: [PATCH 6/9] fix(dashboard): surface localStorage QuotaExceededError as toast in report cache --- dashboard/app/dashboard/reports/report-form.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/dashboard/app/dashboard/reports/report-form.tsx b/dashboard/app/dashboard/reports/report-form.tsx index 7641167..2c0deee 100644 --- a/dashboard/app/dashboard/reports/report-form.tsx +++ b/dashboard/app/dashboard/reports/report-form.tsx @@ -47,7 +47,14 @@ function loadCache(key: string): CachedReport | null { } function saveCache(key: string, data: CachedReport): void { - try { localStorage.setItem(key, JSON.stringify(data)); } catch { /* quota */ } + try { + localStorage.setItem(key, JSON.stringify(data)); + } catch (err) { + if (err instanceof DOMException && (err.name === "QuotaExceededError" || err.name === "NS_ERROR_DOM_QUOTA_REACHED")) { + throw err; // let caller notify the user + } + // other DOMExceptions are unexpected — swallow silently + } } function fmtCreatedAt(iso: string): string { @@ -146,7 +153,11 @@ export function ReportForm({ userEmail }: { userEmail: string }) { try { const result = await generateReport(input); const createdAt = new Date().toISOString(); - saveCache(reportCacheKey(userEmail, range, from, to), { result, createdAt }); + try { + saveCache(reportCacheKey(userEmail, range, from, to), { result, createdAt }); + } catch { + toast.error("Report generated but couldn't be cached — browser storage is full."); + } setStatus({ kind: "success", result, createdAt }); } catch (err) { const message = err instanceof Error ? err.message : "Failed to generate report"; From 3dba0eaf8e73d8967bb1b87c08b9b143e4569d60 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 2 Jun 2026 20:53:33 +0300 Subject: [PATCH 7/9] fix(dashboard): validate Groq key gsk_ prefix before save --- dashboard/app/dashboard/reports/api-key-modal.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dashboard/app/dashboard/reports/api-key-modal.tsx b/dashboard/app/dashboard/reports/api-key-modal.tsx index e50be5d..9025204 100644 --- a/dashboard/app/dashboard/reports/api-key-modal.tsx +++ b/dashboard/app/dashboard/reports/api-key-modal.tsx @@ -18,6 +18,9 @@ export function ApiKeyModal({ open, status, onClose, onSaved }: Props) { const [busy, setBusy] = useState(false); const [error, setError] = useState(null); + const trimmed = value.trim(); + const keyFormatOk = trimmed.startsWith("gsk_") && trimmed.length >= 20; + useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); @@ -105,7 +108,9 @@ export function ApiKeyModal({ open, status, onClose, onSaved }: Props) { className="mb-2 w-full rounded border border-border bg-bg px-2 py-1.5 text-xs text-text outline-none transition-colors focus:border-purple disabled:opacity-50" /> - {error ? ( + {trimmed.length > 0 && !trimmed.startsWith("gsk_") ? ( +

Key must start with gsk_

+ ) : error ? (

{error}

) : null} @@ -135,7 +140,7 @@ export function ApiKeyModal({ open, status, onClose, onSaved }: Props) {