Skip to content

fix(frontend): remediate frontend + extension audit findings#2575

Merged
rmusser01 merged 5 commits into
devfrom
feat/frontend-extension-audit-remediation
Jul 3, 2026
Merged

fix(frontend): remediate frontend + extension audit findings#2575
rmusser01 merged 5 commits into
devfrom
feat/frontend-extension-audit-remediation

Conversation

@rmusser01

@rmusser01 rmusser01 commented Jul 2, 2026

Copy link
Copy Markdown
Owner

What this is

Remediation of the 2026-07-02 frontend + extension audit (apps/FRONTEND_AUDIT.md). It fixes every Critical and High finding, plus config hardening and dead-code cleanup. Both the Next.js web UI and the WXT extension are thin shells over apps/packages/ui, so most fixes land there and benefit both.

Backlog tasks TASK-12091 … TASK-12103 track each item; per-finding status is in apps/FRONTEND_AUDIT.md §0.6.

Critical

  • Chat replies no longer corrupted. A "sanitizer" was rewriting successful non-streaming completions whose text contained "error"/"exception"/a file path into "Chat completion failed.". Removed at the real runtime source (domains/chat-rag.ts) and the base class.
  • Credentials no longer persisted to localStorage. Request-history was storing live Authorization/X-API-KEY headers and login-response tokens, and never cleared them (even on logout). Now redacted centrally and cleared on logout.

High

  • DOM-XSS via unvalidated javascript: source/citation URLs (no CSP backstop): shared safeExternalUrl/openExternalUrl guard at ~17 sinks + a CSP that drops 'unsafe-inline' (the one trusted inline script is SHA-256-hash-allowlisted) + X-Content-Type-Options/Referrer-Policy/X-Frame-Options/Permissions-Policy.
  • MV3 worker suspension orphaning ingest/quick-ingest sessions: state persisted to chrome.storage.session, polls driven by chrome.alarms, remote-job polling resumes after a restart.
  • Extension credential exfil surface: origin allowlist + sender.id guard before attaching the API key on tldw:upload/tldw:stream; guard logic consolidated into one absolute-url-guard.ts.
  • No web token refresh (only wired in the extension): web refresh wired with single-flight; FormData-retry bug fixed.
  • Chat streaming lifecycle: Stop now aborts the transport; per-call abort controllers (fixes Compare mode); ownership-guarded resets; aborts no longer saved as complete; stream_transport_interrupted surfaced in normal-mode pipelines.
  • 5s stream timeout replaying non-idempotent chat POSTs (duplicate messages): timeout is now config-derived and POSTs aren't replayed; generation request timeouts raised 10s → 120s.
  • Zustand + shims: connection-store stale-snapshot race, workspace silent-mutation hydration, folder sticky-404; storage shims get scoped clear(), memory-only session, cross-instance change propagation, dynamic-route useSearchParams.

Config / cleanup

  • version/migrate baseline on the 8 unversioned persisted stores; added a typecheck script.
  • Deleted the dead web-auth provider stack (useAuth/useConfig/useIsAdmin/Header/Layout) + their exclusive tests; documented the runtime-unused (but parity-test-maintained) extension/routes/ mirror.

Verification

New unit tests pass across all touched areas (~167 tests). Four failing tests are pre-existing and unrelated, confirmed identical on baseline dev via git stash:

  • workspace.test.ts "dispatches a quota warning event when localStorage is full"
  • react-router-shim-transition.test.tsx
  • sources-route.test.tsx (cwd artifact)
  • chatModePipeline.conversation-id.test.ts "treats empty model streams as failed"

⚠️ Before merge — please verify manually

  • CSP browser smoke. The CSP now blocks inline scripts; I could not run a browser here. Load /, /chat, /research, /media, /audio-studio, /settings, /login with the console open and confirm no CSP violations, that the theme applies without flash, and that mic features work. Pay attention to the dev/error overlays (dev-only inline scripts).

Known follow-ups (documented, not blocking)

  • TASK-12102 — removing ignoreBuildErrors / enabling TS strict is blocked on ~50 pre-existing tsc errors in unrelated code (a typecheck script was added to measure/burn down); enabling the newer react-hooks rules deferred.
  • CSP — dropping 'unsafe-eval' (WASM) needs per-feature browser verification.
  • extension/routes/ removal needs its ~22 parity tests migrated first.

🤖 Generated with Claude Code


Summary by cubic

Remediates all Critical/High items from the 2026‑07‑02 frontend + extension audit and hardens character chat and TTS/STT. Adds CSP/XSS guards, MV3 session resilience, safer networking/auth (incl. WS auth out of URLs), more robust chat/voice abort lifecycles, and small UI fixes.

  • New Features

    • Networking/auth: centralized allowlist in absolute-url-guard.ts powers both request-core and the background proxy; credentials attach only to allowlisted/same‑origin requests. Persona/voice WS auth moved out of URLs into the Sec-WebSocket-Protocol subprotocol or first auth frame; 10s connect timeouts and attempt guards prevent wedged sockets (TASK-12113).
    • Real‑time voice: barge‑in stops buffered TTS and sends a single interrupt per turn; mic→WS send honors WebSocket.bufferedAmount backpressure; voice and persona streams fail fast on handshake timeout (TASK-12109).
    • Chat streaming: each stream gets its own AbortController with ownership‑guarded release; stream_transport_interrupted is re‑emitted so truncated answers are classified correctly.
    • DOM‑XSS hardening: expanded safeExternalUrl/openExternalUrl adoption across sources, watchlists, search results, and notes; notes link sanitizer now strips control‑char‑obfuscated schemes.
    • Character cards: PNG export fetches avatars only from same‑origin/allowlisted URLs with a short timeout and credentials: "omit"; added size caps for avatar uploads (5 MB) and character imports (10 MB); favorites reconcile to the server flag via the correct React‑Query key (TASK-12110).
    • UI: chat cockpit “context restore” control is anchored to the chat content edge to avoid the app nav rail.
  • Bug Fixes

    • Character chat: a 60s stream‑inactivity watchdog is now present on both shipped character‑chat paths; a swiped‑but‑unpersisted variant no longer inherits a stale serverMessageId (TASK-12108/12110). Dexie edits/deletes now target messages by stable id (not index) when a greeting offsets arrays.
    • Storage/state: persisted quick‑ingest/auth‑replay/ingest sessions survive MV3 worker suspension via chrome.storage.session, resume on restart, and pre‑submission sessions are marked interrupted/cleaned up; background proxy messaging‑ack timeout raised to cover long generations; session‑state writes are serialized to avoid clobbering newer snapshots.
    • Mic capture/TTS: double‑start guards and MediaRecorder.onerror cleanup across useAudioRecorder, useServerDictation, and Speech Playground; streaming→buffer fallback revokes prior blob URLs; useTTS.cancel() settles in‑flight playback and revokes URLs; TtsClipsDrawer stops an existing clip before starting another; useDocumentTTS detaches listeners and revokes on error (TASK-12105/12107).
    • Requests/auth: non‑idempotent POSTs are not replayed on stream errors; generation and TTS timeouts raised to 120s (config‑overridable). Browser token refresh is single‑flighted and now throws on a failed/empty refresh so we don’t retry with a stale token (surfacing “Session expired”).
    • Stores/shims: connection health‑check guard can’t deadlock after an early throw; folder store gets a version/migrate baseline and no longer “sticky‑404” persists API unavailability across sessions; workspace hydration publishes post‑processed state through the store; Plasmo storage hook/listener races and JSON.parse errors are guarded.

Written for commit 4755c56. Summary will update on new commits.

Review in cubic

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8a05582a-2908-46fe-8968-c6914ef2a2b6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly matches the main change: audit-driven remediation across frontend and extension code.
Description check ✅ Passed The description covers summary, validation, audit items, and follow-ups, but it doesn’t fully match the template’s exact headings, especially Risk & Rollback.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/frontend-extension-audit-remediation

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request implements comprehensive fixes for several critical and high-severity issues identified in a frontend and extension audit. Key improvements include redacting sensitive credentials from request history, introducing a Content Security Policy (CSP) and sanitizing external URLs to prevent DOM-XSS, persisting MV3 background session state to survive worker suspension, fixing token refresh in the browser, resolving chat abort and stream lifecycle bugs, and addressing state clobbering and race conditions in the connection and workspace stores. Additionally, several design specs, implementation plans, and robust test suites are added. The code review feedback highlights further opportunities to harden the codebase, such as wrapping initial awaits in checkOnce to prevent deadlocks on error, utilizing the merge option in Zustand's persist middleware to avoid redundant localStorage writes, adding baseline versioning to useFolderStore, wrapping storage deserialization in a try...catch block to handle malformed values, and clearing re-armed request timeouts to prevent dangling timers.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

if (prev.isChecking || checkInFlight) {
return
}
checkInFlight = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If any of the asynchronous helper functions (such as getFirstRunCompleteFlag, getUserPersonaFlag, etc.) throw an error before the try block is entered, checkInFlight will remain true indefinitely. This will permanently deadlock all future health checks in the session. Consider wrapping these initial awaits in a try...catch block to ensure checkInFlight is safely reset to false on any failure.

Comment on lines +3982 to +3990
// Publish the post-processed hydrated state THROUGH the store so
// subscribers (already-mounted components, loading gates keyed on
// `storeHydrated`) are notified. Persist applies the raw persisted
// values via its own `set()` BEFORE this callback, but the mutations
// above (date revival, snapshot application, `storeHydrated`) happen
// afterwards and would otherwise never be broadcast. Spreading into a
// fresh object gives `set` a new reference so the update is not
// dropped as a no-op; `replace: true` matches the shape we mutated.
publishWorkspaceHydration?.({ ...state })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Mutating the state inside onRehydrateStorage and then manually invoking publishWorkspaceHydration (which calls set) causes a redundant, immediate write back to localStorage right after hydration. Instead of using onRehydrateStorage for state post-processing, it is more idiomatic and efficient to perform these mutations (such as date revival, snapshot application, and setting storeHydrated: true) inside the merge option of the persist middleware. This allows Zustand to apply the fully processed state atomically, notifying subscribers correctly without triggering an extra write cycle.

