const crypto = require('node:crypto'); const http = require('node:http'); const { Readable } = require('node:stream'); try { process.loadEnvFile(); } catch { // No .env file: env vars come from the host. } const { PORT = 3000, API_KEY, RESEND_API_KEY, MAIL_FROM, MAIL_TO } = process.env; const missing = ['API_KEY', 'RESEND_API_KEY', 'MAIL_FROM', 'MAIL_TO'].filter((name) => !process.env[name]); if (missing.length) { console.error(`Missing required env vars: ${missing.join(', ')}`); process.exit(1); } const recipients = MAIL_TO.split(',').map((address) => address.trim()).filter(Boolean); const hash = (value) => crypto.createHash('sha256').update(value).digest(); function isAuthorized(header = '') { const [scheme, token] = header.split(' '); return scheme === 'Bearer' && Boolean(token) && crypto.timingSafeEqual(hash(token), hash(API_KEY)); } async function sendEmail(text, recordedAt) { const response = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { Authorization: `Bearer ${RESEND_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ from: MAIL_FROM, to: recipients, subject: `${text}`, text: `${text}\n\nRecorded ${recordedAt.toLocaleString()}`, }), signal: AbortSignal.timeout(10_000), }); if (!response.ok) throw new Error(`Resend ${response.status}: ${await response.text()}`); } function respond(res, status, body) { res.writeHead(status, { 'Content-Type': 'application/json' }).end(JSON.stringify(body)); } // Pebble sends multipart/form-data with fields: transcription, recordedAt (ms epoch), client, audio (optional). async function handleWebhook(req, res) { if (!isAuthorized(req.headers.authorization)) { console.warn(' Rejected: missing or invalid Authorization header'); return respond(res, 401, { error: 'Unauthorized' }); } let form; try { form = await new Response(Readable.toWeb(req), { headers: { 'Content-Type': req.headers['content-type'] ?? '' }, }).formData(); } catch { console.warn(` Rejected: expected multipart/form-data, got ${req.headers['content-type'] ?? 'none'}`); return respond(res, 400, { error: 'Expected multipart/form-data' }); } const transcription = form.get('transcription'); const text = typeof transcription === 'string' ? transcription.trim() : ''; const recordedAt = new Date(Number(form.get('recordedAt')) || Date.now()); console.log( ` Received: client=${form.get('client')} recordedAt=${recordedAt.toISOString()} ` + `chars=${text.length} audio=${form.has('audio')}` ); if (!text) { console.warn(' Rejected: no transcription'); return respond(res, 400, { error: 'No transcription' }); } try { await sendEmail(text, recordedAt); } catch (err) { console.error(' Email failed:', err.message); return respond(res, 502, { error: 'Failed to send email' }); } console.log(' Email sent'); respond(res, 200, { ok: true }); } http .createServer((req, res) => { const { pathname } = new URL(req.url, 'http://localhost'); console.log(`[${new Date().toISOString()}] ${req.method} ${pathname}`); if (req.method === 'GET' && pathname === '/health') { return respond(res, 200, { ok: true }); } if (req.method === 'POST' && pathname === '/transcription') { return handleWebhook(req, res).catch((err) => { console.error(err); respond(res, 500, { error: 'Internal server error' }); }); } respond(res, 404, { error: 'Not found' }); }) .listen(PORT, () => console.log(`Listening on port ${PORT}`));