Skip to content

Add backend API compatibility contract#4

Merged
pablopunk merged 8 commits into
mainfrom
backend-api-compatibility
Jun 5, 2026
Merged

Add backend API compatibility contract#4
pablopunk merged 8 commits into
mainfrom
backend-api-compatibility

Conversation

@pablopunk
Copy link
Copy Markdown
Owner

@pablopunk pablopunk commented Jun 5, 2026

Summary

Adds the first production-safe slice of the desktop/backend compatibility lifecycle so the continuously deployed backend can remain safe for tagged Electron desktop releases.

Plan checklist

Phase 1 — Instrumentation and manifest foundation

  • Add a backend compatibility policy module.
  • Add GET /api/compatibility manifest endpoint.
  • Add backend compatibility/diagnostic response headers.
  • Add desktop version/API/platform headers to auth, token revoke, active-model, and AI proxy calls.
  • Log desktop client metadata on AI proxy usage/rate-limit paths.
  • Add tests for manifest/header parsing and unsupported-client error shape.

Phase 2 — Desktop compatibility UX

  • Add main-process compatibility checks before sign-in and AI setup.
  • Convert unsupported-client compatibility failures into the existing AI limit/update gate shape.
  • Cache the last successful manifest and refresh in place during startup.
  • Reuse the dedicated updater actions for forced-update prompts where possible.
  • Add UI-level tests for unsupported-client/deprecation-warning flows.

Phase 3 — Contract tests and release gates

  • Add backend test runner and include backend tests in root pnpm test.
  • Store full request/response fixtures for supported desktop API flows.
  • Add route-level contract tests for device auth, active-model, proxy errors, rate limits, and streaming.
  • Add release fixtures when tagging desktop versions.
  • Add CI/release checklist for backend contract changes.

Phase 4 — Feature flags and progressive rollout

  • Add backend feature flags scoped by desktop version/user/plan/rollout.
  • Return feature flags in compatibility manifest and relevant descriptors.
  • Gate new desktop behavior by explicit feature presence.
  • Add server-side kill switches for risky backend behavior.
  • Log feature evaluations for support/rollback.

Phase 5 — API major-version migration path

  • Define /api/v1/* as the stable desktop API in docs.
  • Define the formal API-major breaking-change criteria.
  • Add /api/v2 only when a real breaking change exists.
  • Track active client counts before retiring an API major.
  • Document sunset dates and user-visible update messaging.

Docs and agent workflow

  • Add src/docs/backend-api-compatibility.md.
  • Add .agents/skills/backend-api-compatibility/SKILL.md.
  • Add a concise AGENTS.md rule for future desktop/backend API changes.
  • Add backend env examples for compatibility policy values.

Verification

  • mise exec pnpm -- pnpm test
  • mise exec pnpm -- pnpm --dir backend astro check — passes with existing deprecation hints only
  • mise exec pnpm -- pnpm --dir backend test

Follow-ups / open decisions

  • Set production NEVERMIND_MIN_DESKTOP_VERSION, NEVERMIND_LATEST_DESKTOP_VERSION, and NEVERMIND_DESKTOP_UPDATE_URL.
  • Decide whether latest desktop version should be sourced from GitHub Releases, backend config, or app settings.
  • Decide whether compatibility telemetry should be stored in DB or remain log-based.

Summary by CodeRabbit

  • New Features

    • Desktop now checks backend compatibility, shows update prompts, and sends standardized client headers.
    • Backend exposes a compatibility endpoint and includes compatibility/version headers in API responses.
    • Client-side caching and notifications for compatibility manifests; update UX surfaced in desktop UI.
  • Documentation

    • Added backend API compatibility guide and PR checklist for contract changes.
  • Tests / CI

    • New contract fixtures, contract/integration tests, test helpers, and CI/test script additions.

@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nvm Ready Ready Preview, Comment Jun 5, 2026 11:13pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 5, 2026

Too many files changed? Review this PR in Change Stack to see how the pieces fit before you dive in.

Review Change Stack

Warning

Review limit reached

@pablopunk, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 28 minutes and 40 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 03e3549d-d9c4-42a8-ad1a-fd782f6b424f

📥 Commits

Reviewing files that changed from the base of the PR and between eaa8653 and aa6a64e.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • backend/.env.example
  • backend/src/fixtures/contracts/desktop-v1/compatibility-manifest.json
  • backend/src/lib/compatibility.test.ts
  • backend/src/lib/compatibility.ts
  • backend/src/lib/proxy.ts
  • backend/src/pages/api/auth/device/exchange.ts
  • backend/src/pages/api/auth/device/initiate.ts
  • backend/src/pages/api/compatibility.ts
  • backend/src/pages/api/contract-routes.test.ts
  • package.json
  • scripts/check-backend-api-major.cjs
  • src/docs/backend-api-compatibility.md
  • src/docs/backend-api-sunset-template.md
  • src/electron/ai.ts
  • src/electron/main.ts
  • src/electron/nevermind-api.ts
  • src/electron/nevermind-compatibility.ts
  • src/extension-view.test.tsx
  • src/extension-view.tsx
  • src/ui.tsx
  • src/use-ai-chat.ts

Walkthrough

This pull request implements a compatibility protocol between independently deployed desktop and backend services. The backend exports a new /api/compatibility endpoint that advertises supported API versions, minimum supported client versions, and update URLs. The desktop client can fetch this manifest to determine compatibility before executing requests. Desktop clients include standardized headers in all API calls containing client version, platform, and architecture. The backend integrates client metadata into telemetry logs for observability. Both systems handle incompatibility gracefully with HTTP 426 responses containing version information and update prompts.

Sequence Diagram(s)

sequenceDiagram
  participant ElectronClient as Electron Client
  participant CompatibilityAPI as /api/compatibility
  participant Proxy as proxyAndBill
  participant BackendUpstream as Upstream API
  ElectronClient->>CompatibilityAPI: GET /api/compatibility with nevermindDesktopHeaders
  CompatibilityAPI-->>ElectronClient: CompatibilityManifest + x-request-id + x-nevermind-backend-version
  ElectronClient->>Proxy: API request with nevermindDesktopHeaders
  Proxy->>Proxy: Strip x-request-id and x-nevermind-* before forward
  Proxy->>BackendUpstream: Forwarded request
  BackendUpstream-->>Proxy: Upstream response
  Proxy-->>ElectronClient: Response + x-request-id + x-nevermind-backend-version
Loading
🚥 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 'Add backend API compatibility contract' accurately and concisely summarizes the primary change: introducing a backend API compatibility system and contract. It is clear, specific, and directly reflects the main objective described in the PR details.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 (2)
backend/src/lib/compatibility.test.ts (1)

11-76: ⚡ Quick win

Replace anonymous test callbacks with named functions for guideline compliance.

Line 11 and the test(...) blocks use anonymous arrow callbacks; this conflicts with the TS/JS naming guideline.

Proposed refactor
-afterEach(() => {
+afterEach(function resetCompatibilityEnv() {
   delete process.env.NEVERMIND_MIN_DESKTOP_VERSION;
   delete process.env.NEVERMIND_LATEST_DESKTOP_VERSION;
   delete process.env.NEVERMIND_DESKTOP_UPDATE_URL;
 });
 
-test('parses desktop compatibility headers', () => {
+test('parses desktop compatibility headers', function parsesDesktopCompatibilityHeaders() {
   // ...
 });

As per coding guidelines: "**/*.{ts,tsx,js}: Prefer named functions over anonymous arrow functions."