Comment on lines +854 to +862
merge: (persistedState, currentState) => {
const persisted = (persistedState ?? {}) as Partial<FolderState>
const { folderApiAvailable: _legacyFolderApiAvailable, ...rest } =
persisted
return {
...currentState,
...rest
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

While other persisted stores in this PR have been updated to include a baseline version and migrate function to prevent future schema migration issues, useFolderStore is missing these fields. Adding version: 1 and a basic migrate handler here will ensure consistency and protect against silent state discards if the folder store's schema changes in the future.

      version: 1,
      migrate: (persisted) => persisted as any,
      merge: (persistedState, currentState) => {
        const persisted = (persistedState ?? {}) as Partial<FolderState>
        const { folderApiAvailable: _legacyFolderApiAvailable, ...rest } =
          persisted
        return {
          ...currentState,
          ...rest
        }
      }

Comment on lines +109 to +118
notifyGlobal(event.key, {
oldValue:
event.oldValue == null
? undefined
: defaultSerde.deserializer(event.oldValue),
newValue:
event.newValue == null
? undefined
: defaultSerde.deserializer(event.newValue)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If an external script or another tab writes a non-JSON value to localStorage under a key that matches one of our global watchers, defaultSerde.deserializer (which calls JSON.parse) will throw a SyntaxError. Since this listener is registered globally on window, this unhandled exception could disrupt the event loop. Wrapping the deserialization in a try...catch block will make the listener more resilient to malformed or corrupted storage values.

    try {
      notifyGlobal(event.key, {
        oldValue:
          event.oldValue == null
            ? undefined
            : defaultSerde.deserializer(event.oldValue),
        newValue:
          event.newValue == null
            ? undefined
            : defaultSerde.deserializer(event.newValue)
      })
    } catch (error) {
      console.error("Failed to deserialize storage event value:", error)
    }

Comment on lines +413 to +414
if (timeoutId) clearTimeout(timeoutId)
timeoutId = setTimeout(() => controller.abort(), timeoutMs)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The re-armed timeoutId (and retryTimeoutId on line 458) are never cleared after the response body is successfully read. This leaves dangling timers in the event loop for up to 120 seconds (the default timeout for completions/RAG). While aborting a completed request's controller has no functional impact, these dangling timers can prevent Node.js processes (such as Next.js SSR or Vitest test runners) from exiting cleanly and introduce unnecessary timer overhead. Consider clearing these timeouts once the response body has been fully read.

@qodo-code-review

qodo-code-review Bot commented Jul 2, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 74 rules

Grey Divider


Action required

1. Refresh failure masked ✓ Resolved 🐞 Bug ☼ Reliability
Description
In the web/direct fallback, refreshAuthDirect() returns successfully even when /auth/refresh fails
or returns no access_token, so request-core marks refreshSucceeded=true and retries with a stale
token. This suppresses the explicit "Session expired" branch and can leave users stuck in repeated
401s without being forced to re-authenticate.
Code

apps/packages/ui/src/services/background-proxy.ts[R396-421]

+const refreshAuthDirect = async (
+  storage: DirectRuntimeStorage
+): Promise<void> => {
+  if (!webRefreshInFlight) {
+    webRefreshInFlight = (async () => {
+      const cfg =
+        ((await storage.get("tldwConfig").catch(() => null)) as
+          | Record<string, unknown>
+          | null) || null
+      const refreshToken = String((cfg?.refreshToken as string) || "").trim()
+      if (!refreshToken) return
+      const resp = await tldwRequest(
+        {
+          path: "/api/v1/auth/refresh",
+          method: "POST",
+          headers: { "Content-Type": "application/json" },
+          body: { refresh_token: refreshToken },
+          noAuth: true
+        },
+        { getConfig: () => storage.get("tldwConfig").catch(() => null) }
+      )
+      const tokens = (resp?.ok ? resp.data : null) as
+        | { access_token?: string; refresh_token?: string }
+        | null
+      if (!tokens?.access_token) return
+      const latest =
Evidence
The new refreshAuthDirect() exits early without throwing when refresh fails (or returns no access
token), but tldwRequest() treats any non-throwing refreshAuth call as success
(refreshSucceeded=true) and only emits the explicit session-expired error when refreshAuth throws.

apps/packages/ui/src/services/background-proxy.ts[396-440]
apps/packages/ui/src/services/tldw/request-core.ts[416-466]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`refreshAuthDirect()` resolves successfully when refresh fails (non-2xx) or returns no `access_token` (early `return`). `request-core.ts` treats any resolved `runtime.refreshAuth()` as a successful refresh and sets `refreshSucceeded=true`, which suppresses the explicit "Session expired" handling when the retry still returns 401.

### Issue Context
- `request-core.ts` uses a boolean `refreshSucceeded` that is only false when `refreshAuth()` throws.
- `refreshAuthDirect()` currently returns `void` on refresh failure and on missing `access_token`, so failures are indistinguishable from success.

### Fix Focus Areas
- apps/packages/ui/src/services/background-proxy.ts[396-440]
- apps/packages/ui/src/services/tldw/request-core.ts[416-466]

### Implementation notes
- Make `refreshAuthDirect()` throw when the refresh request is not ok, or when the response does not include a usable `access_token`.
- Alternatively, change the runtime contract so `refreshAuth` returns a boolean success indicator and update `request-core.ts` to set `refreshSucceeded` based on that value (but the smallest change is to throw on failure).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Bare catch hides watcher errors ✓ Resolved 📘 Rule violation ☼ Reliability
Description
A newly added bare catch {} in notifyGlobal suppresses all exceptions thrown by storage watch
callbacks without logging or rethrowing. This can hide real runtime errors and make failures hard to
detect and debug.
Code

apps/tldw-frontend/extension/shims/plasmo-storage.ts[R92-97]

+  set.forEach((cb) => {
+    try {
+      cb(change)
+    } catch {
+      // ignore watcher errors
+    }
Evidence
PR Compliance ID 224227 forbids silently swallowing exceptions; the new catch {} block ignores all
watcher callback errors without logging or re-raising.

Rule 224227: Do not silently swallow exceptions
apps/tldw-frontend/extension/shims/plasmo-storage.ts[92-97]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`notifyGlobal` uses a bare `catch {}` around watcher callbacks, silently swallowing exceptions.

## Issue Context
Compliance requires exceptions not be silently suppressed; if watcher failures are intentionally non-fatal, they should still be logged (ideally throttled) with enough context to debug.

## Fix Focus Areas
- apps/tldw-frontend/extension/shims/plasmo-storage.ts[92-97]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Date.now() used in tests ✓ Resolved 📘 Rule violation ▣ Testability
Description
New tests use real wall-clock time via Date.now() when setting up state, which makes the test
inputs non-deterministic. Use a fixed timestamp (or fake timers) to keep tests stable and
reproducible.
Code

apps/packages/ui/src/store/tests/connection.test.ts[R798-809]

+  it("does not revert a concurrent config edit when a slow health check finishes (H7)", async () => {
+    setConnectionState({
+      phase: ConnectionPhase.SEARCHING,
+      isConnected: false,
+      isChecking: false,
+      configStep: "url",
+      hasCompletedFirstRun: false,
+      userPersona: null,
+      knowledgeStatus: "ready",
+      knowledgeLastCheckedAt: Date.now(),
+      lastCheckedAt: Date.now() - 60_000,
+      consecutiveFailures: 0,
Evidence
PR Compliance ID 224239 requires tests to avoid non-deterministic sources like real time; these
tests call Date.now() directly in their setup.

Rule 224239: Tests must be deterministic and isolated from external state
apps/packages/ui/src/store/tests/connection.test.ts[798-809]
apps/packages/ui/src/store/tests/connection.test.ts[849-857]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test setup uses `Date.now()` for `knowledgeLastCheckedAt` and `lastCheckedAt`, introducing non-deterministic inputs.

## Issue Context
Compliance requires tests to be deterministic and isolated from external state like wall-clock time. Vitest supports fixed time via fake timers (`vi.useFakeTimers()` + `vi.setSystemTime(...)`) or a constant timestamp.

## Fix Focus Areas
- apps/packages/ui/src/store/__tests__/connection.test.ts[798-809]
- apps/packages/ui/src/store/__tests__/connection.test.ts[849-857]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Old history not scrubbed ✓ Resolved 🐞 Bug ⛨ Security
Description
addRequestHistory() redacts only the new item being appended, but it persists previously stored
history entries verbatim, so any credentials captured before this PR can remain in localStorage
indefinitely. This undermines the remediation goal because old Authorization/token values are still
readable until logout/401 or overwrite.
Code

apps/tldw-frontend/lib/history.ts[R94-99]

+    // Redact first so a serialization failure can never persist raw secrets.
+    const safeItem = redactHistoryItem(item);
    const raw = localStorage.getItem(KEY);
    const arr: RequestHistoryItem[] = raw ? JSON.parse(raw) : [];
-    const next = [item, ...arr].slice(0, MAX);
+    const next = [safeItem, ...arr].slice(0, MAX);
    localStorage.setItem(KEY, JSON.stringify(next));
Evidence
The new code only applies redactHistoryItem() to the newly added entry, then reuses the previously
persisted array unchanged; existing stored entries are therefore not scrubbed by this change.

apps/tldw-frontend/lib/history.ts[92-103]
apps/tldw-frontend/lib/history.ts[105-114]
apps/tldw-frontend/lib/api.ts[436-446]
apps/tldw-frontend/lib/auth.ts[204-216]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`addRequestHistory()` now redacts the *new* history item, but it prepends that item onto the previously persisted array without sanitizing existing entries. Any secrets stored in `tldw-request-history` before this change can persist across upgrades until the user logs out / hits a 401 / or the ring buffer overwrites them.

### Issue Context
- The code parses existing history from localStorage and writes it back with the new item.
- `getRequestHistory()` returns stored items as-is, so old sensitive values remain accessible.

### Fix Focus Areas
- apps/tldw-frontend/lib/history.ts[92-103]

### Implementation notes
Choose one (or combine):
- Sanitize on write: in `addRequestHistory`, transform `arr` via `arr.map(redactHistoryItem)` (or clear entries that fail validation) before writing back.
- Sanitize on read: apply redaction in `getRequestHistory()` before returning.
- One-time migration: on module init or app bootstrap, load+redact+rewrite the stored array (or clear it) so upgrades immediately remove old secrets.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread apps/tldw-frontend/extension/shims/plasmo-storage.ts
Comment thread apps/packages/ui/src/store/__tests__/connection.test.ts
Comment thread apps/packages/ui/src/services/background-proxy.ts
Comment thread apps/tldw-frontend/lib/history.ts
@rmusser01 rmusser01 force-pushed the feat/frontend-extension-audit-remediation branch from 6157b0b to 323de42 Compare July 2, 2026 21:57
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

fix(frontend): remediate frontend + extension audit findings

🐞 Bug fix ✨ Enhancement ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Fix corrupted non-streaming chat replies by removing the mistaken success-body “sanitizer”.
• Prevent credential persistence by centrally redacting request-history and clearing it on
 logout/401.
• Harden against DOM-XSS with shared URL guards and a CSP that drops 'unsafe-inline'.
• Make MV3 background sessions durable via chrome.storage.session + alarms-based polling resume.
Diagram

graph TD
    subgraph Web["Next.js Web App"]
        NC["next.config.mjs\nCSP + Sec Headers"]
        DOC["_document.tsx\nTheme Inline Script"]
        HIST["lib/history.ts\nRedaction"]
        AUTH["lib/auth.ts\nLogout Clears"]
    end

    subgraph Shared["packages/ui"]
        SEU["safe-external-url.ts\nURL Guard"]
        AUG["absolute-url-guard.ts\nOrigin Allowlist"]
        SINKS["UI URL Sinks"]
        RC["request-core.ts\nTimeout + Retry"]
        BP["background-proxy.ts\nWeb Refresh + Stream"]
        CHAT["TldwChat.ts\nAbort Lifecycle"]
        API["TldwApiClient.ts\nNo Sanitizer"]
    end

    subgraph Ext["MV3 Extension"]
        BG["background.ts\nRehydrate + Alarms"]
        BSS["background-session-store.ts\nstorage.session"]
    end

    NC --> DOC
    HIST --> AUTH
    SEU --> SINKS
    AUG --> RC
    AUG --> BP
    AUG --> BG
    BG --> BSS
    BP --> RC
    CHAT --> BP
    API --> CHAT

    subgraph Legend
      direction LR
      _f["File/Module"] ~~~ _s[("Storage")]
    end
Loading
High-Level Assessment

The chosen approach is appropriate for audit remediation: fixes are applied at the true sources (sanitizer removal where success bodies are produced; centralized history redaction before serialization; single canonical allowlist module to eliminate drift; MV3 persistence via storage.session + alarms). Nonce-based CSP and per-sink bespoke sanitization would be higher-risk and harder to maintain in this Next.js pages-router + extension split.

Files changed (109) +8496 / -1966

Enhancement (4) +502 / -0
background-session-store.tsAdd MV3 worker-survivable session store (storage.session) +276/-0

Add MV3 worker-survivable session store (storage.session)

• Introduces a durable session-state store for ingest/auth-replay/quick-ingest state with pure serialization helpers and read/write wrappers.

apps/packages/ui/src/entries/background-session-store.ts

sources.jsonAdd i18n strings for new interruption/error surfaces +47/-0

Add i18n strings for new interruption/error surfaces

• Adds user-facing strings for stream interruption handling.

apps/packages/ui/src/public/_locales/en/sources.json

absolute-url-guard.tsIntroduce canonical absolute URL allowlist + credential suppression rules +157/-0

Introduce canonical absolute URL allowlist + credential suppression rules

• Creates the single shared implementation for absolute URL allowlisting and same-origin checks; adds evaluateAbsoluteUrlAccess used by background handlers.

apps/packages/ui/src/utils/absolute-url-guard.ts

extract-token-from-chunk.tsAdd chunk token extractor utility for streaming parsing +22/-0

Add chunk token extractor utility for streaming parsing

• Introduces a small helper for extracting tokens from stream chunks in a consistent way.

apps/packages/ui/src/utils/extract-token-from-chunk.ts

Bug fix (40) +1962 / -755
MessageSource.tsxGuard source links with safeExternalUrl +6/-4

Guard source links with safeExternalUrl

• Uses 'safeExternalUrl' before rendering source anchors to block javascript:/data: URL execution.

apps/packages/ui/src/components/Common/Playground/MessageSource.tsx

useFileSearch.tsSanitize file search URLs at render/open sites +2/-1

Sanitize file search URLs at render/open sites

• Ensures file search results use the shared URL safety guard before navigation.

apps/packages/ui/src/components/Knowledge/hooks/useFileSearch.ts

useKnowledgeSearch.tsSanitize knowledge search URLs at render/open sites +2/-1

Sanitize knowledge search URLs at render/open sites

• Ensures knowledge search results use the shared URL safety guard before navigation.

apps/packages/ui/src/components/Knowledge/hooks/useKnowledgeSearch.ts

notes-manager-utils.tsFix control-character URL sanitizer bypass +10/-1

Fix control-character URL sanitizer bypass

• Strips C0 control characters and DEL before protocol checks so obfuscated javascript: schemes cannot slip through.

apps/packages/ui/src/components/Notes/notes-manager-utils.ts

ReadingItemCard.tsxGuard ReadingList card URLs with safeExternalUrl +2/-1

Guard ReadingList card URLs with safeExternalUrl

• Uses the shared URL guard when rendering/opening reading item links.

apps/packages/ui/src/components/Option/Collections/ReadingList/ReadingItemCard.tsx

ReadingItemDetail.tsxGuard ReadingList detail URLs with safeExternalUrl +2/-1

Guard ReadingList detail URLs with safeExternalUrl

• Uses the shared URL guard when rendering/opening reading item links.

apps/packages/ui/src/components/Option/Collections/ReadingList/ReadingItemDetail.tsx

ItemsWorkspace.tsxGuard item external URLs with safeExternalUrl +4/-2

Guard item external URLs with safeExternalUrl

• Applies URL validation before rendering anchors for item links.

apps/packages/ui/src/components/Option/Items/ItemsWorkspace.tsx

SourceCard.tsxReplace window.open with openExternalUrl +2/-1

Replace window.open with openExternalUrl

• Routes external navigation through 'openExternalUrl' to block dangerous schemes.

apps/packages/ui/src/components/Option/KnowledgeQA/SourceCard.tsx

SourceViewerModal.tsxGuard SourceViewerModal links with safeExternalUrl +2/-1

Guard SourceViewerModal links with safeExternalUrl

• Ensures citation/source anchors only use safe URLs.

apps/packages/ui/src/components/Option/KnowledgeQA/SourceViewerModal.tsx

index.tsxGuard processed-content external links with safeExternalUrl +8/-3

Guard processed-content external links with safeExternalUrl

• Applies shared URL guard to external link rendering in processed views.

apps/packages/ui/src/components/Option/Processed/index.tsx

index.tsxGuard research source links with safeExternalUrl +5/-3

Guard research source links with safeExternalUrl

• Applies shared URL guard to research source anchors.

apps/packages/ui/src/components/Option/ResearchWorkspace/SourcesPane/index.tsx

AlertsTab.tsxGuard watchlist alert links with safeExternalUrl +3/-2

Guard watchlist alert links with safeExternalUrl

• Applies shared URL guard to watchlist alert anchors.

apps/packages/ui/src/components/Option/Watchlists/AlertsTab/AlertsTab.tsx

ItemsTab.tsxGuard watchlist item links with safeExternalUrl +2/-1

Guard watchlist item links with safeExternalUrl

• Applies shared URL guard to watchlist item anchors.

apps/packages/ui/src/components/Option/Watchlists/ItemsTab/ItemsTab.tsx

OutputPreviewDrawer.tsxGuard output preview links with safeExternalUrl +4/-1

Guard output preview links with safeExternalUrl

• Applies shared URL guard to output preview anchors.

apps/packages/ui/src/components/Option/Watchlists/OutputsTab/OutputPreviewDrawer.tsx

ReportEvidencePanel.tsxGuard evidence/source links with safeExternalUrl +13/-6

Guard evidence/source links with safeExternalUrl

• Applies shared URL guard when rendering evidence/source anchors.

apps/packages/ui/src/components/Option/Watchlists/OutputsTab/ReportEvidencePanel.tsx

RunDetailDrawer.tsxGuard run detail links with safeExternalUrl +12/-7

Guard run detail links with safeExternalUrl

• Applies shared URL guard for external run detail links.

apps/packages/ui/src/components/Option/Watchlists/RunsTab/RunDetailDrawer.tsx

SourcesTab.tsxGuard watchlist source links with safeExternalUrl +12/-7

Guard watchlist source links with safeExternalUrl

• Applies shared URL guard to watchlist source anchors.

apps/packages/ui/src/components/Option/Watchlists/SourcesTab/SourcesTab.tsx

useRagResultsDisplay.tsxGuard RAG result links with safeExternalUrl +2/-1

Guard RAG result links with safeExternalUrl

• Ensures RAG result source URLs are sanitized before rendering/opening.

apps/packages/ui/src/components/Sidepanel/Chat/hooks/useRagResultsDisplay.tsx

background.tsPersist + rehydrate MV3 ingest sessions and enforce absolute URL guards +790/-224

Persist + rehydrate MV3 ingest sessions and enforce absolute URL guards

• Adds session persistence to storage.session with rehydration on worker restart and alarm-backed polling resume. Enforces allowlist/skip-auth decisions for absolute URLs before attaching credentials on upload/stream handlers.

apps/packages/ui/src/entries/background.ts

chatModePipeline.tsPropagate abort/interrupt handling through chat mode pipeline +163/-3

Propagate abort/interrupt handling through chat mode pipeline

• Tightens abort behavior so transport interruption and Stop signals are surfaced correctly to normal-mode pipelines.

apps/packages/ui/src/hooks/chat-modes/chatModePipeline.ts

useChatActions.tsFix abort-controller ownership and pre-stream failure cleanup +86/-43

Fix abort-controller ownership and pre-stream failure cleanup

• Adds controller ownership tracking so concurrent turns don’t clobber each other’s Stop controller; moves pre-stream awaits into try/catch so streaming state is always reset on failures.

apps/packages/ui/src/hooks/chat/useChatActions.ts

ChatTldw.tsSurface stream transport interruption and abort handling in chat model +20/-0

Surface stream transport interruption and abort handling in chat model

• Wires abort signals into the model and adds handling for transport interruptions so UI can show a distinct interrupted state.

apps/packages/ui/src/models/ChatTldw.ts

background-proxy.tsPrevent non-idempotent stream replay; add web refresh single-flight; fix FormData retry +153/-90

Prevent non-idempotent stream replay; add web refresh single-flight; fix FormData retry

• Derives stream connection timeout from config and disallows replay for non-idempotent POSTs (surfaces STREAM_INTERRUPTED instead). Adds web/direct single-flight token refresh runtime and fixes retry body serialization for FormData/Blob uploads.

apps/packages/ui/src/services/background-proxy.ts

TldwApiClient.tsRemove chat sanitizer and ensure base methods win at runtime +33/-71

Remove chat sanitizer and ensure base methods win at runtime

• Removes the suspicious-string sanitizer used on successful completions. Re-applies base implementations (createChatCompletion/synthesizeSpeech) to avoid domain mixins overwriting the fixed base behavior; increases default TTS timeout to 120s.

apps/packages/ui/src/services/tldw/TldwApiClient.ts

TldwAuth.tsSingle-flight refreshToken and initTokenRefresh on page load +40/-1

Single-flight refreshToken and initTokenRefresh on page load

• Adds refresh single-flight to avoid rotating refresh-token races and adds initTokenRefresh() to re-arm timers after reloads for web users.

apps/packages/ui/src/services/tldw/TldwAuth.ts

TldwChat.tsPer-call AbortControllers; Stop aborts transport without clobbering Compare mode +39/-13

Per-call AbortControllers; Stop aborts transport without clobbering Compare mode

• Switches from a single shared controller to per-call controllers tracked in a Set; wires caller AbortSignal to transport; cancelStream aborts all active streams.

apps/packages/ui/src/services/tldw/TldwChat.ts

chat-rag.tsDelete sanitizer in chat-rag domain implementation +9/-68

Delete sanitizer in chat-rag domain implementation

• Removes the sanitizer functions and stops wrapping successful bodies, documenting why it was harmful (success bodies were being corrupted).

apps/packages/ui/src/services/tldw/domains/chat-rag.ts

models-audio.tsAlign audio domain with new TTS timeout behavior +11/-2

Align audio domain with new TTS timeout behavior

• Adjusts audio domain behavior so long-running TTS calls use the new timeout defaults and base implementation.

apps/packages/ui/src/services/tldw/domains/models-audio.ts

request-core.tsRaise generation/RAG default timeouts; safer timeout coverage; shared allowlist guard +44/-92

Raise generation/RAG default timeouts; safer timeout coverage; shared allowlist guard

• Increases default chat/RAG request timeouts from 10s to 120s; re-arms timeout for body reads; delegates absolute URL allowlist/same-origin logic to absolute-url-guard with diagnostic hooks.

apps/packages/ui/src/services/tldw/request-core.ts

connection.tsxFix connection store race + add versioning baseline +52/-30

Fix connection store race + add versioning baseline

• Fixes stale-snapshot update races and adds version/migrate baseline for persistence.

apps/packages/ui/src/store/connection.tsx

folder.tsxFix sticky error state + add versioning baseline +21/-3

Fix sticky error state + add versioning baseline

• Fixes folder store sticky failure states and adds version/migrate baseline for persistence.

apps/packages/ui/src/store/folder.tsx

workspace.tsFix workspace hydration mutation + add versioning baseline +28/-7

Fix workspace hydration mutation + add versioning baseline

• Fixes workspace hydration updates and adds version/migrate baseline for persistence.

apps/packages/ui/src/store/workspace.ts

safe-external-url.tsIntroduce safeExternalUrl/openExternalUrl shared DOM-XSS guard +65/-0

Introduce safeExternalUrl/openExternalUrl shared DOM-XSS guard

• Adds a URL allowlist guard that strips control characters before scheme detection and allows only http/https/mailto plus relative paths; provides guarded window.open wrapper.

apps/packages/ui/src/utils/safe-external-url.ts

plasmo-storage-hook.tsxHarden plasmo-storage hook shim behavior +27/-10

Harden plasmo-storage hook shim behavior

• Improves shim behavior to match expected storage semantics and inter-instance updates.

apps/tldw-frontend/extension/shims/plasmo-storage-hook.tsx

plasmo-storage.tsFix plasmo storage shim clear/watch semantics +79/-19

Fix plasmo storage shim clear/watch semantics

• Adds scoped clear behavior and more reliable watch propagation to reduce extension state bugs.

apps/tldw-frontend/extension/shims/plasmo-storage.ts

react-router-dom.tsxFix react-router-dom shim search param handling +6/-2

Fix react-router-dom shim search param handling

• Adjusts shim so useSearchParams works correctly with dynamic routes and updates.

apps/tldw-frontend/extension/shims/react-router-dom.tsx

wxt-browser.tsHarden wxt-browser storage shim behavior +119/-31

Harden wxt-browser storage shim behavior

• Adds scoped clear(), memory-only session storage, and cross-instance change propagation for shims used in web build/tests.

apps/tldw-frontend/extension/shims/wxt-browser.ts

api.tsClear request-history on forced logout (401) +4/-1

Clear request-history on forced logout (401)

• Ensures forced-logout code path purges request-history so any captured secrets do not persist across invalidated sessions.

apps/tldw-frontend/lib/api.ts

auth.tsClear request-history on logout +4/-0

Clear request-history on logout

• Purges request-history during logout to ensure previously captured credentials cannot remain in localStorage.

apps/tldw-frontend/lib/auth.ts

history.tsRedact sensitive headers/tokens before persisting request-history +76/-1

Redact sensitive headers/tokens before persisting request-history

• Introduces centralized redaction for Authorization/X-API-KEY and access/refresh tokens; drops auth-route response bodies; ensures redaction happens before serialization.

apps/tldw-frontend/lib/history.ts

Refactor (9) +0 / -947
header-runs-gating.test.tsxRemove tests tied to deleted Header/Layout auth stack +0/-125

Remove tests tied to deleted Header/Layout auth stack

• Deletes test coverage that depended on removed dead web-auth provider UI components.

apps/tldw-frontend/tests/header-runs-gating.test.tsx

admin-maintenance.test.tsxRemove tests tied to deleted web-auth provider pages +0/-11

Remove tests tied to deleted web-auth provider pages

• Deletes tests that referenced removed dead auth stack pages/components.

apps/tldw-frontend/tests/pages/admin-maintenance.test.tsx

content-review.test.tsxRemove tests tied to deleted web-auth provider pages +0/-5

Remove tests tied to deleted web-auth provider pages

• Deletes tests that referenced removed dead auth stack pages/components.

apps/tldw-frontend/tests/pages/content-review.test.tsx

Header.tsxRemove dead Header component from web-auth provider stack +0/-116

Remove dead Header component from web-auth provider stack

• Deletes the unused Header component and related logic that was only used by the removed auth stack.

apps/tldw-frontend/components/layout/Header.tsx

Layout.tsxRemove dead Layout component from web-auth provider stack +0/-17

Remove dead Layout component from web-auth provider stack

• Deletes the unused Layout wrapper component associated with the removed auth stack.

apps/tldw-frontend/components/layout/Layout.tsx

useConfig.networking.test.tsxRemove tests tied to deleted useConfig hook +0/-224

Remove tests tied to deleted useConfig hook

• Deletes tests that were only relevant to the removed web-auth provider configuration hook.

apps/tldw-frontend/hooks/tests/useConfig.networking.test.tsx

useAuth.tsxRemove dead useAuth hook +0/-100

Remove dead useAuth hook

• Deletes unused hook from the half-wired web-auth provider stack.

apps/tldw-frontend/hooks/useAuth.tsx

useConfig.tsxRemove dead useConfig hook +0/-338

Remove dead useConfig hook

• Deletes unused hook from the half-wired web-auth provider stack.

apps/tldw-frontend/hooks/useConfig.tsx

useIsAdmin.tsRemove dead useIsAdmin hook +0/-11

Remove dead useIsAdmin hook

• Deletes unused hook from the half-wired web-auth provider stack.

apps/tldw-frontend/hooks/useIsAdmin.ts

Tests (16) +1806 / -5
notes-manager-utils.sanitize-url.test.tsAdd tests for URL sanitizer control-char bypass +30/-0

Add tests for URL sanitizer control-char bypass

• Adds tests verifying that 'java\tscript:' and newline-obfuscated schemes are neutralized through markdown rendering.

apps/packages/ui/src/components/Notes/tests/notes-manager-utils.sanitize-url.test.ts

background-session-store.test.tsAdd unit tests for MV3 session persistence serializer +191/-0

Add unit tests for MV3 session persistence serializer

• Adds tests for serialize/deserialize behavior and malformed-input tolerance for background session state.

apps/packages/ui/src/entries/tests/background-session-store.test.ts

chatModePipeline.abort-lifecycle.test.tsAdd tests for abort lifecycle and streaming completion semantics +308/-0

Add tests for abort lifecycle and streaming completion semantics

• Adds tests ensuring Stop aborts transport, compare-mode doesn’t clobber controllers, and aborted streams aren’t treated as complete.

apps/packages/ui/src/hooks/chat-modes/tests/chatModePipeline.abort-lifecycle.test.ts

ChatTldw.abort-signal.test.tsAdd tests for ChatTldw abort signal wiring +43/-0

Add tests for ChatTldw abort signal wiring

• Verifies abort signals propagate to underlying streaming transport.

apps/packages/ui/src/models/tests/ChatTldw.abort-signal.test.ts

ChatTldw.stream-transport-interrupted.test.tsAdd tests for transport interruption surfaced to callers +93/-0

Add tests for transport interruption surfaced to callers

• Verifies transport interruption becomes a surfaced error code in normal-mode pipelines.

apps/packages/ui/src/models/tests/ChatTldw.stream-transport-interrupted.test.ts

background-proxy.test.tsExtend background-proxy tests for retry/timeout safety +145/-5

Extend background-proxy tests for retry/timeout safety

• Adds coverage for updated stream fallback behavior and safety rules around retrying non-idempotent requests.

apps/packages/ui/src/services/tests/background-proxy.test.ts

background-proxy.web-refresh.test.tsAdd tests for web/direct token refresh behavior +153/-0

Add tests for web/direct token refresh behavior

• Ensures web/direct fallback path single-flights refresh and retries correctly.

apps/packages/ui/src/services/tests/background-proxy.web-refresh.test.ts

connection.test.tsAdd tests for connection store race fixes +86/-0

Add tests for connection store race fixes

• Adds regression coverage for stale-snapshot race behavior in the connection store.

apps/packages/ui/src/store/tests/connection.test.ts

folder.test.tsAdd tests for folder sticky-404 regression +142/-0

Add tests for folder sticky-404 regression

• Adds regression coverage so transient failures don’t permanently block folder navigation.

apps/packages/ui/src/store/tests/folder.test.ts

workspace.test.tsAdd tests for workspace hydration behavior +78/-0

Add tests for workspace hydration behavior

• Adds regression coverage ensuring workspace hydration triggers correct updates.

apps/packages/ui/src/store/tests/workspace.test.ts

absolute-url-guard.test.tsAdd tests for absolute URL allowlist decisions +121/-0

Add tests for absolute URL allowlist decisions

• Validates same-origin, allowlisted-cross-origin (skip auth), and blocked-cross-origin behaviors and stable error message.

apps/packages/ui/src/utils/tests/absolute-url-guard.test.ts

safe-external-url.test.tsAdd tests for safeExternalUrl scheme allowlist and obfuscation +50/-0

Add tests for safeExternalUrl scheme allowlist and obfuscation

• Covers scheme allowlist, relative URLs, javascript: rejection, and control-character obfuscation.

apps/packages/ui/src/utils/tests/safe-external-url.test.ts

plasmo-storage-watch.test.tsxAdd tests for plasmo storage shim watch propagation +76/-0

Add tests for plasmo storage shim watch propagation

• Adds regression coverage ensuring watch callbacks propagate across instances.

apps/tldw-frontend/tests/extension/plasmo-storage-watch.test.tsx

wxt-browser-storage.test.tsAdd tests for wxt-browser storage shim session/clear semantics +106/-0

Add tests for wxt-browser storage shim session/clear semantics

• Adds regression coverage for scoped clear() and memory-only session storage semantics.

apps/tldw-frontend/tests/extension/wxt-browser-storage.test.ts

react-router-search-params-dynamic.test.tsxAdd tests for dynamic-route useSearchParams shim +53/-0

Add tests for dynamic-route useSearchParams shim

• Adds regression coverage so search params update correctly with dynamic navigation in extension builds.

apps/tldw-frontend/tests/navigation/react-router-search-params-dynamic.test.tsx

history-redaction.test.tsAdd tests ensuring request-history never stores credentials +131/-0

Add tests ensuring request-history never stores credentials

• Verifies sensitive headers and tokens are redacted/dropped before persisting; ensures clearRequestHistory empties storage.

apps/tldw-frontend/lib/tests/history-redaction.test.ts

Documentation (30) +4106 / -259
2026-06-24-audio-studio-media-tickets-implementation-plan.mdDocumentation plan updates for audio-studio media ticket hardening +76/-7

Documentation plan updates for audio-studio media ticket hardening

• Updates the audio-studio media ticket implementation plan with additional hardening and alignment notes.

Docs/superpowers/plans/2026-06-24-audio-studio-media-tickets-implementation-plan.md

2026-07-01-openai-realtime-speech-endpoint-implementation-plan.mdAdd implementation plan for OpenAI-compatible realtime speech endpoint +724/-0

Add implementation plan for OpenAI-compatible realtime speech endpoint

• Introduces a detailed implementation plan document for a realtime speech compatibility endpoint.

Docs/superpowers/plans/2026-07-01-openai-realtime-speech-endpoint-implementation-plan.md

2026-07-01-visual-identity-expression-packs-implementation-plan.mdAdd implementation plan for visual identity expression packs +1073/-0

Add implementation plan for visual identity expression packs

• Introduces a detailed implementation plan for shared visual identity expression packs.

Docs/superpowers/plans/2026-07-01-visual-identity-expression-packs-implementation-plan.md

2026-06-20-research-source-discovery-chokepoint-design.mdRevise research source discovery ingest boundary spec +98/-48

Revise research source discovery ingest boundary spec

• Refines the ingest boundary design for research source discovery, updating structure and clarifying responsibilities.

Docs/superpowers/specs/2026-06-20-research-source-discovery-chokepoint-design.md

2026-06-30-skills-uat-quality-gates-design.mdAdd skills UAT quality gates design doc +196/-0

Add skills UAT quality gates design doc

• Adds a new design document describing UAT quality gates for skills.

Docs/superpowers/specs/2026-06-30-skills-uat-quality-gates-design.md

2026-07-01-openai-realtime-speech-endpoint-design.mdAdd realtime speech endpoint design spec +490/-0

Add realtime speech endpoint design spec

• Adds a new design spec defining the realtime speech compatibility endpoint behavior and constraints.

Docs/superpowers/specs/2026-07-01-openai-realtime-speech-endpoint-design.md

2026-07-01-visual-identity-expression-packs-design.mdAdd visual identity expression packs design spec +474/-0

Add visual identity expression packs design spec

• Adds a new design spec for visual identity expression packs and integration points.

Docs/superpowers/specs/2026-07-01-visual-identity-expression-packs-design.md

FRONTEND_AUDIT.mdUpdate audit findings status and remediation notes +159/-170

Update audit findings status and remediation notes

• Updates the 2026-07-02 frontend + extension audit document with per-finding remediation status, notes, and follow-ups.

apps/FRONTEND_AUDIT.md

AGENTS.mdUpdate frontend agent docs after dead-code removal +8/-17

Update frontend agent docs after dead-code removal

• Updates guidance to reflect removal of dead web-auth stack and extension route mirroring documentation.

apps/tldw-frontend/AGENTS.md

CLAUDE.mdUpdate Claude guidance after dead-code removal +8/-17

Update Claude guidance after dead-code removal

• Updates Claude usage notes consistent with the removed web-auth provider stack.

apps/tldw-frontend/CLAUDE.md

_RUNTIME_UNUSED.mdDocument extension/routes mirror is runtime-unused +16/-0

Document extension/routes mirror is runtime-unused

• Adds documentation noting extension/routes is not runtime loaded but retained for parity tests; removal tracked as follow-up.

apps/tldw-frontend/extension/routes/_RUNTIME_UNUSED.md

_document.tsxAnnotate the only trusted inline script for CSP hash allowlist +6/-0

Annotate the only trusted inline script for CSP hash allowlist

• Documents that the theme bootstrap script is the only executable inline script and must match the CSP hash in next.config.mjs.

apps/tldw-frontend/pages/_document.tsx

task-12082 - Revise-research-discovery-ingest-boundary-design-on-dev.mdAdd backlog task doc for research ingest boundary revision +52/-0

Add backlog task doc for research ingest boundary revision

• Adds a backlog tracking document describing the work item and acceptance criteria.

backlog/tasks/task-12082 - Revise-research-discovery-ingest-boundary-design-on-dev.md

task-12088 - Design-OpenAI-compatible-realtime-speech-endpoint.mdAdd backlog task doc for realtime speech endpoint design +58/-0

Add backlog task doc for realtime speech endpoint design

• Adds a backlog tracking document for the realtime speech endpoint design work.

backlog/tasks/task-12088 - Design-OpenAI-compatible-realtime-speech-endpoint.md

task-12089 - Design-shared-visual-identity-expression-packs-for-character-persona-chat.mdAdd backlog task doc for visual identity expression packs design +39/-0

Add backlog task doc for visual identity expression packs design

• Adds a backlog tracking document for expression packs design.

backlog/tasks/task-12089 - Design-shared-visual-identity-expression-packs-for-character-persona-chat.md

task-12090 - Plan-shared-visual-identity-expression-packs-implementation.mdAdd backlog task doc for expression packs implementation planning +39/-0

Add backlog task doc for expression packs implementation planning

• Adds a backlog tracking document for implementation planning.

backlog/tasks/task-12090 - Plan-shared-visual-identity-expression-packs-implementation.md

task-12091 - Fix-chat-completion-sanitizer-corrupting-successful-non-streaming-replies.mdAdd backlog task doc for chat sanitizer corruption fix +38/-0

Add backlog task doc for chat sanitizer corruption fix

• Adds a backlog tracking document for the C1 sanitizer removal work.

backlog/tasks/task-12091 - Fix-chat-completion-sanitizer-corrupting-successful-non-streaming-replies.md

task-12092 - Stop-persisting-credentials-to-localStorage-request-history.mdAdd backlog task doc for request-history credential redaction +37/-0

Add backlog task doc for request-history credential redaction

• Adds a backlog tracking document for the C2 credential persistence fix.

backlog/tasks/task-12092 - Stop-persisting-credentials-to-localStorage-request-history.md

task-12093 - Fix-javascript-URL-DOM-XSS-in-source-anchors-and-add-CSP.mdAdd backlog task doc for DOM-XSS and CSP remediation +76/-0

Add backlog task doc for DOM-XSS and CSP remediation

• Adds a backlog tracking document for H1 URL guard + CSP hardening.

backlog/tasks/task-12093 - Fix-javascript-URL-DOM-XSS-in-source-anchors-and-add-CSP.md

task-12094 - Persist-MV3-background-session-state-across-worker-suspension.mdAdd backlog task doc for MV3 worker suspension persistence +40/-0

Add backlog task doc for MV3 worker suspension persistence

• Adds a backlog tracking document for H2 background session persistence and resume logic.

backlog/tasks/task-12094 - Persist-MV3-background-session-state-across-worker-suspension.md

task-12095 - Add-URL-allowlist-before-attaching-credentials-in-extension-upload-stream-handlers.mdAdd backlog task doc for extension credential allowlist guard +38/-0

Add backlog task doc for extension credential allowlist guard

• Adds a backlog tracking document for H3 origin allowlist + sender guard before attaching credentials.

backlog/tasks/task-12095 - Add-URL-allowlist-before-attaching-credentials-in-extension-upload-stream-handlers.md

task-12096 - Wire-token-refresh-into-the-web-request-path.mdAdd backlog task doc for web token refresh wiring +39/-0

Add backlog task doc for web token refresh wiring

• Adds a backlog tracking document for H4 web/direct refresh + single-flight behavior.

backlog/tasks/task-12096 - Wire-token-refresh-into-the-web-request-path.md

task-12097 - Fix-chat-abort-and-stream-lifecycle.mdAdd backlog task doc for chat abort/stream lifecycle fixes +42/-0

Add backlog task doc for chat abort/stream lifecycle fixes

• Adds a backlog tracking document for H5 streaming lifecycle remediation.

backlog/tasks/task-12097 - Fix-chat-abort-and-stream-lifecycle.md

task-12098 - Stop-replaying-non-idempotent-chat-POST-on-stream-connection-timeout.mdAdd backlog task doc for preventing non-idempotent stream replay +36/-0

Add backlog task doc for preventing non-idempotent stream replay

• Adds a backlog tracking document for H6 stream replay prevention and timeout derivation.

backlog/tasks/task-12098 - Stop-replaying-non-idempotent-chat-POST-on-stream-connection-timeout.md

task-12099 - Fix-connection-and-workspace-store-races-and-sticky-failure-states.mdAdd backlog task doc for Zustand race fixes +39/-0

Add backlog task doc for Zustand race fixes

• Adds a backlog tracking document for H7 store race/sticky failure fixes.

backlog/tasks/task-12099 - Fix-connection-and-workspace-store-races-and-sticky-failure-states.md

task-12100 - Fix-web-build-browser-storage-shims.mdAdd backlog task doc for storage shim fixes +41/-0

Add backlog task doc for storage shim fixes

• Adds a backlog tracking document for web-build shims hardening and parity tests.

backlog/tasks/task-12100 - Fix-web-build-browser-storage-shims.md

task-12101 - Fix-default-request-timeouts-aborting-normal-LLM-generations.mdAdd backlog task doc for timeout defaults fix +38/-0

Add backlog task doc for timeout defaults fix

• Adds a backlog tracking document for raising default request timeouts to avoid aborting normal generations.

backlog/tasks/task-12101 - Fix-default-request-timeouts-aborting-normal-LLM-generations.md

task-12102 - Re-enable-frontend-type-safety-and-lint-gates.mdAdd backlog task doc for re-enabling type/lint gates +41/-0

Add backlog task doc for re-enabling type/lint gates

• Adds a backlog tracking document for future TS strict/lint gate re-enablement.

backlog/tasks/task-12102 - Re-enable-frontend-type-safety-and-lint-gates.md

task-12103 - Remove-half-wired-dead-web-auth-stack-and-dead-extension-routes-tree.mdAdd backlog task doc for dead-code removal +36/-0

Add backlog task doc for dead-code removal

• Adds a backlog tracking document describing removal of dead web-auth stack and documenting runtime-unused routes mirror.

backlog/tasks/task-12103 - Remove-half-wired-dead-web-auth-stack-and-dead-extension-routes-tree.md

task-530.14 - Implement-Skills-UAT-and-quality-gates.mdAdd backlog task doc for skills UAT quality gates implementation +49/-0

Add backlog task doc for skills UAT quality gates implementation

• Adds a backlog tracking document for implementing skills UAT and quality gates.

backlog/tasks/task-530.14 - Implement-Skills-UAT-and-quality-gates.md

Other (10) +120 / -0
acp-sessions.tsAdd persisted-store version/migrate baseline +4/-0

Add persisted-store version/migrate baseline

• Adds version/migration baseline to previously-unversioned persisted store.

apps/packages/ui/src/store/acp-sessions.ts

actor.tsxAdd persisted-store version/migrate baseline +4/-0

Add persisted-store version/migrate baseline

• Adds version/migration baseline to previously-unversioned persisted store.

apps/packages/ui/src/store/actor.tsx

feedback.tsxAdd persisted-store version/migrate baseline +4/-0

Add persisted-store version/migrate baseline

• Adds version/migration baseline to previously-unversioned persisted store.

apps/packages/ui/src/store/feedback.tsx

notes-dock.tsxAdd persisted-store version/migrate baseline +4/-0

Add persisted-store version/migrate baseline

• Adds version/migration baseline to previously-unversioned persisted store.

apps/packages/ui/src/store/notes-dock.tsx

persona-buddy-shell.tsAdd persisted-store version/migrate baseline +4/-0

Add persisted-store version/migrate baseline

• Adds version/migration baseline to previously-unversioned persisted store.

apps/packages/ui/src/store/persona-buddy-shell.ts

playground-session.tsxAdd persisted-store version/migrate baseline +4/-0

Add persisted-store version/migrate baseline

• Adds version/migration baseline to previously-unversioned persisted store.

apps/packages/ui/src/store/playground-session.tsx

quick-ingest-session.tsAdd persisted-store version/migrate baseline +4/-0

Add persisted-store version/migrate baseline

• Adds version/migration baseline to previously-unversioned persisted store.

apps/packages/ui/src/store/quick-ingest-session.ts

ui-mode.tsxAdd persisted-store version/migrate baseline +4/-0

Add persisted-store version/migrate baseline

• Adds version/migration baseline to previously-unversioned persisted store.

apps/packages/ui/src/store/ui-mode.tsx

next.config.mjsAdd CSP and security headers (nosniff, referrer-policy, etc.) +87/-0

Add CSP and security headers (nosniff, referrer-policy, etc.)

• Adds a defense-in-depth CSP that drops script 'unsafe-inline' and allowlists only the theme bootstrap inline script by SHA-256; sets additional security headers site-wide.

apps/tldw-frontend/next.config.mjs

package.jsonAdd typecheck script +1/-0

Add typecheck script

• Adds a package script to run TypeScript typechecking as part of tightening frontend quality gates.

apps/tldw-frontend/package.json

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
apps/packages/ui/src/store/connection.tsx (1)

608-615: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

checkInFlight can be left permanently true if a pre-try await rejects, wedging all future checkOnce calls.

The guard is released at every explicit exit path and after the try/catch at Line 1037, but the awaits before the try block are unprotected: the persisted-flag reads here (Lines 611–615) and ensurePlaceholderConfig() in the bypass branch (Line 671) run outside any try. If any of them rejects (e.g. a transient chrome.storage/localStorage failure), checkInFlight stays true forever and the guard at Line 605 short-circuits every subsequent checkOnce, permanently disabling connection health checks for the session.

Wrap the body in try/finally so the flag is always released, and drop the now-redundant scattered resets.

🔒️ Proposed fix (try/finally guard release)
     checkInFlight = true
-
-    // Load all persisted flags upfront
-    const persistedFirstRun = await getFirstRunCompleteFlag()
+    try {
+      // Load all persisted flags upfront
+      const persistedFirstRun = await getFirstRunCompleteFlag()
       // ... existing body, with each early `checkInFlight = false; return`
       //     reduced to a plain `return` ...
-    // Release the in-flight guard for both the normal-completion and caught-error
-    // paths (they converge here after the try/catch above).
-    checkInFlight = false
+    } finally {
+      // Always release, including if a pre-existing await above rejects.
+      checkInFlight = false
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/packages/ui/src/store/connection.tsx` around lines 608 - 615, The
`checkOnce` flow in `connection.tsx` can leave `checkInFlight` stuck true if any
await before the existing `try/catch` fails, which blocks all later checks. Wrap
the full `checkOnce` body, including the persisted-flag reads and
`ensurePlaceholderConfig()`, in a `try/finally` and release `checkInFlight` in
the `finally` block so every exit path is covered. After that, remove the
scattered manual resets that become redundant while keeping the existing guard
and exit behavior in `checkOnce` intact.
apps/packages/ui/src/hooks/chat/useChatActions.ts (1)

2971-2978: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Compare-mode cleanup clobbers a newer turn's abort controller.

Every other cleanup site in this file (characterChatMode's finally at Line 2105, the catch block at Line 2990, sendPerModelReply's finally at Line 3157) gates setIsProcessing/setStreaming/setAbortController resets behind releaseAbortControllerIfOwned(signal) to avoid clobbering a newer overlapping turn. This Compare-mode block resets all of that state — plus activeAbortControllerRef.current — unconditionally after Promise.all(comparePromises) resolves. If a newer turn (double-send, regenerate-while-streaming) has taken ownership of activeAbortControllerRef while Compare's promises were still in flight, this unconditionally nulls out the newer turn's controller and flips isProcessing/streaming off, re-enabling Send and orphaning the newer turn's Stop button — exactly the bug this ownership pattern was introduced to prevent.

🔒 Proposed fix to gate the reset on ownership
           markSteeringApplied()
           const compareResults = await Promise.all(comparePromises)
           refreshHistoryFromMessages()
-          setIsProcessing(false)
-          setStreaming(false)
-          setAbortController(null)
-          activeAbortControllerRef.current = null
+          // Only reset shared streaming state if this turn still owns the
+          // controller (mirrors the pattern used elsewhere in this file).
+          if (releaseAbortControllerIfOwned(signal)) {
+            setIsProcessing(false)
+            setStreaming(false)
+            setAbortController(null)
+          }
           return aggregateChatSubmitResults(compareResults)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/packages/ui/src/hooks/chat/useChatActions.ts` around lines 2971 - 2978,
Compare-mode cleanup is resetting shared chat state unconditionally and can
clobber a newer in-flight turn’s abort controller. Update the cleanup path after
Promise.all(comparePromises) in useChatActions to follow the same ownership
check used elsewhere in this file: gate the setIsProcessing, setStreaming,
setAbortController, and activeAbortControllerRef.current resets behind
releaseAbortControllerIfOwned(signal) so only the owning turn clears state. Keep
the aggregateChatSubmitResults(compareResults) return flow unchanged, but avoid
touching controller/state if ownership has already moved to a newer request.
apps/packages/ui/src/services/tldw/request-core.ts (1)

446-458: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Caller abort is not honored during the 401 refresh/retry.

onAbort (registered at Line 393) only aborts the original controller. The retry issues its fetch with a fresh retryController (Line 448/454), so if the caller's abortSignal fires during refreshAuth() or the retry request, the retry proceeds and ignores the cancellation. Consider forwarding the caller signal to retryController (e.g. abort it if abortSignal?.aborted, and have onAbort abort both), so Stop/cancel remains effective across the retry.

The body-reuse and timeout re-arm changes in this block otherwise look correct.

🛡️ Sketch
       const retryController = new AbortController()
+      if (abortSignal) {
+        if (abortSignal.aborted) retryController.abort()
+        else abortSignal.addEventListener("abort", () => retryController.abort(), { once: true })
+      }
       retryTimeoutId = setTimeout(() => retryController.abort(), timeoutMs)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/packages/ui/src/services/tldw/request-core.ts` around lines 446 - 458,
The 401 refresh/retry path in request-core.ts is not honoring the caller’s abort
signal because retryController is isolated from the original abortSignal and
onAbort only cancels the first controller. Update the retry logic in the fetch
retry block so the caller’s cancellation is forwarded to retryController as
well, and make the abort handler abort both controllers or otherwise propagate
abort state before and during the retry request. Use the existing onAbort,
controller, retryController, and abortSignal wiring to keep stop/cancel
effective across refreshAuth() and the retry fetch.
apps/tldw-frontend/extension/shims/wxt-browser.ts (1)

244-271: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

remove()/clear() still silently swallow storage errors, unlike the newly-fixed set().

set() was just fixed to reject on write failure instead of resolving as success (H9 #3), but remove() and clear() still catch-and-ignore backend errors, resolving successfully regardless. This is an inconsistent error-handling contract across the same storage API surface.

Also applies to: 273-292

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/tldw-frontend/extension/shims/wxt-browser.ts` around lines 244 - 271,
`remove()` and `clear()` in the storage area API still swallow backend failures
and always resolve, which is inconsistent with the updated `set()` error
behavior. Update the `remove`/`clear` paths in the same storage shim (around
`getAreaBackend`, `storageOnChanged.trigger`, and the area methods) to catch
backend exceptions and reject the returned promise or surface the error through
the callback instead of silently ignoring it. Ensure both methods follow the
same failure contract as `set()` for all backend operations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/FRONTEND_AUDIT.md`:
- Around line 48-73: The Remediation status section mixes a claim that all
Critical and High findings are fixed with entries that are still partial or
residual, which is contradictory. Reword the summary and the table in
FRONTEND_AUDIT.md so the completed fixes are clearly separated from phased
follow-ups, and make sure the status language for 12102, H1, and 12103 matches
their actual remaining work. Use the existing section heading and the table rows
as the place to update the wording consistently.

In `@apps/packages/ui/src/entries/background.ts`:
- Around line 498-511: `persistSessionState` can be invoked repeatedly and its
async storage writes are currently unqueued, which can lead to out-of-order
persistence. Update the `persistSessionState` flow in `background.ts` to
serialize writes through a single promise queue so each call waits for the
previous `writePersistedSessionState` to finish before starting the next. Use
the existing `persistSessionState` helper and the `writePersistedSessionState`
call site as the main targets, and apply the same queued behavior to all
fire-and-forget callers listed in the diff.
- Around line 2286-2346: The regular ingest rehydration path in
rehydrateSessionState/resumeActiveIngestSessions leaves queued sessions with
empty jobIds stuck after restart because they are filtered out and never
reported. Add a helper similar to reportInterruptedQuickIngestUploads that scans
restored ingestSessions for non-terminal sessions that were interrupted before
job submission, and after resumeActiveIngestSessions emits a terminal failure
state with canRetry enabled so the sidepanel can recover. Use the existing
ingestSessions, resumeActiveIngestSessions, and rehydrateSessionState flow to
locate and handle these orphaned sessions.

In `@apps/packages/ui/src/hooks/chat-modes/chatModePipeline.ts`:
- Around line 962-972: The first-chunk path in chatModePipeline is setting
isProcessing back to true without confirming the current turn still owns the
shared controller, which can let a superseded turn re-toggle the UI after a
newer turn has taken over. Update the count === 0 branch to perform the same
ownership check used by releaseAbortControllerIfOwned before calling
setIsProcessing(true), and only flip the processing flag when the current signal
still owns the turn. Keep the cleanup logic around stillOwnsTurn and the shared
streaming/controller state consistent with the existing ownership handling in
chatModePipeline.

In `@apps/packages/ui/src/services/tldw/TldwApiClient.ts`:
- Around line 7944-7955: The mixin copies of createChatCompletion and
synthesizeSpeech in chat-rag.ts and models-audio.ts are now dead code and can
drift from the canonical base behavior. Remove those duplicate implementations
from the mixins and keep TldwApiClientBase as the single source of truth, then
update TldwDomainMethodOverride so those methods are excluded from the domain
interface only where the base-class versions should be used.

In `@apps/tldw-frontend/extension/shims/plasmo-storage-hook.tsx`:
- Around line 45-76: The initial load in the `useEffect` for
`plasmo-storage-hook` can overwrite a newer value from `storage.watch`, so add
an ordering guard between the `storage.get(options.key)` path and the watch
callback. Use a shared freshness flag or version token inside the effect: mark
when `watch` has applied an update, and have the `get().then(...)` handler skip
`applyValue` if that fresher watch update already landed. Keep the cleanup in
the same `useEffect` and ensure `applyValue`, `storage.get`, and `storage.watch`
remain the only entry points to this state update.

In `@apps/tldw-frontend/extension/shims/wxt-browser.ts`:
- Around line 206-243: The `set()` implementation in `wxt-browser.ts` can
partially persist earlier keys before a later `JSON.stringify`/`setItem`
failure, so `storageOnChanged.trigger()` may fire for a write that ultimately
rejects. Update the `set` method to validate/serialize every entry in `items` up
front, and only if all succeed should you write to `backend`, build `changes`,
and trigger `storageOnChanged`/resolve the callback; if any value fails, reject
without committing anything. Keep the fix localized around `set`, `backend`,
`changes`, and `writeError` handling.

In `@apps/tldw-frontend/lib/history.ts`:
- Around line 34-35: Broaden the credential redaction in history persistence:
the current SENSITIVE_BODY_KEYS check in history.ts only drops access_token and
refresh_token, so redactTokens still saves other secret-like fields from
non-auth routes. Update the redaction logic used by redactTokens and the history
entry persistence path to catch additional credential-shaped keys such as
id_token, session_token, apiKey/api_key, jwt, sessionToken, and similar
case-insensitive variants, ideally with a defensive substring/regex match for
token/secret/password. Keep the full-body drop behavior for the auth routes, but
make the non-auth body redaction more comprehensive before writing to
localStorage.
- Around line 67-82: The depth limit in redactTokens currently fails open by
returning the original subtree once recursion exceeds the limit, which can leak
sensitive data. Update redactTokens in history.ts to fail closed at the boundary
by returning a safe redacted placeholder instead of the raw value, and keep the
existing recursive handling for arrays and plain objects while preserving the
SENSITIVE_BODY_KEYS-based masking behavior.

In `@apps/tldw-frontend/next.config.mjs`:
- Around line 50-51: The hardcoded themeBootstrapScriptHash in next.config.mjs
can drift from THEME_BOOTSTRAP_SCRIPT in pages/_document.tsx and break CSP.
Update the hash generation so next.config.mjs computes it at build time from the
actual script source instead of keeping a manual constant, and wire it into the
existing CSP setup where themeBootstrapScriptHash is used.
- Around line 76-110: The security headers returned by headers() in
next.config.mjs are missing CSP violation reporting and HSTS. Update the
Content-Security-Policy entry to include a reporting target such as report-to or
report-uri so violations are observable in production, and add a
Strict-Transport-Security header in the same headers list if it is not
guaranteed by the hosting layer. Keep the changes localized to the headers()
configuration and preserve the existing headers like X-Frame-Options and
Permissions-Policy.
- Around line 62-65: The CSP in next.config.mjs is too broad because
connect-src, img-src, media-src, and frame-src still allow arbitrary
http(s)/ws(s) origins even though NEXT_PUBLIC_API_URL and
TLDW_INTERNAL_API_ORIGIN are available. Tighten the policy in the CSP builder by
pinning backend egress in the connect-src directive using the API origin
symbols, and split any external thumbnails/previews into a separate explicit
allowlist for img-src/media-src/frame-src instead of leaving generic network
schemes open.

In `@backlog/tasks/task-12093` -
Fix-javascript-URL-DOM-XSS-in-source-anchors-and-add-CSP.md:
- Around line 57-66: Update the CSP task notes to match the final shipped
policy: the current text in the task section describing `next.config.mjs` still
reflects the older header set with `'unsafe-inline'` and the follow-up wording.
Revise the description around the CSP block and the “Follow-up” note so it
matches the actual `headers()` policy, which already uses the hardened
`script-src` setup and includes the additional security headers. Keep the
references aligned with `next.config.mjs`, `async headers()`, and the CSP
summary text in this task file.

In `@backlog/tasks/task-12102` - Re-enable-frontend-type-safety-and-lint-gates.md:
- Around line 22-23: Narrow the lint-gate task description so it only mentions
the globally disabled react-hooks rules and not rules-of-hooks, since that rule
is already active outside e2e. Update the wording in the backlog note to match
the actual disables in eslint.config.mjs, and use the existing symbols
react-hooks and `@typescript-eslint/no-explicit-any` as the anchors for the fix.

In `@backlog/tasks/task-12103` -
Remove-half-wired-dead-web-auth-stack-and-dead-extension-routes-tree.md:
- Around line 19-23: Update the note to reference the correct config entrypoint,
using next.config.mjs instead of next.config.js, and mention that the
dead-looking extension/routes tree is still protected by parity tests so it
should not be framed as a simple deletion. Point the guidance at the relevant
symbols such as extension/routes/*, route-registry.tsx, app-route.tsx, and the
shared UI routes under packages/ui/src/routes/*, and add a build/test guard or
explicit quarantine note so future edits can’t silently diverge.

---

Outside diff comments:
In `@apps/packages/ui/src/hooks/chat/useChatActions.ts`:
- Around line 2971-2978: Compare-mode cleanup is resetting shared chat state
unconditionally and can clobber a newer in-flight turn’s abort controller.
Update the cleanup path after Promise.all(comparePromises) in useChatActions to
follow the same ownership check used elsewhere in this file: gate the
setIsProcessing, setStreaming, setAbortController, and
activeAbortControllerRef.current resets behind
releaseAbortControllerIfOwned(signal) so only the owning turn clears state. Keep
the aggregateChatSubmitResults(compareResults) return flow unchanged, but avoid
touching controller/state if ownership has already moved to a newer request.

In `@apps/packages/ui/src/services/tldw/request-core.ts`:
- Around line 446-458: The 401 refresh/retry path in request-core.ts is not
honoring the caller’s abort signal because retryController is isolated from the
original abortSignal and onAbort only cancels the first controller. Update the
retry logic in the fetch retry block so the caller’s cancellation is forwarded
to retryController as well, and make the abort handler abort both controllers or
otherwise propagate abort state before and during the retry request. Use the
existing onAbort, controller, retryController, and abortSignal wiring to keep
stop/cancel effective across refreshAuth() and the retry fetch.

In `@apps/packages/ui/src/store/connection.tsx`:
- Around line 608-615: The `checkOnce` flow in `connection.tsx` can leave
`checkInFlight` stuck true if any await before the existing `try/catch` fails,
which blocks all later checks. Wrap the full `checkOnce` body, including the
persisted-flag reads and `ensurePlaceholderConfig()`, in a `try/finally` and
release `checkInFlight` in the `finally` block so every exit path is covered.
After that, remove the scattered manual resets that become redundant while
keeping the existing guard and exit behavior in `checkOnce` intact.

In `@apps/tldw-frontend/extension/shims/wxt-browser.ts`:
- Around line 244-271: `remove()` and `clear()` in the storage area API still
swallow backend failures and always resolve, which is inconsistent with the
updated `set()` error behavior. Update the `remove`/`clear` paths in the same
storage shim (around `getAreaBackend`, `storageOnChanged.trigger`, and the area
methods) to catch backend exceptions and reject the returned promise or surface
the error through the callback instead of silently ignoring it. Ensure both
methods follow the same failure contract as `set()` for all backend operations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bf71c9f1-2498-4512-a5c8-1b4fdcb32d1c

📥 Commits

Reviewing files that changed from the base of the PR and between 49e3abe and 323de42.

📒 Files selected for processing (101)
  • apps/FRONTEND_AUDIT.md
  • apps/packages/ui/src/components/Common/Playground/MessageSource.tsx
  • apps/packages/ui/src/components/Knowledge/hooks/useFileSearch.ts
  • apps/packages/ui/src/components/Knowledge/hooks/useKnowledgeSearch.ts
  • apps/packages/ui/src/components/Notes/__tests__/notes-manager-utils.sanitize-url.test.ts
  • apps/packages/ui/src/components/Notes/notes-manager-utils.ts
  • apps/packages/ui/src/components/Option/Collections/ReadingList/ReadingItemCard.tsx
  • apps/packages/ui/src/components/Option/Collections/ReadingList/ReadingItemDetail.tsx
  • apps/packages/ui/src/components/Option/Items/ItemsWorkspace.tsx
  • apps/packages/ui/src/components/Option/KnowledgeQA/SourceCard.tsx
  • apps/packages/ui/src/components/Option/KnowledgeQA/SourceViewerModal.tsx
  • apps/packages/ui/src/components/Option/Processed/index.tsx
  • apps/packages/ui/src/components/Option/ResearchWorkspace/SourcesPane/index.tsx
  • apps/packages/ui/src/components/Option/Watchlists/AlertsTab/AlertsTab.tsx
  • apps/packages/ui/src/components/Option/Watchlists/ItemsTab/ItemsTab.tsx
  • apps/packages/ui/src/components/Option/Watchlists/OutputsTab/OutputPreviewDrawer.tsx
  • apps/packages/ui/src/components/Option/Watchlists/OutputsTab/ReportEvidencePanel.tsx
  • apps/packages/ui/src/components/Option/Watchlists/RunsTab/RunDetailDrawer.tsx
  • apps/packages/ui/src/components/Option/Watchlists/SourcesTab/SourcesTab.tsx
  • apps/packages/ui/src/components/Sidepanel/Chat/hooks/useRagResultsDisplay.tsx
  • apps/packages/ui/src/entries/__tests__/background-session-store.test.ts
  • apps/packages/ui/src/entries/background-session-store.ts
  • apps/packages/ui/src/entries/background.ts
  • apps/packages/ui/src/hooks/chat-modes/__tests__/chatModePipeline.abort-lifecycle.test.ts
  • apps/packages/ui/src/hooks/chat-modes/chatModePipeline.ts
  • apps/packages/ui/src/hooks/chat/useChatActions.ts
  • apps/packages/ui/src/models/ChatTldw.ts
  • apps/packages/ui/src/models/__tests__/ChatTldw.abort-signal.test.ts
  • apps/packages/ui/src/models/__tests__/ChatTldw.stream-transport-interrupted.test.ts
  • apps/packages/ui/src/public/_locales/en/sources.json
  • apps/packages/ui/src/services/__tests__/background-proxy.test.ts
  • apps/packages/ui/src/services/__tests__/background-proxy.web-refresh.test.ts
  • apps/packages/ui/src/services/background-proxy.ts
  • apps/packages/ui/src/services/tldw/TldwApiClient.ts
  • apps/packages/ui/src/services/tldw/TldwAuth.ts
  • apps/packages/ui/src/services/tldw/TldwChat.ts
  • apps/packages/ui/src/services/tldw/__tests__/TldwApiClient.sanitizer.test.ts
  • apps/packages/ui/src/services/tldw/__tests__/TldwAuth.refresh.test.ts
  • apps/packages/ui/src/services/tldw/__tests__/TldwChat.abort.test.ts
  • apps/packages/ui/src/services/tldw/__tests__/request-core.refresh-timeout.test.ts
  • apps/packages/ui/src/services/tldw/domains/chat-rag.ts
  • apps/packages/ui/src/services/tldw/domains/models-audio.ts
  • apps/packages/ui/src/services/tldw/request-core.ts
  • apps/packages/ui/src/store/__tests__/connection.test.ts
  • apps/packages/ui/src/store/__tests__/folder.test.ts
  • apps/packages/ui/src/store/__tests__/workspace.test.ts
  • apps/packages/ui/src/store/acp-sessions.ts
  • apps/packages/ui/src/store/actor.tsx
  • apps/packages/ui/src/store/connection.tsx
  • apps/packages/ui/src/store/feedback.tsx
  • apps/packages/ui/src/store/folder.tsx
  • apps/packages/ui/src/store/notes-dock.tsx
  • apps/packages/ui/src/store/persona-buddy-shell.ts
  • apps/packages/ui/src/store/playground-session.tsx
  • apps/packages/ui/src/store/quick-ingest-session.ts
  • apps/packages/ui/src/store/ui-mode.tsx
  • apps/packages/ui/src/store/workspace.ts
  • apps/packages/ui/src/utils/__tests__/absolute-url-guard.test.ts
  • apps/packages/ui/src/utils/__tests__/safe-external-url.test.ts
  • apps/packages/ui/src/utils/absolute-url-guard.ts
  • apps/packages/ui/src/utils/extract-token-from-chunk.ts
  • apps/packages/ui/src/utils/safe-external-url.ts
  • apps/tldw-frontend/AGENTS.md
  • apps/tldw-frontend/CLAUDE.md
  • apps/tldw-frontend/__tests__/extension/plasmo-storage-watch.test.tsx
  • apps/tldw-frontend/__tests__/extension/wxt-browser-storage.test.ts
  • apps/tldw-frontend/__tests__/header-runs-gating.test.tsx
  • apps/tldw-frontend/__tests__/navigation/react-router-search-params-dynamic.test.tsx
  • apps/tldw-frontend/__tests__/pages/admin-maintenance.test.tsx
  • apps/tldw-frontend/__tests__/pages/content-review.test.tsx
  • apps/tldw-frontend/components/layout/Header.tsx
  • apps/tldw-frontend/components/layout/Layout.tsx
  • apps/tldw-frontend/extension/routes/_RUNTIME_UNUSED.md
  • apps/tldw-frontend/extension/shims/plasmo-storage-hook.tsx
  • apps/tldw-frontend/extension/shims/plasmo-storage.ts
  • apps/tldw-frontend/extension/shims/react-router-dom.tsx
  • apps/tldw-frontend/extension/shims/wxt-browser.ts
  • apps/tldw-frontend/hooks/__tests__/useConfig.networking.test.tsx
  • apps/tldw-frontend/hooks/useAuth.tsx
  • apps/tldw-frontend/hooks/useConfig.tsx
  • apps/tldw-frontend/hooks/useIsAdmin.ts
  • apps/tldw-frontend/lib/__tests__/history-redaction.test.ts
  • apps/tldw-frontend/lib/api.ts
  • apps/tldw-frontend/lib/auth.ts
  • apps/tldw-frontend/lib/history.ts
  • apps/tldw-frontend/next.config.mjs
  • apps/tldw-frontend/package.json
  • apps/tldw-frontend/pages/_document.tsx
  • backlog/tasks/task-12091 - Fix-chat-completion-sanitizer-corrupting-successful-non-streaming-replies.md
  • backlog/tasks/task-12092 - Stop-persisting-credentials-to-localStorage-request-history.md
  • backlog/tasks/task-12093 - Fix-javascript-URL-DOM-XSS-in-source-anchors-and-add-CSP.md
  • backlog/tasks/task-12094 - Persist-MV3-background-session-state-across-worker-suspension.md
  • backlog/tasks/task-12095 - Add-URL-allowlist-before-attaching-credentials-in-extension-upload-stream-handlers.md
  • backlog/tasks/task-12096 - Wire-token-refresh-into-the-web-request-path.md
  • backlog/tasks/task-12097 - Fix-chat-abort-and-stream-lifecycle.md
  • backlog/tasks/task-12098 - Stop-replaying-non-idempotent-chat-POST-on-stream-connection-timeout.md
  • backlog/tasks/task-12099 - Fix-connection-and-workspace-store-races-and-sticky-failure-states.md
  • backlog/tasks/task-12100 - Fix-web-build-browser-storage-shims.md
  • backlog/tasks/task-12101 - Fix-default-request-timeouts-aborting-normal-LLM-generations.md
  • backlog/tasks/task-12102 - Re-enable-frontend-type-safety-and-lint-gates.md
  • backlog/tasks/task-12103 - Remove-half-wired-dead-web-auth-stack-and-dead-extension-routes-tree.md
💤 Files with no reviewable changes (9)
  • apps/tldw-frontend/tests/header-runs-gating.test.tsx
  • apps/tldw-frontend/components/layout/Layout.tsx
  • apps/tldw-frontend/hooks/useAuth.tsx
  • apps/tldw-frontend/tests/pages/content-review.test.tsx
  • apps/tldw-frontend/hooks/tests/useConfig.networking.test.tsx
  • apps/tldw-frontend/hooks/useConfig.tsx
  • apps/tldw-frontend/components/layout/Header.tsx
  • apps/tldw-frontend/hooks/useIsAdmin.ts
  • apps/tldw-frontend/tests/pages/admin-maintenance.test.tsx

Comment thread apps/FRONTEND_AUDIT.md
Comment on lines +48 to +73
## 0.6 Remediation status (2026-07-02)

All Critical and High findings above were **fixed** in this pass (working tree on `dev`; not yet committed). Each fix ships with focused unit tests. Backlog tasks `task-12091`…`task-12103` track the work.

**Verification:** the new + affected suites all pass — 127 tests across 14 packages/ui files, 6 (C2 redaction), 11 (shim/nav), and 23 existing store/audio/client regression tests. The only red test is a **pre-existing** `workspace.ts` quota-warning test that fails identically on baseline `dev` (confirmed via `git stash` by two reviewers) and is unrelated to these changes.

| # | Status | Notes |
|---|--------|-------|
| C1 | ✅ Fixed | Sanitizer removed from the runtime path (`chat-rag.ts`) and base class; buggy helper deleted. |
| C2 | ✅ Fixed | Centralized redaction in `history.ts`; logout clears history. |
| H1 | ✅ Fixed | Shared `safeExternalUrl`/`openExternalUrl` at all sinks + CSP added and **tightened**: `'unsafe-inline'` dropped from `script-src` (the one trusted inline script is SHA-256-hash-allowlisted), plus `X-Content-Type-Options`/`Referrer-Policy`/`X-Frame-Options`/`Permissions-Policy`. `'unsafe-eval'` retained (WASM) as a documented follow-up. ⚠️ **Verify in a browser before merge** — confirm no CSP violations on the main routes and the dev/error overlays (I can't run the browser here). |
| H2 | ✅ Fixed | Session state persisted to `chrome.storage.session` + `chrome.alarms` backstop. Quick-ingest **remote-job polling now resumes** after a worker restart (alarm-driven, from persisted batch records); an in-flight multipart *upload* is non-resumable by design and instead reports interrupted so the UI isn't stuck, and post-restart cancel always finds the session. |
| H3 | ✅ Fixed | Origin allowlist + `sender.id` guard on `tldw:upload`/`tldw:stream`. Guard logic **consolidated** into a single canonical `absolute-url-guard.ts` (request-core + background-proxy now import it; request-core's diagnostic warnings preserved via optional hooks) — no more triplication. |
| H4 | ✅ Fixed | Web `refreshAuth` wired with single-flight; re-armable timer; FormData-retry bug fixed. |
| H5 | ✅ Fixed | Signal threaded, per-call controllers, ownership-guarded resets, early-throw + abort-path fixes. Regenerate-abort-before-first-token now discards the empty variant and restores the prior active variant/index. |
| H6 | ✅ Fixed | 5s→config-derived timeout; non-idempotent POSTs throw `StreamInterruptedError` instead of being replayed. **F10 closed:** the `stream_transport_interrupted` sentinel is now surfaced through the normal/RAG token pipeline (captured + re-emitted in `ChatTldw.stream`), so a post-first-byte truncation is finalized as *interrupted* (parity with character chat), never saved as complete. |
| H7, H8, H11 | ✅ Fixed | Functional `set` + synchronous guard (connection); hydration published via `set` (workspace); availability flag no longer persisted, with self-healing `merge` (folder). |
| H9, H10 | ✅ Fixed | Per-area isolation + memory-only `session` + scoped `clear()`; cross-instance watch bus + `useStorage` subscription; dynamic-route `useSearchParams`. |
| H12 | ✅ Fixed | Generation-endpoint timeout 10s→120s; messaging-ack decoupled (10s→130s); body read bounded; TTS timeout in `synthesizeSpeech`. |
| Config (12102) | ◑ Partial | Done: `version`/`migrate` baseline on the 8 unversioned stores; added a `typecheck` script (`tsc --noEmit`); corrected the audit's `rules-of-hooks` claim (it is already enabled). **Blocked/phased:** removing `ignoreBuildErrors` / enabling `strict` requires first clearing ~47 **pre-existing** `tsc` errors (in unrelated Watchlists components, at the current loose settings) — that cleanup is separate from audit remediation. Enabling the newer `react-hooks` rules (`set-state-in-effect` et al.) is deferred to avoid a large, noisy fix in this PR. |
| Dead code (12103) | ✅ Done | Web auth stack (`useAuth`/`useConfig`/`Header`/`Layout`/`useIsAdmin`) **deleted** (5 files + their exclusive tests; docs updated; live `lib/*`/`WebLayout` left intact). The `extension/routes/` tree was **kept** — investigation showed it's runtime-unused but parity-test-maintained (not deletable without migrating ~22 tests); documented via `_RUNTIME_UNUSED.md`. |

**Residuals — nearly all closed in a follow-up pass.** Fixed since the first draft: F10 partial-stream marking (H6), quick-ingest resume (H2), guard-helper consolidation (H3), regenerate-abort discard (H5), CSP `script-src` tightening + extra security headers (H1). **What genuinely remains** (each a larger effort or needing out-of-band verification, none reintroducing a defect):
- **12102** — removing `ignoreBuildErrors` / enabling TS `strict` is blocked on ~50 **pre-existing** `tsc` errors in unrelated code (measured; must be cleared first); enabling the newer `react-hooks` rules (`set-state-in-effect` et al.) is deferred to avoid a large noisy fix. A `typecheck` script was added so the team can burn the baseline down.
- **H1** — dropping `'unsafe-eval'` from the CSP needs per-feature (WASM/OCR/tokenizer) browser verification; and the tightened CSP overall should get a quick browser smoke before merge (dev/error overlays especially).
- **12103** — the `extension/routes/` mirror is runtime-unused but kept intentionally in sync by ~22 parity tests; removing it needs those tests migrated first.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Separate completed fixes from phased follow-ups.

Remediation status says all Critical/High findings were fixed, but the next rows mark config 12102 as partial and keep H1/12103 as residual follow-ups. Reword this section so the table doesn't contradict itself.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/FRONTEND_AUDIT.md` around lines 48 - 73, The Remediation status section
mixes a claim that all Critical and High findings are fixed with entries that
are still partial or residual, which is contradictory. Reword the summary and
the table in FRONTEND_AUDIT.md so the completed fixes are clearly separated from
phased follow-ups, and make sure the status language for 12102, H1, and 12103
matches their actual remaining work. Use the existing section heading and the
table rows as the place to update the wording consistently.

Comment on lines +498 to +511
const persistSessionState = async (): Promise<void> => {
try {
await writePersistedSessionState({
ingestSessions: serializeIngestSessions(ingestSessions),
pendingAuthReplay: serializePendingReplay(pendingAuthReplay),
quickIngestSessions:
serializeQuickIngestSessions(quickIngestModalSessions),
quickIngestBatches:
serializeQuickIngestBatches(quickIngestBatchRecords),
});
} catch (error) {
logBackgroundError("persist session state", error);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider serializing persistSessionState() writes to avoid out-of-order persistence.

persistSessionState() is called fire-and-forget from many sites in rapid succession (status updates, retries, cancels, quick-ingest lifecycle events). Each call snapshots the live Maps/Sets synchronously, but the actual area.set() write is async and unqueued, so nothing guarantees writes land in invocation order if the underlying storage API ever resolves out of order under back-to-back calls. Given this module exists specifically to make session state durable across worker suspension, a simple write queue removes this risk cheaply.

♻️ Suggested fix: chain writes through a promise queue
+    let persistQueue: Promise<void> = Promise.resolve();
     const persistSessionState = async (): Promise<void> => {
-      try {
-        await writePersistedSessionState({
-          ingestSessions: serializeIngestSessions(ingestSessions),
-          pendingAuthReplay: serializePendingReplay(pendingAuthReplay),
-          quickIngestSessions:
-            serializeQuickIngestSessions(quickIngestModalSessions),
-          quickIngestBatches:
-            serializeQuickIngestBatches(quickIngestBatchRecords),
-        });
-      } catch (error) {
-        logBackgroundError("persist session state", error);
-      }
+      const snapshot = {
+        ingestSessions: serializeIngestSessions(ingestSessions),
+        pendingAuthReplay: serializePendingReplay(pendingAuthReplay),
+        quickIngestSessions:
+          serializeQuickIngestSessions(quickIngestModalSessions),
+        quickIngestBatches:
+          serializeQuickIngestBatches(quickIngestBatchRecords),
+      };
+      persistQueue = persistQueue
+        .then(() => writePersistedSessionState(snapshot))
+        .catch((error) => {
+          logBackgroundError("persist session state", error);
+        });
+      return persistQueue;
     };

Also applies to: 786-788, 2237-2261, 3089-3090, 3131-3131, 3145-3145, 2798-2831

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/packages/ui/src/entries/background.ts` around lines 498 - 511,
`persistSessionState` can be invoked repeatedly and its async storage writes are
currently unqueued, which can lead to out-of-order persistence. Update the
`persistSessionState` flow in `background.ts` to serialize writes through a
single promise queue so each call waits for the previous
`writePersistedSessionState` to finish before starting the next. Use the
existing `persistSessionState` helper and the `writePersistedSessionState` call
site as the main targets, and apply the same queued behavior to all
fire-and-forget callers listed in the diff.

Comment on lines +2286 to 2346
const resumeActiveIngestSessions = async (): Promise<void> => {
const candidates = Array.from(ingestSessions.values()).filter(
(session) =>
(session.status === "queued" || session.status === "running") &&
!session.awaitingAuth &&
Array.isArray(session.jobIds) &&
session.jobIds.length > 0 &&
!activeIngestPollFunnelIds.has(session.funnelId),
);
for (const session of candidates) {
void resumeIngestSessionPoll(session);
}
await scheduleIngestPollAlarm();
};

// Rehydrate persisted session state after a worker restart, then resume any
// interrupted ingest polling and replay pending 401 auth-recoveries. Runs at
// most once per worker lifetime.
const rehydrateSessionState = async (): Promise<void> => {
if (sessionStateHydrated) return;
sessionStateHydrated = true;
let state;
try {
state = await readPersistedSessionState();
} catch (error) {
logBackgroundError("read session state", error);
return;
}
for (const [funnelId, value] of Object.entries(state.ingestSessions)) {
if (!ingestSessions.has(funnelId)) {
ingestSessions.set(funnelId, value as unknown as IngestSession);
}
}
for (const funnelId of state.pendingAuthReplay) {
pendingAuthReplay.add(funnelId);
}
for (const entry of state.quickIngestSessions) {
if (!quickIngestModalSessions.has(entry.sessionId)) {
quickIngestModalSessions.set(entry.sessionId, {
sessionId: entry.sessionId,
cancelled: entry.cancelled,
abortControllers: new Set(),
});
}
}
for (const batch of state.quickIngestBatches) {
if (!quickIngestBatchRecords.has(batch.sessionId)) {
quickIngestBatchRecords.set(batch.sessionId, batch);
}
}
// Quick-ingest modal sessions restored from a previous worker that have no
// resumable remote-job batch were interrupted during their (non-resumable)
// upload phase — report those once so the sidepanel is not left stuck.
const interruptedUploadSessionIds = state.quickIngestSessions
.map((entry) => entry.sessionId)
.filter((sessionId) => !quickIngestBatchRecords.has(sessionId));
await resumeActiveIngestSessions();
void replayPendingAuthSessions();
await resumeQuickIngestBatches();
await reportInterruptedQuickIngestUploads(interruptedUploadSessionIds);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Regular ingest sessions interrupted before job-submission are never reported as failed/stuck after a worker restart.

resumeActiveIngestSessions only resumes sessions with jobIds.length > 0. A session persisted mid "Checking for existing media..."/job-submission (status queued, jobIds: [], canCancel: false, canRetry: false) survives rehydrate but is filtered out of resume, and there is no equivalent of reportInterruptedQuickIngestUploads for this funnel. The session — and its sidepanel status card — is left stuck indefinitely with no user-facing recovery path.

Consider mirroring the quick-ingest "interrupted upload" pattern: after rehydrate, detect ingestSessions with a non-terminal status but empty jobIds that aren't otherwise being resumed, and emit a terminal failure (canRetry: true) so the user can retry.

Want me to draft the equivalent reportInterruptedIngestSessions helper (or open a follow-up issue)?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/packages/ui/src/entries/background.ts` around lines 2286 - 2346, The
regular ingest rehydration path in
rehydrateSessionState/resumeActiveIngestSessions leaves queued sessions with
empty jobIds stuck after restart because they are filtered out and never
reported. Add a helper similar to reportInterruptedQuickIngestUploads that scans
restored ingestSessions for non-terminal sessions that were interrupted before
job submission, and after resumeActiveIngestSessions emits a terminal failure
state with canRetry enabled so the sidepanel can recover. Use the existing
ingestSessions, resumeActiveIngestSessions, and rehydrateSessionState flow to
locate and handle these orphaned sessions.

Comment on lines +962 to +972
// Only reset the shared streaming flag / controller if this turn still owns
// it. A newer in-flight turn may have replaced the controller; clobbering it
// would re-enable the send button and orphan the newer turn's Stop button.
const stillOwnsTurn = params.releaseAbortControllerIfOwned
? params.releaseAbortControllerIfOwned(signal)
: true
if (stillOwnsTurn) {
setIsProcessing(false)
setStreaming(false)
setAbortController(null)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep run --pattern 'releaseAbortControllerIfOwned' --lang typescript apps/packages/ui/src/hooks/chat/useChatActions.ts
rg -n -B3 -A15 'releaseAbortControllerIfOwned' apps/packages/ui/src/hooks/chat/useChatActions.ts

Repository: rmusser01/tldw_server

Length of output: 4945


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant section in chatModePipeline.ts
sed -n '900,1015p' apps/packages/ui/src/hooks/chat-modes/chatModePipeline.ts

# Check whether a non-destructive ownership helper exists anywhere in the chat hooks
rg -n "isOwnerOfAbortController|releaseAbortControllerIfOwned|activeAbortControllerRef|setIsProcessing\\(true\\)" apps/packages/ui/src/hooks/chat -g '*.ts' -g '*.tsx'

Repository: rmusser01/tldw_server

Length of output: 4273


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect controller ownership and turn lifecycle around the shared abort controller.
sed -n '2340,2395p' apps/packages/ui/src/hooks/chat/useChatActions.ts

# Inspect the streaming entry points around the `setIsProcessing(true)` sites.
sed -n '1700,1765p' apps/packages/ui/src/hooks/chat/useChatActions.ts
sed -n '2890,2935p' apps/packages/ui/src/hooks/chat/useChatActions.ts

Repository: rmusser01/tldw_server

Length of output: 6104


Gate the first-chunk setIsProcessing(true) on turn ownership

releaseAbortControllerIfOwned only protects cleanup. The unguarded count === 0 path can still flip isProcessing back on from a superseded turn after the active turn has already reset the UI, leaving the composer stuck in a processing state with no live request. Add a non-destructive ownership check before setting the flag.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/packages/ui/src/hooks/chat-modes/chatModePipeline.ts` around lines 962 -
972, The first-chunk path in chatModePipeline is setting isProcessing back to
true without confirming the current turn still owns the shared controller, which
can let a superseded turn re-toggle the UI after a newer turn has taken over.
Update the count === 0 branch to perform the same ownership check used by
releaseAbortControllerIfOwned before calling setIsProcessing(true), and only
flip the processing flag when the current signal still owns the turn. Keep the
cleanup logic around stillOwnsTurn and the shared streaming/controller state
consistent with the existing ownership handling in chatModePipeline.

Comment on lines +7944 to +7955
// createChatCompletion and synthesizeSpeech are implemented on
// TldwApiClientBase and are intentionally excluded from the domain interface
// via TldwDomainMethodOverride, so the base-class versions are the canonical
// (type-visible) ones. The legacy duplicates in the chat-rag / models-audio
// mixins were overwriting them at runtime, which (a) re-introduced the
// non-streaming sanitizer that corrupts successful assistant replies and
// (b) dropped the generous TTS timeout. Re-apply the base implementations so
// runtime matches the declared types and the fixes take effect.
Object.assign(TldwApiClient.prototype, {
createChatCompletion: TldwApiClientBase.prototype.createChatCompletion,
synthesizeSpeech: TldwApiClientBase.prototype.synthesizeSpeech
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Dead mixin copies of createChatCompletion/synthesizeSpeech remain a divergence hazard.

The rebind correctly restores the base implementations at runtime, but the mixin versions in chat-rag.ts and models-audio.ts still exist and are now unreachable. They already diverge subtly (base synthesizeSpeech uses TTS_REQUEST_TIMEOUT_MS; the mixin hardcodes 120000), so a future edit to a mixin copy will silently do nothing. Consider deleting the mixin implementations (and excluding them from TldwDomainMethodOverride) rather than shadowing them at runtime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/packages/ui/src/services/tldw/TldwApiClient.ts` around lines 7944 -
7955, The mixin copies of createChatCompletion and synthesizeSpeech in
chat-rag.ts and models-audio.ts are now dead code and can drift from the
canonical base behavior. Remove those duplicate implementations from the mixins
and keep TldwApiClientBase as the single source of truth, then update
TldwDomainMethodOverride so those methods are excluded from the domain interface
only where the base-class versions should be used.

Comment on lines +62 to +65
"connect-src 'self' https: http: ws: wss: data: blob:",
"worker-src 'self' blob:",
"frame-src 'self' blob: data: https: http:"
].join('; ');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== next.config.mjs ==\n'
sed -n '1,220p' apps/tldw-frontend/next.config.mjs

printf '\n== env/config references ==\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/.next/**' \
  'NEXT_PUBLIC_API_URL|API_URL|BACKEND|baseUrl|baseURL|backend origin|connect-src|img-src|media-src|frame-src|Content-Security-Policy|csp' \
  apps/tldw-frontend

Repository: rmusser01/tldw_server

Length of output: 28500


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== validate-networking-config.mjs ==\n'
sed -n '1,220p' apps/tldw-frontend/scripts/validate-networking-config.mjs

printf '\n== networking docs ==\n'
sed -n '35,90p' apps/tldw-frontend/README.md

Repository: rmusser01/tldw_server

Length of output: 4457


Scope backend egress separately from remote assets NEXT_PUBLIC_API_URL / TLDW_INTERNAL_API_ORIGIN are available when this CSP is built, but connect-src/img-src/media-src/frame-src still allow arbitrary http(s)/ws(s) origins. That leaves a broad exfil path for XSS; if external thumbnails or previews are required, keep them on a separate allowlist so the backend API can stay pinned.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/tldw-frontend/next.config.mjs` around lines 62 - 65, The CSP in
next.config.mjs is too broad because connect-src, img-src, media-src, and
frame-src still allow arbitrary http(s)/ws(s) origins even though
NEXT_PUBLIC_API_URL and TLDW_INTERNAL_API_ORIGIN are available. Tighten the
policy in the CSP builder by pinning backend egress in the connect-src directive
using the API origin symbols, and split any external thumbnails/previews into a
separate explicit allowlist for img-src/media-src/frame-src instead of leaving
generic network schemes open.

Comment on lines +76 to +110
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: contentSecurityPolicy,
},
// Block MIME sniffing (defense against content-type confusion XSS).
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
// Trim referrer leakage on cross-origin navigations.
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
// Legacy clickjacking guard; complements CSP frame-ancestors 'none'.
{
key: 'X-Frame-Options',
value: 'DENY',
},
// Least-privilege browser features. Mic is allowed for same-origin
// audio capture (realtime transcription/dictation); camera and
// geolocation are unused and denied.
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(self), geolocation=()',
},
],
},
];
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔵 Trivial

Consider adding CSP violation reporting and HSTS.

No report-to/report-uri is configured, so CSP violations (including any hash mismatches from the item above) are invisible in production. A Strict-Transport-Security header is also absent from this list; if it isn't already added by the hosting/proxy layer, consider adding it here for defense-in-depth.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/tldw-frontend/next.config.mjs` around lines 76 - 110, The security
headers returned by headers() in next.config.mjs are missing CSP violation
reporting and HSTS. Update the Content-Security-Policy entry to include a
reporting target such as report-to or report-uri so violations are observable in
production, and add a Strict-Transport-Security header in the same headers list
if it is not guaranteed by the hosting layer. Keep the changes localized to the
headers() configuration and preserve the existing headers like X-Frame-Options
and Permissions-Policy.

Comment on lines +57 to +66
### CSP (next.config.mjs `async headers()`, applied to `/:path*`)
```
default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none';
script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: https: http:; font-src 'self' data:;
media-src 'self' data: blob: https: http:; connect-src 'self' https: http: ws: wss: data: blob:;
worker-src 'self' blob:; frame-src 'self' blob: data: https: http:
```
`object-src 'none'` + `base-uri 'self'` + `frame-ancestors 'none'` are the hard protections. `script-src` keeps `'unsafe-inline'`/`'unsafe-eval'` (required by the `_document` theme-bootstrap inline script, antd runtime, Next.js) and `blob:` (Web Workers: OCR/diff/tokenizer). Resource directives (img/media/connect/frame) stay broad so external thumbnails, remote backends, realtime-audio WebSockets, and blob:/PDF iframe previews keep working.
**Follow-up:** tighten `script-src` to nonce/hash-based (drop `'unsafe-inline'`/`'unsafe-eval'`) — this also lets CSP block `javascript:` navigation directly. Consider adding `X-Content-Type-Options: nosniff` and `Referrer-Policy`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Refresh the CSP notes to match the shipped policy.

These lines still describe the pre-hardened header set ('unsafe-inline' retained and the extra headers as follow-ups), but the referenced next.config.mjs already drops 'unsafe-inline', hashes the theme bootstrap script, and includes those headers. Update the task text so it reflects the final state.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 57-57: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 58-58: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 58-58: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 64-64: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backlog/tasks/task-12093` -
Fix-javascript-URL-DOM-XSS-in-source-anchors-and-add-CSP.md around lines 57 -
66, Update the CSP task notes to match the final shipped policy: the current
text in the task section describing `next.config.mjs` still reflects the older
header set with `'unsafe-inline'` and the follow-up wording. Revise the
description around the CSP block and the “Follow-up” note so it matches the
actual `headers()` policy, which already uses the hardened `script-src` setup
and includes the additional security headers. Keep the references aligned with
`next.config.mjs`, `async headers()`, and the CSP summary text in this task
file.

Comment on lines +22 to +23
- `apps/tldw-frontend/eslint.config.mjs:78-84,118` — **`react-hooks` rules disabled**, including `rules-of-hooks` (`:118`) which catches genuine conditional-hook crash bugs. `@typescript-eslint/no-explicit-any` is only `warn`.
- **Persisted stores lack `version`/`migrate`** (8 of 9): `playground-session`, `persona-buddy-shell`, `notes-dock`, `ui-mode`, `actor`, `quick-ingest-session`, `folder`, `feedback`, `acp-sessions`. The day someone adds `version:1` to reshape a store without a `migrate`, all users' persisted state is discarded; a field rename before then ships `undefined` into consumers.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Narrow the lint-gate description to the rules that are actually disabled.

The audit correction says rules-of-hooks is only disabled under e2e/**; the global disables are the newer react-hooks/* checks. As written, this backlog item sends readers after a rule that is already active outside e2e.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backlog/tasks/task-12102` - Re-enable-frontend-type-safety-and-lint-gates.md
around lines 22 - 23, Narrow the lint-gate task description so it only mentions
the globally disabled react-hooks rules and not rules-of-hooks, since that rule
is already active outside e2e. Update the wording in the backlog note to match
the actual disables in eslint.config.mjs, and use the existing symbols
react-hooks and `@typescript-eslint/no-explicit-any` as the anchors for the fix.

Comment on lines +19 to +23
1. **Half-wired web auth stack.** The web `AuthProvider` (`apps/tldw-frontend/hooks/useAuth.tsx:16`) and web `ConfigProvider` (`hooks/useConfig.tsx:237`) are **never mounted** — the `ConfigProvider` in `components/AppProviders.tsx:80` is antd's, not this one; these two appear only in their own definitions and in `__tests__`. Their only consumers, `components/layout/Header.tsx` → `components/layout/Layout.tsx`, are imported by nothing (`WebLayout` uses a different shared Header). So `useAuth`/`useConfig` would throw if rendered, and `api.defaults.baseURL` is never synced from user config. It's ~500 lines of code a maintainer will mistake for live.

2. **Dead `extension/routes` tree.** `apps/tldw-frontend/extension/routes/*` (`route-registry.tsx`, `app-route.tsx`, all `option-*.tsx`) is not used in the web build — pages mount `packages/ui/src/routes/*` via the `@/` alias (`next.config.js:69,85`). The two copies have diverged (e.g. the live route wraps `RouteErrorBoundary`, the dead copy doesn't); edits to the dead copy silently no-op. Note: the `extension/shims/*` in the same directory ARE live — keep those.

Goal: delete or clearly quarantine the dead code so future maintainers don't edit or trust it. If any piece is intended for future use, document that and add a build/test guard so it can't silently rot.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Call out the parity-test dependency and correct the config path.

This note points at next.config.js, but the surrounding audit context uses next.config.mjs; it also omits that extension/routes/* is intentionally kept in sync by parity tests and isn't safely deletable yet. Please update the path and add that guard so the ticket doesn't read like a trivial delete.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backlog/tasks/task-12103` -
Remove-half-wired-dead-web-auth-stack-and-dead-extension-routes-tree.md around
lines 19 - 23, Update the note to reference the correct config entrypoint, using
next.config.mjs instead of next.config.js, and mention that the dead-looking
extension/routes tree is still protected by parity tests so it should not be
framed as a simple deletion. Point the guidance at the relevant symbols such as
extension/routes/*, route-registry.tsx, app-route.tsx, and the shared UI routes
under packages/ui/src/routes/*, and add a build/test guard or explicit
quarantine note so future edits can’t silently diverge.

rmusser01 and others added 2 commits July 2, 2026 15:58
Address the Critical and High findings from the 2026-07-02 frontend/extension
audit (apps/FRONTEND_AUDIT.md), plus config hardening and dead-code cleanup.

Critical:
- Stop the chat-completion "sanitizer" from corrupting successful non-streaming
  replies that mention "error"/"exception"/a file path (removed at the runtime
  source in domains/chat-rag.ts and the base class).
- Stop persisting auth headers and login tokens to the localStorage request
  history; redact centrally and clear history on logout.

High:
- DOM-XSS: shared safeExternalUrl/openExternalUrl guard at all source-URL anchor
  and window.open sinks; add a CSP (script-src SHA-256-hash-allowlisted, drop
  'unsafe-inline') plus X-Content-Type-Options/Referrer-Policy/X-Frame-Options/
  Permissions-Policy.
- MV3: persist ingest/quick-ingest/auth-replay session state and drive long polls
  via chrome.alarms so they survive service-worker suspension; resume remote-job
  polling after a restart.
- Extension: origin allowlist + sender.id guard before attaching credentials on
  tldw:upload/tldw:stream; consolidate the guard into absolute-url-guard.ts.
- Web token refresh wired with single-flight; post-refresh FormData retry fix.
- Chat: Stop aborts the transport; per-call abort controllers (fixes Compare mode);
  ownership-guarded resets; user abort no longer saved as a complete answer;
  stream_transport_interrupted surfaced through the normal-mode pipeline.
- Stream connection timeout no longer replays non-idempotent chat POSTs; generation
  request timeouts raised 10s -> 120s (and messaging-ack decoupled).
- Zustand: connection-store stale-snapshot race, workspace silent-mutation
  hydration, folder sticky-404; storage shims (scoped clear, memory-only session,
  cross-instance change propagation, dynamic-route useSearchParams).

Config / cleanup:
- version/migrate baseline on 8 persisted stores; add a typecheck script.
- Delete the dead web-auth provider stack (useAuth/useConfig/useIsAdmin/Header/
  Layout); document the runtime-unused extension/routes mirror.

Verification: new unit tests pass across all touched areas (~167 tests). Four
pre-existing test failures are unrelated (confirmed on baseline dev). Backlog
tasks task-12091..task-12103 track the work; remaining follow-ups (TS strict
gate, CSP unsafe-eval, extension/routes removal) are documented in
apps/FRONTEND_AUDIT.md section 0.6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ch 1)

Round-2 focused audit of character chat and TTS/STT (apps/FRONTEND_AUDIT.md
"Round 2"). Fixes the safe, client-only first batch; the rest is ticketed
(task-12104..task-12110).

Fixed:
- Character chat delete/edit hit the wrong Dexie row when a greeting shifted
  UI-array indices out of alignment with the Dexie history. Now address the
  row by stable message id (removeMessageById/updateMessageById/
  deleteChatAfterMessageId), with index fallback for id-less rows. (task-12104)
- Microphone could stay live after a MediaRecorder error or a double-start.
  Brought useAudioRecorder / useServerDictation / SpeechPlaygroundPage up to
  the useMicStream template: MediaRecorder onerror stops tracks + releases the
  capture lock, a synchronous re-entry guard prevents double-start orphans, and
  the acquired stream is held in a catch-reachable ref. (task-12105)
- TTS playback leaks + overlapping audio: revoke the MediaSource blob URL before
  the streaming->buffered fallback overwrites it; useTTS.cancel() now settles
  the in-flight playback promise and revokes the segment URL (no orphaned
  generator / leaked URL); TtsClipsDrawer stops the prior clip before playing a
  different one; audiobook cancel aborts the in-flight chapter synthesis;
  useDocumentTTS detaches listeners + revokes on error. (task-12107)

Ticketed for follow-up (not in this batch):
- WS auth token in the URL query string (task-12106) — backend already supports
  subprotocol / first-message auth, but switching the client and dropping the URL
  token must be validated against a live server (would break voice/STT if wrong).
- characterChatMode triplication + inactivity watchdog (task-12108); real-time
  voice hardening: barge-in audio-stop, backpressure, handshake timeout, WS
  unmount leaks (task-12109); character card handling: export SSRF, size caps,
  favorite desync, variant server-id (task-12110).

Deferred within task-12104: greeting is still not persisted to Dexie, so a
Dexie-sourced rehydrate shows one fewer message than a server-sourced one
(cosmetic; delete/edit no longer corrupt the store).

Tests: 68 passing across the touched areas (message-target-by-id, useAudioRecorder,
useServerDictation, useTTS.cancel, useStreamingAudioPlayer.leak,
useAudiobookGeneration.abort, TtsClipsDrawer.overlap, useDocumentTTS) + regressions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rmusser01 rmusser01 force-pushed the feat/frontend-extension-audit-remediation branch from 066502b to c6e5a20 Compare July 2, 2026 23:00
rmusser01 and others added 3 commits July 2, 2026 16:38
…tch 2)

Second remediation batch for the Round-2 audit (apps/FRONTEND_AUDIT.md).

Fixed (with tests):
- R8 (task-12109) real-time voice hardening: barge-in now stops buffered TTS and
  sends one interrupt per turn; mic->WS send honors ws.bufferedAmount backpressure;
  10s handshake timeout clears wedged "connecting" state; attempt-id/mounted guards
  stop usePersonaLiveSession/usePersonaLiveControl leaking a WebSocket on
  unmount-mid-connect.
- R9/R10 (task-12110) character cards: PNG export no longer fetches an arbitrary
  attacker-controlled avatar_url (same-origin/allowlisted only, AbortController
  timeout, credentials:omit, 5MB cap); avatar upload + card import size caps;
  favorites reconciled to the server flag with the correct React-Query cache key;
  a swiped-but-unpersisted variant no longer inherits a stale serverMessageId.
- R5 (task-12108) partial: a 60s stream-inactivity watchdog + recovery
  classification is now on BOTH live character-chat paths (a source-grep guard
  test verifies the shipped code got it), so a stalled character stream times out
  instead of hanging. Full 3-copy consolidation deferred (large refactor while dev
  is actively churning chat).

Partially done / needs live-server validation:
- R3 (task-12113) WS token moved OUT of the URL query string: persona uses the
  Sec-WebSocket-Protocol ["bearer", cred] subprotocol (persona.py:3705-3712);
  audio/STT + voice send {type:"auth",token} as the first message
  (streaming_service.py). A charset fallback keeps a non-token-safe custom API key
  from crashing new WebSocket() by reverting that case to the query token.
  MUST be smoke-tested against a running backend before merge (subprotocol
  handshake, single-user key charset, auth-before-config ordering, extension STT).

Deliberately deferred (documented, not silently skipped): TS-strict enablement
(blocked on ~66 pre-existing type errors), dropping CSP 'unsafe-eval' (needs WASM
browser verification), deleting the extension/routes mirror (kept in sync by ~22
parity tests; churn during dev's active route refactor).

Ticket renumbering: this branch's round-2 tickets were renumbered off 12104-12106
to 12111-12113 because teammates used 12104-12106 on origin/dev for PR-2573 work.

Tests: all round-2 follow-up + regression suites pass; one pre-existing
usePersonaLiveControl failure (confirmed identical on HEAD) is unrelated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve actionable findings from the automated PR reviewers (gemini, qodo,
coderabbit).

Bugs:
- connection.tsx: release the checkInFlight guard on ALL paths — a throw in the
  pre-`try` awaits (getFirstRunCompleteFlag etc.) no longer deadlocks every future
  health check for the session (full-body try/catch that resets the guard).
- background-proxy.ts refreshAuthDirect: throw on a failed/empty /auth/refresh so
  request-core sees refreshSucceeded=false and doesn't retry with a stale token
  (surfaces "Session expired" instead).
- background.ts: pre-submission ingest sessions (queued, no jobIds) are now
  reported as interrupted + cleaned up on worker restart instead of silently
  stranded; persistSessionState writes are serialized (write-again-if-dirty) so an
  older snapshot can't clobber a newer one.

Security:
- history.ts: broaden request/response body redaction (id_token, session_token,
  api_key/apiKey/x-api-key, jwt, secret, password, client_secret); make the
  depth-limited redactor fail CLOSED (return [REDACTED] past depth 6, not raw).

Shims:
- plasmo-storage.ts: guard the global `storage` listener's JSON.parse; log
  watcher-callback errors instead of swallowing them.
- plasmo-storage-hook.tsx: a `watch()` update that races the initial `get()` is no
  longer clobbered by the stale snapshot.

Stores/tests/docs:
- folder.tsx: add version/migrate baseline (consistency with the other stores).
- connection.test.ts: use a fixed timestamp instead of Date.now() for determinism.
- FRONTEND_AUDIT.md + tasks 12093/12102/12103: reword to match the shipped CSP
  (unsafe-inline dropped, hash-based, extra headers), correct the rules-of-hooks
  scope note (e2e-only), and note the extension/routes parity-test dependency.

Verified already-addressed (no change): old history entries re-scrubbed on write;
request-core dangling timers cleared in finally; wxt-browser multi-key set only
emits onChanged for committed keys.

Deliberately not taken (deployment/infra-sensitive): HSTS (self-hosted HTTP/LAN),
CSP report-uri (needs a collector), build-time script-hash (manual constant works).

Tests: 145 passing across the touched areas.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rmusser01 rmusser01 merged commit e101f19 into dev Jul 3, 2026
25 of 27 checks passed
@rmusser01 rmusser01 deleted the feat/frontend-extension-audit-remediation branch July 3, 2026 00:53
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.

1 participant