diff --git a/apps/web/src/app/api/internal/auto-routing-benchmark/decider-candidates/route.ts b/apps/web/src/app/api/internal/auto-routing-benchmark/decider-candidates/route.ts index 50837abe86..f14ceba3c4 100644 --- a/apps/web/src/app/api/internal/auto-routing-benchmark/decider-candidates/route.ts +++ b/apps/web/src/app/api/internal/auto-routing-benchmark/decider-candidates/route.ts @@ -1,6 +1,7 @@ import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; import { timingSafeEqual } from '@kilocode/encryption'; +import { extractBearerToken } from '@kilocode/worker-utils/extract-bearer-token'; import { AUTO_DECIDER_MAX_COST_USD, AUTO_DECIDER_MIN_COST_USD, @@ -8,13 +9,6 @@ import { } from '@/lib/model-stats/auto-routing-decider-candidates'; import { INTERNAL_API_SECRET } from '@/lib/config.server'; -function extractBearerToken(authHeader: string | null): string | null { - if (!authHeader) return null; - const trimmed = authHeader.trim(); - if (trimmed.slice(0, 7).toLowerCase() !== 'bearer ') return null; - return trimmed.slice(7).trim() || null; -} - function parseCostBound(value: string | null, fallback: number): number { if (value === null) return fallback; const parsed = Number(value); diff --git a/apps/web/src/app/api/internal/auto-routing-benchmark/token/route.ts b/apps/web/src/app/api/internal/auto-routing-benchmark/token/route.ts index 71c7f94d37..472eeda0bb 100644 --- a/apps/web/src/app/api/internal/auto-routing-benchmark/token/route.ts +++ b/apps/web/src/app/api/internal/auto-routing-benchmark/token/route.ts @@ -24,6 +24,7 @@ import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; import { timingSafeEqual } from '@kilocode/encryption'; +import { extractBearerToken } from '@kilocode/worker-utils/extract-bearer-token'; import { z } from 'zod'; import { and, eq } from 'drizzle-orm'; import { kilocode_users, organization_memberships } from '@kilocode/db/schema'; @@ -38,16 +39,6 @@ const RequestSchema = z.object({ const SIX_HOURS_IN_SECONDS = 6 * 60 * 60; -// Inline bearer extraction (case-insensitive prefix, RFC 6750 ยง2.1). Kept local -// to avoid importing @kilocode/worker-utils, whose transitive `jose` ESM import -// breaks under jest's CJS transform. -function extractBearerToken(authHeader: string | null): string | null { - if (!authHeader) return null; - const trimmed = authHeader.trim(); - if (trimmed.slice(0, 7).toLowerCase() !== 'bearer ') return null; - return trimmed.slice(7).trim() || null; -} - export async function POST(req: NextRequest) { const token = extractBearerToken(req.headers.get('authorization')); if (!INTERNAL_API_SECRET || !token || !timingSafeEqual(token, INTERNAL_API_SECRET)) { diff --git a/apps/web/src/lib/mcp-gateway/http.ts b/apps/web/src/lib/mcp-gateway/http.ts index b18d68b8ac..d870c97030 100644 --- a/apps/web/src/lib/mcp-gateway/http.ts +++ b/apps/web/src/lib/mcp-gateway/http.ts @@ -1,6 +1,7 @@ import 'server-only'; import { NextResponse } from 'next/server'; import { GatewayError } from '@kilocode/mcp-gateway'; +import { extractBearerToken as extractBearerTokenFromHeader } from '@kilocode/worker-utils/extract-bearer-token'; export function gatewayErrorResponse(error: unknown) { if (error instanceof GatewayError) { @@ -16,8 +17,5 @@ export function gatewayErrorResponse(error: unknown) { } export function extractBearerToken(headers: Headers): string | null { - const authorization = headers.get('authorization'); - if (!authorization?.toLowerCase().startsWith('bearer ')) return null; - const token = authorization.slice(7).trim(); - return token.length > 0 ? token : null; + return extractBearerTokenFromHeader(headers.get('authorization')); } diff --git a/packages/worker-utils/package.json b/packages/worker-utils/package.json index 366c68d14b..5716fb87b5 100644 --- a/packages/worker-utils/package.json +++ b/packages/worker-utils/package.json @@ -24,6 +24,7 @@ "./cloud-agent-next-client": "./src/cloud-agent-next-client.ts", "./cloud-agent-session-access": "./src/cloud-agent-session-access.ts", "./kilo-model-id": "./src/kilo-model-id.ts", + "./extract-bearer-token": "./src/extract-bearer-token.ts", "./cloud-agent-queue-report": "./src/cloud-agent-queue-report.ts", "./cloud-agent-failure": "./src/cloud-agent-failure.ts", "./security-auto-analysis-policy": "./src/security-auto-analysis-policy.ts", diff --git a/services/db-proxy/src/utils/auth.test.ts b/services/db-proxy/src/utils/auth.test.ts index 25ad4c463c..2b828a19db 100644 --- a/services/db-proxy/src/utils/auth.test.ts +++ b/services/db-proxy/src/utils/auth.test.ts @@ -71,7 +71,19 @@ describe('auth utilities', () => { const token = extractBearerToken(c); - expect(token).toBe(''); + expect(token).toBeNull(); + }); + + it('extracts token case-insensitively per RFC 6750', () => { + const c = createMockContext(); + (c.req.header as jest.Mock).mockImplementation((name: string) => { + if (name === 'Authorization') return 'bearer my-token-123'; + return undefined; + }); + + const token = extractBearerToken(c); + + expect(token).toBe('my-token-123'); }); }); diff --git a/services/db-proxy/src/utils/auth.ts b/services/db-proxy/src/utils/auth.ts index e5c4108465..a9b62a7fb8 100644 --- a/services/db-proxy/src/utils/auth.ts +++ b/services/db-proxy/src/utils/auth.ts @@ -1,17 +1,14 @@ import type { Context } from 'hono'; import type { ContentfulStatusCode } from 'hono/utils/http-status'; import { timingSafeEqual } from '@kilocode/encryption'; +import { extractBearerToken as extractBearerTokenFromHeader } from '@kilocode/worker-utils/extract-bearer-token'; import type { Env, ErrorCode } from '../types'; /** * Extract bearer token from Authorization header */ export function extractBearerToken(c: Context<{ Bindings: Env }>): string | null { - const authHeader = c.req.header('Authorization'); - if (!authHeader?.startsWith('Bearer ')) { - return null; - } - return authHeader.slice(7); + return extractBearerTokenFromHeader(c.req.header('Authorization')); } /**