🤖 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 `@backend/src/lib/compatibility.test.ts` around lines 11 - 76, Replace
anonymous arrow callbacks in the test file with named functions: convert the
afterEach callback and each test(...) arrow callback (e.g., the callbacks for
"parses desktop compatibility headers", "keeps older clients without headers
compatible by default", "detects unsupported desktop versions and API versions",
"compares semver-like desktop versions", and "returns a stable
unsupported-client error shape") into separately declared named functions (e.g.,
function afterEachCleanup(), function parsesDesktopCompatibilityHeaders(),
function keepsOlderClientsCompatible(), function
detectsUnsupportedDesktopAndAPI(), function comparesSemverLikeDesktopVersions(),
function returnsStableUnsupportedClientErrorShape()) and pass those function
identifiers to afterEach and test instead of inline arrows; ensure async tests
remain async function declarations so existing awaits work and keep the current
assertions and environment setup/teardown logic unchanged.
backend/src/lib/compatibility.ts (1)

109-113: 💤 Low value

Consider removing the type cast for better type safety.

Line 110 requires as 1 because SUPPORTED_API_VERSIONS is typed as readonly [1] (literal type), but this creates a type safety gap. Consider one of these alternatives:

♻️ Alternative approaches

Option 1: Use .some() for clearer intent and better type inference:

-  if (client.apiVersion !== null && !SUPPORTED_API_VERSIONS.includes(client.apiVersion as 1)) return 'unsupported_api_version';
+  if (client.apiVersion !== null && !SUPPORTED_API_VERSIONS.some(v => v === client.apiVersion)) return 'unsupported_api_version';

Option 2: Type the constant as readonly number[] instead of using as const:

-export const SUPPORTED_API_VERSIONS = [1] as const;
+export const SUPPORTED_API_VERSIONS: readonly number[] = [1];

Then remove the cast:

-  if (client.apiVersion !== null && !SUPPORTED_API_VERSIONS.includes(client.apiVersion as 1)) return 'unsupported_api_version';
+  if (client.apiVersion !== null && !SUPPORTED_API_VERSIONS.includes(client.apiVersion)) return 'unsupported_api_version';
🤖 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 `@backend/src/lib/compatibility.ts` around lines 109 - 113, In
unsupportedClientReason, remove the unsafe cast "as 1" and update the
API-version membership check to avoid literal typing on SUPPORTED_API_VERSIONS;
for example, change the check to use SUPPORTED_API_VERSIONS.some(v => v ===
client.apiVersion) (or alternatively change SUPPORTED_API_VERSIONS from a
readonly tuple to readonly number[] and keep includes) so the comparison is
type-safe and no cast is required, referencing the function
unsupportedClientReason and the SUPPORTED_API_VERSIONS constant.
🤖 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 `@package.json`:
- Line 14: The root "test" script in package.json runs backend tests via "pnpm
--dir backend test" but CI only installs deps at repo root (mise exec -- pnpm
install --frozen-lockfile) and there is no pnpm workspace wiring, so backend's
devDependency tsx won't be installed; fix by either adding a pnpm-workspace.yaml
(and ensure backend is included) so root install populates backend/node_modules,
or modify the root test workflow/script to run "pnpm --dir backend install
--frozen-lockfile" before "pnpm --dir backend test" so backend devDependencies
(e.g., tsx from backend/package.json) are installed prior to testing.

In `@src/electron/ai.ts`:
- Around line 410-417: The code currently sets dashboardUrl in the
'unsupported_client' response by calling updateUrlFromErrorText(text), which
trusts arbitrary error text and can produce a malicious download link; change
this to never trust extracted URLs — either remove updateUrlFromErrorText(text)
and always use NEVERMIND_UPDATE_URL, or validate the extracted URL against a
strict allowlist/expected host(s) before using it; update the code paths around
the 'unsupported_client' return (the object with kind:'unsupported_client') and
the updateUrlFromErrorText usage to perform that validation or fall back to
NEVERMIND_UPDATE_URL.

In `@src/electron/nevermind-compatibility.ts`:
- Around line 33-40: The compatibility fetch in checkNevermindCompatibility
currently has no timeout and can hang; modify checkNevermindCompatibility to use
an AbortController (create controller and pass controller.signal to fetch),
start a timer (e.g., 5s or configurable) that calls controller.abort() when
elapsed, and clear the timer after fetch completes; ensure the catch handles
abort errors appropriately (log a timeout-specific warning) and keep existing
behaviour of returning null on failure — reference the function name
checkNevermindCompatibility, the local variables trimmed and res, and the fetch
call to `${trimmed}/api/compatibility`.

---

