🪻 distributed transcription service thistle.dunkirk.sh
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: validate env on startup

+71 -63
+17 -12
.env.example
··· 3 3 # See README for setup instructions 4 4 WHISPER_SERVICE_URL=http://localhost:8000 5 5 6 - # LLM API Configuration (Required for VTT cleaning) 6 + # LLM API Configuration (REQUIRED for VTT cleaning) 7 7 # Configure your LLM service endpoint and credentials 8 8 LLM_API_KEY=your_api_key_here 9 - LLM_API_BASE_URL=https://api.openai.com/v1 10 - LLM_MODEL=gpt-4o-mini 9 + LLM_API_BASE_URL=https://openrouter.ai/api/v1 10 + LLM_MODEL=anthropic/claude-3.5-sonnet 11 11 12 12 # WebAuthn/Passkey Configuration (Production Only) 13 13 # In development, these default to localhost values ··· 17 17 # Must match the domain where your app is hosted 18 18 # RP_ID=thistle.app 19 19 20 - # Origin - full URL of your app 20 + # Origin - full URL of your app (RECOMMENDED - used for email links) 21 21 # Must match exactly where users access your app 22 - # ORIGIN=https://thistle.app 22 + # In production, set this to your public URL 23 + ORIGIN=http://localhost:3000 23 24 24 - # Polar.sh payment stuff 25 + # Polar.sh Payment Configuration (REQUIRED) 26 + # Get your organization ID from https://polar.sh/settings 27 + POLAR_ORGANIZATION_ID=your_org_id_here 25 28 # Get your access token from https://polar.sh/settings (or sandbox.polar.sh for testing) 26 - POLAR_ACCESS_TOKEN=XXX 29 + POLAR_ACCESS_TOKEN=polar_at_xxxxxxxxxxxxx 27 30 # Get product ID from your Polar dashboard (create a product first) 28 - POLAR_PRODUCT_ID=3f1ab9f9-d573-49d4-ac0a-a78bfb06c347 31 + POLAR_PRODUCT_ID=prod_xxxxxxxxxxxxx 29 32 # Redirect URL after successful checkout (use {CHECKOUT_ID} placeholder) 30 33 POLAR_SUCCESS_URL=http://localhost:3000/checkout?checkout_id={CHECKOUT_ID} 31 34 # Webhook secret for verifying Polar webhook signatures (get from Polar dashboard) 32 35 POLAR_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxx 33 36 34 - # Environment (set to 'production' in production) 35 - NODE_ENV=development 36 - 37 - # Email Configuration (MailChannels) 37 + # Email Configuration (REQUIRED - MailChannels) 38 + # API key from MailChannels dashboard 39 + MAILCHANNELS_API_KEY=your_mailchannels_api_key_here 38 40 # DKIM private key for email authentication (required for sending emails) 39 41 # Generate: openssl genrsa -out dkim-private.pem 2048 40 42 # Then add TXT record: mailchannels._domainkey.yourdomain.com ··· 42 44 DKIM_DOMAIN=thistle.app 43 45 SMTP_FROM_EMAIL=noreply@thistle.app 44 46 SMTP_FROM_NAME=Thistle 47 + 48 + # Environment (set to 'production' in production) 49 + NODE_ENV=development
+47 -27
src/index.ts
··· 96 96 import settingsHTML from "./pages/settings.html"; 97 97 import transcribeHTML from "./pages/transcribe.html"; 98 98 99 + // Validate required environment variables at startup 100 + function validateEnvVars() { 101 + const required = [ 102 + "POLAR_ORGANIZATION_ID", 103 + "POLAR_PRODUCT_ID", 104 + "POLAR_SUCCESS_URL", 105 + "POLAR_WEBHOOK_SECRET", 106 + "MAILCHANNELS_API_KEY", 107 + "DKIM_PRIVATE_KEY", 108 + "LLM_API_KEY", 109 + "LLM_API_BASE_URL", 110 + "LLM_MODEL", 111 + ]; 112 + 113 + const missing = required.filter((key) => !process.env[key]); 114 + 115 + if (missing.length > 0) { 116 + console.error( 117 + `[Startup] Missing required environment variables: ${missing.join(", ")}`, 118 + ); 119 + console.error("[Startup] Please check your .env file"); 120 + process.exit(1); 121 + } 122 + 123 + // Validate ORIGIN is set for production 124 + if (!process.env.ORIGIN) { 125 + console.warn( 126 + "[Startup] ORIGIN not set, defaulting to http://localhost:3000", 127 + ); 128 + console.warn( 129 + "[Startup] Set ORIGIN in production for correct email links", 130 + ); 131 + } 132 + 133 + console.log("[Startup] Environment variable validation passed"); 134 + } 135 + 136 + validateEnvVars(); 137 + 99 138 // Environment variables 100 139 const WHISPER_SERVICE_URL = 101 140 process.env.WHISPER_SERVICE_URL || "http://localhost:8000"; ··· 126 165 try { 127 166 const { polar } = await import("./lib/polar"); 128 167 129 - // Search for customer by email 168 + // Search for customer by email (validated at startup) 130 169 const customers = await polar.customers.list({ 131 - organizationId: process.env.POLAR_ORGANIZATION_ID, 170 + organizationId: process.env.POLAR_ORGANIZATION_ID as string, 132 171 query: email, 133 172 }); 134 173 ··· 1306 1345 try { 1307 1346 const { polar } = await import("./lib/polar"); 1308 1347 1309 - const productId = process.env.POLAR_PRODUCT_ID; 1310 - if (!productId) { 1311 - return Response.json( 1312 - { error: "Product not configured" }, 1313 - { status: 500 }, 1314 - ); 1315 - } 1316 - 1317 - const successUrl = process.env.POLAR_SUCCESS_URL; 1318 - if (!successUrl) { 1319 - return Response.json( 1320 - { error: "Success URL not configured" }, 1321 - { status: 500 }, 1322 - ); 1323 - } 1348 + // Validated at startup 1349 + const productId = process.env.POLAR_PRODUCT_ID as string; 1350 + const successUrl = 1351 + process.env.POLAR_SUCCESS_URL || "http://localhost:3000"; 1324 1352 1325 1353 const checkout = await polar.checkouts.create({ 1326 1354 products: [productId], ··· 1442 1470 const rawBody = await req.text(); 1443 1471 const headers = Object.fromEntries(req.headers.entries()); 1444 1472 1445 - // Validate webhook signature 1446 - const webhookSecret = process.env.POLAR_WEBHOOK_SECRET; 1447 - if (!webhookSecret) { 1448 - console.error("[Webhook] POLAR_WEBHOOK_SECRET not configured"); 1449 - return Response.json( 1450 - { error: "Webhook secret not configured" }, 1451 - { status: 500 }, 1452 - ); 1453 - } 1454 - 1473 + // Validate webhook signature (validated at startup) 1474 + const webhookSecret = process.env.POLAR_WEBHOOK_SECRET as string; 1455 1475 const event = validateEvent(rawBody, headers, webhookSecret); 1456 1476 1457 1477 console.log(`[Webhook] Received event: ${event.type}`);
+3 -14
src/lib/email.ts
··· 28 28 const fromEmail = process.env.SMTP_FROM_EMAIL || "noreply@thistle.app"; 29 29 const fromName = process.env.SMTP_FROM_NAME || "Thistle"; 30 30 const dkimDomain = process.env.DKIM_DOMAIN || "thistle.app"; 31 - const dkimPrivateKey = process.env.DKIM_PRIVATE_KEY; 32 - const mailchannelsApiKey = process.env.MAILCHANNELS_API_KEY; 33 - 34 - if (!dkimPrivateKey) { 35 - throw new Error( 36 - "DKIM_PRIVATE_KEY environment variable is required for sending emails", 37 - ); 38 - } 39 - 40 - if (!mailchannelsApiKey) { 41 - throw new Error( 42 - "MAILCHANNELS_API_KEY environment variable is required for sending emails", 43 - ); 44 - } 31 + // Validated at startup 32 + const dkimPrivateKey = process.env.DKIM_PRIVATE_KEY as string; 33 + const mailchannelsApiKey = process.env.MAILCHANNELS_API_KEY as string; 45 34 46 35 // Normalize recipient 47 36 const recipient =
+4 -10
src/lib/vtt-cleaner.ts
··· 338 338 `[VTTCleaner] Processing ${segments.length} segments for ${transcriptionId}`, 339 339 ); 340 340 341 - const apiKey = process.env.LLM_API_KEY; 342 - const apiBaseUrl = process.env.LLM_API_BASE_URL; 343 - const model = process.env.LLM_MODEL; 344 - 345 - if (!apiKey || !apiBaseUrl || !model) { 346 - console.warn( 347 - "[VTTCleaner] LLM configuration incomplete (need LLM_API_KEY, LLM_API_BASE_URL, LLM_MODEL), returning uncleaned VTT", 348 - ); 349 - return vttContent; 350 - } 341 + // Validated at startup 342 + const apiKey = process.env.LLM_API_KEY as string; 343 + const apiBaseUrl = process.env.LLM_API_BASE_URL as string; 344 + const model = process.env.LLM_MODEL as string; 351 345 352 346 try { 353 347 // Build the input segments