diff --git a/src/App.tsx b/src/App.tsx index 2e7a09a..3a94ac4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,12 +9,14 @@ const ResearchInterface = lazy(() => import('./components/ResearchInterface').th const LandingPage = lazy(() => import('./components/LandingPage').then(m => ({ default: m.LandingPage }))); const PrivacyPolicy = lazy(() => import('./components/PrivacyPolicy').then(m => ({ default: m.PrivacyPolicy }))); const TermsOfUse = lazy(() => import('./components/TermsOfUse').then(m => ({ default: m.TermsOfUse }))); -import { clearVault, loadVault, loadVaultFromServer, syncVaultToServer } from './lib/apiKeyVault'; +import { clearVault, loadVault, loadVaultFromServer, syncVaultToServer, saveVault } from './lib/apiKeyVault'; +import type { ProviderVault } from './lib/apiKeyVault'; export interface WorkshopData { name: string; welcome: string; provider: string; + apiKey?: string | null; hasKey: boolean; scenario: { botAPrompt: string; botBPrompt: string; sharedPrompt: string; stopKeywords: string; botMode: 'symmetric' | 'asymmetric'; startingBot?: 'a' | 'b' } | null; config: Record | null; @@ -130,6 +132,17 @@ function App() { if (error || !data || data.error) return; setWorkshopData(data as WorkshopData); + + // Preload the workshop's API key into the participant's vault so the link + // "just works". The key is a dedicated, spend-capped workshop key. + const wd = data as WorkshopData; + if (wd.apiKey && wd.provider) { + const vault = loadVault(); + const provider = wd.provider as keyof ProviderVault; + if (provider in vault) { + saveVault({ ...vault, [provider]: wd.apiKey }); + } + } } catch { /* non-blocking */ } }; diff --git a/supabase/functions/workshop-config/index.ts b/supabase/functions/workshop-config/index.ts index 9240b90..ff480b4 100644 --- a/supabase/functions/workshop-config/index.ts +++ b/supabase/functions/workshop-config/index.ts @@ -156,10 +156,24 @@ Deno.serve(async (req) => { return jsonResponse({ error: 'Workshop not found' }, 404, corsHeaders); } + // Decrypt the workshop key so it can be preloaded into the participant's + // vault on join. NOTE: this returns the organizer's key to the browser — + // only use a dedicated, spend-capped key per workshop and revoke it after. + let apiKey: string | null = null; + if (data.api_key) { + const cryptoKey = await deriveKey(encryptionSecret, `workshop-${data.id}`); + try { + apiKey = await decrypt(cryptoKey, data.api_key); + } catch { + return jsonResponse({ error: 'Failed to decrypt workshop key' }, 500, corsHeaders); + } + } + return jsonResponse({ name: data.name, welcome: data.welcome, provider: data.provider, + apiKey, hasKey: !!data.api_key, scenario: data.scenario, config: data.config,