// deno-lint-ignore-file import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', } const json = (body: Record, status: number) => new Response(JSON.stringify(body), { status, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }) function encodeBase64(bytes: Uint8Array) { return btoa(String.fromCharCode(...bytes)) } function decodeBase64(value: string) { return Uint8Array.from(atob(value), char => char.charCodeAt(0)) } async function requireTeam(authHeader: string) { const supabaseUrl = Deno.env.get('SUPABASE_URL') ?? '' const supabaseKey = Deno.env.get('SUPABASE_ANON_KEY') || Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') || '' const callerClient = createClient( supabaseUrl, supabaseKey, { global: { headers: { Authorization: authHeader } } } ) const { data: userData, error: userError } = await callerClient.auth.getUser() if (userError || !userData?.user) return { ok: false, error: `Auth failed: ${userError?.message ?? 'no user'}`, status: 401 } const { data: profile, error: profileError } = await callerClient .from('profiles').select('role').eq('id', userData.user.id).single() if (profileError) return { ok: false, error: `Profile error: ${profileError.message}`, status: 500 } if (profile?.role !== 'team') return { ok: false, error: 'Forbidden: team only', status: 403 } return { ok: true } } async function getKey() { const secret = Deno.env.get('PASSWORD_VAULT_KEY') ?? '' if (!secret) throw new Error('PASSWORD_VAULT_KEY is not configured.') const rawKey = decodeBase64(secret) return crypto.subtle.importKey('raw', rawKey, 'AES-GCM', false, ['encrypt', 'decrypt']) } Deno.serve(async (req) => { if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders }) try { const authHeader = req.headers.get('Authorization') ?? '' if (!authHeader) return json({ error: 'No authorization header' }, 401) const auth = await requireTeam(authHeader) if (!auth.ok) return json({ error: auth.error as string }, auth.status as number) const body = await req.json() const action = body?.action const key = await getKey() if (action === 'encrypt') { const plaintext = String(body?.plaintext ?? '') if (!plaintext) return json({ error: 'plaintext required' }, 400) const iv = crypto.getRandomValues(new Uint8Array(12)) const encoded = new TextEncoder().encode(plaintext) const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded) return json({ ciphertext: encodeBase64(new Uint8Array(ciphertext)), iv: encodeBase64(iv), }, 200) } if (action === 'decrypt') { const ciphertext = String(body?.ciphertext ?? '') const ivValue = String(body?.iv ?? '') if (!ciphertext || !ivValue) return json({ error: 'ciphertext and iv required' }, 400) const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: decodeBase64(ivValue) }, key, decodeBase64(ciphertext), ) return json({ plaintext: new TextDecoder().decode(decrypted) }, 200) } return json({ error: 'Invalid action' }, 400) } catch (err) { return json({ error: `Unexpected: ${String(err)}` }, 500) } })