From 1b67a2fe7f05e05d33d32ab795c874e743cb70ef Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 29 Dec 2025 19:53:06 +0100 Subject: [PATCH] fix: move Daily.co API key to server-side for security MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Worker now uses DAILY_API_KEY secret instead of client-sent auth header - Added GET /daily/rooms/:roomName endpoint for room info lookup - Frontend no longer exposes or sends API key - All Daily.co API calls now proxied securely through worker 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/shapes/VideoChatShapeUtil.tsx | 29 ++----- worker/worker.ts | 132 ++++++++++++++++++++---------- 2 files changed, 100 insertions(+), 61 deletions(-) diff --git a/src/shapes/VideoChatShapeUtil.tsx b/src/shapes/VideoChatShapeUtil.tsx index a9ee30d..ca05365 100644 --- a/src/shapes/VideoChatShapeUtil.tsx +++ b/src/shapes/VideoChatShapeUtil.tsx @@ -58,11 +58,6 @@ export class VideoChatShape extends BaseBoxShapeUtil { async generateMeetingToken(roomName: string) { const workerUrl = WORKER_URL; - const apiKey = import.meta.env.VITE_DAILY_API_KEY; - - if (!apiKey) { - throw new Error('Daily.co API key not configured'); - } if (!workerUrl) { throw new Error('Worker URL is not configured'); @@ -138,11 +133,6 @@ export class VideoChatShape extends BaseBoxShapeUtil { try { const workerUrl = WORKER_URL; - const apiKey = import.meta.env.VITE_DAILY_API_KEY; - - if (!apiKey) { - throw new Error('Daily.co API key not configured'); - } if (!workerUrl) { throw new Error('Worker URL is not configured'); @@ -154,11 +144,11 @@ export class VideoChatShape extends BaseBoxShapeUtil { const cleanId = shortId.replace(/[^A-Za-z0-9]/g, ''); const roomName = `canvas-${cleanId}`; + // Worker uses server-side API key, no need to send it from client const response = await fetch(`${workerUrl}/daily/rooms`, { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ name: roomName, @@ -181,12 +171,12 @@ export class VideoChatShape extends BaseBoxShapeUtil { if (response.status === 400 && error.info && error.info.includes('already exists')) { isNewRoom = false; - // Try to get the existing room info from Daily.co API + // Try to get the existing room info via worker (API key is server-side) try { - const getRoomResponse = await fetch(`https://api.daily.co/v1/rooms/${roomName}`, { + const getRoomResponse = await fetch(`${workerUrl}/daily/rooms/${roomName}`, { method: 'GET', headers: { - 'Authorization': `Bearer ${apiKey}` + 'Content-Type': 'application/json' } }); @@ -239,9 +229,8 @@ export class VideoChatShape extends BaseBoxShapeUtil { async startRecording(shape: IVideoChatShape) { if (!shape.props.roomUrl) return; - - const workerUrl = WORKER_URL; - const apiKey = import.meta.env.VITE_DAILY_API_KEY; + + const workerUrl = WORKER_URL; try { // Extract room name from URL (same as transcription methods) @@ -250,10 +239,10 @@ export class VideoChatShape extends BaseBoxShapeUtil { throw new Error('Could not extract room name from URL'); } + // Worker uses server-side API key, no need to send it from client const response = await fetch(`${workerUrl}/daily/recordings/start`, { method: 'POST', headers: { - 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -287,13 +276,13 @@ export class VideoChatShape extends BaseBoxShapeUtil { if (!shape.props.recordingId) return; const workerUrl = WORKER_URL; - const apiKey = import.meta.env.VITE_DAILY_API_KEY; try { + // Worker uses server-side API key, no need to send it from client await fetch(`${workerUrl}/daily/recordings/${shape.props.recordingId}/stop`, { method: 'POST', headers: { - 'Authorization': `Bearer ${apiKey}` + 'Content-Type': 'application/json' } }); diff --git a/worker/worker.ts b/worker/worker.ts index 0a73e18..c96a27c 100644 --- a/worker/worker.ts +++ b/worker/worker.ts @@ -313,12 +313,13 @@ const router = AutoRouter({ }) }) - .post("/daily/rooms", async (req) => { - const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1] - + .post("/daily/rooms", async (req, env) => { + // Use server-side API key - never expose to client + const apiKey = env.DAILY_API_KEY + if (!apiKey) { - return new Response(JSON.stringify({ error: 'No API key provided' }), { - status: 401, + return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), { + status: 500, headers: { 'Content-Type': 'application/json' } }) } @@ -326,7 +327,7 @@ const router = AutoRouter({ try { // Get the request body from the client const body = await req.json() - + const response = await fetch('https://api.daily.co/v1/rooms', { method: 'POST', headers: { @@ -356,12 +357,55 @@ const router = AutoRouter({ } }) - .post("/daily/tokens", async (req) => { - const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1] - + // Get room info by name + .get("/daily/rooms/:roomName", async (req, env) => { + // Use server-side API key - never expose to client + const apiKey = env.DAILY_API_KEY + const { roomName } = req.params + if (!apiKey) { - return new Response(JSON.stringify({ error: 'No API key provided' }), { - status: 401, + return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }) + } + + try { + const response = await fetch(`https://api.daily.co/v1/rooms/${roomName}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + } + }) + + if (!response.ok) { + const error = await response.json() + return new Response(JSON.stringify(error), { + status: response.status, + headers: { 'Content-Type': 'application/json' } + }) + } + + const data = await response.json() + return new Response(JSON.stringify(data), { + headers: { 'Content-Type': 'application/json' } + }) + } catch (error) { + return new Response(JSON.stringify({ error: (error as Error).message }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }) + } + }) + + .post("/daily/tokens", async (req, env) => { + // Use server-side API key - never expose to client + const apiKey = env.DAILY_API_KEY + + if (!apiKey) { + return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), { + status: 500, headers: { 'Content-Type': 'application/json' } }) } @@ -401,13 +445,14 @@ const router = AutoRouter({ }) // Add new transcription endpoints - .post("/daily/rooms/:roomName/start-transcription", async (req) => { - const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1] + .post("/daily/rooms/:roomName/start-transcription", async (req, env) => { + // Use server-side API key - never expose to client + const apiKey = env.DAILY_API_KEY const { roomName } = req.params - + if (!apiKey) { - return new Response(JSON.stringify({ error: 'No API key provided' }), { - status: 401, + return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), { + status: 500, headers: { 'Content-Type': 'application/json' } }) } @@ -441,13 +486,14 @@ const router = AutoRouter({ } }) - .post("/daily/rooms/:roomName/stop-transcription", async (req) => { - const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1] + .post("/daily/rooms/:roomName/stop-transcription", async (req, env) => { + // Use server-side API key - never expose to client + const apiKey = env.DAILY_API_KEY const { roomName } = req.params - + if (!apiKey) { - return new Response(JSON.stringify({ error: 'No API key provided' }), { - status: 401, + return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), { + status: 500, headers: { 'Content-Type': 'application/json' } }) } @@ -482,13 +528,14 @@ const router = AutoRouter({ }) // Add endpoint to get transcript access link - .get("/daily/transcript/:transcriptId/access-link", async (req) => { - const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1] + .get("/daily/transcript/:transcriptId/access-link", async (req, env) => { + // Use server-side API key - never expose to client + const apiKey = env.DAILY_API_KEY const { transcriptId } = req.params - + if (!apiKey) { - return new Response(JSON.stringify({ error: 'No API key provided' }), { - status: 401, + return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), { + status: 500, headers: { 'Content-Type': 'application/json' } }) } @@ -523,13 +570,14 @@ const router = AutoRouter({ }) // Add endpoint to get transcript text - .get("/daily/transcript/:transcriptId", async (req) => { - const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1] + .get("/daily/transcript/:transcriptId", async (req, env) => { + // Use server-side API key - never expose to client + const apiKey = env.DAILY_API_KEY const { transcriptId } = req.params - + if (!apiKey) { - return new Response(JSON.stringify({ error: 'No API key provided' }), { - status: 401, + return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), { + status: 500, headers: { 'Content-Type': 'application/json' } }) } @@ -564,12 +612,13 @@ const router = AutoRouter({ }) // Recording endpoints - .post("/daily/recordings/start", async (req) => { - const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1] - + .post("/daily/recordings/start", async (req, env) => { + // Use server-side API key - never expose to client + const apiKey = env.DAILY_API_KEY + if (!apiKey) { - return new Response(JSON.stringify({ error: 'No API key provided' }), { - status: 401, + return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), { + status: 500, headers: { 'Content-Type': 'application/json' } }) } @@ -605,13 +654,14 @@ const router = AutoRouter({ } }) - .post("/daily/recordings/:recordingId/stop", async (req) => { - const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1] + .post("/daily/recordings/:recordingId/stop", async (req, env) => { + // Use server-side API key - never expose to client + const apiKey = env.DAILY_API_KEY const { recordingId } = req.params - + if (!apiKey) { - return new Response(JSON.stringify({ error: 'No API key provided' }), { - status: 401, + return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), { + status: 500, headers: { 'Content-Type': 'application/json' } }) }