From ec6456ba73c802e0ecadc602d61ade7e30b403bd Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sun, 10 May 2026 16:05:21 -0300 Subject: [PATCH] chore(release): align migration compatibility and packaged CLI runtime Skip the superseded 041 session_account_affinity migration when the canonical 050 file is present, and remap legacy migration markers so upgraded databases do not replay the duplicate slot. Also include the CLI entrypoints in packaged artifacts and extend management-auth coverage across admin memory, pricing, routing, provider validation, and usage endpoints to keep release bundles runnable and sensitive operations protected. --- scripts/pack-artifact-policy.ts | 4 + src/app/api/memory/[id]/route.ts | 7 + src/app/api/memory/route.ts | 7 + .../api/model-combo-mappings/[id]/route.ts | 14 +- src/app/api/model-combo-mappings/route.ts | 9 +- src/app/api/pricing/route.ts | 10 ++ src/app/api/pricing/sync/route.ts | 14 +- src/app/api/provider-nodes/validate/route.ts | 4 + src/app/api/providers/validate/route.ts | 4 + src/app/api/usage/analytics/route.ts | 6 +- src/app/api/usage/history/route.ts | 6 +- src/app/api/usage/logs/route.ts | 6 +- src/app/api/usage/request-logs/route.ts | 6 +- src/lib/db/migrationRunner.ts | 50 ++++++- tests/unit/db-migration-runner.test.ts | 135 ++++++++++++++++++ tests/unit/management-auth-hardening.test.ts | 73 ++++++++++ tests/unit/pack-artifact-policy.test.ts | 2 + 17 files changed, 345 insertions(+), 12 deletions(-) diff --git a/scripts/pack-artifact-policy.ts b/scripts/pack-artifact-policy.ts index 06793c10c6..5417abecce 100644 --- a/scripts/pack-artifact-policy.ts +++ b/scripts/pack-artifact-policy.ts @@ -61,6 +61,7 @@ export const PACK_ARTIFACT_ROOT_ALLOWED_EXACT_PATHS: string[] = [ ".env.example", "LICENSE", "README.md", + "bin/cli-commands.mjs", "bin/mcp-server.mjs", "bin/nodeRuntimeSupport.mjs", "bin/omniroute.mjs", @@ -84,6 +85,7 @@ export const PACK_ARTIFACT_ROOT_ALLOWED_EXACT_PATHS: string[] = [ ]; export const PACK_ARTIFACT_ROOT_ALLOWED_PATH_PREFIXES: string[] = [ + "bin/cli/", "open-sse/mcp-server/schemas/", "open-sse/mcp-server/tools/", "src/shared/contracts/", @@ -95,6 +97,8 @@ export const PACK_ARTIFACT_REQUIRED_PATHS: string[] = [ "app/server.js", "app/server-ws.mjs", "app/responses-ws-proxy.mjs", + "bin/cli-commands.mjs", + "bin/cli/index.mjs", "bin/mcp-server.mjs", "bin/nodeRuntimeSupport.mjs", "bin/omniroute.mjs", diff --git a/src/app/api/memory/[id]/route.ts b/src/app/api/memory/[id]/route.ts index a7be2e7748..b4a27e16c1 100644 --- a/src/app/api/memory/[id]/route.ts +++ b/src/app/api/memory/[id]/route.ts @@ -1,7 +1,11 @@ import { NextResponse } from "next/server"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { deleteMemory, getMemory } from "@/lib/memory/store"; export async function DELETE(request: Request, props: { params: Promise<{ id: string }> }) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + try { const { id } = await props.params; const success = await deleteMemory(id); @@ -16,6 +20,9 @@ export async function DELETE(request: Request, props: { params: Promise<{ id: st } export async function GET(request: Request, props: { params: Promise<{ id: string }> }) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + try { const { id } = await props.params; const memory = await getMemory(id); diff --git a/src/app/api/memory/route.ts b/src/app/api/memory/route.ts index 785b75002f..4d3cf913e1 100644 --- a/src/app/api/memory/route.ts +++ b/src/app/api/memory/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { listMemories, createMemory } from "@/lib/memory/store"; import { MemoryType } from "@/lib/memory/types"; import { parsePaginationParams, buildPaginatedResponse } from "@/shared/types/pagination"; @@ -16,6 +17,9 @@ const createMemorySchema = z.object({ }); export async function GET(request: Request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + try { const url = new URL(request.url); const { searchParams } = url; @@ -68,6 +72,9 @@ export async function GET(request: Request) { } export async function POST(request: Request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + try { const rawBody = await request.json(); const validation = validateBody(createMemorySchema, rawBody); diff --git a/src/app/api/model-combo-mappings/[id]/route.ts b/src/app/api/model-combo-mappings/[id]/route.ts index 88c0733c53..57d0cd6d74 100644 --- a/src/app/api/model-combo-mappings/[id]/route.ts +++ b/src/app/api/model-combo-mappings/[id]/route.ts @@ -6,6 +6,7 @@ import { z } from "zod"; import { NextResponse } from "next/server"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { updateModelComboMapping, deleteModelComboMapping, @@ -21,7 +22,10 @@ const updateMappingSchema = z.object({ description: z.string().max(1000).optional(), }); -export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) { +export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + try { const { id } = await params; const mapping = await getModelComboMappingById(id); @@ -35,6 +39,9 @@ export async function GET(_request: Request, { params }: { params: Promise<{ id: } export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + try { const { id } = await params; const rawBody = await request.json(); @@ -58,7 +65,10 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id: } } -export async function DELETE(_request: Request, { params }: { params: Promise<{ id: string }> }) { +export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + try { const { id } = await params; const deleted = await deleteModelComboMapping(id); diff --git a/src/app/api/model-combo-mappings/route.ts b/src/app/api/model-combo-mappings/route.ts index 359a7fb569..5b5e54dc62 100644 --- a/src/app/api/model-combo-mappings/route.ts +++ b/src/app/api/model-combo-mappings/route.ts @@ -6,6 +6,7 @@ import { z } from "zod"; import { NextResponse } from "next/server"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { getModelComboMappings, createModelComboMapping } from "@/lib/localDb"; import { validateBody, isValidationFailure } from "@/shared/validation/helpers"; @@ -17,7 +18,10 @@ const createMappingSchema = z.object({ description: z.string().max(1000).optional().default(""), }); -export async function GET() { +export async function GET(request: Request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + try { const mappings = await getModelComboMappings(); return NextResponse.json({ mappings }); @@ -30,6 +34,9 @@ export async function GET() { } export async function POST(request: Request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + try { const rawBody = await request.json(); const validation = validateBody(createMappingSchema, rawBody); diff --git a/src/app/api/pricing/route.ts b/src/app/api/pricing/route.ts index 3ea891cb78..7db9659574 100644 --- a/src/app/api/pricing/route.ts +++ b/src/app/api/pricing/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { getPricing, getPricingWithSources, @@ -14,6 +15,9 @@ import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; * Get current pricing configuration (merged user + defaults) */ export async function GET(request: Request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + try { const includeSources = new URL(request.url).searchParams.get("includeSources") === "1"; if (includeSources) { @@ -34,6 +38,9 @@ export async function GET(request: Request) { * Body: { provider: { model: { input: number, output: number, cached: number, ... } } } */ export async function PATCH(request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + let rawBody; try { rawBody = await request.json(); @@ -70,6 +77,9 @@ export async function PATCH(request) { * Query params: ?provider=xxx&model=yyy (optional) */ export async function DELETE(request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + try { const { searchParams } = new URL(request.url); const provider = searchParams.get("provider"); diff --git a/src/app/api/pricing/sync/route.ts b/src/app/api/pricing/sync/route.ts index b60d78fccf..1ff9ece74f 100644 --- a/src/app/api/pricing/sync/route.ts +++ b/src/app/api/pricing/sync/route.ts @@ -7,10 +7,14 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { pricingSyncRequestSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; export async function POST(request: NextRequest) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + let rawBody: unknown; try { rawBody = await request.json(); @@ -43,7 +47,10 @@ export async function POST(request: NextRequest) { } } -export async function GET() { +export async function GET(request: NextRequest) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + try { const { getSyncStatus } = await import("@/lib/pricingSync"); return NextResponse.json(getSyncStatus()); @@ -53,7 +60,10 @@ export async function GET() { } } -export async function DELETE() { +export async function DELETE(request: NextRequest) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + try { const { clearSyncedPricing } = await import("@/lib/pricingSync"); clearSyncedPricing(); diff --git a/src/app/api/provider-nodes/validate/route.ts b/src/app/api/provider-nodes/validate/route.ts index bc2d13c962..22d9136f73 100644 --- a/src/app/api/provider-nodes/validate/route.ts +++ b/src/app/api/provider-nodes/validate/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { getAuditRequestContext, logAuditEvent } from "@/lib/compliance/index"; import { validateClaudeCodeCompatibleProvider } from "@/lib/providers/validation"; import { @@ -41,6 +42,9 @@ function sanitizeAuditBaseUrl(baseUrl: string) { // POST /api/provider-nodes/validate - Validate API key against base URL export async function POST(request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + const auditContext = getAuditRequestContext(request); let rawBody; try { diff --git a/src/app/api/providers/validate/route.ts b/src/app/api/providers/validate/route.ts index 41f198d992..687d935bf9 100644 --- a/src/app/api/providers/validate/route.ts +++ b/src/app/api/providers/validate/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { getAuditRequestContext, logAuditEvent } from "@/lib/compliance/index"; import { getProviderNodeById } from "@/models"; import { @@ -24,6 +25,9 @@ function sanitizeAuditUrl(url: string | null | undefined) { // POST /api/providers/validate - Validate API key with provider export async function POST(request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + const auditContext = getAuditRequestContext(request); let rawBody; try { diff --git a/src/app/api/usage/analytics/route.ts b/src/app/api/usage/analytics/route.ts index 546d41c481..17fc459e74 100644 --- a/src/app/api/usage/analytics/route.ts +++ b/src/app/api/usage/analytics/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { getApiKeys } from "@/lib/db/apiKeys"; import { getDbInstance } from "@/lib/db/core"; @@ -87,7 +88,6 @@ function uniqueValues(values: Array): string[] { return result; } - function makeApiKeyUsageGroup(apiKeyId: string, fallbackName: string): string { return apiKeyId ? `id:${apiKeyId}` : `name:${fallbackName}`; } @@ -98,7 +98,6 @@ function addApiKeyAlias(target: Set, value: unknown): void { if (trimmed) target.add(trimmed); } - function stripCodexEffortSuffix(model: string): string { return model.replace(/-(?:xhigh|high|medium|low|none)$/i, ""); } @@ -263,6 +262,9 @@ function computeActivityStreak(activityMap: Record): number { } export async function GET(request: Request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + try { const { searchParams } = new URL(request.url); const range = searchParams.get("range") || "30d"; diff --git a/src/app/api/usage/history/route.ts b/src/app/api/usage/history/route.ts index 16a5d40725..6094e7baec 100644 --- a/src/app/api/usage/history/route.ts +++ b/src/app/api/usage/history/route.ts @@ -1,7 +1,11 @@ import { NextResponse } from "next/server"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { getUsageStats } from "@/lib/usageDb"; -export async function GET() { +export async function GET(request: Request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + try { const stats = await getUsageStats(); return NextResponse.json(stats); diff --git a/src/app/api/usage/logs/route.ts b/src/app/api/usage/logs/route.ts index b5b875ec94..ebb409d2f2 100644 --- a/src/app/api/usage/logs/route.ts +++ b/src/app/api/usage/logs/route.ts @@ -1,7 +1,11 @@ import { NextResponse } from "next/server"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { getRecentLogs } from "@/lib/usageDb"; -export async function GET() { +export async function GET(request: Request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + try { const logs = await getRecentLogs(200); return NextResponse.json(logs); diff --git a/src/app/api/usage/request-logs/route.ts b/src/app/api/usage/request-logs/route.ts index 0ae5e961cc..0e335c26d9 100644 --- a/src/app/api/usage/request-logs/route.ts +++ b/src/app/api/usage/request-logs/route.ts @@ -1,7 +1,11 @@ import { NextResponse } from "next/server"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { getRecentLogs } from "@/lib/usageDb"; -export async function GET() { +export async function GET(request: Request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + try { const logs = await getRecentLogs(200); return NextResponse.json(logs); diff --git a/src/lib/db/migrationRunner.ts b/src/lib/db/migrationRunner.ts index bfc32b5e63..d53c66e9a9 100644 --- a/src/lib/db/migrationRunner.ts +++ b/src/lib/db/migrationRunner.ts @@ -133,6 +133,12 @@ const RENAMED_MIGRATION_COMPATIBILITY = [ toVersion: "039", toName: "compression_cache_stats", }, + { + fromVersion: "041", + fromName: "session_account_affinity", + toVersion: "050", + toName: "session_account_affinity", + }, ] as const; const LEGACY_VERSION_SLOT_MIGRATIONS = [ @@ -144,6 +150,15 @@ const LEGACY_VERSION_SLOT_MIGRATIONS = [ { version: "033", name: "provider_connections_block_extra_usage" }, ] as const; +const SUPERSEDED_DUPLICATE_MIGRATIONS = [ + { + version: "041", + name: "session_account_affinity", + supersededByVersion: "050", + supersededByName: "session_account_affinity", + }, +] as const; + const PHYSICAL_SCHEMA_SENTINELS = [ { version: "028", tableName: "batches", description: "batches table" }, { version: "024", tableName: "sync_tokens", description: "sync_tokens table" }, @@ -198,6 +213,34 @@ function getMigrationFiles(): Array<{ version: string; name: string; path: strin .filter(Boolean) as Array<{ version: string; name: string; path: string }>; } +function filterSupersededDuplicateMigrations( + files: Array<{ version: string; name: string; path: string }> +): Array<{ version: string; name: string; path: string }> { + return files.filter((file) => { + const superseded = SUPERSEDED_DUPLICATE_MIGRATIONS.find( + (migration) => migration.version === file.version && migration.name === file.name + ); + if (!superseded) { + return true; + } + + const hasReplacement = files.some( + (candidate) => + candidate.version === superseded.supersededByVersion && + candidate.name === superseded.supersededByName + ); + if (!hasReplacement) { + return true; + } + + console.warn( + `[Migration] Ignoring superseded duplicate migration ${file.version}_${file.name}; ` + + `${superseded.supersededByVersion}_${superseded.supersededByName} is the canonical slot.` + ); + return false; + }); +} + /** * Get list of already-applied migration versions. */ @@ -286,6 +329,9 @@ function isSchemaAlreadyApplied( case "040": return hasColumn(db, "proxy_registry", "source"); case "041": + if (migration.name === "session_account_affinity") { + return hasTable(db, "session_account_affinity"); + } return ( hasColumn(db, "compression_analytics", "actual_prompt_tokens") && hasColumn(db, "compression_analytics", "actual_completion_tokens") && @@ -667,7 +713,7 @@ export function runMigrations(db: Database.Database, options?: { isNewDb?: boole const isNewDb = options?.isNewDb === true; ensureMigrationsTable(db); - const files = getMigrationFiles(); + const files = filterSupersededDuplicateMigrations(getMigrationFiles()); rehomeLegacyVersionSlotMigrations(db, files); reconcileRenumberedMigrations(db, files); const applied = getAppliedVersions(db); @@ -779,7 +825,7 @@ export function runMigrations(db: Database.Database, options?: { isNewDb?: boole ); } else if (migration.version === "032") { applyApiKeyLifecycleMigration(db); - } else if (migration.version === "041") { + } else if (migration.version === "041" && migration.name === "compression_receipts") { applyCompressionReceiptsMigration(db); } else if (migration.version === "042") { applyCompressionCombosMigration(db, migration.path); diff --git a/tests/unit/db-migration-runner.test.ts b/tests/unit/db-migration-runner.test.ts index a0595ad532..519fcebf74 100644 --- a/tests/unit/db-migration-runner.test.ts +++ b/tests/unit/db-migration-runner.test.ts @@ -857,6 +857,141 @@ test( } ); +test( + "runMigrations ignores superseded 041 session affinity duplicate when 050 exists", + serial, + async () => { + const runner = await importFresh("src/lib/db/migrationRunner.ts"); + const db = createDb(); + + try { + const count = withMockedMigrationFs( + { + "038_compression_analytics.sql": ` + CREATE TABLE compression_analytics ( + id TEXT PRIMARY KEY, + request_id TEXT + ); + `, + "041_compression_receipts.sql": "-- handled by migrationRunner", + "041_session_account_affinity.sql": ` + CREATE TABLE duplicate_041_session_account_affinity (id TEXT PRIMARY KEY); + `, + "050_session_account_affinity.sql": ` + CREATE TABLE IF NOT EXISTS session_account_affinity ( + session_key TEXT NOT NULL, + provider TEXT NOT NULL, + connection_id TEXT NOT NULL, + created_at INTEGER NOT NULL, + last_seen_at INTEGER NOT NULL, + PRIMARY KEY (session_key, provider) + ); + `, + }, + () => runner.runMigrations(db) + ); + + assert.equal(count, 3); + assert.equal( + db.prepare("SELECT name FROM _omniroute_migrations WHERE version = ?").get("041")?.name, + "compression_receipts" + ); + assert.equal( + db.prepare("SELECT name FROM _omniroute_migrations WHERE version = ?").get("050")?.name, + "session_account_affinity" + ); + assert.deepEqual( + db + .prepare( + "SELECT name FROM sqlite_master WHERE type = 'table' AND name IN (?, ?) ORDER BY name" + ) + .all("duplicate_041_session_account_affinity", "session_account_affinity"), + [{ name: "session_account_affinity" }] + ); + } finally { + db.close(); + } + } +); + +test( + "reconcileRenumberedMigrations moves legacy 041 session affinity marker to 050", + serial, + async () => { + const runner = await importFresh("src/lib/db/migrationRunner.ts"); + const db = createDb(); + + try { + db.exec(` + CREATE TABLE _omniroute_migrations ( + version TEXT PRIMARY KEY, + name TEXT NOT NULL, + applied_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + CREATE TABLE compression_analytics ( + id TEXT PRIMARY KEY, + request_id TEXT + ); + `); + db.prepare("INSERT INTO _omniroute_migrations (version, name) VALUES (?, ?)").run( + "041", + "session_account_affinity" + ); + + const consoleErrors: string[] = []; + const originalError = console.error; + console.error = (...args: any[]) => { + consoleErrors.push(args.map(String).join(" ")); + }; + + try { + const count = withMockedMigrationFs( + { + "041_compression_receipts.sql": "-- handled by migrationRunner", + "041_session_account_affinity.sql": ` + CREATE TABLE duplicate_041_session_account_affinity (id TEXT PRIMARY KEY); + `, + "050_session_account_affinity.sql": ` + CREATE TABLE IF NOT EXISTS session_account_affinity ( + session_key TEXT NOT NULL, + provider TEXT NOT NULL, + connection_id TEXT NOT NULL, + created_at INTEGER NOT NULL, + last_seen_at INTEGER NOT NULL, + PRIMARY KEY (session_key, provider) + ); + `, + }, + () => runner.runMigrations(db) + ); + + assert.equal(count, 1); + assert.equal( + db.prepare("SELECT name FROM _omniroute_migrations WHERE version = ?").get("041")?.name, + "compression_receipts" + ); + assert.equal( + db.prepare("SELECT name FROM _omniroute_migrations WHERE version = ?").get("050")?.name, + "session_account_affinity" + ); + + const renumberingWarnings = consoleErrors.filter( + (e) => e.includes("CRITICAL") && e.includes("renumbered") + ); + assert.equal( + renumberingWarnings.length, + 0, + `Expected no renumbering warnings, got: ${renumberingWarnings.join("; ")}` + ); + } finally { + console.error = originalError; + } + } finally { + db.close(); + } + } +); + test( "full upgrade simulation: all 3 renumbered migrations reconciled without CRITICAL warnings", serial, diff --git a/tests/unit/management-auth-hardening.test.ts b/tests/unit/management-auth-hardening.test.ts index 2b14e81a3e..dd9540b3a5 100644 --- a/tests/unit/management-auth-hardening.test.ts +++ b/tests/unit/management-auth-hardening.test.ts @@ -43,3 +43,76 @@ test("compression analytics route requires management authentication before retu content.indexOf("getCompressionAnalyticsSummary(") ); }); + +test("administrative pricing and routing routes require management authentication", () => { + const routePaths = [ + "src/app/api/pricing/route.ts", + "src/app/api/pricing/sync/route.ts", + "src/app/api/model-combo-mappings/route.ts", + "src/app/api/model-combo-mappings/[id]/route.ts", + ]; + + for (const routePath of routePaths) { + const content = fs.readFileSync(routePath, "utf8"); + assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath); + assert.ok( + content.includes("const authError = await requireManagementAuth(request);"), + routePath + ); + assert.ok(content.includes("if (authError) return authError;"), routePath); + } +}); + +test("memory management routes require management authentication", () => { + const routePaths = ["src/app/api/memory/route.ts", "src/app/api/memory/[id]/route.ts"]; + + for (const routePath of routePaths) { + const content = fs.readFileSync(routePath, "utf8"); + assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath); + assert.ok( + content.includes("const authError = await requireManagementAuth(request);"), + routePath + ); + assert.ok(content.includes("if (authError) return authError;"), routePath); + } +}); + +test("provider validation routes require management authentication before reading credentials", () => { + const routePaths = [ + "src/app/api/provider-nodes/validate/route.ts", + "src/app/api/providers/validate/route.ts", + ]; + + for (const routePath of routePaths) { + const content = fs.readFileSync(routePath, "utf8"); + assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath); + assert.ok( + content.includes("const authError = await requireManagementAuth(request);"), + routePath + ); + assert.ok(content.includes("if (authError) return authError;"), routePath); + assert.ok( + content.indexOf("requireManagementAuth(request)") < content.indexOf("request.json()"), + `${routePath} should authenticate before parsing submitted provider credentials` + ); + } +}); + +test("usage analytics and request log routes require management authentication", () => { + const routePaths = [ + "src/app/api/usage/analytics/route.ts", + "src/app/api/usage/history/route.ts", + "src/app/api/usage/request-logs/route.ts", + "src/app/api/usage/logs/route.ts", + ]; + + for (const routePath of routePaths) { + const content = fs.readFileSync(routePath, "utf8"); + assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath); + assert.ok( + content.includes("const authError = await requireManagementAuth(request);"), + routePath + ); + assert.ok(content.includes("if (authError) return authError;"), routePath); + } +}); diff --git a/tests/unit/pack-artifact-policy.test.ts b/tests/unit/pack-artifact-policy.test.ts index e47925474f..21f09440f1 100644 --- a/tests/unit/pack-artifact-policy.test.ts +++ b/tests/unit/pack-artifact-policy.test.ts @@ -73,6 +73,8 @@ test("findMissingArtifactPaths flags missing root runtime files in the tarball", "app/open-sse/services/compression/rules/en/filler.json", "app/responses-ws-proxy.mjs", "app/server-ws.mjs", + "bin/cli-commands.mjs", + "bin/cli/index.mjs", "bin/mcp-server.mjs", "bin/nodeRuntimeSupport.mjs", "scripts/native-binary-compat.mjs",