Ctrl K
ComunidadGuías…

app/scripts/validate-cloudflare-config.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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.`,
    )
  }
}