Batch commit of the pending working tree on security/audit-fixes-2026-07. Major areas: - Outbound webhooks / Zapier: schema + signed delivery with retries, public v1 API (REST-hook subscribe/unsubscribe), settings UI, cron drain. - Deploy hardening: email via SMTP2GO (Resend fully removed), verified DB TLS (DATABASE_SSL=require + DATABASE_CA), storage fails loud in production when Spaces is unconfigured instead of silently using ephemeral disk. - Integrations & features (concurrent work): accounting (QuickBooks/Xero), e-signature (DocuSign/Dropbox Sign), PayPal, geocoding/maps, onboarding, expanded legal pages. - DB migrations 0006–0009. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
69 lines
2.3 KiB
JavaScript
69 lines
2.3 KiB
JavaScript
// One-off production migration runner (DigitalOcean Managed Postgres).
|
|
//
|
|
// Applies any pending Drizzle migrations in lib/db/migrations to the production
|
|
// database, using the doadmin connection. Drizzle tracks what's already applied
|
|
// in the __drizzle_migrations table, so this only runs the missing migrations
|
|
// and is safe to re-run.
|
|
//
|
|
// The connection string is read from PROD_DATABASE_URL so the credential never
|
|
// has to be typed on the command line. Set it in .env.local (git-ignored):
|
|
//
|
|
// PROD_DATABASE_URL=postgresql://doadmin:<pw>@<cluster>...:25060/<app-db>
|
|
//
|
|
// Then run: node scripts/migrate-prod.mjs
|
|
//
|
|
// TLS: verified against ca-certificate.crt when present, otherwise encrypted
|
|
// but unverified. Uses the direct port (25060), NOT the 25061 pooler — the
|
|
// migrator needs a real session (transactions + advisory locks).
|
|
import { readFileSync } from "node:fs"
|
|
import { config } from "dotenv"
|
|
import { drizzle } from "drizzle-orm/node-postgres"
|
|
import { migrate } from "drizzle-orm/node-postgres/migrator"
|
|
import pg from "pg"
|
|
|
|
config({ path: ".env.local", quiet: true })
|
|
|
|
const url = process.env.PROD_DATABASE_URL
|
|
if (!url) {
|
|
console.error(
|
|
"[migrate-prod] PROD_DATABASE_URL is not set. Add the doadmin connection " +
|
|
"string (port 25060, app database) to .env.local and re-run."
|
|
)
|
|
process.exit(1)
|
|
}
|
|
|
|
if (url.includes(":25061")) {
|
|
console.error(
|
|
"[migrate-prod] Refusing to run migrations through the 25061 connection " +
|
|
"pooler. Use the direct port 25060."
|
|
)
|
|
process.exit(1)
|
|
}
|
|
|
|
let ssl
|
|
try {
|
|
const ca = readFileSync("ca-certificate.crt", "utf8")
|
|
ssl = { rejectUnauthorized: true, ca }
|
|
console.log("[migrate-prod] TLS: verifying against ca-certificate.crt")
|
|
} catch {
|
|
ssl = { rejectUnauthorized: false }
|
|
console.log("[migrate-prod] TLS: encrypted (certificate not verified)")
|
|
}
|
|
|
|
const { Pool } = pg
|
|
const pool = new Pool({ connectionString: url, ssl })
|
|
const db = drizzle(pool)
|
|
|
|
try {
|
|
const host = new URL(url).host
|
|
console.log(`[migrate-prod] Connecting to ${host} ...`)
|
|
await migrate(db, { migrationsFolder: "./lib/db/migrations" })
|
|
console.log("[migrate-prod] Migrations applied successfully.")
|
|
await pool.end()
|
|
process.exit(0)
|
|
} catch (err) {
|
|
console.error(`[migrate-prod] Failed: ${err?.message ?? err}`)
|
|
await pool.end().catch(() => {})
|
|
process.exit(1)
|
|
}
|