Skip to content

fix(core): lazy-load @napi-rs/keyring so a missing native binding can't kill the launcher - #1943

Open
olaservo wants to merge 1 commit into
v2/mainfrom
fix/1852-keyring-lazy-load
Open

fix(core): lazy-load @napi-rs/keyring so a missing native binding can't kill the launcher#1943
olaservo wants to merge 1 commit into
v2/mainfrom
fix/1852-keyring-lazy-load

Conversation

@olaservo

@olaservo olaservo commented Aug 6, 2026

Copy link
Copy Markdown
Member

Closes #1852

Problem

core/auth/node/secret-store.ts imported @napi-rs/keyring eagerly at module top level. The module's loader throws at require time when the platform binary package (an npm optionalDependency, e.g. @napi-rs/keyring-win32-x64-msvc) is missing — which is exactly what npm's optional-deps bug (npm/cli#4828) produces, typically via an in-place npx cache upgrade ("worked before today's update"). Because the import chain runs at backend startup (bundled into the web runner, CLI, and TUI alike), the whole launcher died before any UI existed:

Error running MCP Inspector: Cannot find native binding. npm has a bug related to optional dependencies…

Reproduced on Windows 11 against published 2.1.0: install → delete node_modules/@napi-rs/keyring-win32-x64-msvc → run → exact reported error; restore the package → boots fine.

Fix

  • Load @napi-rs/keyring lazily on first use (cached dynamic import; the package stays external in every bundler config, so this is a plain runtime require). A load failure engages the store's documented availability contract instead of crashing the process: getnull, delete/deleteAllForServer → no-op, and only set throws KeychainUnavailableError (translated to a 503 by the routes; migratePlaintextSecrets skip branch engages as designed).
  • AsyncEntry construction moves inside the existing try blocks, so the constructor throwing on a box with no reachable secret service degrades the same way instead of escaping raw — this is the fix described in KeyringSecretStore throws from the AsyncEntry constructor, defeating its own degradation contract (500 on GET /api/servers) #1848 (the GET /api/servers 500 in containers), implemented here because the lazy-load refactor makes them one mechanism.
  • KeychainUnavailableError now steers a missing-native-binding cause to a reinstall / clear-npx-cache hint; other causes keep the Linux libsecret advice.

Result: the Inspector always boots; secrets are simply unavailable until the install is repaired, and the user only hits a (actionable, typed) error when actually saving a secret.

Tests

  • Extended the vi.mock in secret-store.test.ts with a constructorThrows hook (the gap called out in KeyringSecretStore throws from the AsyncEntry constructor, defeating its own degradation contract (500 on GET /api/servers) #1848 — the stub constructor previously could not fail) and covered get/set/delete under it.
  • New describe simulating the module-load failure itself (fresh module instance via vi.resetModules + a rejecting vi.doMock): get null, deletes no-op, set rejects typed. The reinstall-hint wording is covered via direct construction (vitest substitutes its own message when a mock factory throws).
  • End-to-end on Windows: built launcher boots cleanly with the platform package deleted.

Gate status (run on Windows)

validate:core/web/tui/launcher, coverage:web (gates the changed core/auth files, ≥90 all dimensions), coverage:launcher, smoke (tui self-skip: no TTY), and Storybook all pass locally. The remaining npm run ci steps are currently unrunnable on any Windows machine for pre-existing reasons found and filed while validating this change: #1939 (verify:typecheck-coverage + verify:build-gate can't spawn npx), #1941 (CLI vitest worker teardown crash), #1942 (TUI coverage instrumentation flake), #1940 (.gitattributes eol churn). Linux CI on this PR is the authoritative full gate.

🤖 Generated with Claude Code

…'t kill the launcher (#1852)

The keyring module was imported eagerly at the top of secret-store.ts, so
when the platform binary package (an npm optionalDependency, e.g.
@napi-rs/keyring-win32-x64-msvc) is missing — npm's optional-deps bug
npm/cli#4828, typically hit via an in-place npx cache upgrade — the
loader's throw took down the whole launcher before any UI existed.

Load the module lazily on first use instead, and route a load failure
through the store's documented availability contract: get returns null,
delete/deleteAllForServer no-op, and only set throws
KeychainUnavailableError (503 at the routes). Entry construction also
moves inside the try blocks, so the AsyncEntry constructor throwing on a
keychain-less box (#1848) degrades the same way instead of escaping raw.

KeychainUnavailableError now steers a missing-native-binding cause to a
reinstall/npx-cache hint rather than the Linux libsecret advice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@olaservo olaservo added the v2 Issues and PRs for v2 label Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Issues and PRs for v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant