// Firebase HTTPS Function (2nd gen) that holds the TonePerfect key for a Flutter app. // Secret: `firebase functions:secrets:set TONEPERFECT_API_KEY`. Callers must send a // Firebase ID token (anonymous sign-in is enough to start); replace with your rules. const { onRequest } = require('firebase-functions/v2/https'); const { defineSecret } = require('firebase-functions/params'); const admin = require('firebase-admin'); const { randomUUID } = require('node:crypto'); admin.initializeApp(); const KEY = defineSecret('TONEPERFECT_API_KEY'); const LANGUAGES = new Set(['zh-CN', 'en-US', 'fr-FR', 'es-419', 'de-DE', 'it-IT', 'pt-BR', 'ru-RU', 'ko-KR', 'ja-JP']); exports.assess = onRequest({ secrets: [KEY], timeoutSeconds: 60, memory: '256MiB' }, async (req, res) => { if (req.method !== 'POST') return res.status(405).json({ error: { code: 'method_not_allowed', message: 'POST only' } }); try { await admin.auth().verifyIdToken(String(req.get('authorization') || '').replace(/^Bearer /, '')); } catch { return res.status(401).json({ error: { code: 'unauthorized', message: 'Sign in first.' } }); } // The Flutter app sends JSON: {text, language, audio_base64}; simpler than multipart in a Function. const { text = '', language = 'zh-CN', audio_base64: b64 = '' } = req.body || {}; const audio = Buffer.from(String(b64), 'base64'); if (!audio.length || audio.length > 10 * 1024 * 1024) return res.status(422).json({ error: { code: 'invalid_audio', message: 'Send one recording up to 10 MB.' } }); if (!text || String(text).length > 500 || !LANGUAGES.has(language)) return res.status(422).json({ error: { code: 'invalid_input', message: 'Send text and a supported language.' } }); const form = new FormData(); form.append('audio', new Blob([audio]), 'recording.m4a'); form.append('text', String(text)); form.append('language', language); const response = await fetch('https://api.toneperfect.app/v1/assess', { method: 'POST', body: form, headers: { Authorization: `Bearer ${KEY.value()}`, 'Idempotency-Key': String(req.get('idempotency-key') || randomUUID()), 'TonePerfect-Response': 'compact', }, }); res.status(response.status).type('application/json').send(await response.text()); });