import { decryptFrom, encryptFor } from '../crypto/box'; import type { AppSupabaseClient } from '../supabase/client'; export const SOUNDBOARDS_BUCKET = 'soundboards'; export interface RemoteSound { id: string; userId: string; name: string; mime: string; size: number; category: string | null; hotkey: string | null; gain: number; sortOrder: number; storagePath: string; createdAt: string; updatedAt: string; } export interface UpsertSoundInput { id: string; name: string; mime: string; size: number; category: string | null; hotkey: string | null; gain: number; sortOrder: number; storagePath: string; updatedAtIso: string; } export async function listOwnSounds(client: AppSupabaseClient): Promise { const { data: session } = await client.auth.getUser(); if (!session.user) throw new Error('not authenticated'); const { data, error } = await client .from('user_soundboards') .select( 'id, user_id, name, mime, size, category, hotkey, gain, sort_order, storage_path, created_at, updated_at', ) .eq('user_id', session.user.id) .order('updated_at', { ascending: false }); if (error) throw error; return data.map(mapRow); } export async function upsertSound( client: AppSupabaseClient, input: UpsertSoundInput, ): Promise { const { data: session } = await client.auth.getUser(); if (!session.user) throw new Error('not authenticated'); const { data, error } = await client .from('user_soundboards') .upsert( { id: input.id, user_id: session.user.id, name: input.name, mime: input.mime, size: input.size, category: input.category, hotkey: input.hotkey, gain: input.gain, sort_order: input.sortOrder, storage_path: input.storagePath, updated_at: input.updatedAtIso, }, { onConflict: 'id' }, ) .select( 'id, user_id, name, mime, size, category, hotkey, gain, sort_order, storage_path, created_at, updated_at', ) .single(); if (error) throw error; return mapRow(data); } export async function deleteSound( client: AppSupabaseClient, soundId: string, ): Promise { const { data: session } = await client.auth.getUser(); if (!session.user) throw new Error('not authenticated'); const { data: row } = await client .from('user_soundboards') .select('storage_path') .eq('id', soundId) .eq('user_id', session.user.id) .maybeSingle(); if (row?.storage_path) { const { error: storageErr } = await client.storage .from(SOUNDBOARDS_BUCKET) .remove([row.storage_path]); if (storageErr && !/not found/i.test(storageErr.message)) throw storageErr; } const { error } = await client .from('user_soundboards') .delete() .eq('id', soundId) .eq('user_id', session.user.id); if (error) throw error; } // Sealed-to-self envelope: nonce || ciphertext. encryptFor's sender and // recipient are both the current user, equivalent to crypto_box_seal but // reuses the existing helper (no new backend method). export async function encryptSoundBlob( blob: Blob, myPublicKey: Uint8Array, myPrivateKey: Uint8Array, ): Promise { const bytes = new Uint8Array(await blob.arrayBuffer()); const { ciphertext, nonce } = await encryptFor(bytes, myPublicKey, myPrivateKey); const out = new Uint8Array(nonce.length + ciphertext.length); out.set(nonce, 0); out.set(ciphertext, nonce.length); return out; } export async function decryptSoundEnvelope( envelope: Uint8Array, myPublicKey: Uint8Array, myPrivateKey: Uint8Array, ): Promise { const NONCE_LEN = 24; if (envelope.length < NONCE_LEN + 16) { throw new Error('sound_envelope_too_short'); } const nonce = envelope.slice(0, NONCE_LEN); const ciphertext = envelope.slice(NONCE_LEN); return decryptFrom(ciphertext, nonce, myPublicKey, myPrivateKey); } export async function uploadSoundCiphertext( client: AppSupabaseClient, storagePath: string, ciphertext: Uint8Array, ): Promise { const { error } = await client.storage .from(SOUNDBOARDS_BUCKET) .upload(storagePath, ciphertext, { contentType: 'application/octet-stream', upsert: true, }); if (error) throw error; } export async function downloadSoundCiphertext( client: AppSupabaseClient, storagePath: string, ): Promise { const { data, error } = await client.storage .from(SOUNDBOARDS_BUCKET) .download(storagePath); if (error) throw error; return new Uint8Array(await data.arrayBuffer()); } function mapRow(row: { id: string; user_id: string; name: string; mime: string; size: number; category: string | null; hotkey: string | null; gain: number; sort_order: number; storage_path: string; created_at: string; updated_at: string; }): RemoteSound { return { id: row.id, userId: row.user_id, name: row.name, mime: row.mime, size: row.size, category: row.category, hotkey: row.hotkey, gain: row.gain, sortOrder: row.sort_order, storagePath: row.storage_path, createdAt: row.created_at, updatedAt: row.updated_at, }; }