Nitpick comments:
In `@backend/src/lib/compatibility.test.ts`:
- Around line 11-76: Replace anonymous arrow callbacks in the test file with
named functions: convert the afterEach callback and each test(...) arrow
callback (e.g., the callbacks for "parses desktop compatibility headers", "keeps
older clients without headers compatible by default", "detects unsupported
desktop versions and API versions", "compares semver-like desktop versions", and
"returns a stable unsupported-client error shape") into separately declared
named functions (e.g., function afterEachCleanup(), function
parsesDesktopCompatibilityHeaders(), function keepsOlderClientsCompatible(),
function detectsUnsupportedDesktopAndAPI(), function
comparesSemverLikeDesktopVersions(), function
returnsStableUnsupportedClientErrorShape()) and pass those function identifiers
to afterEach and test instead of inline arrows; ensure async tests remain async
function declarations so existing awaits work and keep the current assertions
and environment setup/teardown logic unchanged.

In `@backend/src/lib/compatibility.ts`:
- Around line 109-113: In unsupportedClientReason, remove the unsafe cast "as 1"
and update the API-version membership check to avoid literal typing on
SUPPORTED_API_VERSIONS; for example, change the check to use
SUPPORTED_API_VERSIONS.some(v => v === client.apiVersion) (or alternatively
change SUPPORTED_API_VERSIONS from a readonly tuple to readonly number[] and
keep includes) so the comparison is type-safe and no cast is required,
referencing the function unsupportedClientReason and the SUPPORTED_API_VERSIONS
constant.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 220c3099-848f-4317-98cd-14f381165ad4

📥 Commits

Reviewing files that changed from the base of the PR and between 1266535 and e3d5380.

📒 Files selected for processing (16)
  • .agents/skills/backend-api-compatibility/SKILL.md
  • AGENTS.md
  • backend/.env.example
  • backend/package.json
  • backend/src/lib/compatibility.test.ts
  • backend/src/lib/compatibility.ts
  • backend/src/lib/proxy.ts
  • backend/src/pages/api/compatibility.ts
  • backend/src/pages/api/health.ts
  • backend/src/pages/api/v1/active-model.ts
  • package.json
  • src/docs/backend-api-compatibility.md
  • src/electron/ai.ts
  • src/electron/nevermind-api.ts
  • src/electron/nevermind-auth.ts
  • src/electron/nevermind-compatibility.ts

Comment thread package.json Outdated
Comment thread src/electron/ai.ts
Comment thread src/electron/nevermind-compatibility.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (1)
src/use-ai-chat.ts (1)

7-10: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate data.action when parsing limit events.

AiLimitState now carries action, but limitStateFromEvent only validates title/message. Add a minimal runtime guard so malformed action payloads don’t propagate.

Proposed fix
 function limitStateFromEvent(event: AiChatEvent): AiLimitState | null {
   const data = event.data as AiLimitState | undefined
   if (!data || typeof data !== 'object' || !data.title || !data.message) return null
-  return data
+  if (data.action && (typeof data.action !== 'object' || !data.action.type || !data.action.title)) {
+    return { ...data, action: undefined }
+  }
+  return data
 }

As per coding guidelines, "Keep every action/search/view payload that crosses Electron IPC structuredClone-safe; strip handlers/functions after registering them and add clone-safety checks for new payload shapes."

🤖 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 `@src/use-ai-chat.ts` around lines 7 - 10, The limitStateFromEvent helper
currently only checks title/message and can let malformed actions through;
update limitStateFromEvent to validate data.action too by confirming event.data
(AiLimitState) has an action property that is clone-safe (e.g. typeof
data.action === 'string' and not a function) and meets any expected shape
(non-empty string or specific allowed values if available); if the action check
fails, return null so malformed payloads don't propagate. Ensure you modify the
limitStateFromEvent function and its local variable `data` checks to include
this guard.
♻️ Duplicate comments (1)
src/electron/nevermind-compatibility.ts (1)

90-93: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a timeout to compatibility manifest fetch.

Line 92 performs an unbounded network call in a preflight path; a hung backend can block sign-in/AI flows indefinitely.

Proposed fix
 async function fetchCompatibilityManifest(baseUrl: string) {
   const trimmed = normalizeBaseUrl(baseUrl)
-  const res = await fetch(`${trimmed}/api/compatibility`, { headers: nevermindDesktopHeaders() })
+  const controller = new AbortController()
+  const timeout = setTimeout(() => controller.abort(), 5_000)
+  let res: Response
+  try {
+    res = await fetch(`${trimmed}/api/compatibility`, {
+      headers: nevermindDesktopHeaders(),
+      signal: controller.signal,
+    })
+  } finally {
+    clearTimeout(timeout)
+  }
   if (res.status === 404) return null
🤖 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 `@src/electron/nevermind-compatibility.ts` around lines 90 - 93, The
fetchCompatibilityManifest function makes an unbounded fetch to
`${trimmed}/api/compatibility`; wrap that fetch in an AbortController-based
timeout (e.g., 3-5s) so the request is aborted if it hangs: create an
AbortController, pass controller.signal to fetch in fetchCompatibilityManifest,
set a timer to call controller.abort() after the timeout, and clear the timer
after fetch completes; catch the abort/timeout error and return null (same as
handling 404) so sign-in/AI preflight won't block indefinitely. Ensure you
reference normalizeBaseUrl and nevermindDesktopHeaders unchanged and clean up
the timer in all code paths.
🧹 Nitpick comments (2)
backend/src/pages/api/compatibility.ts (1)

4-9: ⚡ Quick win

Use a named handler for the route export.

Convert the anonymous arrow at Line 4 to a named function expression to align with the repository convention.

Proposed refactor
-export const GET: APIRoute = ({ request }) => {
+export const GET: APIRoute = function GET({ request }) {
   const requestId = requestIdFromHeaders(request.headers);
   return Response.json(compatibilityManifestForRequest(request, { requestId, route: 'compatibility' }), {
     headers: compatibilityHeaders(requestId),
   });
 };

As per coding guidelines, **/*.{ts,tsx,js,jsx}: Prefer named functions over anonymous arrow functions.

🤖 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 `@backend/src/pages/api/compatibility.ts` around lines 4 - 9, Replace the
anonymous arrow exported as GET with a named function expression (e.g., export
const GET: APIRoute = function getCompatibility({ request }) { ... }) so the
handler has an explicit name; keep the same implementation using
requestIdFromHeaders(request.headers), compatibilityManifestForRequest(request,
{ requestId, route: 'compatibility' }), and compatibilityHeaders(requestId), and
preserve the APIRoute type and returned Response.json call.
backend/src/pages/api/contract-routes.test.ts (1)

48-50: 💤 Low value

Consider typing the route context more precisely.

The as any assertion bypasses type checking and could hide signature changes. Consider defining a minimal typed context that matches the actual route handler requirements.

♻️ Optional: Add a typed context interface
+type MinimalAPIContext = Pick<import('astro').APIContext, 'request' | 'url'>;
+
 function routeContext(request: Request, url = new URL(request.url)) {
-  return { request, url } as any;
+  return { request, url } as MinimalAPIContext;
 }
🤖 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 `@backend/src/pages/api/contract-routes.test.ts` around lines 48 - 50, The test
helper routeContext currently returns an untyped any which hides signature
mismatches; define a small typed interface (e.g., RouteContext { request:
Request; url: URL; /* add other fields the handlers expect */ }) and change the
function routeContext(request: Request, url = new URL(request.url)) to return
that type instead of using `as any` (replace `as any` with the concrete
interface or explicit return type) so the route handler tests get proper
compile-time checks.
🤖 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 `@backend/src/lib/compatibility.ts`:
- Line 85: The API-version parsing currently uses parsePositiveInteger which
maps non-positive values (e.g. 0) to null and therefore bypasses the
compatibility gate; update the parsing so that non-positive values are preserved
as numeric (not converted to null) so they are treated as explicitly invalid by
the enforcement logic: replace the parsePositiveInteger(...) call used to set
apiVersion (the expression using request.headers.get('x-nevermind-api-version'))
with a parseInteger(...) or equivalent parse that returns the numeric value
(including 0 or negatives) and ensure you do not coerce non-positive numbers
back to null; apply the same change to the other occurrences referenced around
parsePositiveInteger usage (the blocks around the apiVersion assignment and the
checks at lines noted in the comment) so the compatibility checks see an
explicit invalid version instead of a missing one.

