Repository navigation
fix(auth+build): Bearer manage scope on management routes + lazy-load deepseek PoW solver - #2308
diegosouzapw merged 4 commits into
Conversation
…scope on management routes
requireManagementAuth (the active gate on 106 management routes) already
accepts Bearer keys carrying the 'manage' or 'admin' scope. The parallel
isAuthenticated helper (~58 routes, including settings/database,
v1/registered-keys, etc.) and the underlying verifyAuth used to hard-reject
ALL Bearer tokens on management routes, requiring a dashboard JWT cookie.
This split-brain forced the bundled MCP server (open-sse/mcp-server) to
authenticate with a hand-extracted browser cookie instead of its
OMNIROUTE_API_KEY, since the routes it calls go through isAuthenticated,
not requireManagementAuth.
Aligning the two helpers: a new validateBearerApiKeyForManagement() checks
the key is valid AND carries one of the same scopes accepted by
requireManagementAuth ('manage', 'admin'). Unscoped keys remain rejected
on management routes, preserving the default-deny posture.
Tests:
- bearer + manage scope → accepted (verifyAuth + isAuthenticated)
- bearer + admin scope → accepted
- bearer + no scope → still rejected with 'Invalid management token'
Closes the gap that made #2260's MCP server unusable against a remote
OmniRoute without copying a dashboard cookie into the client.
There was a problem hiding this comment.
Code Review
This pull request implements scoped API key authentication for management routes by introducing the validateBearerApiKeyForManagement function and updating the verifyAuth and isAuthenticated flows. It also adds unit tests to verify that keys with 'manage' or 'admin' scopes are correctly authorized. Feedback suggests optimizing the authentication logic to avoid redundant database and cache operations when retrieving key metadata, and centralizing the management scope constants in a shared file to prevent logic duplication across modules.
…cope to constants (PR #2308 follow-up) Address gemini-code-assist review comment 3253030964 — eliminate the duplicated management scope list between apiAuth.ts and requireManagementAuth.ts. - New src/shared/constants/managementScopes.ts exports MANAGE_SCOPE, MANAGEMENT_API_KEY_SCOPES, and hasManageScope as the single source of truth. - src/shared/utils/apiAuth.ts now imports hasManageScope from the shared module instead of redeclaring the Set inline. - src/lib/api/requireManagementAuth.ts now delegates to the shared helper; its local hasManageScope re-exports for backwards compat with existing callers (src/server/authz/policies/management.ts). Tests: 4 new PR #2308 tests still pass; pre-existing 13 failures in requireManagementAuth.* unchanged (Next.js headers() outside request scope — unrelated to this patch).
…ide next/headers scope Previously isCliTokenAuthValid called headers() from next/headers, which throws when invoked outside a server-component request scope (e.g. raw fetch tests, custom middleware paths). This caused 6 pre-existing requireManagementAuth.* test failures and would also affect any callers hitting these helpers from non-Next contexts. Added readHeader(request, name) helper that: - prefers request.headers (always present in tests + raw fetch) - falls back to next/headers headers() wrapped in try/catch for the Next request-scope path isCliTokenAuthValid now reads x-omniroute-cli-token, x-forwarded-for, and x-real-ip via readHeader. Behaviour identical inside a Next request scope; now also works in plain tests.
The module-load-time require(join(__dirname, 'deepseek-pow-solver.cjs')) causes the Next.js standalone build's page-data collector to fail at /api/evals with 'Cannot find module .../deepseek-pow-solver.cjs' because webpack's static analyzer cannot resolve the runtime-computed path before file-tracing emits the .cjs to the standalone output. Moving the require inside a memoized loadU() helper lets module load succeed without touching the filesystem, and the solver is materialized only when solveDeepSeekPow() is actually invoked (by then file-tracing has copied the .cjs alongside).
|
@diegosouzapw — ready for review when you have a minute 🙏 TLDR: Two-part v3.8.0 fix:
Approach safety:
Happy to split into two PRs again or rework the approach if you'd prefer a different shape — just flag what works best for v3.8.0. |
ec138c6
into
cheaper-inference:release/v3.8.0
|
Thanks @mrmm for these crucial fixes! The MCP remote usage is a huge quality-of-life win, and fixing the Docker build is essential for the v3.8.0 release. Awesome work. |
- Add auth+build fix entry to [Unreleased] - Update @mrmm PR count (2 -> 3)
… deepseek PoW solver (cheaper-inference#2308) Integrated into release/v3.8.0
- Add auth+build fix entry to [Unreleased] - Update @mrmm PR count (2 -> 3)
… deepseek PoW solver (cheaper-inference#2308) Integrated into release/v3.8.0
- Add auth+build fix entry to [Unreleased] - Update @mrmm PR count (2 -> 3)
… deepseek PoW solver (cheaper-inference#2308) Integrated into release/v3.8.0
- Add auth+build fix entry to [Unreleased] - Update @mrmm PR count (2 -> 3)
Summary
Two related v3.8.0 fixes bundled together so the management-auth path can actually be exercised against a freshly-built Docker image:
isAuthenticated/verifyAuthinsrc/shared/utils/apiAuth.tsnow accept Bearer API keys withmanage(oradmin) scope on management routes, closing a split-brain where 58 routes hard-rejected what 106 sibling routes already accepted.open-sse/lib/deepseek-pow.tsnow lazy-loadsdeepseek-pow-solver.cjsinstead ofrequire()-ing it at module load, so the Next.js standalone build can collect page data for/api/evalswithout crashing.(2) was previously tracked as #2307; merged in here because (1) cannot be verified locally without a working Docker build, and shipping them together avoids "fix A, can't rebuild, fix B, rebuild, verify A" round-tripping.
Part 1 — Management auth Bearer scope
Problem
The MCP server (
open-sse/mcp-server/server.ts) and other Bearer-only callers cannot reach management endpoints (/api/usage/call-logs,/api/settings,/api/v1/registered-keys, monitoring, …) on a remote OmniRoute instance withrequireLogin: true. The only workaround today is local-only JWT cookie injection via~/.config/opencode/plugins/omniroute-bridge/mcp-cookie-preload.cjsmonkey-patchingglobalThis.fetch— fine for localhost, impossible for remote deployments.Root cause — split-brain between two parallel auth helpers
OmniRoute has two management-auth surfaces:
managescopesrc/lib/api/requireManagementAuth.tshasManageScope()src/shared/utils/apiAuth.tsisAuthenticated/verifyAuthgetApiKeyMetadata()insrc/lib/db/apiKeys.tsalready exposes thescopesarray, the default-key flow atapiKeys.ts:966already passesscopes: ["manage"]through from env. The capability is there; onlyapiAuth.tswas opting out.Fix
New
validateBearerApiKeyForManagement(apiKey)insrc/shared/utils/apiAuth.ts:validateApiKey()(lifecycle:isActive,revokedAt,expiresAt,isBanned, plusmarkApiKeyUsed+ Redis cache hydration side effects).getApiKeyMetadata()and checksmetadata.scopesagainst the sharedMANAGEMENT_API_KEY_SCOPESset ({manage, admin}).isAuthenticatedandverifyAuthnow consult it on management routes before the reject branch.Default-deny preserved: unscoped Bearer keys still get
403 Invalid management tokenexactly as today.Follow-up commits in this PR (auth)
014b9f30—refactor(auth): extract shared MANAGEMENT_API_KEY_SCOPES + hasManageScope to constants. Addresses gemini comment 3253030964 by creatingsrc/shared/constants/managementScopes.ts(MANAGE_SCOPE,MANAGEMENT_API_KEY_SCOPES,hasManageScope()). BothapiAuth.tsandrequireManagementAuth.tsimport from it. LocalhasManageScopeinrequireManagementAuth.tsnow delegates to the shared helper;MANAGE_SCOPEis re-exported for backwards compat.05399ebf—fix(auth): read headers from request first so cliTokenAuth works outside next/headers scope.cliTokenAuth.tswas callingnext/headersheaders()unconditionally, which blew up the 6 pre-existingrequireManagementAuth.*tests with "called outside request scope". NewreadHeader(request, name)reads fromrequest.headersfirst and falls back tonext/headerswrapped in try/catch. All 28tests/unit/api-auth.test.tstests now pass.How to configure MCP against a remote OmniRoute server (with this fix)
Three steps; no cookie monkey-patch, no JWT injection.
1. Mint a Bearer key with
managescopeIn the dashboard: API Keys → Create key → check
managescope → copysk-….Or via curl (assuming you already have a logged-in cookie or a key with
manage):2. Point your MCP client at the remote
Claude Desktop / Cursor / VS Code (
mcp.json/claude_desktop_config.json):{ "mcpServers": { "omniroute": { "command": "npx", "args": ["-y", "@omniroute/mcp-server"], "env": { "OMNIROUTE_BASE_URL": "https://omniroute.example.com", "OMNIROUTE_API_KEY": "sk-…", "OMNIROUTE_MCP_ENFORCE_SCOPES": "true", }, }, }, }Plain shell:
3. Verify
Security note: default-deny posture unchanged. Unscoped keys still 403 on management routes; only keys explicitly granted
manageoradminpass.Part 2 — Standalone build unblock (was #2307)
Problem
docker compose --profile base buildfails at the builder stage with:Root cause
open-sse/lib/deepseek-pow.tsdoes:at module-load time. The Next.js standalone build's page-data collector evaluates the module before file-tracing has copied the sibling
.cjsinto the standalone output, so the require throws.Fix
72b79325wraps the require in a memoizedloadU()helper invoked only whensolveDeepSeekPow()actually runs:Module load now succeeds without touching the filesystem; the solver is materialized on first solve call, by which time file-tracing has emitted the
.cjsalongside.Verification (build)
Tests
tests/unit/api-auth.test.ts— 28/28 pass aftercliTokenAuthfix unblocked the 6 pre-existingheaders()failures.verifyAuth accepts bearer API keys with manage scope on management routesverifyAuth accepts bearer API keys with admin scope on management routesverifyAuth still rejects unscoped bearer API keys on management routesisAuthenticated accepts bearer API keys with manage scope on management routestests/unit/deepseek-pow.test.ts(if any path tests it) continues to pass — solver behaviour unchanged, only load-timing moved.Live smoke (Docker)
72b79325).PATCH /api/keys/d925c634-…grantedmanagescope to dev key.PATCH /api/settings/require-login→{"requireLogin": true}.Result:
manageon/api/usage/call-logs/api/usage/call-logsInvalid management token✅Scope
src/shared/utils/apiAuth.ts— newvalidateBearerApiKeyForManagement+ wire into both helpers.src/shared/constants/managementScopes.ts— new shared constants module.src/lib/api/requireManagementAuth.ts— delegatehasManageScopeto shared helper, re-exportMANAGE_SCOPE.src/lib/middleware/cliTokenAuth.ts—readHeader(request, name)helper, swapheaders()to it.open-sse/lib/deepseek-pow.ts— lazyloadU().tests/unit/api-auth.test.ts— 4 new tests covering accept/reject permutations.Closes / supersedes