import assert from "node:assert/strict"; import { cpSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import test from "node:test"; import { handleAdminModelMutation } from "../src/lib/admin/model-mutation-handler.ts"; import { assertAllowedModelProviderUrl } from "../src/lib/epay/gateway-policy.ts"; import { encryptModelProviderApiKey } from "../src/lib/model-provider-policy.ts"; import { runMigrations } from "../scripts/db-migrate.mjs"; import { startPostgresFixture } from "./helpers/postgres-fixture.ts"; const migrationPath = fileURLToPath(new URL("../supabase/migrations/20260807160000_model_provider_encrypted_credentials.sql", import.meta.url)); const emergencyDisableMigrationPath = fileURLToPath(new URL("../supabase/migrations/20260808010000_model_provider_emergency_disable.sql", import.meta.url)); const handlerPath = fileURLToPath(new URL("../src/lib/admin/model-mutation-handler.ts", import.meta.url)); const routePath = fileURLToPath(new URL("../src/app/api/admin/models/route.ts", import.meta.url)); const catalogPath = fileURLToPath(new URL("../src/lib/model-catalog.ts", import.meta.url)); const componentPath = fileURLToPath(new URL("../src/components/admin/model-management.tsx", import.meta.url)); const actorId = "10000000-0000-4000-8000-000000000003"; const deniedActorId = "10000000-0000-4000-8000-000000000004"; const encryptionKey = Buffer.alloc(32, 17).toString("base64"); const environment = { MODEL_PROVIDER_CONFIG_ENCRYPTION_KEY: encryptionKey }; const publicLookup = async () => [{ address: "93.184.216.34", family: 4 }] as const; async function migrateModelConfigurationFixture(connectionString: string) { const root = fileURLToPath(new URL("..", import.meta.url)); const temporaryRoot = mkdtempSync(join(tmpdir(), "jyotisha-model-migrations-")); const directories = ["db/migrations", "supabase/migrations"].map((relative) => { const source = join(root, relative); const target = join(temporaryRoot, relative.replace("/", "-")); mkdirSync(target, { recursive: true }); for (const filename of readdirSync(source)) { if (!filename.endsWith(".sql") || filename > "20260808020000_model_admin_runtime_read_policies.sql") continue; if ([ "20260806030000_settle_order_usage_authorization.sql", "20260806050000_operations_feature_flags.sql", "20260806060000_unified_rectification_usage.sql", "20260807010000_retire_legacy_rectification_runtime.sql", ].includes(filename)) continue; cpSync(join(source, filename), join(target, filename)); } return target; }); try { await runMigrations({ connectionString, migrationsDirectory: undefined, migrationsDirectories: directories, logger: console, }); } finally { rmSync(temporaryRoot, { recursive: true, force: true }); } } test("model provider URLs allow arbitrary public HTTPS origins but retain SSRF boundaries", async () => { let lookupOptions: unknown; const resolved = await assertAllowedModelProviderUrl( "https://attacker.example/v1", {}, async (_hostname, options) => { lookupOptions = options; return publicLookup(); }, ); assert.equal(resolved.url.origin, "https://attacker.example"); assert.deepEqual(lookupOptions, { all: true, verbatim: true }); for (const value of [ "http://attacker.example/v1", "https://user:password@attacker.example/v1", "https://localhost/v1", "https://service.internal/v1", "https://10.0.0.1/v1", "https://192.168.1.1/v1", "https://169.254.169.254/v1", "https://192.0.2.1/v1", "https://[2001:db8::1]/v1", ]) { await assert.rejects( assertAllowedModelProviderUrl(value, {}, publicLookup), /HTTPS|本机|内网|内部域名|保留地址/, value, ); } await assert.rejects( assertAllowedModelProviderUrl( "https://public.example/v1", {}, async () => [{ address: "169.254.169.254", family: 4 }], ), /内网|保留地址/, ); const gatewayPolicy = readFileSync(new URL("../src/lib/epay/gateway-policy.ts", import.meta.url), "utf8"); assert.match(gatewayPolicy, /lookup: pinnedAddressLookup\(pinned\)/); assert.match(gatewayPolicy, /status >= 300 && status < 400/); assert.match(gatewayPolicy, /不允许重定向/); }); test("admin and runtime source expose only secretConfigured and contain no secretRef contract", () => { const route = readFileSync(routePath, "utf8"); const handler = readFileSync(handlerPath, "utf8"); const catalog = readFileSync(catalogPath, "utf8"); const component = readFileSync(componentPath, "utf8"); assert.doesNotMatch(`${route}\n${handler}\n${catalog}`, /secretRef|secret_ref/); assert.doesNotMatch(component, /name="secretRef"/); assert.match(route, /\(encrypted_api_key is not null\) secret_configured/); assert.match(route, /secretConfigured:/); assert.doesNotMatch(route, /encryptedApiKey|encrypted_api_key:\s*provider/); assert.match(handler, /p\.encrypted_api_key/); assert.match(catalog, /resolveSessionLanguageModel/); }); test("mutation handler preserves an omitted key on edit and requires a key for enabled creates", async () => { let createQueries = 0; const missing = await handleAdminModelMutation({ action: "saveProvider", name: "OpenAI", providerType: "openai", enabled: true, reason: "create provider", }, { actorUserId: actorId, requestId: "provider-missing-key" }, { queryRows: async () => { createQueries += 1; return []; }, assertAllowedUrl: async () => undefined, probeAllowed: async () => 200, invalidateCatalog: () => undefined, environment, }); assert.equal(missing.status, 409); assert.equal(createQueries, 0); const calls: Array<{ sql: string; values?: readonly unknown[] }> = []; let invalidations = 0; const edited = await handleAdminModelMutation({ action: "saveProvider", id: "40000000-0000-4000-8000-000000000001", name: "OpenAI Renamed", providerType: "openai", apiKey: " ", enabled: true, reason: "rename provider", }, { actorUserId: actorId, requestId: "provider-edit" }, { queryRows: async (sql, values) => { calls.push({ sql, values }); return [{ id: "40000000-0000-4000-8000-000000000001" }]; }, assertAllowedUrl: async () => undefined, probeAllowed: async () => 200, invalidateCatalog: () => { invalidations += 1; }, environment, }); assert.equal(edited.status, 200); assert.equal(invalidations, 1); assert.match(calls[0]!.sql, /admin_save_model_provider\(\$1,\$2,\$3,\$4,\$5,\$6,\$7,\$8,\$9\)/); assert.equal(calls[0]!.values?.[5], null); }); test("mutation handler decrypts only the database ciphertext and records failed probes", async () => { const encrypted = encryptModelProviderApiKey("database-provider-key", environment); const calls: Array<{ sql: string; values?: readonly unknown[] }> = []; let observedHeaders: Readonly> | undefined; const failed = await handleAdminModelMutation({ action: "test", versionId: "30000000-0000-4000-8000-000000000001", }, { actorUserId: actorId, requestId: "test-unauthorized" }, { queryRows: async (sql, values) => { calls.push({ sql, values }); if (sql.includes("from public.model_config_versions")) { return [{ id: "40000000-0000-4000-8000-000000000001", code: "anthropic", provider_type: "anthropic", base_url: null, encrypted_api_key: encrypted, enabled: false, version_id: "30000000-0000-4000-8000-000000000001", version_enabled: false, }]; } return [{ id: "50000000-0000-4000-8000-000000000001" }]; }, assertAllowedUrl: async () => undefined, probeAllowed: async (_url, headers) => { observedHeaders = headers; return 401; }, invalidateCatalog: () => undefined, environment, }); assert.equal(failed.status, 409); assert.deepEqual(observedHeaders, { "x-api-key": "database-provider-key", "anthropic-version": "2023-06-01", }); assert.match(calls[1]!.sql, /admin_record_model_connection_test/); assert.deepEqual(calls[1]!.values, [actorId, "30000000-0000-4000-8000-000000000001", 401, "test-unauthorized"]); }); test("database drops secret refs, generates immutable codes, and invalidates evidence after key rotation", async () => { const fixture = startPostgresFixture(); const sql = (statement: string) => fixture.psql(statement); const sqlAsOwner = (statement: string) => fixture.psqlAs("schema_owner", "schema-owner-test-password", statement); const expectSqlError = (statement: string, pattern: RegExp) => assert.throws(() => sqlAsOwner(statement), pattern); const sqlAsAdminRuntime = (statement: string) => fixture.psqlAs("admin_runtime", "admin-runtime-test-password", statement); try { await migrateModelConfigurationFixture( fixture.connectionUrl("schema_owner", "schema-owner-test-password"), ); assert.equal(sql("select count(*) from information_schema.columns where table_schema='public' and table_name='model_providers' and column_name='secret_ref'"), "0"); assert.equal(sql("select to_regprocedure('public.expected_model_provider_secret_ref(text,text)') is null"), "t"); assert.equal(sql("select to_regprocedure('public.admin_save_model_provider(uuid,uuid,text,text,text,text,text,boolean,text,text)') is null"), "t"); assert.equal(sql("select to_regprocedure('public.admin_save_model_provider(uuid,uuid,text,text,text,text,boolean,text,text)') is not null"), "t"); fixture.psqlAs("identity_runtime", "identity-runtime-test-password", ` insert into identity.users (id,name,email,email_verified,email_verified_at,role) values ('${actorId}','Model Admin','model-admin@example.com',true,now(),'user') `); sql(` insert into public.admin_users(user_id,created_by) values ('${actorId}','${actorId}'); insert into public.admin_user_roles(admin_user_id,role_id,assigned_by) select '${actorId}',id,'${actorId}' from public.admin_roles where code='model_admin' `); expectSqlError( `select public.admin_save_model_provider('${deniedActorId}',null,'Denied','openai',null,null,false,'no permission','denied-provider')`, /admin_permission_denied/, ); expectSqlError( `select public.admin_save_model_provider('${actorId}',null,'Missing Key','openai',null,null,true,'missing key','missing-key')`, /model_provider_key_required/, ); const provider = sqlAsAdminRuntime(`select public.admin_save_model_provider('${actorId}',null,'OpenAI','openai',null,'ciphertext-v1',true,'create provider','provider-create')`); assert.equal(sqlAsAdminRuntime(`select code from public.model_providers where id='${provider}'`), "openai"); const conflictingProvider = sql(`select public.admin_save_model_provider('${actorId}',null,'OpenAI','openai',null,'ciphertext-other',true,'create collision','provider-collision')`); assert.equal(sql(`select code from public.model_providers where id='${provider}'`), "openai"); assert.equal(sql(`select code from public.model_providers where id='${conflictingProvider}'`), "openai-2"); const numericProvider = sql(`select public.admin_save_model_provider('${actorId}',null,'123 AI','openai',null,'ciphertext-v1',true,'create numeric provider','provider-numeric')`); const numericCollision = sql(`select public.admin_save_model_provider('${actorId}',null,'123 AI','openai',null,'ciphertext-v1',true,'create numeric collision','provider-numeric-collision')`); const oneLetterProvider = sql(`select public.admin_save_model_provider('${actorId}',null,'A','openai',null,'ciphertext-v1',true,'create one-letter provider','provider-one-letter')`); const chineseProvider = sql(`select public.admin_save_model_provider('${actorId}',null,'纯中文供应商','anthropic',null,'ciphertext-v1',true,'create Chinese provider','provider-chinese')`); assert.equal(sql(`select code from public.model_providers where id='${numericProvider}'`), "p-123-ai"); assert.equal(sql(`select code from public.model_providers where id='${numericCollision}'`), "p-123-ai-2"); assert.equal(sql(`select code from public.model_providers where id='${oneLetterProvider}'`), "a-p"); assert.equal(sql(`select code from public.model_providers where id='${chineseProvider}'`), "anthropic"); assert.equal(sql("select bool_and(code ~ '^[a-z][a-z0-9_-]{1,63}$') from public.model_providers"), "t"); const anthropic = sql(`select public.admin_save_model_provider('${actorId}',null,'Claude','anthropic','https://ignored.example/v1','anthropic-ciphertext',true,'create anthropic','provider-anthropic')`); assert.equal(sql(`select provider_type||':'||coalesce(base_url,'native') from public.model_providers where id='${anthropic}'`), "anthropic:native"); sql(`select public.admin_save_model_provider('${actorId}','${provider}','OpenAI Renamed','openai',null,null,true,'rename and preserve key','provider-edit')`); assert.equal(sql(`select code||':'||encrypted_api_key from public.model_providers where id='${provider}'`), "openai:ciphertext-v1"); const versionOne = sql(`select public.admin_save_model_draft('${actorId}','default-model',null,'${provider}','Default v1','', 'model-v1','standard',1,64000,0,0,true,true,null,'{}','create v1','draft-v1')`); sql(`select public.admin_record_model_connection_test('${actorId}','${versionOne}',200,'evidence-before-key-change')`); assert.equal(sql(`select public.model_connection_test_is_fresh('${versionOne}',null)`), "t"); sql(`select public.admin_save_model_provider('${actorId}','${provider}','OpenAI Renamed','openai',null,'ciphertext-v2',true,'rotate key','provider-rotate')`); assert.equal(sql(`select public.model_connection_test_is_fresh('${versionOne}',null)`), "f"); sql(`select public.admin_record_model_connection_test('${actorId}','${versionOne}',200,'publish-v1')`); sql(`select public.admin_publish_model('${actorId}','${versionOne}','publish v1','publish-v1')`); assert.equal(sqlAsAdminRuntime(`select model_id from public.model_configs where model_id='default-model'`), "default-model"); assert.equal(sqlAsAdminRuntime(`select id from public.model_config_versions where id='${versionOne}'`), versionOne); assert.equal(sqlAsAdminRuntime(`select to_version_id from public.model_publish_events where to_version_id='${versionOne}' and action='publish'`), versionOne); assert.equal(sqlAsAdminRuntime(`select version_id from public.model_connection_test_evidence where version_id='${versionOne}' and request_id='publish-v1'`), versionOne); for (const mutation of [ `update public.model_providers set code='mutated' where id='${provider}'`, `update public.model_providers set provider_type='anthropic' where id='${provider}'`, `update public.model_providers set base_url='https://changed.example/v1' where id='${provider}'`, ]) { expectSqlError(mutation, /model_provider_runtime_immutable/); } sql(`select public.admin_save_model_provider('${actorId}','${provider}','OpenAI Renamed','openai',null,null,false,'emergency disable','provider-disable')`); assert.equal(sql(`select enabled::text from public.model_providers where id='${provider}'`), "f"); assert.equal(sql(`select public.model_connection_test_is_fresh('${versionOne}',null)`), "t"); assert.equal(sql(`select exists(select 1 from public.model_connection_test_evidence e join public.model_providers p on p.id=e.provider_id where e.provider_id='${provider}' and e.version_id='${versionOne}' and e.request_id='publish-v1' and e.tested_at<=p.updated_at)::text`), "true"); expectSqlError( `select public.admin_save_model_provider('${actorId}','${provider}','OpenAI Renamed','openai',null,'untested-ciphertext',true,'unsafe key and enable','provider-reenable-new-key')`, /model_provider_reenable_test_required/, ); expectSqlError( `select public.admin_save_model_provider('${actorId}','${provider}','OpenAI Renamed','openai',null,null,true,'unsafe re-enable with pre-disable evidence','provider-reenable-pre-disable-test')`, /model_provider_reenable_test_required/, ); sql(`select public.admin_record_model_connection_test('${actorId}','${versionOne}',200,'provider-reenable-test')`); assert.equal(sql(`select exists(select 1 from public.model_connection_test_evidence e join public.model_providers p on p.id=e.provider_id where e.provider_id='${provider}' and e.version_id='${versionOne}' and e.request_id='provider-reenable-test' and e.tested_at>p.updated_at)::text`), "true"); sql(`select public.admin_save_model_provider('${actorId}','${provider}','OpenAI Renamed','openai',null,null,true,'tested re-enable','provider-reenable-tested')`); assert.equal(sql(`select enabled::text from public.model_providers where id='${provider}'`), "true"); const providerTwo = conflictingProvider; const versionTwo = sql(`select public.admin_save_model_draft('${actorId}','default-model',null,'${providerTwo}','Default v2','', 'model-v2','standard',1,64000,0,0,true,true,null,'{}','create v2','draft-v2')`); sql(`select public.admin_record_model_connection_test('${actorId}','${versionTwo}',204,'publish-v2')`); sql(`select public.admin_publish_model('${actorId}','${versionTwo}','publish v2','publish-v2')`); sql(`update public.model_providers set encrypted_api_key=null where id='${provider}'`); const configId = sql("select id from public.model_configs where model_id='default-model'"); expectSqlError( `select public.admin_rollback_model('${actorId}','${configId}',1,'rollback without database key','rollback-v1')`, /model_provider_unavailable/, ); sql(`update public.model_providers set encrypted_api_key='ciphertext-v3' where id='${provider}'`); sql(`select public.admin_record_model_connection_test('${actorId}','${versionOne}',200,'rollback-v1')`); sql(`select public.admin_rollback_model('${actorId}','${configId}',1,'rollback with database key','rollback-v1')`); assert.equal(sql(`select version from public.model_config_versions where config_id='${configId}' and status='published'`), "1"); const noKeyProvider = sql(`select public.admin_save_model_provider('${actorId}',null,'No Key','openai',null,null,false,'create disabled','provider-no-key')`); sql(`update public.model_providers set enabled=true where id='${noKeyProvider}'`); const noKeyVersion = sql(`select public.admin_save_model_draft('${actorId}','no-key-model',null,'${noKeyProvider}','No key','', 'model','standard',1,64000,0,0,true,false,null,'{}','create no-key draft','draft-no-key')`); sql(`select public.admin_record_model_connection_test('${actorId}','${noKeyVersion}',200,'publish-no-key')`); expectSqlError( `select public.admin_publish_model('${actorId}','${noKeyVersion}','publish no-key','publish-no-key')`, /model_provider_unavailable/, ); const audit = sql("select coalesce(string_agg(after_value::text,' '),'') from audit.admin_audit_logs where target_type='model_provider'"); assert.doesNotMatch(audit, /ciphertext-v1|ciphertext-v2|ciphertext-v3|anthropic-ciphertext|encrypted_api_key/i); } finally { fixture.stop(); } }); test("forward migration removes secret-ref dependencies and hashes credential state into evidence", () => { const migration = readFileSync(migrationPath, "utf8"); assert.match(migration, /drop trigger if exists model_providers_prevent_published_runtime_mutation/); assert.match(migration, /drop function if exists public\.prevent_published_model_provider_runtime_mutation\(\)/); assert.match(migration, /alter table public\.model_providers drop column if exists secret_ref/); assert.match(migration, /drop function if exists public\.expected_model_provider_secret_ref\(text,text\)/); assert.match(migration, /'providerCredentialHash',[\s\S]*digest\(convert_to\(p\.encrypted_api_key/); assert.doesNotMatch(migration, /'providerSecretRef'|secretRef/); assert.match(migration, /p\.encrypted_api_key is not null/); assert.match(migration, /models\.write/); assert.match(migration, /audit\.admin_audit_logs/); }); test("emergency-disable migration keeps runtime identity immutable and gates re-enable on fresh evidence", () => { const migration = readFileSync(emergencyDisableMigrationPath, "utf8"); assert.match(migration, /\(new\.code,new\.provider_type,new\.base_url\)/); assert.doesNotMatch(migration, /\(new\.code,new\.provider_type,new\.base_url,new\.enabled\)/); assert.match(migration, /old\.enabled and not new\.enabled/); assert.match(migration, /new\.updated_at:=clock_timestamp\(\)/); assert.match(migration, /not old\.enabled and new\.enabled/); assert.match(migration, /e\.tested_at>old\.updated_at/); assert.match(migration, /e\.http_status between 200 and 299/); assert.match(migration, /e\.config_hash=public\.model_version_config_hash\(v\.id\)/); assert.match(migration, /new\.encrypted_api_key is distinct from old\.encrypted_api_key/); assert.match(migration, /model_provider_reenable_test_required/); });