import { createHash } from 'node:crypto' import { spawnSync } from 'node:child_process' import { createReadStream, realpathSync, readFileSync } from 'node:fs' import { lstat, mkdir, readFile, readdir, rename, unlink, writeFile } from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const MIGRATIONS_ROOT = path.join(ROOT, 'prisma', 'migrations') const BAKED_COMMIT_FILE = path.join(ROOT, 'DEPLOYMENT_COMMIT') const TARGET_MIGRATIONS = Object.freeze([ '20260811_add_video_transcode_lease', '20260811_update_brand_logo_defaults', '20260812_add_product_package_upload_job_lease', '20260812_align_material_upload_idempotency_index', '20260813_add_material_custody_department', '20260813_add_organization_units_and_employment_state', '20260813_add_user_employee_binding', '20260817_add_material_is_incomplete_shell', '20260820_add_ding_workflow_snapshots', '20260824_expand_ding_workflow_insights', '20260826_add_ding_workflow_demand_type', '20260827_add_ding_workflow_detail_fields', '20260826_add_pack_video_early_access', '20260828_add_pack_item_can_edit_assets', '20260828_add_task_detail_change_approvals', '20260829_add_recoverable_api_key_secret', '20260831_add_private_task_detail', '20260902_add_employee_roster_history', '20260903_add_work_review_brand', '20260908_add_brand_knowledge', '20260909_expand_brand_knowledge_upload', '20260914_add_brand_knowledge_engagement', '20260914_expand_brand_knowledge_download_count', '20260914_expand_brand_knowledge_preview_slices', '20260915_add_brand_knowledge_soft_delete', '20260918_add_performance_conversion_sync', '20260923_add_certificate_report_metadata', '20260924_add_certificate_tested_on', '20261008_permission_group_department_identity', '20261008_team_function_configs', '20261009_material_purge_outbox', '20261009_material_purge_reference_indexes', '20261012_detail_generator', '20261012_ai_model_registry', '20261013_ai_model_capabilities', '20261013_ai_model_verifications', ]) const VERIFIED_BACKUP_GENERATOR = 'scripts/create-verified-production-backup.mjs' const VERIFIED_BACKUP_MAINTENANCE_CONFIRMATION = 'NO_WRITES_DURING_SNAPSHOT' const VERIFIED_BACKUP_SUMMARY_TABLES = Object.freeze([ 'materials', 'users', 'systemSettings', 'prismaMigrations', 'productPackageUploadJobs', ]) const ISOLATED_DATABASE_PATTERN = /^gongxiangp2_restore_[a-z0-9][a-z0-9_]{7,55}$/ function fail(message) { throw new Error(message) } export function requireExactCommit(value, label) { const normalized = String(value || '').trim() if (!/^[0-9a-f]{40}$/.test(normalized)) { fail(`${label} must be an exact 40-character lowercase Git SHA`) } return normalized } export function sha256(buffer) { return createHash('sha256').update(buffer).digest('hex') } export function normalizeColumnDefault(value) { let normalized = String(value ?? '').trim().replace(/::(?:text|character varying|boolean)$/i, '') if (normalized.startsWith("'") && normalized.endsWith("'")) { normalized = normalized.slice(1, -1) } return normalized } function normalizeConstraintExpression(value) { return String(value ?? '') .trim() .replace(/::(?:text|character varying|boolean)/gi, '') .replace(/["\s()]/g, '') .toLowerCase() } function assertExactCheckConstraint(constraint, expectedTable, expectedName, expectedColumns, expectedExpression) { if ( !constraint || String(constraint.tablename || '') !== expectedTable || String(constraint.constraint_name || '') !== expectedName || String(constraint.contype || '') !== 'c' || constraint.convalidated !== true ) { fail(`Database constraint mismatch: ${expectedName}`) } const actualColumns = Array.isArray(constraint.columns) ? constraint.columns.map((column) => String(column)).sort() : [] const normalizedExpectedColumns = [...expectedColumns].sort() if ( actualColumns.length !== normalizedExpectedColumns.length || actualColumns.some((column, index) => column !== normalizedExpectedColumns[index]) ) { fail(`Database constraint columns mismatch: ${expectedName}`) } if ( normalizeConstraintExpression(constraint.expression) !== normalizeConstraintExpression(expectedExpression) ) { fail(`Database constraint expression mismatch: ${expectedName}`) } } function assertExactForeignKeyConstraint(constraint, expectedTable, expectedName, expectedColumn, expectedReferencedTable, expectedReferencedColumn) { if ( !constraint || String(constraint.tablename || '') !== expectedTable || String(constraint.constraint_name || '') !== expectedName || String(constraint.contype || '') !== 'f' || constraint.convalidated !== true || String(constraint.referenced_table || '') !== expectedReferencedTable || !Array.isArray(constraint.columns) || constraint.columns.length !== 1 || String(constraint.columns[0]) !== expectedColumn || !Array.isArray(constraint.referenced_columns) || constraint.referenced_columns.length !== 1 || String(constraint.referenced_columns[0]) !== expectedReferencedColumn ) { fail(`Database foreign key mismatch: ${expectedName}`) } } function normalizedPort(value) { const port = String(value || '').trim() if (!/^\d{1,5}$/.test(port) || Number(port) < 1 || Number(port) > 65535) { fail('Database port must be an integer between 1 and 65535') } return String(Number(port)) } export function assertExactBtreeIndex(indexDefinition, expectedTable, expectedColumns) { const definition = String(indexDefinition || '').trim().replace(/\s+/g, ' ') if (!definition || /\bUNIQUE\b/i.test(definition) || /\bWHERE\b/i.test(definition)) { fail(`Database index must be a non-unique, non-partial btree index on ${expectedTable}`) } const match = definition.match( /^CREATE INDEX \S+ ON (?:ONLY )?(?:(?:"[^"]+"|[a-zA-Z_][\w$]*)\.)?("[^"]+"|[a-zA-Z_][\w$]*) USING ([a-zA-Z0-9_]+) \(([^()]*)\)$/i, ) if (!match) fail(`Database index definition is not a simple index on ${expectedTable}`) const table = match[1].replace(/^"|"$/g, '') const method = match[2].toLowerCase() const columns = match[3].split(',').map((part) => { const column = part.trim() const identifier = column.match(/^"?([a-zA-Z_][\w$]*)"?$/) if (!identifier) fail(`Database index contains an expression or sort modifier: ${column}`) return identifier[1] }) if ( table !== expectedTable || method !== 'btree' || columns.length !== expectedColumns.length || columns.some((column, index) => column !== expectedColumns[index]) ) { fail(`Database index columns or method mismatch: ${expectedTable}`) } } function assertExactUniqueBtreeIndex(index, expectedTable, expectedName, expectedColumns) { if ( !index || String(index.tablename || '') !== expectedTable || String(index.indexname || '') !== expectedName || index.indisunique !== true || index.indisprimary !== false || index.indisvalid !== true || index.indisready !== true || String(index.access_method || '') !== 'btree' || String(index.predicate || '') !== '' || !Array.isArray(index.columns) || index.columns.length !== expectedColumns.length || index.columns.some((column, position) => String(column).replaceAll('"', '') !== expectedColumns[position]) ) { fail(`Database unique index mismatch: ${expectedName}`) } } function normalizedPredicate(value) { return String(value || '') .replace(/[()\"]/g, '') .replace(/\s+/g, ' ') .trim() } export function assertExactActiveUploadIdIndex(index) { if ( !index || String(index.tablename || '') !== 'materials' || String(index.indexname || '') !== 'materials_uploadId_key' || index.indisunique !== true || index.indisprimary !== false || index.indisvalid !== true || index.indisready !== true || String(index.access_method || '') !== 'btree' || !Array.isArray(index.columns) || index.columns.length !== 1 || String(index.columns[0]) !== '"uploadId"' || normalizedPredicate(index.predicate) !== 'uploadId IS NOT NULL AND deletedAt IS NULL' ) { fail('Database index mismatch: materials_uploadId_key must be the exact active-only uploadId unique btree') } } async function sha256File(filePath) { const hash = createHash('sha256') await new Promise((resolve, reject) => { const stream = createReadStream(filePath) stream.on('data', (chunk) => hash.update(chunk)) stream.once('error', reject) stream.once('end', resolve) }) return hash.digest('hex') } export function databaseFingerprintHash(database) { return sha256(Buffer.from(JSON.stringify({ configuredHost: String(database.configuredHost || ''), configuredPort: String(database.configuredPort || ''), configuredDatabase: String(database.configuredDatabase || ''), configuredUser: String(database.configuredUser || ''), serverAddress: String(database.serverAddress || ''), serverPort: String(database.serverPort || ''), }))) } function stableJson(value) { if (Array.isArray(value)) return value.map(stableJson) if (value && typeof value === 'object') { return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableJson(value[key])])) } return value } export function verifiedBackupSummaryHash(summary) { return sha256(Buffer.from(JSON.stringify(stableJson(summary)))) } function assertBoundedBackupSummary(summary, label) { if ( !summary || typeof summary !== 'object' || Array.isArray(summary) || summary.schemaVersion !== 1 || summary.sampleLimit !== 32 ) { fail(`${label} must use the bounded backup summary schemaVersion 1`) } for (const table of VERIFIED_BACKUP_SUMMARY_TABLES) { const evidence = summary[table] if ( !evidence || typeof evidence !== 'object' || Array.isArray(evidence) || !/^(?:0|[1-9]\d*)$/.test(String(evidence.count || '')) || !/^[0-9a-f]{32}$/.test(String(evidence.sampleSha256InputMd5 || '')) ) { fail(`${label} is missing valid bounded evidence for ${table}`) } } } function assertIsolatedRestoreFingerprint(production, isolated) { const keys = [ 'configuredHost', 'configuredPort', 'configuredDatabase', 'configuredUser', 'serverAddress', 'serverPort', ] if ( !isolated || typeof isolated !== 'object' || Array.isArray(isolated) || keys.some((key) => !String(isolated[key] || '').trim()) || !ISOLATED_DATABASE_PATTERN.test(String(isolated.configuredDatabase)) ) { fail('Backup receipt must contain the exact isolated restore database fingerprint') } normalizedPort(isolated.configuredPort) normalizedPort(isolated.serverPort) if ( production.configuredDatabase === isolated.configuredDatabase || ( production.configuredHost === isolated.configuredHost && String(production.configuredPort) === String(isolated.configuredPort) ) || ( production.serverAddress === isolated.serverAddress && String(production.serverPort) === String(isolated.serverPort) ) ) { fail('Backup receipt isolated restore target is not distinct from production') } } function redactDatabaseFingerprint(url) { return { configuredHost: url.hostname, configuredPort: url.port || '5432', configuredDatabase: decodeURIComponent(url.pathname.replace(/^\//, '')), configuredUser: decodeURIComponent(url.username), } } async function loadLocalMigrations() { const entries = await readdir(MIGRATIONS_ROOT, { withFileTypes: true }) const result = new Map() for (const entry of entries) { if (!entry.isDirectory()) continue const sqlPath = path.join(MIGRATIONS_ROOT, entry.name, 'migration.sql') try { const sql = await readFile(sqlPath) result.set(entry.name, { sqlPath, checksum: sha256(sql) }) } catch { // Directories without migration.sql are not Prisma migrations. } } return result } async function queryEvidence(prisma, localMigrations) { const names = Array.from(localMigrations.keys()) const migrationRows = await prisma.$queryRawUnsafe( `SELECT migration_name, checksum, finished_at::text AS finished_at, rolled_back_at::text AS rolled_back_at, logs FROM "_prisma_migrations" WHERE migration_name = ANY($1::text[])`, names, ) const identityRows = await prisma.$queryRawUnsafe( `SELECT current_database() AS database, current_user AS "user", COALESCE(inet_server_addr()::text, '') AS server_address, COALESCE(inet_server_port()::text, '') AS server_port`, ) const columns = await prisma.$queryRawUnsafe( `SELECT table_name, column_name, data_type, udt_name, is_nullable, column_default FROM information_schema.columns WHERE table_schema = current_schema() AND ((table_name = 'materials' AND column_name IN ('uploadId', 'transcode_lease_token', 'transcode_lease_expires_at')) OR (table_name = 'materials' AND column_name IN ('custody_department_id', 'custody_at', 'custody_reason', 'is_incomplete_shell', 'task_detail_demand_type', 'task_detail_private', 'report_kind', 'certificate_expires_on', 'certificate_tested_on', 'certificate_number', 'certificate_usage_instructions', 'report_usage_rules')) OR (table_name = 'product_package_upload_jobs' AND column_name IN ('worker_id', 'lease_token', 'lease_expires_at')) OR (table_name = 'material_permission_pack_items' AND column_name IN ('can_access_private')) OR table_name IN ('material_permission_pack_departments', 'team_function_configs', 'team_function_config_audits', 'material_purge_jobs', 'material_purge_audits') OR table_name IN ('detail_generator_projects', 'detail_generator_assets', 'detail_generator_jobs', 'detail_generator_brand_profiles') OR table_name IN ('ai_model_providers', 'ai_model_catalog', 'ai_model_configuration', 'ai_model_audit', 'ai_model_verifications') OR (table_name = 'api_keys' AND column_name IN ('secret_ciphertext', 'secret_nonce', 'secret_auth_tag')) OR (table_name = 'api_key_reveal_audits' AND column_name IN ('id', 'api_key_id', 'viewer_user_id', 'created_at')) OR (table_name = 'system_settings' AND column_name IN ('headerLogoUrl', 'loginLogoUrl', 'logoUrl')) OR (table_name = 'organization_units' AND column_name IN ('id', 'source_dept_id', 'parent_id', 'path', 'level', 'is_active')) OR (table_name = 'performance_survey_employee_profiles' AND column_name IN ('employment_state', 'resigned_at', 'handover_dingtalk_user_id')) OR (table_name = 'performance_survey_employee_roster_snapshots' AND column_name IN ('id', 'captured_at', 'profile_count', 'employee_profile_id', 'dingtalk_user_id', 'name', 'department_name', 'department_path', 'employment_state', 'is_brand_center', 'created_at')) OR (table_name = 'user_employee_bindings' AND column_name IN ('id', 'user_id', 'employee_profile_id', 'binding_source', 'status')) OR (table_name = 'ding_workflow_snapshots' AND column_name IN ('process_instance_id', 'status', 'is_ongoing', 'source_updated_at_raw', 'scheme_field_count', 'task_count', 'active_task_count', 'synced_at', 'brand', 'demand_type', 'requester_name', 'requester_department', 'project_type', 'active_tasks', 'form_fields')) OR (table_name = 'ding_workflow_sync_states' AND column_name IN ('id', 'status', 'source_cursor_raw', 'last_completed_at', 'lease_owner', 'lease_expires_at', 'current_ongoing_count')) OR (table_name = 'task_detail_change_requests' AND column_name IN ('id', 'material_id', 'requester_user_id', 'manager_dingtalk_user_id', 'status', 'operations', 'base_material_updated_at', 'expires_at')) OR (table_name = 'task_detail_change_notifications' AND column_name IN ('id', 'request_id', 'event_type', 'business_key', 'status', 'next_retry_at')) OR (table_name = 'brand_knowledge' AND column_name IN ('id', 'title', 'knowledge_type', 'brand', 'product_category_name', 'file_name', 'file_path', 'mime_type', 'file_size', 'download_count', 'preview_image_name', 'preview_image_path', 'preview_image_slices', 'preview_image_mime_type', 'preview_image_size', 'uploaded_by', 'created_at', 'updated_at', 'deleted_at')) OR (table_name IN ('brand_knowledge_likes', 'brand_knowledge_favorites') AND column_name IN ('id', 'knowledge_id', 'user_id', 'created_at')) OR (table_name = 'performance_conversion_metric_snapshots' AND column_name IN ('id', 'source_key', 'source_row_id', 'product_id', 'metric_date', 'product_name', 'source_brand', 'category1', 'category2', 'category3', 'payment_amount', 'payment_users', 'cart_users', 'visitors', 'conversion_rate', 'favorites', 'impressions', 'clicks', 'click_rate', 'promotion_spend', 'promotion_revenue', 'roi', 'synced_at', 'created_at', 'updated_at')) OR (table_name = 'performance_conversion_sync_states' AND column_name IN ('id', 'status', 'source_cursor_raw', 'last_started_at', 'last_completed_at', 'last_error', 'lease_owner', 'lease_expires_at', 'rows_upserted', 'products_synced', 'created_at', 'updated_at'))) ORDER BY table_name, column_name`, ) const indexes = await prisma.$queryRawUnsafe( `SELECT tbl.relname AS tablename, idx.relname AS indexname, pg_get_indexdef(i.indexrelid) AS indexdef, i.indisunique, i.indisprimary, i.indisvalid, i.indisready, am.amname AS access_method, ARRAY( SELECT pg_get_indexdef(i.indexrelid, ordinal, true) FROM generate_series(1, i.indnkeyatts) AS ordinal ORDER BY ordinal ) AS columns, COALESCE(pg_get_expr(i.indpred, i.indrelid), '') AS predicate FROM pg_index i JOIN pg_class idx ON idx.oid = i.indexrelid JOIN pg_class tbl ON tbl.oid = i.indrelid JOIN pg_namespace ns ON ns.oid = tbl.relnamespace JOIN pg_am am ON am.oid = idx.relam WHERE ns.nspname = current_schema() AND idx.relname IN ( 'materials_transcode_lease_idx', 'materials_uploadId_key', 'materials_is_incomplete_shell_idx', 'brand_knowledge_product_category_name_idx', 'brand_knowledge_deleted_at_idx', 'brand_knowledge_likes_knowledge_id_user_id_key', 'brand_knowledge_likes_knowledge_id_idx', 'brand_knowledge_likes_user_id_idx', 'brand_knowledge_favorites_knowledge_id_user_id_key', 'brand_knowledge_favorites_knowledge_id_idx', 'brand_knowledge_favorites_user_id_idx', 'product_package_upload_jobs_lease_idx', 'ding_workflow_snapshots_ongoing_updated_idx', 'ding_workflow_snapshots_brand_idx', 'ding_workflow_snapshots_demand_type_idx', 'ding_workflow_snapshots_requester_department_idx', 'ding_workflow_sync_states_lease_idx', 'task_detail_change_requests_status_expires_idx', 'task_detail_change_requests_status_updated_idx', 'task_detail_change_notifications_status_retry_idx', 'api_key_reveal_audits_api_key_id_created_at_idx', 'api_key_reveal_audits_viewer_user_id_created_at_idx', 'performance_conversion_metric_snapshots_source_key_key', 'performance_conversion_metric_product_date_idx', 'performance_conversion_metric_date_idx', 'performance_conversion_sync_states_lease_idx' ,'material_permission_pack_dept_unit_key', 'material_permission_pack_departments_pack_id_idx', 'team_function_configs_organization_unit_id_key', 'team_function_config_audits_config_id_created_at_idx', 'material_purge_jobs_material_id_key', 'material_purge_jobs_status_next_attempt_at_idx', 'material_purge_audits_job_id_created_at_idx', 'materials_purge_references_idx', 'material_assets_purge_references_idx', 'material_skus_purge_references_idx', 'brand_knowledge_purge_references_idx', 'system_settings_purge_references_idx', 'performance_work_items_purge_references_idx', 'work_review_versions_purge_references_idx', 'package_upload_jobs_purge_references_idx', 'task_detail_changes_purge_references_idx', 'ai_model_providers_legacy_binding_key', 'ai_model_catalog_provider_model_key', 'ai_model_audit_created_idx', 'ai_model_verifications_idempotency_key', 'ai_model_verifications_unresolved_key', 'ai_model_verifications_model_stage_idx' ) ORDER BY idx.relname`, ) const constraints = await prisma.$queryRawUnsafe( `SELECT cls.relname AS tablename, con.conname AS constraint_name, con.contype::text AS contype, con.convalidated, con.confdeltype::text AS delete_action, ref.relname AS referenced_table, ARRAY( SELECT att.attname FROM unnest(con.conkey) WITH ORDINALITY AS keys(attnum, ordinal) JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = keys.attnum ORDER BY keys.ordinal ) AS columns, ARRAY( SELECT att.attname FROM unnest(con.confkey) WITH ORDINALITY AS keys(attnum, ordinal) JOIN pg_attribute att ON att.attrelid = con.confrelid AND att.attnum = keys.attnum ORDER BY keys.ordinal ) AS referenced_columns, pg_get_expr(con.conbin, con.conrelid, true) AS expression FROM pg_constraint con JOIN pg_class cls ON cls.oid = con.conrelid LEFT JOIN pg_class ref ON ref.oid = con.confrelid JOIN pg_namespace ns ON ns.oid = cls.relnamespace WHERE ns.nspname = current_schema() AND (con.conname IN ( 'materials_private_optimization_check', 'brand_knowledge_likes_knowledge_id_fkey', 'brand_knowledge_likes_user_id_fkey', 'brand_knowledge_favorites_knowledge_id_fkey', 'brand_knowledge_favorites_user_id_fkey' ,'material_permission_pack_departments_organization_unit_id_fkey', 'team_function_configs_organization_unit_id_fkey', 'team_function_configs_lead_employee_profile_id_fkey', 'team_function_config_audits_config_id_fkey', 'team_function_configs_revision_check', 'material_purge_audits_job_id_fkey' ) OR cls.relname IN ('ai_model_providers', 'ai_model_catalog', 'ai_model_configuration', 'ai_model_verifications') OR (cls.relname IN ('material_logs', 'material_download_logs') AND ref.relname = 'materials')) ORDER BY con.conname`, ) const brandRows = await prisma.$queryRawUnsafe( `SELECT id, "headerLogoUrl", "loginLogoUrl" FROM "system_settings" ORDER BY id`, ) const referenceFunctions = await prisma.$queryRawUnsafe(` SELECT proname, prosrc, provolatile::text, proparallel::text, proisstrict, pg_get_function_identity_arguments(p.oid) AS arguments FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = current_schema() AND proname IN ('purge_decode_reference', 'purge_json_reference_strings', 'purge_reference_keys')`) return { migrationRows, identity: identityRows[0], columns, indexes, constraints, brandRows, referenceFunctions } } function completedMigrationMap(rows) { const map = new Map() for (const row of rows) { if (!row.finished_at || row.rolled_back_at) continue map.set(String(row.migration_name), row) } return map } export function assertTargetMigrationEvidence(localMigrations, evidence) { const completed = completedMigrationMap(evidence.migrationRows) for (const name of TARGET_MIGRATIONS) { const local = localMigrations.get(name) if (!local) fail(`Required migration file is missing: ${name}`) const row = completed.get(name) if (!row) fail(`Required migration is not completed: ${name}`) if (String(row.checksum) !== local.checksum) { fail(`Database checksum differs from the exact source tree: ${name}`) } } const columnByKey = new Map(evidence.columns.map((row) => [ `${row.table_name}.${row.column_name}`, row, ])) const requireColumn = (table, column, type, nullable) => { const row = columnByKey.get(`${table}.${column}`) if (!row || row.data_type !== type || row.is_nullable !== nullable) { fail(`Database column mismatch: ${table}.${column}`) } } requireColumn('materials', 'transcode_lease_token', 'text', 'YES') requireColumn('materials', 'transcode_lease_expires_at', 'timestamp without time zone', 'YES') requireColumn('materials', 'uploadId', 'text', 'YES') requireColumn('materials', 'report_kind', 'text', 'YES') requireColumn('materials', 'certificate_expires_on', 'date', 'YES') requireColumn('materials', 'certificate_tested_on', 'date', 'YES') requireColumn('materials', 'certificate_number', 'text', 'YES') requireColumn('materials', 'certificate_usage_instructions', 'text', 'YES') requireColumn('materials', 'report_usage_rules', 'text', 'YES') requireColumn('api_keys', 'secret_ciphertext', 'text', 'YES') requireColumn('api_keys', 'secret_nonce', 'text', 'YES') requireColumn('api_keys', 'secret_auth_tag', 'text', 'YES') requireColumn('api_key_reveal_audits', 'id', 'text', 'NO') requireColumn('api_key_reveal_audits', 'api_key_id', 'text', 'NO') requireColumn('api_key_reveal_audits', 'viewer_user_id', 'text', 'NO') requireColumn('api_key_reveal_audits', 'created_at', 'timestamp without time zone', 'NO') requireColumn('task_detail_change_requests', 'id', 'text', 'NO') requireColumn('task_detail_change_requests', 'material_id', 'text', 'NO') requireColumn('task_detail_change_requests', 'requester_user_id', 'text', 'NO') requireColumn('task_detail_change_requests', 'manager_dingtalk_user_id', 'text', 'NO') requireColumn('brand_knowledge', 'id', 'text', 'NO') requireColumn('brand_knowledge', 'title', 'text', 'NO') requireColumn('brand_knowledge', 'knowledge_type', 'text', 'NO') requireColumn('brand_knowledge', 'brand', 'text', 'NO') requireColumn('brand_knowledge', 'product_category_name', 'text', 'NO') requireColumn('brand_knowledge', 'file_name', 'text', 'NO') requireColumn('brand_knowledge', 'file_path', 'text', 'NO') requireColumn('brand_knowledge', 'mime_type', 'text', 'NO') requireColumn('brand_knowledge', 'file_size', 'bigint', 'NO') requireColumn('brand_knowledge', 'download_count', 'integer', 'NO') requireColumn('brand_knowledge', 'preview_image_name', 'text', 'YES') requireColumn('brand_knowledge', 'preview_image_path', 'text', 'YES') requireColumn('brand_knowledge', 'preview_image_slices', 'ARRAY', 'NO') requireColumn('brand_knowledge', 'preview_image_mime_type', 'text', 'YES') requireColumn('brand_knowledge', 'preview_image_size', 'bigint', 'YES') requireColumn('brand_knowledge', 'uploaded_by', 'text', 'NO') requireColumn('brand_knowledge', 'created_at', 'timestamp without time zone', 'NO') requireColumn('brand_knowledge', 'updated_at', 'timestamp without time zone', 'NO') requireColumn('brand_knowledge', 'deleted_at', 'timestamp without time zone', 'YES') for (const table of ['brand_knowledge_likes', 'brand_knowledge_favorites']) { requireColumn(table, 'id', 'text', 'NO') requireColumn(table, 'knowledge_id', 'text', 'NO') requireColumn(table, 'user_id', 'text', 'NO') requireColumn(table, 'created_at', 'timestamp without time zone', 'NO') } requireColumn('task_detail_change_requests', 'status', 'text', 'NO') requireColumn('task_detail_change_requests', 'operations', 'jsonb', 'NO') requireColumn('task_detail_change_requests', 'base_material_updated_at', 'timestamp without time zone', 'NO') requireColumn('task_detail_change_requests', 'expires_at', 'timestamp without time zone', 'NO') requireColumn('task_detail_change_notifications', 'id', 'text', 'NO') requireColumn('task_detail_change_notifications', 'request_id', 'text', 'NO') requireColumn('task_detail_change_notifications', 'event_type', 'text', 'NO') requireColumn('task_detail_change_notifications', 'business_key', 'text', 'NO') requireColumn('task_detail_change_notifications', 'status', 'text', 'NO') requireColumn('task_detail_change_notifications', 'next_retry_at', 'timestamp without time zone', 'YES') requireColumn('materials', 'custody_department_id', 'text', 'YES') requireColumn('materials', 'custody_at', 'timestamp without time zone', 'YES') requireColumn('materials', 'custody_reason', 'text', 'YES') requireColumn('materials', 'task_detail_demand_type', 'text', 'YES') requireColumn('materials', 'task_detail_private', 'boolean', 'NO') requireColumn('materials', 'is_incomplete_shell', 'boolean', 'NO') requireColumn('material_permission_pack_items', 'can_access_private', 'boolean', 'NO') requireColumn('product_package_upload_jobs', 'worker_id', 'text', 'YES') requireColumn('product_package_upload_jobs', 'lease_token', 'text', 'YES') requireColumn('product_package_upload_jobs', 'lease_expires_at', 'timestamp without time zone', 'YES') requireColumn('organization_units', 'id', 'text', 'NO') requireColumn('organization_units', 'source_dept_id', 'integer', 'NO') requireColumn('organization_units', 'parent_id', 'text', 'YES') requireColumn('organization_units', 'path', 'text', 'NO') requireColumn('organization_units', 'level', 'integer', 'NO') requireColumn('organization_units', 'is_active', 'boolean', 'NO') requireColumn('performance_survey_employee_profiles', 'employment_state', 'text', 'YES') requireColumn('performance_survey_employee_profiles', 'resigned_at', 'timestamp without time zone', 'YES') requireColumn('performance_survey_employee_profiles', 'handover_dingtalk_user_id', 'text', 'YES') requireColumn('performance_survey_employee_roster_snapshots', 'id', 'text', 'NO') requireColumn('performance_survey_employee_roster_snapshots', 'captured_at', 'timestamp without time zone', 'NO') requireColumn('performance_survey_employee_roster_snapshots', 'profile_count', 'integer', 'NO') requireColumn('performance_survey_employee_roster_snapshots', 'employee_profile_id', 'text', 'NO') requireColumn('performance_survey_employee_roster_snapshots', 'dingtalk_user_id', 'text', 'YES') requireColumn('performance_survey_employee_roster_snapshots', 'name', 'text', 'NO') requireColumn('performance_survey_employee_roster_snapshots', 'department_name', 'text', 'YES') requireColumn('performance_survey_employee_roster_snapshots', 'department_path', 'text', 'YES') requireColumn('performance_survey_employee_roster_snapshots', 'employment_state', 'text', 'YES') requireColumn('performance_survey_employee_roster_snapshots', 'is_brand_center', 'boolean', 'NO') requireColumn('performance_survey_employee_roster_snapshots', 'created_at', 'timestamp without time zone', 'NO') requireColumn('user_employee_bindings', 'id', 'text', 'NO') requireColumn('user_employee_bindings', 'user_id', 'text', 'NO') requireColumn('user_employee_bindings', 'employee_profile_id', 'text', 'NO') requireColumn('user_employee_bindings', 'binding_source', 'text', 'NO') requireColumn('user_employee_bindings', 'status', 'text', 'NO') requireColumn('ding_workflow_snapshots', 'process_instance_id', 'text', 'NO') requireColumn('ding_workflow_snapshots', 'status', 'text', 'NO') requireColumn('ding_workflow_snapshots', 'is_ongoing', 'boolean', 'NO') requireColumn('ding_workflow_snapshots', 'source_updated_at_raw', 'text', 'YES') requireColumn('ding_workflow_snapshots', 'scheme_field_count', 'integer', 'NO') requireColumn('ding_workflow_snapshots', 'task_count', 'integer', 'NO') requireColumn('ding_workflow_snapshots', 'active_task_count', 'integer', 'NO') requireColumn('ding_workflow_snapshots', 'synced_at', 'timestamp without time zone', 'NO') requireColumn('ding_workflow_snapshots', 'brand', 'text', 'YES') requireColumn('ding_workflow_snapshots', 'demand_type', 'text', 'YES') requireColumn('ding_workflow_snapshots', 'requester_name', 'text', 'YES') requireColumn('ding_workflow_snapshots', 'requester_department', 'text', 'YES') requireColumn('ding_workflow_snapshots', 'project_type', 'text', 'YES') requireColumn('ding_workflow_snapshots', 'active_tasks', 'jsonb', 'YES') requireColumn('ding_workflow_snapshots', 'form_fields', 'jsonb', 'YES') requireColumn('ding_workflow_sync_states', 'id', 'text', 'NO') requireColumn('ding_workflow_sync_states', 'status', 'text', 'NO') requireColumn('ding_workflow_sync_states', 'source_cursor_raw', 'text', 'YES') requireColumn('ding_workflow_sync_states', 'last_completed_at', 'timestamp without time zone', 'YES') requireColumn('ding_workflow_sync_states', 'lease_owner', 'text', 'YES') requireColumn('ding_workflow_sync_states', 'lease_expires_at', 'timestamp without time zone', 'YES') requireColumn('ding_workflow_sync_states', 'current_ongoing_count', 'integer', 'NO') requireColumn('system_settings', 'headerLogoUrl', 'text', 'NO') requireColumn('system_settings', 'loginLogoUrl', 'text', 'NO') requireColumn('performance_conversion_metric_snapshots', 'id', 'text', 'NO') requireColumn('performance_conversion_metric_snapshots', 'source_key', 'text', 'NO') requireColumn('performance_conversion_metric_snapshots', 'source_row_id', 'text', 'YES') requireColumn('performance_conversion_metric_snapshots', 'product_id', 'text', 'NO') requireColumn('performance_conversion_metric_snapshots', 'metric_date', 'date', 'NO') for (const column of [ 'product_name', 'source_brand', 'category1', 'category2', 'category3', 'payment_amount', 'payment_users', 'cart_users', 'visitors', 'conversion_rate', 'favorites', 'impressions', 'clicks', 'click_rate', 'promotion_spend', 'promotion_revenue', 'roi', ]) { requireColumn('performance_conversion_metric_snapshots', column, 'text', 'YES') } requireColumn('performance_conversion_metric_snapshots', 'synced_at', 'timestamp without time zone', 'NO') requireColumn('performance_conversion_metric_snapshots', 'created_at', 'timestamp without time zone', 'NO') requireColumn('performance_conversion_metric_snapshots', 'updated_at', 'timestamp without time zone', 'NO') requireColumn('performance_conversion_sync_states', 'id', 'text', 'NO') requireColumn('performance_conversion_sync_states', 'status', 'text', 'NO') requireColumn('performance_conversion_sync_states', 'source_cursor_raw', 'text', 'YES') requireColumn('performance_conversion_sync_states', 'last_started_at', 'timestamp without time zone', 'YES') requireColumn('performance_conversion_sync_states', 'last_completed_at', 'timestamp without time zone', 'YES') requireColumn('performance_conversion_sync_states', 'last_error', 'text', 'YES') requireColumn('performance_conversion_sync_states', 'lease_owner', 'text', 'YES') requireColumn('performance_conversion_sync_states', 'lease_expires_at', 'timestamp without time zone', 'YES') requireColumn('performance_conversion_sync_states', 'rows_upserted', 'integer', 'NO') requireColumn('performance_conversion_sync_states', 'products_synced', 'integer', 'NO') requireColumn('performance_conversion_sync_states', 'created_at', 'timestamp without time zone', 'NO') requireColumn('performance_conversion_sync_states', 'updated_at', 'timestamp without time zone', 'NO') const header = columnByKey.get('system_settings.headerLogoUrl') const login = columnByKey.get('system_settings.loginLogoUrl') const legacy = columnByKey.get('system_settings.logoUrl') const taskDetailDemandType = columnByKey.get('materials.task_detail_demand_type') const taskDetailPrivate = columnByKey.get('materials.task_detail_private') const canAccessPrivate = columnByKey.get('material_permission_pack_items.can_access_private') const brandKnowledgeDownloadCount = columnByKey.get('brand_knowledge.download_count') const brandKnowledgePreviewSlices = columnByKey.get('brand_knowledge.preview_image_slices') const performanceSyncStatus = columnByKey.get('performance_conversion_sync_states.status') const performanceSyncRowsUpserted = columnByKey.get('performance_conversion_sync_states.rows_upserted') const performanceSyncProductsSynced = columnByKey.get('performance_conversion_sync_states.products_synced') if (!header || normalizeColumnDefault(header.column_default) !== '/brand/header-logo.png') { fail('Database default mismatch: system_settings.headerLogoUrl') } if (!login || normalizeColumnDefault(login.column_default) !== '/brand/login-logo.png') { fail('Database default mismatch: system_settings.loginLogoUrl') } if (!taskDetailDemandType || normalizeColumnDefault(taskDetailDemandType.column_default) !== '') { fail('Database default mismatch: materials.task_detail_demand_type') } if (!taskDetailPrivate || normalizeColumnDefault(taskDetailPrivate.column_default) !== 'false') { fail('Database default mismatch: materials.task_detail_private') } if (!canAccessPrivate || normalizeColumnDefault(canAccessPrivate.column_default) !== 'false') { fail('Database default mismatch: material_permission_pack_items.can_access_private') } if (!brandKnowledgeDownloadCount || normalizeColumnDefault(brandKnowledgeDownloadCount.column_default) !== '0') { fail('Database default mismatch: brand_knowledge.download_count') } if (!brandKnowledgePreviewSlices || String(brandKnowledgePreviewSlices.column_default || '').replace(/\s+/g, '').toLowerCase() !== 'array[]::text[]') { fail('Database default mismatch: brand_knowledge.preview_image_slices') } if (!performanceSyncStatus || normalizeColumnDefault(performanceSyncStatus.column_default) !== 'idle') { fail('Database default mismatch: performance_conversion_sync_states.status') } if (!performanceSyncRowsUpserted || normalizeColumnDefault(performanceSyncRowsUpserted.column_default) !== '0') { fail('Database default mismatch: performance_conversion_sync_states.rows_upserted') } if (!performanceSyncProductsSynced || normalizeColumnDefault(performanceSyncProductsSynced.column_default) !== '0') { fail('Database default mismatch: performance_conversion_sync_states.products_synced') } if (legacy && (legacy.data_type !== 'text' || legacy.is_nullable !== 'NO')) { fail('Database column mismatch: system_settings.logoUrl') } if (legacy && normalizeColumnDefault(legacy.column_default) !== '/brand/header-logo.png') { fail('Database default mismatch: system_settings.logoUrl') } const indexByName = new Map(evidence.indexes.map((row) => [String(row.indexname), String(row.indexdef)])) assertExactBtreeIndex( indexByName.get('materials_transcode_lease_idx'), 'materials', ['transcode_lease_expires_at', 'transcode_lease_token'], ) assertExactBtreeIndex( indexByName.get('product_package_upload_jobs_lease_idx'), 'product_package_upload_jobs', ['status', 'lease_expires_at'], ) assertExactActiveUploadIdIndex( evidence.indexes.find((row) => String(row.indexname) === 'materials_uploadId_key'), ) assertExactBtreeIndex( indexByName.get('materials_is_incomplete_shell_idx'), 'materials', ['is_incomplete_shell'], ) assertExactBtreeIndex( indexByName.get('brand_knowledge_product_category_name_idx'), 'brand_knowledge', ['product_category_name'], ) assertExactBtreeIndex( indexByName.get('brand_knowledge_deleted_at_idx'), 'brand_knowledge', ['deleted_at'], ) for (const table of ['brand_knowledge_likes', 'brand_knowledge_favorites']) { assertExactUniqueBtreeIndex( evidence.indexes.find((row) => String(row.indexname) === `${table}_knowledge_id_user_id_key`), table, `${table}_knowledge_id_user_id_key`, ['knowledge_id', 'user_id'], ) assertExactBtreeIndex(indexByName.get(`${table}_knowledge_id_idx`), table, ['knowledge_id']) assertExactBtreeIndex(indexByName.get(`${table}_user_id_idx`), table, ['user_id']) } assertExactBtreeIndex( indexByName.get('ding_workflow_snapshots_ongoing_updated_idx'), 'ding_workflow_snapshots', ['is_ongoing', 'source_updated_at_raw'], ) assertExactBtreeIndex( indexByName.get('ding_workflow_snapshots_brand_idx'), 'ding_workflow_snapshots', ['brand'], ) assertExactBtreeIndex( indexByName.get('ding_workflow_snapshots_demand_type_idx'), 'ding_workflow_snapshots', ['demand_type'], ) assertExactBtreeIndex( indexByName.get('ding_workflow_snapshots_requester_department_idx'), 'ding_workflow_snapshots', ['requester_department'], ) assertExactBtreeIndex( indexByName.get('ding_workflow_sync_states_lease_idx'), 'ding_workflow_sync_states', ['lease_expires_at'], ) assertExactBtreeIndex( indexByName.get('task_detail_change_requests_status_expires_idx'), 'task_detail_change_requests', ['status', 'expires_at'], ) assertExactBtreeIndex( indexByName.get('task_detail_change_requests_status_updated_idx'), 'task_detail_change_requests', ['status', 'updated_at'], ) assertExactBtreeIndex( indexByName.get('task_detail_change_notifications_status_retry_idx'), 'task_detail_change_notifications', ['status', 'next_retry_at'], ) assertExactBtreeIndex( indexByName.get('api_key_reveal_audits_api_key_id_created_at_idx'), 'api_key_reveal_audits', ['api_key_id', 'created_at'], ) assertExactBtreeIndex( indexByName.get('api_key_reveal_audits_viewer_user_id_created_at_idx'), 'api_key_reveal_audits', ['viewer_user_id', 'created_at'], ) assertExactUniqueBtreeIndex( evidence.indexes.find((row) => String(row.indexname) === 'performance_conversion_metric_snapshots_source_key_key'), 'performance_conversion_metric_snapshots', 'performance_conversion_metric_snapshots_source_key_key', ['source_key'], ) assertExactBtreeIndex( indexByName.get('performance_conversion_metric_product_date_idx'), 'performance_conversion_metric_snapshots', ['product_id', 'metric_date'], ) assertExactBtreeIndex( indexByName.get('performance_conversion_metric_date_idx'), 'performance_conversion_metric_snapshots', ['metric_date'], ) assertExactBtreeIndex( indexByName.get('performance_conversion_sync_states_lease_idx'), 'performance_conversion_sync_states', ['lease_expires_at'], ) assertExactCheckConstraint( (evidence.constraints || []).find((row) => String(row.constraint_name) === 'materials_private_optimization_check'), 'materials', 'materials_private_optimization_check', ['materialType', 'task_detail_demand_type', 'task_detail_private'], `NOT task_detail_private OR ((materialType = '任务-详情' AND task_detail_demand_type = '优化') IS TRUE)`, ) for (const table of ['brand_knowledge_likes', 'brand_knowledge_favorites']) { assertExactForeignKeyConstraint( (evidence.constraints || []).find((row) => String(row.constraint_name) === `${table}_knowledge_id_fkey`), table, `${table}_knowledge_id_fkey`, 'knowledge_id', 'brand_knowledge', 'id', ) assertExactForeignKeyConstraint( (evidence.constraints || []).find((row) => String(row.constraint_name) === `${table}_user_id_fkey`), table, `${table}_user_id_fkey`, 'user_id', 'users', 'id', ) } const isTrustedBrandValue = (value, localDefault) => { const normalized = String(value || '').trim() if (normalized === localDefault) return true try { const parsed = new URL(normalized) return parsed.protocol === 'https:' && parsed.hostname === 's3.jiansun.vip' && parsed.port === '' && parsed.username === '' && parsed.password === '' && parsed.pathname.startsWith('/gongxiangpan/') && parsed.search === '' && parsed.hash === '' } catch { return false } } for (const row of evidence.brandRows || []) { if (!isTrustedBrandValue(row.headerLogoUrl, '/brand/header-logo.png')) { fail(`Stored brand logo is untrusted: system_settings.${row.id}.headerLogoUrl`) } if (!isTrustedBrandValue(row.loginLogoUrl, '/brand/login-logo.png')) { fail(`Stored brand logo is untrusted: system_settings.${row.id}.loginLogoUrl`) } } assertOrganizationConfigurationEvidence(evidence) assertDetailGeneratorEvidence(evidence) assertAiModelRegistryEvidence(evidence) assertMaterialPurgeEvidence(evidence) assertMaterialPurgeReferenceEvidence(evidence) } export function materialPurgeReferenceExpectations() { const sql = readFileSync(path.join(MIGRATIONS_ROOT, '20261009_material_purge_reference_indexes/migration.sql'), 'utf8').replace(/\r\n/g, '\n') const functions = [...sql.matchAll(/CREATE FUNCTION (\w+)\(([^)]*)\) RETURNS [^\n]+\nLANGUAGE plpgsql IMMUTABLE STRICT PARALLEL UNSAFE AS \$\$([\s\S]*?)\$\$;/g)] .map(match => ({ proname: match[1], arguments: match[2], prosrc: match[3], provolatile: 'i', proparallel: 'u', proisstrict: true })) const indexes = [...sql.matchAll(/CREATE INDEX (\w+) ON (\w+) USING gin\n \(([^;]+)\);/g)] .map(match => { const [expression, predicate = ''] = match[3].split(') WHERE ') const normalizedPredicate = predicate ? predicate.startsWith('status NOT IN') ? "status <> ALL (ARRAY['applied','rejected','expired','cancelled'])" : "status = ANY (ARRAY['queued','processing'])" : '' return { indexname: match[1], tablename: match[2], access_method: 'gin', indisunique: false, indisprimary: false, indisvalid: true, indisready: true, columns: [expression], predicate: normalizedPredicate } }) if (functions.length !== 3 || indexes.length !== 9) fail('Purge reference migration definitions are incomplete') return { referenceFunctions: functions, indexes } } export function assertDetailGeneratorEvidence(evidence) { const columns = new Map(evidence.columns.map(row => [`${row.table_name}.${row.column_name}`, row])) const required = { detail_generator_projects: { id: 'text', owner_id: 'text', document: 'jsonb', revision: 'integer' }, detail_generator_assets: { id: 'text', project_id: 'text', owner_id: 'text', storage_key: 'text', byte_size: 'integer', mime_type: 'text' }, detail_generator_jobs: { id: 'text', project_id: 'text', owner_id: 'text', status: 'text', idempotency_key: 'text', checkpoint: 'jsonb', project_revision: 'integer', lease_token: 'text', lease_expires_at: 'timestamp with time zone' }, detail_generator_brand_profiles: { id: 'text', name: 'text', profile: 'jsonb', revision: 'integer' }, } for (const [table, fields] of Object.entries(required)) { for (const [column, type] of Object.entries(fields)) { if (columns.get(`${table}.${column}`)?.data_type !== type) fail(`Database column mismatch: ${table}.${column}`) } } } export function assertAiModelRegistryEvidence(evidence) { const columns = new Map(evidence.columns.map(row => [`${row.table_name}.${row.column_name}`, row])) const required = { ai_model_providers: { id: 'text', name: 'text', base_url: 'text', enabled: 'boolean', credential_source: 'text', secret_ciphertext: 'text?', secret_nonce: 'text?', secret_auth_tag: 'text?', legacy_binding: 'boolean', revision: 'integer', synced_at: 'timestamp with time zone?', created_at: 'timestamp with time zone', updated_at: 'timestamp with time zone' }, ai_model_catalog: { id: 'text', provider_id: 'text', model_id: 'text', enabled: 'boolean', stages: 'ARRAY', present: 'boolean', compatibility: 'text', capability_metadata: 'jsonb?', created_at: 'timestamp with time zone', updated_at: 'timestamp with time zone' }, ai_model_configuration: { id: 'text', default_analysis: 'text?', default_copy: 'text?', default_image: 'text?', revision: 'integer', updated_at: 'timestamp with time zone' }, ai_model_audit: { id: 'text', actor_id: 'text', action: 'text', provider_id: 'text?', details: 'jsonb', created_at: 'timestamp with time zone' }, ai_model_verifications: { id: 'text', model_key: 'text', stage: 'text', context_hash: 'text', idempotency_key: 'text', actor_id: 'text', status: 'text', request_id: 'text?', task_id: 'text?', message: 'text?', evidence: 'jsonb', started_at: 'timestamp with time zone', deadline_at: 'timestamp with time zone', finished_at: 'timestamp with time zone?' }, } for (const [table, fields] of Object.entries(required)) for (const [column, definition] of Object.entries(fields)) { const nullable = definition.endsWith('?'), type = nullable ? definition.slice(0, -1) : definition const row = columns.get(`${table}.${column}`) if (!row || row.data_type !== type || row.is_nullable !== (nullable ? 'YES' : 'NO') || type === 'ARRAY' && row.udt_name !== '_text') fail(`Database column mismatch: ${table}.${column}`) } for (const [table, column, expected] of [ ['ai_model_providers', 'enabled', 'false'], ['ai_model_providers', 'legacy_binding', 'false'], ['ai_model_catalog', 'enabled', 'false'], ['ai_model_catalog', 'present', 'true'], ['ai_model_catalog', 'compatibility', 'unconfirmed'], ]) if (normalizeColumnDefault(columns.get(`${table}.${column}`)?.column_default) !== expected) fail(`Database default mismatch: ${table}.${column}`) const indexes = evidence.indexes || [], constraints = evidence.constraints || [] assertExactUniqueBtreeIndex(indexes.find(row => row.indexname === 'ai_model_catalog_provider_model_key'), 'ai_model_catalog', 'ai_model_catalog_provider_model_key', ['provider_id', 'model_id']) const legacy = indexes.find(row => row.indexname === 'ai_model_providers_legacy_binding_key') if (normalizeConstraintExpression(legacy?.predicate) !== 'legacy_binding=true') fail('Database index predicate mismatch: ai_model_providers_legacy_binding_key') assertExactUniqueBtreeIndex(legacy && { ...legacy, predicate: '' }, 'ai_model_providers', 'ai_model_providers_legacy_binding_key', ['legacy_binding']) assertExactBtreeIndex(indexes.find(row => row.indexname === 'ai_model_audit_created_idx')?.indexdef, 'ai_model_audit', ['created_at']) assertExactUniqueBtreeIndex(indexes.find(row => row.indexname === 'ai_model_verifications_idempotency_key'), 'ai_model_verifications', 'ai_model_verifications_idempotency_key', ['model_key', 'stage', 'idempotency_key']) const unresolved = indexes.find(row => row.indexname === 'ai_model_verifications_unresolved_key') if (normalizeConstraintExpression(unresolved?.predicate) !== normalizeConstraintExpression("status = ANY (ARRAY['running', 'unknown'])")) fail('Database index predicate mismatch: ai_model_verifications_unresolved_key') assertExactUniqueBtreeIndex(unresolved && { ...unresolved, predicate: '' }, 'ai_model_verifications', 'ai_model_verifications_unresolved_key', ['model_key', 'stage']) const verificationOrder = indexes.find(row => row.indexname === 'ai_model_verifications_model_stage_idx') if (!verificationOrder || verificationOrder.indisvalid !== true || verificationOrder.indisready !== true || verificationOrder.indisunique !== false || String(verificationOrder.predicate || '') !== '' || !/^CREATE INDEX \S+ ON (?:public\.)?ai_model_verifications USING btree \(model_key, stage, started_at DESC\)$/.test(String(verificationOrder.indexdef || ''))) fail('Database index mismatch: ai_model_verifications_model_stage_idx') for (const [table, column, referenced] of [ ['ai_model_catalog', 'provider_id', 'ai_model_providers'], ['ai_model_verifications', 'model_key', 'ai_model_catalog'], ...['default_analysis', 'default_copy', 'default_image'].map(column => ['ai_model_configuration', column, 'ai_model_catalog']), ]) { const name = `${table}_${column}_fkey`, constraint = constraints.find(row => row.constraint_name === name) assertExactForeignKeyConstraint(constraint, table, name, column, referenced, 'id') if (constraint.delete_action !== 'r') fail(`Database foreign key deletion mismatch: ${name}`) } for (const [table, name, fields, expression] of [ ['ai_model_providers', 'ai_model_providers_credential_source_check', ['credential_source'], "credential_source = ANY (ARRAY['environment', 'encrypted'])"], ['ai_model_providers', 'ai_model_providers_revision_check', ['revision'], 'revision > 0'], ['ai_model_providers', 'ai_model_credential_complete', ['credential_source', 'secret_ciphertext', 'secret_nonce', 'secret_auth_tag'], "credential_source = 'environment' AND secret_ciphertext IS NULL AND secret_nonce IS NULL AND secret_auth_tag IS NULL OR credential_source = 'encrypted' AND secret_ciphertext IS NOT NULL AND secret_nonce IS NOT NULL AND secret_auth_tag IS NOT NULL"], ['ai_model_catalog', 'ai_model_catalog_stages_check', ['stages'], "stages <@ ARRAY['analysis', 'copy', 'image']"], ['ai_model_catalog', 'ai_model_catalog_compatibility_check', ['compatibility'], "compatibility = ANY (ARRAY['confirmed', 'unconfirmed'])"], ['ai_model_catalog', 'ai_model_enabled_stages', ['enabled', 'stages'], 'NOT enabled OR cardinality(stages) > 0'], ['ai_model_verifications', 'ai_model_verifications_stage_check', ['stage'], "stage = ANY (ARRAY['analysis', 'copy', 'image'])"], ['ai_model_verifications', 'ai_model_verifications_context_hash_check', ['context_hash'], "context_hash ~ '^[0-9a-f]{64}$'"], ['ai_model_verifications', 'ai_model_verifications_status_check', ['status'], "status = ANY (ARRAY['running', 'passed', 'failed', 'unknown'])"], ['ai_model_configuration', 'ai_model_configuration_id_check', ['id'], "id = 'detail-generator'"], ['ai_model_configuration', 'ai_model_configuration_revision_check', ['revision'], 'revision > 0'], ]) assertExactCheckConstraint(constraints.find(row => row.constraint_name === name), table, name, fields, expression) } export function assertMaterialPurgeReferenceEvidence(evidence) { const expected = materialPurgeReferenceExpectations() for (const item of expected.indexes) { const actual = evidence.indexes.find(index => index.indexname === item.indexname) if (!actual || actual.tablename !== item.tablename || actual.access_method !== 'gin' || actual.indisunique !== false || actual.indisprimary !== false || actual.indisvalid !== true || actual.indisready !== true || actual.columns?.length !== 1 || normalizeConstraintExpression(actual.columns[0]) !== normalizeConstraintExpression(item.columns[0]) || normalizeConstraintExpression(actual.predicate) !== normalizeConstraintExpression(item.predicate)) { fail(`Purge reference index mismatch: ${item.indexname}`) } } for (const item of expected.referenceFunctions) { const matches = (evidence.referenceFunctions || []).filter(fn => fn.proname === item.proname) const actual = matches[0] if (matches.length !== 1 || actual.arguments !== item.arguments || actual.provolatile !== 'i' || actual.proparallel !== 'u' || actual.proisstrict !== true || String(actual.prosrc).replace(/\r\n/g, '\n') !== item.prosrc) { fail(`Purge reference function mismatch: ${item.proname}`) } } } export function assertMaterialPurgeEvidence(evidence) { const columns = new Map(evidence.columns.map(row => [`${row.table_name}.${row.column_name}`, row])) const job = 'material_purge_jobs' const audit = 'material_purge_audits' for (const [table, column, type, nullable] of [ ...['id', 'material_id', 'operator_user_id', 'status'].map(column => [job, column, 'text', 'NO']), ...['material_snapshot', 'files'].map(column => [job, column, 'jsonb', 'NO']), [job, 'attempts', 'integer', 'NO'], ...['error_message', 'lease_token'].map(column => [job, column, 'text', 'YES']), ...['lease_expires_at', 'completed_at'].map(column => [job, column, 'timestamp without time zone', 'YES']), ...['next_attempt_at', 'created_at', 'updated_at'].map(column => [job, column, 'timestamp without time zone', 'NO']), ...['id', 'job_id', 'event'].map(column => [audit, column, 'text', 'NO']), ...['operator_user_id', 'message'].map(column => [audit, column, 'text', 'YES']), [audit, 'created_at', 'timestamp without time zone', 'NO'], ]) { const row = columns.get(`${table}.${column}`) if (!row || row.data_type !== type || row.is_nullable !== nullable) fail(`Database column mismatch: ${table}.${column}`) } const uniqueName = 'material_purge_jobs_material_id_key' assertExactUniqueBtreeIndex(evidence.indexes.find(row => row.indexname === uniqueName), job, uniqueName, ['material_id']) for (const [table, name, fields] of [ [job, 'material_purge_jobs_status_next_attempt_at_idx', ['status', 'next_attempt_at']], [audit, 'material_purge_audits_job_id_created_at_idx', ['job_id', 'created_at']], ]) assertExactBtreeIndex(evidence.indexes.find(row => row.indexname === name)?.indexdef, table, fields) const fk = 'material_purge_audits_job_id_fkey' assertExactForeignKeyConstraint(evidence.constraints.find(row => row.constraint_name === fk), audit, fk, 'job_id', job, 'id') if (evidence.constraints.some(row => ['material_logs', 'material_download_logs'].includes(row.tablename) && row.referenced_table === 'materials')) { fail('Material audit logs must survive permanent deletion; material foreign key is still present') } } export function assertOrganizationConfigurationEvidence(evidence) { const columns = new Map(evidence.columns.map(row => [`${row.table_name}.${row.column_name}`, row])) for (const [table, column, type, nullable] of [ ['material_permission_pack_departments', 'organization_unit_id', 'text', 'YES'], ...['id', 'organization_unit_id', 'description', 'updated_by_user_id'].map(column => ['team_function_configs', column, 'text', 'NO']), ['team_function_configs', 'lead_employee_profile_id', 'text', 'YES'], ['team_function_configs', 'branches', 'jsonb', 'NO'], ['team_function_configs', 'revision', 'integer', 'NO'], ...['created_at', 'updated_at'].map(column => ['team_function_configs', column, 'timestamp without time zone', 'NO']), ...['id', 'config_id', 'actor_user_id'].map(column => ['team_function_config_audits', column, 'text', 'NO']), ['team_function_config_audits', 'before', 'jsonb', 'YES'], ['team_function_config_audits', 'after', 'jsonb', 'NO'], ['team_function_config_audits', 'created_at', 'timestamp without time zone', 'NO'], ]) { const row = columns.get(`${table}.${column}`) if (!row || row.data_type !== type || row.is_nullable !== nullable) fail(`Database column mismatch: ${table}.${column}`) } for (const [table, name, column] of [ ['material_permission_pack_departments', 'material_permission_pack_dept_unit_key', 'organization_unit_id'], ['team_function_configs', 'team_function_configs_organization_unit_id_key', 'organization_unit_id'], ]) assertExactUniqueBtreeIndex(evidence.indexes.find(row => row.indexname === name), table, name, [column]) for (const [table, name, columns] of [ ['material_permission_pack_departments', 'material_permission_pack_departments_pack_id_idx', ['pack_id']], ['team_function_config_audits', 'team_function_config_audits_config_id_created_at_idx', ['config_id', 'created_at']], ]) assertExactBtreeIndex(evidence.indexes.find(row => row.indexname === name)?.indexdef, table, columns) for (const [table, name, column, referenced] of [ ['material_permission_pack_departments', 'material_permission_pack_departments_organization_unit_id_fkey', 'organization_unit_id', 'organization_units'], ['team_function_configs', 'team_function_configs_organization_unit_id_fkey', 'organization_unit_id', 'organization_units'], ['team_function_configs', 'team_function_configs_lead_employee_profile_id_fkey', 'lead_employee_profile_id', 'performance_survey_employee_profiles'], ['team_function_config_audits', 'team_function_config_audits_config_id_fkey', 'config_id', 'team_function_configs'], ]) assertExactForeignKeyConstraint(evidence.constraints.find(row => row.constraint_name === name), table, name, column, referenced, 'id') assertExactCheckConstraint(evidence.constraints.find(row => row.constraint_name === 'team_function_configs_revision_check'), 'team_function_configs', 'team_function_configs_revision_check', ['revision'], 'revision > 0') } function assertNoUnexpectedPendingMigrations(localMigrations, evidence) { const completed = completedMigrationMap(evidence.migrationRows) const targetSet = new Set(TARGET_MIGRATIONS) const unexpected = Array.from(localMigrations.keys()).filter( (name) => !completed.has(name) && !targetSet.has(name), ) if (unexpected.length > 0) { fail(`Unexpected pending migrations exist; refusing a bundled production write: ${unexpected.join(', ')}`) } const failed = evidence.migrationRows.filter((row) => !row.finished_at && !row.rolled_back_at) if (failed.length > 0) { fail(`Unresolved failed migrations exist: ${failed.map((row) => row.migration_name).join(', ')}`) } } export function assertExpectedDatabaseIdentity(databaseUrl, identity) { const expected = { configuredHost: String(process.env.EXPECTED_DATABASE_HOST || '').trim(), configuredPort: String(process.env.EXPECTED_DATABASE_PORT || '').trim(), configuredDatabase: String(process.env.EXPECTED_DATABASE_NAME || '').trim(), configuredUser: String(process.env.EXPECTED_DATABASE_USER || '').trim(), serverAddress: String(process.env.EXPECTED_DATABASE_SERVER_ADDRESS || '').trim(), serverPort: String(process.env.EXPECTED_DATABASE_SERVER_PORT || '').trim(), } if (Object.values(expected).some((value) => !value)) { fail('Apply mode requires the configured and server database host, port, name and user fingerprint') } expected.configuredPort = normalizedPort(expected.configuredPort) expected.serverPort = normalizedPort(expected.serverPort) const configured = redactDatabaseFingerprint(databaseUrl) if ( configured.configuredHost !== expected.configuredHost || configured.configuredPort !== expected.configuredPort || configured.configuredDatabase !== expected.configuredDatabase || configured.configuredUser !== expected.configuredUser || String(identity.database) !== expected.configuredDatabase || String(identity.user) !== expected.configuredUser || String(identity.server_address) !== expected.serverAddress || normalizedPort(identity.server_port) !== expected.serverPort ) { fail('Production database identity does not match the explicit expected fingerprint') } return { ...configured, serverAddress: expected.serverAddress, serverPort: expected.serverPort } } export function assertBackupReceiptDocument(receipt, context, now = new Date()) { if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt) || receipt.schemaVersion !== 1) { fail('Backup receipt must use schemaVersion 1') } const maxAgeMinutes = Number(context.maxAgeMinutes) if (!Number.isInteger(maxAgeMinutes) || maxAgeMinutes < 1 || maxAgeMinutes > 1440) { fail('BACKUP_MAX_AGE_MINUTES must be an integer from 1 to 1440') } if (receipt.sourceCommit !== context.expectedCommit) { fail('Backup receipt sourceCommit does not match the migrator commit') } if (receipt.generatedBy !== VERIFIED_BACKUP_GENERATOR) { fail('Backup receipt must be machine-generated by the verified backup helper') } const database = receipt.database const expectedDatabase = context.database if (!database || typeof database !== 'object' || [ 'configuredHost', 'configuredPort', 'configuredDatabase', 'configuredUser', 'serverAddress', 'serverPort', ].some((key) => String(database[key] || '') !== String(expectedDatabase[key] || ''))) { fail('Backup receipt database fingerprint does not match the verified production database') } const backup = receipt.backup const restore = receipt.restoreValidation if (!backup || typeof backup !== 'object' || !restore || typeof restore !== 'object') { fail('Backup receipt must contain backup and restoreValidation evidence') } if (backup.format !== 'pg_dump-custom') { fail('Backup receipt format must be pg_dump-custom') } if (!path.isAbsolute(String(backup.artifactPath || ''))) { fail('Backup artifactPath must be absolute') } if (!Number.isSafeInteger(backup.artifactSize) || backup.artifactSize <= 0) { fail('Backup artifactSize must be a positive integer') } if (!/^[0-9a-f]{64}$/.test(String(backup.artifactSha256 || ''))) { fail('Backup artifactSha256 must be a lowercase SHA-256') } if ( restore.status !== 'passed' || restore.artifactSha256 !== backup.artifactSha256 || restore.databaseFingerprint !== databaseFingerprintHash(database) ) { fail('Backup receipt does not prove a restore validation for this artifact and database fingerprint') } const isolated = restore.isolatedDatabase assertIsolatedRestoreFingerprint(database, isolated) if (restore.isolatedDatabaseFingerprint !== databaseFingerprintHash(isolated)) { fail('Backup receipt isolated restore database fingerprint hash mismatch') } const summary = restore.verificationSummary if ( !summary || typeof summary !== 'object' || Array.isArray(summary) || summary.schemaVersion !== 1 || summary.maintenanceWindowConfirmation !== VERIFIED_BACKUP_MAINTENANCE_CONFIRMATION || summary.statementTimeoutMs !== 30_000 || summary.sampleLimit !== 32 ) { fail('Backup receipt must contain machine-generated bounded restore verification evidence') } assertBoundedBackupSummary(summary.sourceBefore, 'Production pre-dump summary') assertBoundedBackupSummary(summary.sourceAfter, 'Production post-dump summary') assertBoundedBackupSummary(summary.restored, 'Isolated restore summary') const sourceBeforeHash = verifiedBackupSummaryHash(summary.sourceBefore) const sourceAfterHash = verifiedBackupSummaryHash(summary.sourceAfter) const restoredHash = verifiedBackupSummaryHash(summary.restored) if ( sourceBeforeHash !== sourceAfterHash || sourceAfterHash !== restoredHash || summary.sourceSummarySha256 !== sourceAfterHash || summary.restoredSummarySha256 !== restoredHash ) { fail('Backup receipt bounded source and restore verification summaries do not match') } const startedAt = new Date(backup.startedAt) const completedAt = new Date(backup.completedAt) const restoreStartedAt = new Date(restore.startedAt) const restoreCompletedAt = new Date(restore.completedAt) if ( !Number.isFinite(startedAt.getTime()) || !Number.isFinite(completedAt.getTime()) || !Number.isFinite(restoreStartedAt.getTime()) || !Number.isFinite(restoreCompletedAt.getTime()) ) { fail('Backup and restore validation timestamps must be valid ISO timestamps') } const futureToleranceMs = 5 * 60 * 1000 if ( completedAt.getTime() > now.getTime() + futureToleranceMs || restoreCompletedAt.getTime() > now.getTime() + futureToleranceMs || completedAt.getTime() < startedAt.getTime() || restoreStartedAt.getTime() < completedAt.getTime() || restoreCompletedAt.getTime() < restoreStartedAt.getTime() || restoreCompletedAt.getTime() < completedAt.getTime() || now.getTime() - completedAt.getTime() > maxAgeMinutes * 60 * 1000 ) { fail('Backup receipt is stale or has an invalid validation timeline') } return { backup, restore } } async function assertBackupReceipt(context) { const receiptPathInput = String(process.env.BACKUP_RECEIPT_PATH || '').trim() const artifactRootInput = String(process.env.BACKUP_ARTIFACT_ROOT || '').trim() const receiptPath = receiptPathInput ? path.resolve(receiptPathInput) : '' const expectedHash = String(process.env.EXPECTED_BACKUP_RECEIPT_SHA256 || '').trim().toLowerCase() const artifactRoot = artifactRootInput ? path.resolve(artifactRootInput) : '' const maxAgeMinutes = String(process.env.BACKUP_MAX_AGE_MINUTES || '').trim() if (!receiptPath || !artifactRoot || !/^[0-9a-f]{64}$/.test(expectedHash) || !maxAgeMinutes) { fail('Apply mode requires the backup receipt hash, artifact root and freshness limit') } const stat = await lstat(receiptPath) if (!stat.isFile() || stat.isSymbolicLink() || stat.size <= 0) { fail('Backup receipt must be a non-empty regular file') } const receiptBytes = await readFile(receiptPath) const actualHash = sha256(receiptBytes) if (actualHash !== expectedHash) fail('Backup receipt checksum mismatch') let receipt try { receipt = JSON.parse(receiptBytes.toString('utf8')) } catch { fail('Backup receipt must be valid JSON') } const { backup, restore } = assertBackupReceiptDocument(receipt, { ...context, maxAgeMinutes, }) const artifactPath = path.resolve(backup.artifactPath) const relativeArtifact = path.relative(artifactRoot, artifactPath) if (!relativeArtifact || relativeArtifact.startsWith('..') || path.isAbsolute(relativeArtifact)) { fail('Backup artifact must be a file below BACKUP_ARTIFACT_ROOT') } const artifactStat = await lstat(artifactPath) if (!artifactStat.isFile() || artifactStat.isSymbolicLink() || artifactStat.size !== backup.artifactSize) { fail('Backup artifact is missing, unsafe or has an unexpected size') } const artifactHash = await sha256File(artifactPath) if (artifactHash !== backup.artifactSha256) fail('Backup artifact checksum mismatch') return { receiptPath, receiptSha256: actualHash, receiptSize: stat.size, artifactPath, artifactSha256: artifactHash, artifactSize: artifactStat.size, backupCompletedAt: backup.completedAt, restoreValidatedAt: restore.completedAt, } } async function writeAuditRecord(record) { const outputPath = String(process.env.MIGRATION_AUDIT_OUTPUT || '').trim() if (!outputPath) { process.stdout.write(`${JSON.stringify(record, null, 2)}\n`) return } const resolved = path.resolve(outputPath) await mkdir(path.dirname(resolved), { recursive: true, mode: 0o750 }) const temp = `${resolved}.tmp-${process.pid}-${Date.now()}` await writeFile(temp, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600, flag: 'wx' }) await rename(temp, resolved) } async function assertApplyAuditOutputWritable() { const outputInput = String(process.env.MIGRATION_AUDIT_OUTPUT || '').trim() if (!outputInput) fail('Apply mode requires MIGRATION_AUDIT_OUTPUT') const outputPath = path.resolve(outputInput) try { await lstat(outputPath) fail('MIGRATION_AUDIT_OUTPUT already exists; use a new immutable audit filename') } catch (error) { if (error?.code !== 'ENOENT') throw error } const parent = path.dirname(outputPath) const parentStat = await lstat(parent) if (!parentStat.isDirectory() || parentStat.isSymbolicLink()) { fail('Migration audit output parent must be a real directory') } const suffix = `.write-probe-${process.pid}-${Date.now()}` const source = path.join(parent, suffix) const destination = `${source}.renamed` try { await writeFile(source, 'audit-write-probe\n', { mode: 0o600, flag: 'wx' }) await rename(source, destination) } finally { await unlink(source).catch((error) => { if (error?.code !== 'ENOENT') throw error }) await unlink(destination).catch((error) => { if (error?.code !== 'ENOENT') throw error }) } } async function main() { const userArgs = process.argv.slice(2) const apply = userArgs.includes('--apply') if (userArgs.some((arg) => !['--apply', '--audit'].includes(arg))) { fail('Only --audit (default) or --apply is supported') } if (apply && userArgs.includes('--audit')) fail('Choose exactly one mode') const bakedCommit = requireExactCommit(await readFile(BAKED_COMMIT_FILE, 'utf8'), 'Baked commit') const expectedCommit = requireExactCommit(process.env.EXPECTED_COMMIT_SHA, 'EXPECTED_COMMIT_SHA') if (bakedCommit !== expectedCommit) fail('Migrator image commit does not match EXPECTED_COMMIT_SHA') if (!process.env.DATABASE_URL) fail('DATABASE_URL is required') const databaseUrl = new URL(process.env.DATABASE_URL) if (databaseUrl.protocol !== 'postgresql:' && databaseUrl.protocol !== 'postgres:') { fail('DATABASE_URL must use PostgreSQL') } const localMigrations = await loadLocalMigrations() const { PrismaClient } = await import('@prisma/client') const prisma = new PrismaClient() let before let backup = null let verifiedDatabaseFingerprint = null try { before = await queryEvidence(prisma, localMigrations) assertNoUnexpectedPendingMigrations(localMigrations, before) if (apply) { if (process.env.ALLOW_PRODUCTION_MIGRATIONS !== 'APPLY') { fail('Apply mode requires ALLOW_PRODUCTION_MIGRATIONS=APPLY') } await assertApplyAuditOutputWritable() verifiedDatabaseFingerprint = assertExpectedDatabaseIdentity(databaseUrl, before.identity) backup = await assertBackupReceipt({ expectedCommit, database: verifiedDatabaseFingerprint, }) } } finally { await prisma.$disconnect() } if (apply) { const prismaCli = path.join(ROOT, 'node_modules', '.bin', 'prisma') const result = spawnSync( prismaCli, ['migrate', 'deploy', '--schema', path.join(ROOT, 'prisma', 'schema.prisma')], { cwd: ROOT, env: process.env, stdio: 'inherit', shell: process.platform === 'win32' }, ) if (result.error) throw result.error if (result.status !== 0) fail(`prisma migrate deploy failed with exit code ${result.status}`) } const verifyClient = new PrismaClient() let after try { after = await queryEvidence(verifyClient, localMigrations) if (apply) { const postApplyDatabaseFingerprint = assertExpectedDatabaseIdentity(databaseUrl, after.identity) if (databaseFingerprintHash(postApplyDatabaseFingerprint) !== databaseFingerprintHash(verifiedDatabaseFingerprint)) { fail('Production database identity changed while migrations were being applied') } } if (apply || TARGET_MIGRATIONS.every((name) => completedMigrationMap(after.migrationRows).has(name))) { assertTargetMigrationEvidence(localMigrations, after) } } finally { await verifyClient.$disconnect() } const completed = completedMigrationMap(after.migrationRows) const record = { mode: apply ? 'apply' : 'audit', timestamp: new Date().toISOString(), sourceCommit: bakedCommit, database: { ...redactDatabaseFingerprint(databaseUrl), actualDatabase: String(after.identity.database || ''), actualUser: String(after.identity.user || ''), serverAddress: String(after.identity.server_address || ''), serverPort: String(after.identity.server_port || ''), }, backup, targetMigrations: TARGET_MIGRATIONS.map((name) => ({ name, sourceChecksum: localMigrations.get(name)?.checksum || null, databaseChecksum: completed.get(name)?.checksum || null, completed: completed.has(name), })), verified: apply || TARGET_MIGRATIONS.every((name) => completed.has(name)), } await writeAuditRecord(record) if (apply) process.stdout.write('Production migrations applied and verified.\n') else process.stdout.write('Read-only production migration audit completed.\n') } if ( process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)) ) { main().catch((error) => { process.stderr.write(`[SAFE_MIGRATION_ERROR] ${error instanceof Error ? error.message : String(error)}\n`) process.exitCode = 1 }) }