In `@package.json`:
- Line 45: The dependency "module-details-from-path" is present in package.json
but not imported anywhere; search the codebase for any require/import of
module-details-from-path (or dynamic uses) and if there are none remove
"module-details-from-path" from package.json and update lockfile (pnpm)
accordingly; if it is required at runtime (e.g., used by an entrypoint script or
dynamically loaded), add a comment in package.json or add the explicit
import/require in the appropriate bootstrap/entrypoint (refer to the entrypoint
file or startup function that needs module-details-from-path) and document why
it’s required so the dependency is justified.

In `@src/electron/main.ts`:
- Around line 1238-1240: The call to currentNevermindCompatibilityManifest()
must be scoped to the active backend by passing its baseUrl so we don't pick
manifests from other cached backends; change the call to
currentNevermindCompatibilityManifest(activeBaseUrl) (or
currentNevermindCompatibilityManifest(getActiveBackendBaseUrl()) /
currentNevermindCompatibilityManifest(activeBackend.baseUrl) depending on which
symbol exists) and ensure the same change is applied at the other site that
calls currentNevermindCompatibilityManifest so the “Update Nevermind” prompt
uses the active backend's manifest.
- Around line 1228-1235: The updateActionForCompatibilityPrompt function
currently returns an openUrl action using backend-provided updateUrl without
validating the scheme; validate updateUrl before returning an {type: 'openUrl',
...} action by parsing it (new URL(updateUrl)) and allowing only http: or https:
schemes (reject other protocols or malformed URLs), and return undefined if
validation fails; update references: updateActionForCompatibilityPrompt,
updateUrl, and the openUrl action so only safe URLs reach shell.openExternal.

In `@src/electron/nevermind-api.ts`:
- Around line 5-13: nevermindDesktopHeaders currently spreads caller-supplied
headers last, allowing callers to override contract fields; update
nevermindDesktopHeaders to prevent overriding the reserved keys
('X-Nevermind-Client','X-Nevermind-Client-Version','X-Nevermind-API-Version','X-Nevermind-Platform','X-Nevermind-Arch')
by either spreading the incoming headers first and then the fixed keys, or by
filtering those reserved keys out of the input headers before merging, so the
function always returns the canonical contract values for those symbols.

---

Outside diff comments:
In `@src/use-ai-chat.ts`:
- Around line 7-10: The limitStateFromEvent helper currently only checks
title/message and can let malformed actions through; update limitStateFromEvent
to validate data.action too by confirming event.data (AiLimitState) has an
action property that is clone-safe (e.g. typeof data.action === 'string' and not
a function) and meets any expected shape (non-empty string or specific allowed
values if available); if the action check fails, return null so malformed
payloads don't propagate. Ensure you modify the limitStateFromEvent function and
its local variable `data` checks to include this guard.

---

Duplicate comments:
In `@src/electron/nevermind-compatibility.ts`:
- Around line 90-93: The fetchCompatibilityManifest function makes an unbounded
fetch to `${trimmed}/api/compatibility`; wrap that fetch in an
AbortController-based timeout (e.g., 3-5s) so the request is aborted if it
hangs: create an AbortController, pass controller.signal to fetch in
fetchCompatibilityManifest, set a timer to call controller.abort() after the
timeout, and clear the timer after fetch completes; catch the abort/timeout
error and return null (same as handling 404) so sign-in/AI preflight won't block
indefinitely. Ensure you reference normalizeBaseUrl and nevermindDesktopHeaders
unchanged and clean up the timer in all code paths.

---

Nitpick comments:
In `@backend/src/pages/api/compatibility.ts`:
- Around line 4-9: Replace the anonymous arrow exported as GET with a named
function expression (e.g., export const GET: APIRoute = function
getCompatibility({ request }) { ... }) so the handler has an explicit name; keep
the same implementation using requestIdFromHeaders(request.headers),
compatibilityManifestForRequest(request, { requestId, route: 'compatibility' }),
and compatibilityHeaders(requestId), and preserve the APIRoute type and returned
Response.json call.

