Skip to content

fix(frontend): centralize cookie credentials in fetchWrapper and route callers through it - #322

Open
chriswritescode-dev wants to merge 1 commit into
mainfrom
fix/frontend-centralize-fetch-credentials
Open

fix(frontend): centralize cookie credentials in fetchWrapper and route callers through it#322
chriswritescode-dev wants to merge 1 commit into
mainfrom
fix/frontend-centralize-fetch-credentials

Conversation

@chriswritescode-dev

@chriswritescode-dev chriswritescode-dev commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Centralize credentials: 'include' in fetchWrapper (with caller-signal abort propagation) and route all frontend auth-cookie fetches through it, removing per-call credentials/inline-fetch duplication across auth-info, files, TTS/STT, account settings, auth context, file browser, and the opencode event stream transport. Documents the same-origin deployment constraint and the deprecated CORS_ORIGIN / split-origin open question.

Summary

  • Extend fetchWrapper to propagate caller AbortSignal and expose credentials uniformly; add 499 CANCELED handling
  • Add authInfo API module (getAuthConfig, listUserPasskeys) backed by fetchWrapper
  • Route files, tts, stt, FileBrowser/FilePreview, AccountSettings, AuthContext, TTSContext, useSTT, auth-loaders, and opencode-event-stream browserTransport through fetchWrapper
  • Documentation: note same-origin default for VITE_API_URL and SameSite=Lax cross-site cookie caveat in authentication.md; mark CORS_ORIGIN as deprecated in environment.md
  • Add tests for authInfo, fetchWrapper, files, tts, useSTT, and browserTransport

Type of Change

  • Bug fix
  • New feature
  • Refactor
  • Documentation

Checklist

  • Code follows project style (no comments, named imports)
  • TypeScript types are properly defined
  • Tests added/updated (80% coverage target)
  • pnpm lint passes locally
  • pnpm typecheck passes locally

Summary by CodeRabbit

  • New Features
    • Improved file browsing with more reliable upload, save, delete, rename, and folder-creation actions.
    • Added centralized authentication configuration and passkey loading.
  • Bug Fixes
    • User-canceled requests are now distinguished from timeouts.
    • Improved error handling for speech-to-text and text-to-speech requests.
    • Enhanced authentication fallback behavior when configuration or network requests fail.
  • Documentation
    • Clarified same-origin authentication, session cookies, and supported environment settings.

@chriswritescode-dev

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Authentication, file, speech, and event-stream requests now use shared frontend API wrappers with consistent credentials, timeout, cancellation, and error handling. New helpers and tests cover authentication, file operations, STT/TTS behavior, and browser transport. Configuration documentation describes same-origin and cookie constraints.

Changes

Frontend request and API centralization

