Contact form update

This commit is contained in:
Greg Kilmer
2026-07-07 21:58:37 -04:00
parent 738f978120
commit e7c5e9d66e
4 changed files with 176 additions and 44 deletions
+86
View File
@@ -0,0 +1,86 @@
/**
* env.ts
* ---------------------------------------------------------------------------
* Runtime env-var reader that works in BOTH local dev (`astro dev`, where the
* `.env` file is NOT auto-loaded into `process.env`) AND the Docker production
* runtime (where docker-compose `env_file` sets real `process.env`).
*
* Background: Astro/Vite loads `.env` into `import.meta.env`, NOT `process.env`
* (https://github.com/withastro/roadmap/issues/853). We deliberately AVOID
* `import.meta.env` for these reads because Vite statically replaces it at
* build time — and for SECRET values (SMTP password, Turnstile secret key)
* that means the secret gets baked into the built JS bundle in plaintext,
* which is a security leak and defeats the point of env vars.
*
* Solution: at module load (once), pull values from the `.env` file via Vite's
* `loadEnv` and merge any MISSING keys into `process.env`. Existing real env
* vars (e.g. Docker's) always take precedence. All reads then go through
* `process.env`, which is runtime-only and never baked into the bundle.
*
* `loadEnv` is imported from `vite`, which is already a transitive dependency
* via Astro — no new dependency required.
*/
import { loadEnv } from 'vite';
// One-time: populate process.env from the .env file for keys not already set
// by the real environment (Docker env_file / shell exports). This makes local
// dev work without any extra setup, and is a no-op in Docker where process.env
// is already fully populated.
//
// NOTE: do NOT reference `import.meta.env` anywhere in this module. Vite
// statically replaces any `import.meta.env` usage at build time, and for a
// bare reference it inlines the ENTIRE env object (including secrets like
// SMTP_PASS / TURNSTILE_SECRET_KEY) as a literal in the bundle. Use
// process.env.NODE_ENV to determine mode instead.
try {
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
const fromFile = loadEnv(
mode,
process.cwd(),
// Empty prefix = load ALL vars (not just PUBLIC_*). They land only in the
// returned object, not the client bundle, because we copy them into
// process.env here rather than referencing import.meta.env elsewhere.
'',
);
for (const [key, value] of Object.entries(fromFile)) {
if (process.env[key] === undefined && value !== '') {
process.env[key] = value;
}
}
} catch {
// loadEnv throws if it can't find a project root / .env; safe to ignore in
// environments where there's no .env (e.g. some serverless runtimes). Real
// env vars set via the platform still work via process.env directly.
}
/** Read a single env var from process.env (runtime only; never baked into bundle). */
export function env(key: string): string | undefined {
return process.env[key] || undefined;
}
/** Public Cloudflare Turnstile site key (safe to ship to the client). */
export const turnstileSiteKey = (): string | undefined => env('TURNSTILE_SITE_KEY');
/** Cloudflare Turnstile secret key (server-only). */
export const turnstileSecretKey = (): string | undefined => env('TURNSTILE_SECRET_KEY');
/** SMTP relay host. When unset, the contact form returns 503. */
export const smtpHost = (): string | undefined => env('SMTP_HOST');
/** SMTP relay port (defaults to 587 at the call site). */
export const smtpPort = (): string | undefined => env('SMTP_PORT');
/** SMTP auth username, if the relay requires auth. */
export const smtpUser = (): string | undefined => env('SMTP_USER');
/** SMTP auth password, if the relay requires auth. */
export const smtpPass = (): string | undefined => env('SMTP_PASS');
/** Inbox that receives contact form submissions. */
export const contactTo = (): string | undefined => env('CONTACT_TO');
/** From address for outgoing mail. */
export const smtpFrom = (): string | undefined => env('SMTP_FROM');
/** Optional webhook URL for delivery fallback (Discord/Slack/n8n). */
export const contactWebhookUrl = (): string | undefined => env('CONTACT_WEBHOOK_URL');