In `@backend/src/pages/api/contract-routes.test.ts`:
- Around line 48-50: The test helper routeContext currently returns an untyped
any which hides signature mismatches; define a small typed interface (e.g.,
RouteContext { request: Request; url: URL; /* add other fields the handlers
expect */ }) and change the function routeContext(request: Request, url = new
URL(request.url)) to return that type instead of using `as any` (replace `as
any` with the concrete interface or explicit return type) so the route handler
tests get proper compile-time checks.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 37cdd9bd-b47e-4604-8588-4ac1b2d9bdc7

📥 Commits

Reviewing files that changed from the base of the PR and between e3d5380 and eaa8653.

📒 Files selected for processing (33)
  • .agents/skills/backend-api-compatibility/SKILL.md
  • .github/pull_request_template.md
  • .github/workflows/ci.yml
  • AGENTS.md
  • backend/.env.example
  • backend/package.json
  • backend/src/db/client.ts
  • backend/src/fixtures/contracts/desktop-v1/compatibility-manifest.json
  • backend/src/fixtures/contracts/desktop-v1/unsupported-client-error.json
  • backend/src/lib/compatibility.test.ts
  • backend/src/lib/compatibility.ts
  • backend/src/lib/cost.ts
  • backend/src/lib/env.ts
  • backend/src/lib/proxy.ts
  • backend/src/lib/ratelimit.ts
  • backend/src/lib/upstream.ts
  • backend/src/lib/workos.ts
  • backend/src/pages/api/auth/signin.ts
  • backend/src/pages/api/compatibility.ts
  • backend/src/pages/api/contract-routes.test.ts
  • backend/src/pages/api/health.ts
  • backend/src/pages/api/v1/active-model.ts
  • package.json
  • scripts/check-backend-contract-fixtures.cjs
  • scripts/release.sh
  • src/docs/backend-api-compatibility.md
  • src/electron/ai.ts
  • src/electron/main.ts
  • src/electron/nevermind-api.ts
  • src/electron/nevermind-auth.ts
  • src/electron/nevermind-compatibility.ts
  • src/extension-view.tsx
  • src/use-ai-chat.ts
✅ Files skipped from review due to trivial changes (9)
  • backend/src/fixtures/contracts/desktop-v1/unsupported-client-error.json
  • backend/src/lib/env.ts
  • backend/src/pages/api/auth/signin.ts
  • .github/pull_request_template.md
  • scripts/release.sh
  • AGENTS.md
  • backend/src/fixtures/contracts/desktop-v1/compatibility-manifest.json
  • .agents/skills/backend-api-compatibility/SKILL.md
  • backend/src/lib/compatibility.test.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • backend/src/pages/api/v1/active-model.ts
  • backend/package.json
  • backend/src/pages/api/health.ts
  • backend/src/lib/proxy.ts
  • src/docs/backend-api-compatibility.md
  • src/electron/nevermind-auth.ts
  • src/electron/ai.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (1)
src/use-ai-chat.ts (1)

7-10: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate data.action when parsing limit events.

AiLimitState now carries action, but limitStateFromEvent only validates title/message. Add a minimal runtime guard so malformed action payloads don’t propagate.

Proposed fix
 function limitStateFromEvent(event: AiChatEvent): AiLimitState | null {
   const data = event.data as AiLimitState | undefined
   if (!data || typeof data !== 'object' || !data.title || !data.message) return null
-  return data
+  if (data.action && (typeof data.action !== 'object' || !data.action.type || !data.action.title)) {
+    return { ...data, action: undefined }
+  }
+  return data
 }

As per coding guidelines, "Keep every action/search/view payload that crosses Electron IPC structuredClone-safe; strip handlers/functions after registering them and add clone-safety checks for new payload shapes."

🤖 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 `@src/use-ai-chat.ts` around lines 7 - 10, The limitStateFromEvent helper
currently only checks title/message and can let malformed actions through;
update limitStateFromEvent to validate data.action too by confirming event.data
(AiLimitState) has an action property that is clone-safe (e.g. typeof
data.action === 'string' and not a function) and meets any expected shape
(non-empty string or specific allowed values if available); if the action check
fails, return null so malformed payloads don't propagate. Ensure you modify the
limitStateFromEvent function and its local variable `data` checks to include
this guard.
♻️ Duplicate comments (1)
src/electron/nevermind-compatibility.ts (1)

90-93: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a timeout to compatibility manifest fetch.

Line 92 performs an unbounded network call in a preflight path; a hung backend can block sign-in/AI flows indefinitely.