Layer / File(s) Summary
Request wrapper cancellation behavior
frontend/src/api/fetchWrapper.ts, frontend/src/api/fetchWrapper.test.ts
Caller aborts now produce cancellation errors distinct from timeouts, with listener cleanup and coverage for request credentials and abort behavior.
Authentication API helpers
frontend/src/api/authInfo.*, frontend/src/contexts/AuthContext.tsx, frontend/src/lib/auth-loaders.ts, frontend/src/components/settings/AccountSettings.tsx, docs/configuration/*
Authentication configuration and passkeys use shared helpers with fallback behavior; related consumers and configuration documentation were updated.
File API helpers and consumers
frontend/src/api/files.*, frontend/src/components/file-browser/*
File reads and mutations use centralized helpers for request construction, credentials, JSON or multipart bodies, and error handling.
Speech API request paths
frontend/src/api/stt.*, frontend/src/api/tts.*, frontend/src/contexts/TTSContext.tsx, frontend/src/hooks/useSTT.*
STT and TTS requests delegate timeout and abort handling to wrappers, while TTS responses and speech cancellation errors receive normalized handling.
Browser event transport
frontend/src/lib/opencode-event-stream/browserTransport.*
Browser event POSTs use the void wrapper and return success or failure booleans with corresponding tests.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: centralizing cookie credentials in fetchWrapper and routing callers through it.
Description check ✅ Passed The description matches the repository template and includes Summary, Type of Change, and Checklist sections with the key details filled in.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/frontend-centralize-fetch-credentials

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

@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: 3

🧹 Nitpick comments (1)
frontend/src/contexts/TTSContext.tsx (1)

135-146: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fragile string-matching for fallback error messages.

isFallback compares err.message against hardcoded strings ('Request failed', 'An error occurred') produced elsewhere in the fetch stack. If that wording changes, the 401/429/5xx friendly messages silently stop applying and users see the raw generic string.

♻️ Suggested approach: key off status instead of message text
       if (err instanceof FetchError) {
         const status = err.statusCode ?? 0
-        const backendMessage = err.message
-        const isFallback = backendMessage === 'Request failed' || backendMessage === 'An error occurred' || !backendMessage
-        let errorMessage = backendMessage
-        if (isFallback) {
-          if (status === 401) errorMessage = 'Invalid API key'
-          else if (status === 429) errorMessage = 'Rate limit exceeded'
-          else if (status >= 500) errorMessage = 'Service unavailable'
-        }
+        let errorMessage = err.message
+        if (status === 401) errorMessage = 'Invalid API key'
+        else if (status === 429) errorMessage = 'Rate limit exceeded'
+        else if (status >= 500) errorMessage = 'Service unavailable'
         throw new Error(errorMessage)
       }

Adjust if a distinct backend-provided message should still take precedence for these statuses.

🤖 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 `@frontend/src/contexts/TTSContext.tsx` around lines 135 - 146, Update the
FetchError handling in the TTS error path to determine friendly 401, 429, and
5xx messages from statusCode rather than matching hardcoded err.message strings.
Preserve distinct backend-provided messages where they should take precedence,
while ensuring generic or absent messages still receive the status-specific
fallback.
🤖 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 `@docs/configuration/authentication.md`:
- Line 180: Update the cross-site authentication note to accurately reflect the
settings in the auth configuration: `advanced.useSecureCookies` does not
override the default `SameSite=Lax`, so state that cross-site access is
unsupported unless the cookie configuration explicitly uses `sameSite: 'none'`
with `secure: true`. Clarify whether that configuration is supported, without
implying that CORS or client credentials alone enables cross-site cookies.

In `@frontend/src/api/authInfo.ts`:
- Around line 34-39: Propagate fetchWrapper failures from listUserPasskeys
instead of converting them to an empty array; update
frontend/src/components/settings/AccountSettings.tsx lines 27-31 to render React
Query’s error state for failed loads, and update
frontend/src/api/authInfo.test.ts lines 83-91 to assert rejection/error handling
rather than an empty-list fallback.

In `@frontend/src/api/files.ts`:
- Around line 90-96: Update writeFileEntry and renameFileEntry to pass timeout:
0 in their fetchWrapperVoid request options, disabling the default timeout for
both file mutations while preserving their existing methods, headers, and
bodies.

---

Nitpick comments:
In `@frontend/src/contexts/TTSContext.tsx`:
- Around line 135-146: Update the FetchError handling in the TTS error path to
determine friendly 401, 429, and 5xx messages from statusCode rather than
matching hardcoded err.message strings. Preserve distinct backend-provided
messages where they should take precedence, while ensuring generic or absent
messages still receive the status-specific fallback.
🪄 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: CHILL

Plan: Pro Plus

Run ID: 234e849e-ff82-4c66-912a-b5ef3382ebfa

📥 Commits

Reviewing files that changed from the base of the PR and between 8d5102d and 6a32215.

📒 Files selected for processing (21)
  • docs/configuration/authentication.md
  • docs/configuration/environment.md
  • frontend/src/api/authInfo.test.ts
  • frontend/src/api/authInfo.ts
  • frontend/src/api/fetchWrapper.test.ts
  • frontend/src/api/fetchWrapper.ts
  • frontend/src/api/files.test.ts
  • frontend/src/api/files.ts
  • frontend/src/api/stt.ts
  • frontend/src/api/tts.test.ts
  • frontend/src/api/tts.ts
  • frontend/src/components/file-browser/FileBrowser.tsx
  • frontend/src/components/file-browser/FilePreview.tsx
  • frontend/src/components/settings/AccountSettings.tsx
  • frontend/src/contexts/AuthContext.tsx
  • frontend/src/contexts/TTSContext.tsx
  • frontend/src/hooks/useSTT.test.tsx
  • frontend/src/hooks/useSTT.ts
  • frontend/src/lib/auth-loaders.ts
  • frontend/src/lib/opencode-event-stream/browserTransport.test.ts
  • frontend/src/lib/opencode-event-stream/browserTransport.ts

1. Check AUTH_SECRET is persistent across restarts
2. Verify cookies aren't being blocked
3. Check AUTH_SECURE_COOKIES setting
4. If accessing from a different site (cross-site, not just cross-origin), note that CORS alone is not sufficient: the auth config in `backend/src/auth/index.ts` sets no `sameSite` override, so better-auth's `SameSite=Lax` default prevents the session cookie from being attached to cross-site requests regardless of `credentials: 'include'` on the client. The app is designed same-origin (`VITE_API_URL` defaults to `''`); cross-site access requires changing `sameSite` to `'none'` with `secure: true`, which is an open question rather than a supported configuration.

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C4 'sameSite|defaultCookieAttributes|cookieOptions|cookies' backend/src/auth/index.ts

Repository: chriswritescode-dev/opencode-manager

Length of output: 174


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== backend/src/auth/index.ts (exists? + line count) ==\n'
if [ -f backend/src/auth/index.ts ]; then
  wc -l backend/src/auth/index.ts
  echo
  cat -n backend/src/auth/index.ts | sed -n '1,240p'
else
  echo "missing"
fi

printf '\n== backend auth-related search ==\n'
rg -n -C3 'sameSite|secure: true|defaultCookieAttributes|cookieOptions|sessionCookie|cookie.*site|better-auth|createAuth|authConfig' backend/src -g '!**/node_modules/**'

Repository: chriswritescode-dev/opencode-manager

Length of output: 7645


Clarify the cross-site auth note: backend/src/auth/index.ts only sets advanced.useSecureCookies; it does not override sameSite, so the default SameSite=Lax behavior still blocks cross-site session cookies. If cross-site access isn’t supported, say that explicitly; otherwise document the required sameSite: 'none' + secure: true settings.

🤖 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 `@docs/configuration/authentication.md` at line 180, Update the cross-site
authentication note to accurately reflect the settings in the auth
configuration: `advanced.useSecureCookies` does not override the default
`SameSite=Lax`, so state that cross-site access is unsupported unless the cookie
configuration explicitly uses `sameSite: 'none'` with `secure: true`. Clarify
whether that configuration is supported, without implying that CORS or client
credentials alone enables cross-site cookies.

Comment on lines +34 to +39
export async function listUserPasskeys(): Promise<Passkey[]> {
try {
return await fetchWrapper<Passkey[]>(`${API_BASE_URL}/api/auth/passkey/list-user-passkeys`)
} catch {
return []
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not present passkey-load failures as an empty passkey list.

Catching FetchError values—including 401/500, timeout, and the new 499 cancellation—in listUserPasskeys makes Account Settings display “No passkeys registered” for failed requests.

  • frontend/src/api/authInfo.ts#L34-L39: propagate fetchWrapper failures instead of returning [].
  • frontend/src/components/settings/AccountSettings.tsx#L27-L31: render React Query’s error state rather than the empty-list message when loading fails.
  • frontend/src/api/authInfo.test.ts#L83-L91: assert rejection/error handling instead of an empty-list fallback.
📍 Affects 3 files
  • frontend/src/api/authInfo.ts#L34-L39 (this comment)
  • frontend/src/components/settings/AccountSettings.tsx#L27-L31
  • frontend/src/api/authInfo.test.ts#L83-L91
🤖 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 `@frontend/src/api/authInfo.ts` around lines 34 - 39, Propagate fetchWrapper
failures from listUserPasskeys instead of converting them to an empty array;
update frontend/src/components/settings/AccountSettings.tsx lines 27-31 to
render React Query’s error state for failed loads, and update
frontend/src/api/authInfo.test.ts lines 83-91 to assert rejection/error handling
rather than an empty-list fallback.

Comment thread frontend/src/api/files.ts
Comment on lines +90 to +96
export async function writeFileEntry(path: string, type: 'file' | 'folder', content?: string): Promise<void> {
await fetchWrapperVoid(getFileApiUrl(path), {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type, content }),
})
}

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 | 🟡 Minor | ⚡ Quick win

Disable the default timeout for all file mutations.

writeFileEntry and renameFileEntry inherit the wrapper’s 30-second timeout, unlike upload and delete. A slow mutation can be reported as failed while the server completes it, leaving the UI stale and retries unreliable. Add timeout: 0 to both calls.

Proposed fix
 await fetchWrapperVoid(getFileApiUrl(path), {
   method: 'PUT',
   headers: { 'Content-Type': 'application/json' },
   body: JSON.stringify({ type, content }),
+  timeout: 0,
 })

 await fetchWrapperVoid(getFileApiUrl(oldPath), {
   method: 'PATCH',
   headers: { 'Content-Type': 'application/json' },
   body: JSON.stringify({ newPath }),
+  timeout: 0,
 })

Also applies to: 102-108

🤖 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 `@frontend/src/api/files.ts` around lines 90 - 96, Update writeFileEntry and
renameFileEntry to pass timeout: 0 in their fetchWrapperVoid request options,
disabling the default timeout for both file mutations while preserving their
existing methods, headers, and bodies.

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