refactor: 应用 Oracle round-4 复核,硬化 migrator 与默认安全值

- migrate: 校验已应用 migration 的 SHA-256,拒绝 schema drift;
  split 后 trim + skip empty,避免空 statement 触发 SQL 错误
- todo.contract: update 拒绝空 patch
- env: DATABASE_URL 限定 postgres(ql):// scheme,配置错误更早失败
- compile: autoloadDotenv: false,二进制部署不再吞 cwd 的 .env
- Error.tsx: 生产环境隐藏 error.message,避免内部错误泄露
- AGENTS: 同步 generatedFieldKeys / migrator 行为新描述
This commit is contained in:
2026-04-25 14:38:44 +08:00
parent 695e826dcf
commit ed257fe4e6
6 changed files with 33 additions and 14 deletions
+24 -8
View File
@@ -19,6 +19,8 @@ export default defineCommand({
return
}
const sha256 = (s: string) => createHash('sha256').update(s).digest('hex')
const db = drizzle({ connection: { url: env.DATABASE_URL, max: 1, onnotice: () => {} } })
try {
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS "drizzle"`)
@@ -29,12 +31,25 @@ export default defineCommand({
created_at bigint
)
`)
const last = await db.execute<{ created_at: string | null }>(
sql`SELECT created_at FROM "drizzle"."__drizzle_migrations" ORDER BY created_at DESC LIMIT 1`,
)
const lastMillis = Number(last[0]?.created_at ?? 0)
const pending = embeddedMigrations.filter((m) => m.when > lastMillis)
const applied = await db.execute<{ hash: string; created_at: string | null }>(
sql`SELECT hash, created_at FROM "drizzle"."__drizzle_migrations" ORDER BY created_at ASC`,
)
// Reject schema drift: any applied migration whose embedded SQL has changed (or is missing) is fatal.
for (const row of applied) {
const when = Number(row.created_at)
const m = embeddedMigrations.find((e) => e.when === when)
if (!m) {
throw new Error(`Applied migration when=${when} is not in this binary; do not roll back applied migrations.`)
}
if (sha256(m.sql) !== row.hash) {
throw new Error(`Migration hash mismatch at when=${when}; do not edit migrations after they are applied.`)
}
}
const appliedWhens = new Set(applied.map((r) => Number(r.created_at)))
const pending = embeddedMigrations.filter((m) => !appliedWhens.has(m.when))
if (pending.length === 0) {
console.log('Database is up to date.')
return
@@ -43,12 +58,13 @@ export default defineCommand({
console.log(`Applying ${pending.length} migration(s)...`)
await db.transaction(async (tx) => {
for (const m of pending) {
for (const stmt of m.sql.split('--> statement-breakpoint')) {
for (const rawStmt of m.sql.split('--> statement-breakpoint')) {
const stmt = rawStmt.trim()
if (!stmt) continue
await tx.execute(sql.raw(stmt))
}
const hash = createHash('sha256').update(m.sql).digest('hex')
await tx.execute(
sql`INSERT INTO "drizzle"."__drizzle_migrations" ("hash", "created_at") VALUES (${hash}, ${m.when})`,
sql`INSERT INTO "drizzle"."__drizzle_migrations" ("hash", "created_at") VALUES (${sha256(m.sql)}, ${m.when})`,
)
}
})