import crypto from 'crypto'; import { createClient } from '@supabase/supabase-js'; function json(res, status, body) { res.status(status).setHeader('Content-Type', 'application/json'); res.setHeader('Cache-Control', 'no-store'); res.send(JSON.stringify(body)); } async function requireTeam(authHeader) { const supabaseUrl = process.env.VITE_SUPABASE_URL || process.env.SUPABASE_URL; const supabaseAnonKey = process.env.VITE_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY; if (!supabaseUrl || !supabaseAnonKey) { throw new Error('Supabase auth env is not configured on Vercel.'); } const callerClient = createClient(supabaseUrl, supabaseAnonKey, { auth: { persistSession: false, autoRefreshToken: false }, global: { headers: { Authorization: authHeader } }, }); const { data: userData, error: userError } = await callerClient.auth.getUser(); if (userError || !userData?.user) { return { ok: false, status: 401, message: 'Unauthorized' }; } const { data: profile, error: profileError } = await callerClient .from('profiles') .select('role') .eq('id', userData.user.id) .single(); if (profileError) { return { ok: false, status: 500, message: profileError.message }; } if (profile?.role !== 'team') { return { ok: false, status: 403, message: 'Forbidden' }; } return { ok: true }; } function getKey() { const secret = process.env.PASSWORD_VAULT_KEY || ''; if (!secret) throw new Error('PASSWORD_VAULT_KEY is not configured on Vercel.'); return Buffer.from(secret, 'base64'); } export default async function handler(req, res) { if (req.method !== 'POST') return json(res, 405, { error: 'Method not allowed' }); try { const authHeader = req.headers.authorization || ''; if (!authHeader) return json(res, 401, { error: 'No authorization header' }); const auth = await requireTeam(authHeader); if (!auth.ok) return json(res, auth.status, { error: auth.message }); const { action, plaintext, ciphertext, iv } = req.body || {}; const key = getKey(); if (action === 'encrypt') { if (!plaintext) return json(res, 400, { error: 'plaintext required' }); const ivBuffer = crypto.randomBytes(12); const cipher = crypto.createCipheriv('aes-256-gcm', key, ivBuffer); const encrypted = Buffer.concat([cipher.update(String(plaintext), 'utf8'), cipher.final()]); const tag = cipher.getAuthTag(); const packed = Buffer.concat([encrypted, tag]); return json(res, 200, { ciphertext: packed.toString('base64'), iv: ivBuffer.toString('base64'), }); } if (action === 'decrypt') { if (!ciphertext || !iv) return json(res, 400, { error: 'ciphertext and iv required' }); const raw = Buffer.from(String(ciphertext), 'base64'); const ivBuffer = Buffer.from(String(iv), 'base64'); const encrypted = raw.subarray(0, raw.length - 16); const tag = raw.subarray(raw.length - 16); const decipher = crypto.createDecipheriv('aes-256-gcm', key, ivBuffer); decipher.setAuthTag(tag); const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]); return json(res, 200, { plaintext: decrypted.toString('utf8') }); } return json(res, 400, { error: 'Invalid action' }); } catch (error) { return json(res, 500, { error: error.message || 'Unexpected server error' }); } }