app/scripts/validate-cloudflare-config.mjs
Rama main · réplica de lectura del repositorio original.
import { remoteDatabaseConfig } from '../src/server/db/remote-config.ts'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { parse, printParseErrorCode } from 'jsonc-parser'
import { getD1Bindings } from '../../scripts/d1-config.mjs'
import { validateMcpUrl } from '../../scripts/setup-config.mjs'
const configPath = resolve(
process.cwd(),
process.env.DEVPUSH_WRANGLER_CONFIG || 'wrangler.jsonc',
)
const secretsPath = resolve(
process.cwd(),
process.env.DEVPUSH_CLOUDFLARE_SECRETS_FILE || '.cloudflare-secrets.json',
)
const source = await readFile(configPath, 'utf8').catch(() => {
throw new Error('Run `pnpm configure` to generate app/wrangler.jsonc.')
})
const parseErrors = []
const config = parse(source, parseErrors, { allowTrailingComma: true })
if (parseErrors.length > 0) {
const error = parseErrors[0]
throw new Error(
`Could not parse ${configPath}: ${printParseErrorCode(error.error)} at offset ${error.offset}.`,
)
}
const bucketName = config.r2_buckets?.find(
(item) => item.binding === 'DEVPUSH_OBJECTS',
)?.bucket_name
const secrets = JSON.parse(
await readFile(secretsPath, 'utf8').catch(() => {
throw new Error('Run `pnpm configure` to generate Cloudflare secrets.')
}),
)
const values = { ...config.vars, ...secrets }
const remote = remoteDatabaseConfig(values)
const bindings = remote ? [] : getD1Bindings(config)
const missingSecrets = [
'BETTER_AUTH_SECRET',
'DEVPUSH_SECRET_ENCRYPTION_KEY',
].filter((key) => typeof values[key] !== 'string' || !values[key])
if (missingSecrets.length > 0) {
throw new Error(
`Cloudflare secrets are missing: ${missingSecrets.join(', ')}. Re-run \`pnpm configure\`.`,
)
}
requireGeneratedSecret('BETTER_AUTH_SECRET', secrets.BETTER_AUTH_SECRET)
requireGeneratedSecret(
'DEVPUSH_SECRET_ENCRYPTION_KEY',
secrets.DEVPUSH_SECRET_ENCRYPTION_KEY,
)
if (secrets.BETTER_AUTH_SECRET === secrets.DEVPUSH_SECRET_ENCRYPTION_KEY) {
throw new Error(
'DEVPUSH_SECRET_ENCRYPTION_KEY must differ from BETTER_AUTH_SECRET.',
)
}
if (secrets.DEVPUSH_CRON_SECRET !== undefined) {
requireGeneratedSecret('DEVPUSH_CRON_SECRET', secrets.DEVPUSH_CRON_SECRET)
if (secrets.DEVPUSH_CRON_SECRET === secrets.BETTER_AUTH_SECRET) {
throw new Error('DEVPUSH_CRON_SECRET must differ from BETTER_AUTH_SECRET.')
}
}
if (typeof config.vars?.DEVPUSH_BASE_URL !== 'string') {
throw new Error(
'Cloudflare configuration is missing DEVPUSH_BASE_URL. Re-run `pnpm configure`.',
)
}
for (const key of ['DEVPUSH_BASE_URL', 'BETTER_AUTH_URL']) {
const value = config.vars?.[key]
if (value === undefined) continue
let url
try {
url = new URL(value)
} catch {
throw new Error(`${key} must be a valid URL in the generated config.`)
}
if (
!['http:', 'https:'].includes(url.protocol) ||
url.pathname !== '/' ||
url.search ||
url.hash
) {
throw new Error(`${key} must be an HTTP(S) origin without a path.`)
}
}
if (values.DEVPUSH_MCP_URL !== undefined && values.DEVPUSH_MCP_URL !== '') {
const error = validateMcpUrl(values.DEVPUSH_MCP_URL)
if (error) throw new Error(`DEVPUSH_MCP_URL: ${error}`)
}
if (
config.vars?.DEVPUSH_ADMIN_EMAIL &&
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(config.vars.DEVPUSH_ADMIN_EMAIL)
) {
throw new Error('DEVPUSH_ADMIN_EMAIL must be a valid email address.')
}
console.log(
`Cloudflare bindings configured: ${remote ? 'remote libSQL (main and logs)' : `D1 ${bindings.map((item) => item.database_name).join(' and ')}`}, R2 ${bucketName || 'disabled (image uploads unavailable)'}.`,
)
function requireGeneratedSecret(name, value) {
if (
typeof value !== 'string' ||
value.length < 32 ||
value.startsWith('replace-with-') ||
value === 'dev-secret-change-me'
) {
throw new Error(
`${name} must be a generated secret of at least 32 characters.`,
)
}
}