Proposed fix
 async function fetchCompatibilityManifest(baseUrl: string) {
   const trimmed = normalizeBaseUrl(baseUrl)
-  const res = await fetch(`${trimmed}/api/compatibility`, { headers: nevermindDesktopHeaders() })
+  const controller = new AbortController()
+  const timeout = setTimeout(() => controller.abort(), 5_000)
+  let res: Response
+  try {
+    res = await fetch(`${trimmed}/api/compatibility`, {
+      headers: nevermindDesktopHeaders(),
+      signal: controller.signal,
+    })
+  } finally {
+    clearTimeout(timeout)
+  }
   if (res.status === 404) return null
🤖 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 `@src/electron/nevermind-compatibility.ts` around lines 90 - 93, The
fetchCompatibilityManifest function makes an unbounded fetch to
`${trimmed}/api/compatibility`; wrap that fetch in an AbortController-based
timeout (e.g., 3-5s) so the request is aborted if it hangs: create an
AbortController, pass controller.signal to fetch in fetchCompatibilityManifest,
set a timer to call controller.abort() after the timeout, and clear the timer
after fetch completes; catch the abort/timeout error and return null (same as
handling 404) so sign-in/AI preflight won't block indefinitely. Ensure you
reference normalizeBaseUrl and nevermindDesktopHeaders unchanged and clean up
the timer in all code paths.
🧹 Nitpick comments (2)
backend/src/pages/api/compatibility.ts (1)

4-9: ⚡ Quick win

Use a named handler for the route export.

Convert the anonymous arrow at Line 4 to a named function expression to align with the repository convention.

Proposed refactor
-export const GET: APIRoute = ({ request }) => {
+export const GET: APIRoute = function GET({ request }) {
   const requestId = requestIdFromHeaders(request.headers);
   return Response.json(compatibilityManifestForRequest(request, { requestId, route: 'compatibility' }), {
     headers: compatibilityHeaders(requestId),
   });
 };

As per coding guidelines, **/*.{ts,tsx,js,jsx}: Prefer named functions over anonymous arrow functions.

🤖 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 `@backend/src/pages/api/compatibility.ts` around lines 4 - 9, Replace the
anonymous arrow exported as GET with a named function expression (e.g., export
const GET: APIRoute = function getCompatibility({ request }) { ... }) so the
handler has an explicit name; keep the same implementation using
requestIdFromHeaders(request.headers), compatibilityManifestForRequest(request,
{ requestId, route: 'compatibility' }), and compatibilityHeaders(requestId), and
preserve the APIRoute type and returned Response.json call.
backend/src/pages/api/contract-routes.test.ts (1)

48-50: 💤 Low value

Consider typing the route context more precisely.

The as any assertion bypasses type checking and could hide signature changes. Consider defining a minimal typed context that matches the actual route handler requirements.

♻️ Optional: Add a typed context interface
+type MinimalAPIContext = Pick<import('astro').APIContext, 'request' | 'url'>;
+
 function routeContext(request: Request, url = new URL(request.url)) {
-  return { request, url } as any;
+  return { request, url } as MinimalAPIContext;
 }
🤖 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 `@backend/src/pages/api/contract-routes.test.ts` around lines 48 - 50, The test
helper routeContext currently returns an untyped any which hides signature
mismatches; define a small typed interface (e.g., RouteContext { request:
Request; url: URL; /* add other fields the handlers expect */ }) and change the
function routeContext(request: Request, url = new URL(request.url)) to return
that type instead of using `as any` (replace `as any` with the concrete
interface or explicit return type) so the route handler tests get proper
compile-time checks.
🤖 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 `@backend/src/lib/compatibility.ts`:
- Line 85: The API-version parsing currently uses parsePositiveInteger which
maps non-positive values (e.g. 0) to null and therefore bypasses the
compatibility gate; update the parsing so that non-positive values are preserved
as numeric (not converted to null) so they are treated as explicitly invalid by
the enforcement logic: replace the parsePositiveInteger(...) call used to set
apiVersion (the expression using request.headers.get('x-nevermind-api-version'))
with a parseInteger(...) or equivalent parse that returns the numeric value
(including 0 or negatives) and ensure you do not coerce non-positive numbers
back to null; apply the same change to the other occurrences referenced around
parsePositiveInteger usage (the blocks around the apiVersion assignment and the
checks at lines noted in the comment) so the compatibility checks see an
explicit invalid version instead of a missing one.

In `@package.json`:
- Line 45: The dependency "module-details-from-path" is present in package.json
but not imported anywhere; search the codebase for any require/import of
module-details-from-path (or dynamic uses) and if there are none remove
"module-details-from-path" from package.json and update lockfile (pnpm)
accordingly; if it is required at runtime (e.g., used by an entrypoint script or
dynamically loaded), add a comment in package.json or add the explicit
import/require in the appropriate bootstrap/entrypoint (refer to the entrypoint
file or startup function that needs module-details-from-path) and document why
it’s required so the dependency is justified.

In `@src/electron/main.ts`:
- Around line 1238-1240: The call to currentNevermindCompatibilityManifest()
must be scoped to the active backend by passing its baseUrl so we don't pick
manifests from other cached backends; change the call to
currentNevermindCompatibilityManifest(activeBaseUrl) (or
currentNevermindCompatibilityManifest(getActiveBackendBaseUrl()) /
currentNevermindCompatibilityManifest(activeBackend.baseUrl) depending on which
symbol exists) and ensure the same change is applied at the other site that
calls currentNevermindCompatibilityManifest so the “Update Nevermind” prompt
uses the active backend's manifest.
- Around line 1228-1235: The updateActionForCompatibilityPrompt function
currently returns an openUrl action using backend-provided updateUrl without
validating the scheme; validate updateUrl before returning an {type: 'openUrl',
...} action by parsing it (new URL(updateUrl)) and allowing only http: or https:
schemes (reject other protocols or malformed URLs), and return undefined if
validation fails; update references: updateActionForCompatibilityPrompt,
updateUrl, and the openUrl action so only safe URLs reach shell.openExternal.

In `@src/electron/nevermind-api.ts`:
- Around line 5-13: nevermindDesktopHeaders currently spreads caller-supplied
headers last, allowing callers to override contract fields; update
nevermindDesktopHeaders to prevent overriding the reserved keys
('X-Nevermind-Client','X-Nevermind-Client-Version','X-Nevermind-API-Version','X-Nevermind-Platform','X-Nevermind-Arch')
by either spreading the incoming headers first and then the fixed keys, or by
filtering those reserved keys out of the input headers before merging, so the
function always returns the canonical contract values for those symbols.

---

Outside diff comments:
In `@src/use-ai-chat.ts`:
- Around line 7-10: The limitStateFromEvent helper currently only checks
title/message and can let malformed actions through; update limitStateFromEvent
to validate data.action too by confirming event.data (AiLimitState) has an
action property that is clone-safe (e.g. typeof data.action === 'string' and not
a function) and meets any expected shape (non-empty string or specific allowed
values if available); if the action check fails, return null so malformed
payloads don't propagate. Ensure you modify the limitStateFromEvent function and
its local variable `data` checks to include this guard.

---

Duplicate comments:
In `@src/electron/nevermind-compatibility.ts`:
- Around line 90-93: The fetchCompatibilityManifest function makes an unbounded
fetch to `${trimmed}/api/compatibility`; wrap that fetch in an
AbortController-based timeout (e.g., 3-5s) so the request is aborted if it
hangs: create an AbortController, pass controller.signal to fetch in
fetchCompatibilityManifest, set a timer to call controller.abort() after the
timeout, and clear the timer after fetch completes; catch the abort/timeout
error and return null (same as handling 404) so sign-in/AI preflight won't block
indefinitely. Ensure you reference normalizeBaseUrl and nevermindDesktopHeaders
unchanged and clean up the timer in all code paths.

