mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
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.
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 | null | undefined>): string[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
function makeApiKeyUsageGroup(apiKeyId: string, fallbackName: string): string {
|
||||
return apiKeyId ? `id:${apiKeyId}` : `name:${fallbackName}`;
|
||||
}
|
||||
@@ -98,7 +98,6 @@ function addApiKeyAlias(target: Set<string>, 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<string, number>): 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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user