🪻 distributed transcription service thistle.dunkirk.sh

feat: validate env on startup

dunkirk.sh a299d435 0e3ccd2d

verified
Changed files
+71 -63
src
+17 -12
.env.example
···
# See README for setup instructions
WHISPER_SERVICE_URL=http://localhost:8000
-
# LLM API Configuration (Required for VTT cleaning)
+
# LLM API Configuration (REQUIRED for VTT cleaning)
# Configure your LLM service endpoint and credentials
LLM_API_KEY=your_api_key_here
-
LLM_API_BASE_URL=https://api.openai.com/v1
-
LLM_MODEL=gpt-4o-mini
+
LLM_API_BASE_URL=https://openrouter.ai/api/v1
+
LLM_MODEL=anthropic/claude-3.5-sonnet
# WebAuthn/Passkey Configuration (Production Only)
# In development, these default to localhost values
···
# Must match the domain where your app is hosted
# RP_ID=thistle.app
-
# Origin - full URL of your app
+
# Origin - full URL of your app (RECOMMENDED - used for email links)
# Must match exactly where users access your app
-
# ORIGIN=https://thistle.app
+
# In production, set this to your public URL
+
ORIGIN=http://localhost:3000
-
# Polar.sh payment stuff
+
# Polar.sh Payment Configuration (REQUIRED)
+
# Get your organization ID from https://polar.sh/settings
+
POLAR_ORGANIZATION_ID=your_org_id_here
# Get your access token from https://polar.sh/settings (or sandbox.polar.sh for testing)
-
POLAR_ACCESS_TOKEN=XXX
+
POLAR_ACCESS_TOKEN=polar_at_xxxxxxxxxxxxx
# Get product ID from your Polar dashboard (create a product first)
-
POLAR_PRODUCT_ID=3f1ab9f9-d573-49d4-ac0a-a78bfb06c347
+
POLAR_PRODUCT_ID=prod_xxxxxxxxxxxxx
# Redirect URL after successful checkout (use {CHECKOUT_ID} placeholder)
POLAR_SUCCESS_URL=http://localhost:3000/checkout?checkout_id={CHECKOUT_ID}
# Webhook secret for verifying Polar webhook signatures (get from Polar dashboard)
POLAR_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxx
-
# Environment (set to 'production' in production)
-
NODE_ENV=development
-
-
# Email Configuration (MailChannels)
+
# Email Configuration (REQUIRED - MailChannels)
+
# API key from MailChannels dashboard
+
MAILCHANNELS_API_KEY=your_mailchannels_api_key_here
# DKIM private key for email authentication (required for sending emails)
# Generate: openssl genrsa -out dkim-private.pem 2048
# Then add TXT record: mailchannels._domainkey.yourdomain.com
···
DKIM_DOMAIN=thistle.app
SMTP_FROM_EMAIL=noreply@thistle.app
SMTP_FROM_NAME=Thistle
+
+
# Environment (set to 'production' in production)
+
NODE_ENV=development
+47 -27
src/index.ts
···
import settingsHTML from "./pages/settings.html";
import transcribeHTML from "./pages/transcribe.html";
+
// Validate required environment variables at startup
+
function validateEnvVars() {
+
const required = [
+
"POLAR_ORGANIZATION_ID",
+
"POLAR_PRODUCT_ID",
+
"POLAR_SUCCESS_URL",
+
"POLAR_WEBHOOK_SECRET",
+
"MAILCHANNELS_API_KEY",
+
"DKIM_PRIVATE_KEY",
+
"LLM_API_KEY",
+
"LLM_API_BASE_URL",
+
"LLM_MODEL",
+
];
+
+
const missing = required.filter((key) => !process.env[key]);
+
+
if (missing.length > 0) {
+
console.error(
+
`[Startup] Missing required environment variables: ${missing.join(", ")}`,
+
);
+
console.error("[Startup] Please check your .env file");
+
process.exit(1);
+
}
+
+
// Validate ORIGIN is set for production
+
if (!process.env.ORIGIN) {
+
console.warn(
+
"[Startup] ORIGIN not set, defaulting to http://localhost:3000",
+
);
+
console.warn(
+
"[Startup] Set ORIGIN in production for correct email links",
+
);
+
}
+
+
console.log("[Startup] Environment variable validation passed");
+
}
+
+
validateEnvVars();
+
// Environment variables
const WHISPER_SERVICE_URL =
process.env.WHISPER_SERVICE_URL || "http://localhost:8000";
···
try {
const { polar } = await import("./lib/polar");
-
// Search for customer by email
+
// Search for customer by email (validated at startup)
const customers = await polar.customers.list({
-
organizationId: process.env.POLAR_ORGANIZATION_ID,
+
organizationId: process.env.POLAR_ORGANIZATION_ID as string,
query: email,
});
···
try {
const { polar } = await import("./lib/polar");
-
const productId = process.env.POLAR_PRODUCT_ID;
-
if (!productId) {
-
return Response.json(
-
{ error: "Product not configured" },
-
{ status: 500 },
-
);
-
}
-
-
const successUrl = process.env.POLAR_SUCCESS_URL;
-
if (!successUrl) {
-
return Response.json(
-
{ error: "Success URL not configured" },
-
{ status: 500 },
-
);
-
}
+
// Validated at startup
+
const productId = process.env.POLAR_PRODUCT_ID as string;
+
const successUrl =
+
process.env.POLAR_SUCCESS_URL || "http://localhost:3000";
const checkout = await polar.checkouts.create({
products: [productId],
···
const rawBody = await req.text();
const headers = Object.fromEntries(req.headers.entries());
-
// Validate webhook signature
-
const webhookSecret = process.env.POLAR_WEBHOOK_SECRET;
-
if (!webhookSecret) {
-
console.error("[Webhook] POLAR_WEBHOOK_SECRET not configured");
-
return Response.json(
-
{ error: "Webhook secret not configured" },
-
{ status: 500 },
-
);
-
}
-
+
// Validate webhook signature (validated at startup)
+
const webhookSecret = process.env.POLAR_WEBHOOK_SECRET as string;
const event = validateEvent(rawBody, headers, webhookSecret);
console.log(`[Webhook] Received event: ${event.type}`);
+3 -14
src/lib/email.ts
···
const fromEmail = process.env.SMTP_FROM_EMAIL || "noreply@thistle.app";
const fromName = process.env.SMTP_FROM_NAME || "Thistle";
const dkimDomain = process.env.DKIM_DOMAIN || "thistle.app";
-
const dkimPrivateKey = process.env.DKIM_PRIVATE_KEY;
-
const mailchannelsApiKey = process.env.MAILCHANNELS_API_KEY;
-
-
if (!dkimPrivateKey) {
-
throw new Error(
-
"DKIM_PRIVATE_KEY environment variable is required for sending emails",
-
);
-
}
-
-
if (!mailchannelsApiKey) {
-
throw new Error(
-
"MAILCHANNELS_API_KEY environment variable is required for sending emails",
-
);
-
}
+
// Validated at startup
+
const dkimPrivateKey = process.env.DKIM_PRIVATE_KEY as string;
+
const mailchannelsApiKey = process.env.MAILCHANNELS_API_KEY as string;
// Normalize recipient
const recipient =
+4 -10
src/lib/vtt-cleaner.ts
···
`[VTTCleaner] Processing ${segments.length} segments for ${transcriptionId}`,
);
-
const apiKey = process.env.LLM_API_KEY;
-
const apiBaseUrl = process.env.LLM_API_BASE_URL;
-
const model = process.env.LLM_MODEL;
-
-
if (!apiKey || !apiBaseUrl || !model) {
-
console.warn(
-
"[VTTCleaner] LLM configuration incomplete (need LLM_API_KEY, LLM_API_BASE_URL, LLM_MODEL), returning uncleaned VTT",
-
);
-
return vttContent;
-
}
+
// Validated at startup
+
const apiKey = process.env.LLM_API_KEY as string;
+
const apiBaseUrl = process.env.LLM_API_BASE_URL as string;
+
const model = process.env.LLM_MODEL as string;
try {
// Build the input segments