---

Nitpick comments:
In `@backend/src/pages/api/compatibility.ts`:
- Around line 4-9: Replace the anonymous arrow exported as GET with a named
function expression (e.g., export const GET: APIRoute = function
getCompatibility({ request }) { ... }) so the handler has an explicit name; keep
the same implementation using requestIdFromHeaders(request.headers),
compatibilityManifestForRequest(request, { requestId, route: 'compatibility' }),
and compatibilityHeaders(requestId), and preserve the APIRoute type and returned
Response.json call.

In `@backend/src/pages/api/contract-routes.test.ts`:
- Around line 48-50: The test helper routeContext currently returns an untyped
any which hides signature mismatches; define a small typed interface (e.g.,
RouteContext { request: Request; url: URL; /* add other fields the handlers
expect */ }) and change the function routeContext(request: Request, url = new
URL(request.url)) to return that type instead of using `as any` (replace `as
any` with the concrete interface or explicit return type) so the route handler
tests get proper compile-time checks.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 37cdd9bd-b47e-4604-8588-4ac1b2d9bdc7

📥 Commits

Reviewing files that changed from the base of the PR and between e3d5380 and eaa8653.

📒 Files selected for processing (33)
  • .agents/skills/backend-api-compatibility/SKILL.md
  • .github/pull_request_template.md
  • .github/workflows/ci.yml
  • AGENTS.md
  • backend/.env.example
  • backend/package.json
  • backend/src/db/client.ts
  • backend/src/fixtures/contracts/desktop-v1/compatibility-manifest.json
  • backend/src/fixtures/contracts/desktop-v1/unsupported-client-error.json
  • backend/src/lib/compatibility.test.ts
  • backend/src/lib/compatibility.ts
  • backend/src/lib/cost.ts
  • backend/src/lib/env.ts
  • backend/src/lib/proxy.ts
  • backend/src/lib/ratelimit.ts
  • backend/src/lib/upstream.ts
  • backend/src/lib/workos.ts
  • backend/src/pages/api/auth/signin.ts
  • backend/src/pages/api/compatibility.ts
  • backend/src/pages/api/contract-routes.test.ts
  • backend/src/pages/api/health.ts
  • backend/src/pages/api/v1/active-model.ts
  • package.json
  • scripts/check-backend-contract-fixtures.cjs
  • scripts/release.sh
  • src/docs/backend-api-compatibility.md
  • src/electron/ai.ts
  • src/electron/main.ts
  • src/electron/nevermind-api.ts
  • src/electron/nevermind-auth.ts
  • src/electron/nevermind-compatibility.ts
  • src/extension-view.tsx
  • src/use-ai-chat.ts
✅ Files skipped from review due to trivial changes (9)
  • backend/src/fixtures/contracts/desktop-v1/unsupported-client-error.json
  • backend/src/lib/env.ts
  • backend/src/pages/api/auth/signin.ts
  • .github/pull_request_template.md
  • scripts/release.sh
  • AGENTS.md
  • backend/src/fixtures/contracts/desktop-v1/compatibility-manifest.json
  • .agents/skills/backend-api-compatibility/SKILL.md
  • backend/src/lib/compatibility.test.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • backend/src/pages/api/v1/active-model.ts
  • backend/package.json
  • backend/src/pages/api/health.ts
  • backend/src/lib/proxy.ts
  • src/docs/backend-api-compatibility.md
  • src/electron/nevermind-auth.ts
  • src/electron/ai.ts
🛑 Comments failed to post (5)
backend/src/lib/compatibility.ts (1)

85-85: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Invalid API version values can bypass the compatibility gate.

At Line 227, non-positive versions (for example 0) are converted to null, so Line 134 skips API-version enforcement. An explicitly invalid API version should be treated as unsupported, not as “missing”.

