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:
diegosouzapw
2026-05-10 16:05:21 -03:00
parent e58aea9df7
commit ec6456ba73
17 changed files with 345 additions and 12 deletions

View File

@@ -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",

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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");

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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";

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,

View File

@@ -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);
}
});

View File

@@ -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",