fix(auth+build): Bearer manage scope on management routes + lazy-load deepseek PoW solver (#2308)

Integrated into release/v3.8.0
This commit is contained in:
Mourad Maatoug
2026-05-16 18:28:57 +02:00
committed by GitHub
parent 04335c5a6b
commit ec138c6fee
6 changed files with 171 additions and 10 deletions

View File

@@ -9,9 +9,17 @@ import { dirname, join } from "node:path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Load the exact solver extracted from DeepSeek's worker chunk
// Load the exact solver extracted from DeepSeek's worker chunk.
// Lazy-loaded inside the function so the standalone Next build can collect
// page data without executing a dynamic require() at module-load time.
const require = createRequire(import.meta.url);
const { U } = require(join(__dirname, "deepseek-pow-solver.cjs"));
let _U: any | undefined;
function loadU(): any {
if (_U === undefined) {
_U = require(join(__dirname, "deepseek-pow-solver.cjs")).U;
}
return _U;
}
export function solveDeepSeekPow(
algorithm: string,
@@ -23,6 +31,7 @@ export function solveDeepSeekPow(
if (algorithm !== "DeepSeekHashV1") throw new Error(`Unsupported: ${algorithm}`);
const prefix = `${salt}_${expireAt}_`;
const U = loadU();
const createHash = () => {
const self: any = {};
self._sponge = new U({ capacity: 256, padding: 6 });

View File

@@ -3,11 +3,21 @@ import { createErrorResponse } from "@/lib/api/errorResponse";
import { extractApiKey, isValidApiKey } from "@/sse/services/auth";
import { getApiKeyMetadata } from "@/lib/db/apiKeys";
import { isCliTokenAuthValid } from "@/lib/middleware/cliTokenAuth";
import {
MANAGE_SCOPE,
hasManageScope as hasManageScopeShared,
} from "@/shared/constants/managementScopes";
export const MANAGE_SCOPE = "manage";
export { MANAGE_SCOPE };
/**
* Check whether any of the supplied scopes authorizes management API access.
*
* Re-exported here for backwards compatibility with existing callers. The
* canonical definition lives in `@/shared/constants/managementScopes`.
*/
export function hasManageScope(scopes: string[] = []): boolean {
return scopes.includes("manage") || scopes.includes("admin");
return hasManageScopeShared(scopes);
}
export async function requireManagementAuth(request: Request): Promise<Response | null> {

View File

@@ -9,6 +9,25 @@ export function isLoopback(ip: string): boolean {
return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
}
/**
* Read a header value preferring the Request's own headers (works in any
* context — App Router request handlers, unit tests, raw fetch) and falling
* back to `next/headers` only when the request object isn't carrying them.
*
* Calling `headers()` outside a request scope throws (see Next.js
* `next-dynamic-api-wrong-context`), so we guard the import.
*/
async function readHeader(request: Request, name: string): Promise<string | null> {
const fromRequest = request.headers?.get(name);
if (fromRequest != null) return fromRequest;
try {
const hdrs = await headers();
return hdrs.get(name);
} catch {
return null;
}
}
/**
* Validates the CLI machine-id token sent by the local omniroute CLI.
* Only accepted from loopback IPs. Disabled via OMNIROUTE_DISABLE_CLI_TOKEN=true.
@@ -16,13 +35,13 @@ export function isLoopback(ip: string): boolean {
export async function isCliTokenAuthValid(request: Request): Promise<boolean> {
if (process.env.OMNIROUTE_DISABLE_CLI_TOKEN === "true") return false;
const hdrs = await headers();
const token = hdrs.get(HEADER_NAME);
const token = await readHeader(request, HEADER_NAME);
if (!token || token.length !== 32) return false;
// Only allow loopback origin — check forwarded-for, real-ip, then host header.
const ip =
(hdrs.get("x-forwarded-for") ?? "").split(",")[0].trim() || hdrs.get("x-real-ip") || "";
const forwardedFor = (await readHeader(request, "x-forwarded-for")) ?? "";
const realIp = (await readHeader(request, "x-real-ip")) ?? "";
const ip = forwardedFor.split(",")[0].trim() || realIp;
if (ip && !isLoopback(ip)) return false;
let expected: string;

View File

@@ -0,0 +1,31 @@
/**
* Management API key scopes — the set of API key scopes that authorize a
* Bearer key on management routes (`/api/*` excluding `/api/v1/*` and the
* public allowlist).
*
* Single source of truth shared by:
* - `src/lib/api/requireManagementAuth.ts` (`hasManageScope`)
* - `src/shared/utils/apiAuth.ts` (`validateBearerApiKeyForManagement`)
*
* Keep both helpers in sync by importing `MANAGEMENT_API_KEY_SCOPES` from
* here — never re-declare the list inline.
*/
/** Canonical scope name granted to the default environment key. */
export const MANAGE_SCOPE = "manage";
/**
* Set of scopes that grant access to management API routes.
* `admin` is treated as a superset of `manage`.
*/
export const MANAGEMENT_API_KEY_SCOPES = new Set<string>(["manage", "admin"]);
/**
* Check whether any of the given scopes authorizes management API access.
*/
export function hasManageScope(scopes: readonly string[] = []): boolean {
for (const scope of scopes) {
if (MANAGEMENT_API_KEY_SCOPES.has(scope)) return true;
}
return false;
}

View File

@@ -160,6 +160,35 @@ async function validateBearerApiKey(apiKey: string | null): Promise<boolean> {
}
}
/**
* Check whether a Bearer API key is valid AND carries a scope that authorizes
* it on management API routes (`/api/*` excluding `/api/v1/*` and the public
* allowlist). Returns `false` for unscoped keys so that the existing
* default-deny posture on management routes is preserved.
*
* Scope set is sourced from `@/shared/constants/managementScopes` so this
* helper stays in lockstep with `requireManagementAuth.hasManageScope`.
*/
async function validateBearerApiKeyForManagement(apiKey: string | null): Promise<boolean> {
if (!apiKey) return false;
try {
const [{ validateApiKey, getApiKeyMetadata }, { hasManageScope }] = await Promise.all([
import("@/lib/db/apiKeys"),
import("@/shared/constants/managementScopes"),
]);
const valid = await validateApiKey(apiKey);
if (!valid) return false;
const metadata = await getApiKeyMetadata(apiKey);
if (!metadata) return false;
return hasManageScope(metadata.scopes);
} catch {
return false;
}
}
export function isManagementApiRequest(request: RequestLike | Request): boolean {
const pathname = getRequestPathname(request);
if (!pathname?.startsWith("/api/")) return false;
@@ -221,6 +250,9 @@ export async function verifyAuth(request: any): Promise<string | null> {
const bearerToken = getBearerToken(request);
if (isManagementApiRequest(request)) {
if (await validateBearerApiKeyForManagement(bearerToken)) {
return null;
}
return bearerToken ? "Invalid management token" : "Authentication required";
}
@@ -250,11 +282,12 @@ export async function isAuthenticated(request: Request): Promise<boolean> {
return true;
}
const bearerToken = getBearerToken(request);
if (isManagementApiRequest(request)) {
return false;
return validateBearerApiKeyForManagement(bearerToken);
}
return validateBearerApiKey(getBearerToken(request));
return validateBearerApiKey(bearerToken);
}
/**

View File

@@ -155,6 +155,65 @@ test("isAuthenticated rejects bearer API keys on management routes", async () =>
assert.equal(result, false);
});
test("verifyAuth accepts bearer API keys with manage scope on management routes", async () => {
const key = await apiKeysDb.createApiKey("mcp-management", "machine1234567890", ["manage"]);
const result = await apiAuth.verifyAuth({
cookies: {
get() {
return undefined;
},
},
headers: new Headers({ authorization: `Bearer ${key.key}` }),
url: "https://example.com/api/providers",
});
assert.equal(result, null);
});
test("verifyAuth accepts bearer API keys with admin scope on management routes", async () => {
const key = await apiKeysDb.createApiKey("mcp-admin", "machine1234567890", ["admin"]);
const result = await apiAuth.verifyAuth({
cookies: {
get() {
return undefined;
},
},
headers: new Headers({ authorization: `Bearer ${key.key}` }),
url: "https://example.com/api/settings",
});
assert.equal(result, null);
});
test("verifyAuth still rejects unscoped bearer API keys on management routes", async () => {
const key = await apiKeysDb.createApiKey("integration-no-scope", "machine1234567890");
const result = await apiAuth.verifyAuth({
cookies: {
get() {
return undefined;
},
},
headers: new Headers({ authorization: `Bearer ${key.key}` }),
url: "https://example.com/api/providers",
});
assert.equal(result, "Invalid management token");
});
test("isAuthenticated accepts bearer API keys with manage scope on management routes", async () => {
process.env.INITIAL_PASSWORD = "bootstrap-password";
await localDb.updateSettings({ requireLogin: true, password: "" });
const key = await apiKeysDb.createApiKey("mcp-management", "machine1234567890", ["manage"]);
const request = new Request("https://example.com/api/providers", {
headers: { authorization: `Bearer ${key.key}` },
});
const result = await apiAuth.isAuthenticated(request);
assert.equal(result, true);
});
test("monitoring health reset route requires dashboard authentication", async () => {
process.env.INITIAL_PASSWORD = "bootstrap-password";
await localDb.updateSettings({ requireLogin: true, password: "" });