Proposed fix
-    apiVersion: parsePositiveInteger(request.headers.get('x-nevermind-api-version')),
+    apiVersion: parseInteger(request.headers.get('x-nevermind-api-version')),
...
-function parsePositiveInteger(value: string | null) {
+function parseInteger(value: string | null) {
   const parsed = Number.parseInt(value || '', 10);
-  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
+  return Number.isFinite(parsed) ? parsed : null;
 }

Also applies to: 133-136, 225-227

🤖 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 `@backend/src/lib/compatibility.ts` at line 85, The API-version parsing
currently uses parsePositiveInteger which maps non-positive values (e.g. 0) to
null and therefore bypasses the compatibility gate; update the parsing so that
non-positive values are preserved as numeric (not converted to null) so they are
treated as explicitly invalid by the enforcement logic: replace the
parsePositiveInteger(...) call used to set apiVersion (the expression using
request.headers.get('x-nevermind-api-version')) with a parseInteger(...) or
equivalent parse that returns the numeric value (including 0 or negatives) and
ensure you do not coerce non-positive numbers back to null; apply the same
change to the other occurrences referenced around parsePositiveInteger usage
(the blocks around the apiVersion assignment and the checks at lines noted in
the comment) so the compatibility checks see an explicit invalid version instead
of a missing one.
package.json (1)

45-45: ⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for imports or requires of module-details-from-path

rg -n "module-details-from-path" --type ts --type js -C2

Repository: pablopunk/nvm

Length of output: 39


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Confirm presence in package.json around the cited line
echo "== package.json context =="
python3 - <<'PY'
import itertools, json
path="package.json"
with open(path,'r',encoding='utf-8') as f:
    for i,line in enumerate(f, start=1):
        if i>=35 and i<=60:
            print(f"{i}:{line.rstrip()}")
PY

# 2) Search for direct usage across more file types
echo "== rg usage across repo (all file types) =="
rg -n "module-details-from-path" -C2 || true

# 3) Search for related strings that might indicate indirect usage
echo "== rg 'details-from-path' =="
rg -n "details-from-path" -C2 || true

# 4) Check lockfiles for transitive inclusion
echo "== lockfile occurrences =="
for f in pnpm-lock.yaml package-lock.json yarn.lock; do
  if [ -f "$f" ]; then
    echo "-- $f --"
    rg -n "module-details-from-path" "$f" -C1 || true
  fi
done

Repository: pablopunk/nvm

Length of output: 7169


🌐 Web query:

module-details-from-path 1.0.4 native bindings pure javascript files

💡 Result:

module-details-from-path 1.0.4 does not use native bindings; it’s implemented in pure JavaScript. Evidence: the package on npm describes its purpose and usage (parsing an absolute file path to return { name, basedir, path }) and lists only JavaScript source files (e.g., index.js) in the package contents, with no mention of native .node binaries or build steps [1]. The npm listing shows “0 Dependencies,” consistent with a small, pure-JS utility [1].

Citations:


Confirm module-details-from-path is actually needed (Electron app.asar should be fine)

  • package.json includes module-details-from-path at 1.0.4, but there are no code imports/requires of it anywhere in the repo (only package.json/pnpm-lock.yaml references).
  • The package is pure JavaScript (no native bindings/build artifacts), so it shouldn’t have app.asar compatibility issues.
  • If it isn’t used at runtime, remove it from dependencies or add a clear justification/entrypoint that requires it.
🤖 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 `@package.json` at line 45, The dependency "module-details-from-path" is
present in package.json but not imported anywhere; search the codebase for any
require/import of module-details-from-path (or dynamic uses) and if there are
none remove "module-details-from-path" from package.json and update lockfile
(pnpm) accordingly; if it is required at runtime (e.g., used by an entrypoint
script or dynamically loaded), add a comment in package.json or add the explicit
import/require in the appropriate bootstrap/entrypoint (refer to the entrypoint
file or startup function that needs module-details-from-path) and document why
it’s required so the dependency is justified.
src/electron/main.ts (2)

1228-1235: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate updateUrl before creating an openUrl action.

Line 1234 trusts backend-provided URL as-is. This allows non-HTTP(S) schemes to flow into shell.openExternal, which expands attack surface.

Proposed fix
+function safeExternalUpdateUrl(raw?: string) {
+  if (!raw) return null
+  try {
+    const parsed = new URL(raw)
+    return parsed.protocol === 'https:' || parsed.protocol === 'http:' ? parsed.toString() : null
+  } catch {
+    return null
+  }
+}
+
 function updateActionForCompatibilityPrompt(updateUrl?: string) {
+  const safeUrl = safeExternalUpdateUrl(updateUrl)
   const downloadedInfo = isNewerVersion(updateManager.state.downloadedInfo?.version) ? updateManager.state.downloadedInfo : null
   const availableInfo = isNewerVersion(updateManager.state.availableInfo?.version) ? updateManager.state.availableInfo : null
   if (downloadedInfo) return { type: 'installUpdate', title: `Install Nevermind ${downloadedInfo.version || ''}`.trim() }
   if (availableInfo) return { type: 'downloadUpdate', title: `Download Nevermind ${availableInfo.version || ''}`.trim() }
   if (updateManager.canUseAutoUpdates()) return { type: 'checkForUpdates', title: 'Check for Update' }
-  return updateUrl ? { type: 'openUrl', title: 'Download Update', url: updateUrl } : undefined
+  return safeUrl ? { type: 'openUrl', title: 'Download Update', url: safeUrl } : undefined
 }
🤖 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 `@src/electron/main.ts` around lines 1228 - 1235, The
updateActionForCompatibilityPrompt function currently returns an openUrl action
using backend-provided updateUrl without validating the scheme; validate
updateUrl before returning an {type: 'openUrl', ...} action by parsing it (new
URL(updateUrl)) and allowing only http: or https: schemes (reject other
protocols or malformed URLs), and return undefined if validation fails; update
references: updateActionForCompatibilityPrompt, updateUrl, and the openUrl
action so only safe URLs reach shell.openExternal.

1238-1240: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope compatibility prompt to the active backend base URL.

At Line 1238, calling currentNevermindCompatibilityManifest() without baseUrl selects the latest manifest across all cached backends, which can show the wrong “Update Nevermind” prompt.

Also applies to: 3161-3161

🤖 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 `@src/electron/main.ts` around lines 1238 - 1240, The call to
currentNevermindCompatibilityManifest() must be scoped to the active backend by
passing its baseUrl so we don't pick manifests from other cached backends;
change the call to currentNevermindCompatibilityManifest(activeBaseUrl) (or
currentNevermindCompatibilityManifest(getActiveBackendBaseUrl()) /
currentNevermindCompatibilityManifest(activeBackend.baseUrl) depending on which
symbol exists) and ensure the same change is applied at the other site that
calls currentNevermindCompatibilityManifest so the “Update Nevermind” prompt
uses the active backend's manifest.
src/electron/nevermind-api.ts (1)

5-13: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Lock compatibility headers so callers can’t override contract fields.

At Line 12, ...headers is merged last, so any caller can accidentally override X-Nevermind-API-Version / client metadata and break compatibility signaling.

Proposed fix
 export function nevermindDesktopHeaders(headers: Record<string, string> = {}) {
   return {
+    ...headers,
     'X-Nevermind-Client': 'desktop',
     'X-Nevermind-Client-Version': app.getVersion(),
     'X-Nevermind-API-Version': NEVERMIND_DESKTOP_API_VERSION,
     'X-Nevermind-Platform': process.platform,
     'X-Nevermind-Arch': process.arch,
-    ...headers,
   }
 }

Based on learnings: Keep desktop/backend API changes backward-compatible for supported released clients; see src/docs/backend-api-compatibility.md.

🤖 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 `@src/electron/nevermind-api.ts` around lines 5 - 13, nevermindDesktopHeaders
currently spreads caller-supplied headers last, allowing callers to override
contract fields; update nevermindDesktopHeaders to prevent overriding the
reserved keys
('X-Nevermind-Client','X-Nevermind-Client-Version','X-Nevermind-API-Version','X-Nevermind-Platform','X-Nevermind-Arch')
by either spreading the incoming headers first and then the fixed keys, or by
filtering those reserved keys out of the input headers before merging, so the
function always returns the canonical contract values for those symbols.

@pablopunk pablopunk merged commit b347c4b into main Jun 5, 2026
8 checks passed
@pablopunk pablopunk deleted the backend-api-compatibility branch June 5, 2026 23:16
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