fix: move Daily.co API key to server-side for security
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
d2101ef1cf
commit
1b67a2fe7f
|
|
@ -58,11 +58,6 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
|
|
||||||
async generateMeetingToken(roomName: string) {
|
async generateMeetingToken(roomName: string) {
|
||||||
const workerUrl = WORKER_URL;
|
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) {
|
if (!workerUrl) {
|
||||||
throw new Error('Worker URL is not configured');
|
throw new Error('Worker URL is not configured');
|
||||||
|
|
@ -138,11 +133,6 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const workerUrl = WORKER_URL;
|
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) {
|
if (!workerUrl) {
|
||||||
throw new Error('Worker URL is not configured');
|
throw new Error('Worker URL is not configured');
|
||||||
|
|
@ -154,11 +144,11 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
const cleanId = shortId.replace(/[^A-Za-z0-9]/g, '');
|
const cleanId = shortId.replace(/[^A-Za-z0-9]/g, '');
|
||||||
const roomName = `canvas-${cleanId}`;
|
const roomName = `canvas-${cleanId}`;
|
||||||
|
|
||||||
|
// Worker uses server-side API key, no need to send it from client
|
||||||
const response = await fetch(`${workerUrl}/daily/rooms`, {
|
const response = await fetch(`${workerUrl}/daily/rooms`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${apiKey}`
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: roomName,
|
name: roomName,
|
||||||
|
|
@ -181,12 +171,12 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
if (response.status === 400 && error.info && error.info.includes('already exists')) {
|
if (response.status === 400 && error.info && error.info.includes('already exists')) {
|
||||||
isNewRoom = false;
|
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 {
|
try {
|
||||||
const getRoomResponse = await fetch(`https://api.daily.co/v1/rooms/${roomName}`, {
|
const getRoomResponse = await fetch(`${workerUrl}/daily/rooms/${roomName}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${apiKey}`
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -240,8 +230,7 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
async startRecording(shape: IVideoChatShape) {
|
async startRecording(shape: IVideoChatShape) {
|
||||||
if (!shape.props.roomUrl) return;
|
if (!shape.props.roomUrl) return;
|
||||||
|
|
||||||
const workerUrl = WORKER_URL;
|
const workerUrl = WORKER_URL;
|
||||||
const apiKey = import.meta.env.VITE_DAILY_API_KEY;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Extract room name from URL (same as transcription methods)
|
// Extract room name from URL (same as transcription methods)
|
||||||
|
|
@ -250,10 +239,10 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
throw new Error('Could not extract room name from URL');
|
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`, {
|
const response = await fetch(`${workerUrl}/daily/recordings/start`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${apiKey}`,
|
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
@ -287,13 +276,13 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
if (!shape.props.recordingId) return;
|
if (!shape.props.recordingId) return;
|
||||||
|
|
||||||
const workerUrl = WORKER_URL;
|
const workerUrl = WORKER_URL;
|
||||||
const apiKey = import.meta.env.VITE_DAILY_API_KEY;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Worker uses server-side API key, no need to send it from client
|
||||||
await fetch(`${workerUrl}/daily/recordings/${shape.props.recordingId}/stop`, {
|
await fetch(`${workerUrl}/daily/recordings/${shape.props.recordingId}/stop`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${apiKey}`
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
114
worker/worker.ts
114
worker/worker.ts
|
|
@ -313,12 +313,13 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
.post("/daily/rooms", async (req) => {
|
.post("/daily/rooms", async (req, env) => {
|
||||||
const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1]
|
// Use server-side API key - never expose to client
|
||||||
|
const apiKey = env.DAILY_API_KEY
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return new Response(JSON.stringify({ error: 'No API key provided' }), {
|
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
||||||
status: 401,
|
status: 500,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -356,12 +357,55 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
.post("/daily/tokens", async (req) => {
|
// Get room info by name
|
||||||
const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1]
|
.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) {
|
if (!apiKey) {
|
||||||
return new Response(JSON.stringify({ error: 'No API key provided' }), {
|
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
||||||
status: 401,
|
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' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -401,13 +445,14 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add new transcription endpoints
|
// Add new transcription endpoints
|
||||||
.post("/daily/rooms/:roomName/start-transcription", async (req) => {
|
.post("/daily/rooms/:roomName/start-transcription", async (req, env) => {
|
||||||
const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1]
|
// Use server-side API key - never expose to client
|
||||||
|
const apiKey = env.DAILY_API_KEY
|
||||||
const { roomName } = req.params
|
const { roomName } = req.params
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return new Response(JSON.stringify({ error: 'No API key provided' }), {
|
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
||||||
status: 401,
|
status: 500,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -441,13 +486,14 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
.post("/daily/rooms/:roomName/stop-transcription", async (req) => {
|
.post("/daily/rooms/:roomName/stop-transcription", async (req, env) => {
|
||||||
const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1]
|
// Use server-side API key - never expose to client
|
||||||
|
const apiKey = env.DAILY_API_KEY
|
||||||
const { roomName } = req.params
|
const { roomName } = req.params
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return new Response(JSON.stringify({ error: 'No API key provided' }), {
|
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
||||||
status: 401,
|
status: 500,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -482,13 +528,14 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add endpoint to get transcript access link
|
// Add endpoint to get transcript access link
|
||||||
.get("/daily/transcript/:transcriptId/access-link", async (req) => {
|
.get("/daily/transcript/:transcriptId/access-link", async (req, env) => {
|
||||||
const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1]
|
// Use server-side API key - never expose to client
|
||||||
|
const apiKey = env.DAILY_API_KEY
|
||||||
const { transcriptId } = req.params
|
const { transcriptId } = req.params
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return new Response(JSON.stringify({ error: 'No API key provided' }), {
|
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
||||||
status: 401,
|
status: 500,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -523,13 +570,14 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add endpoint to get transcript text
|
// Add endpoint to get transcript text
|
||||||
.get("/daily/transcript/:transcriptId", async (req) => {
|
.get("/daily/transcript/:transcriptId", async (req, env) => {
|
||||||
const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1]
|
// Use server-side API key - never expose to client
|
||||||
|
const apiKey = env.DAILY_API_KEY
|
||||||
const { transcriptId } = req.params
|
const { transcriptId } = req.params
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return new Response(JSON.stringify({ error: 'No API key provided' }), {
|
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
||||||
status: 401,
|
status: 500,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -564,12 +612,13 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||||
})
|
})
|
||||||
|
|
||||||
// Recording endpoints
|
// Recording endpoints
|
||||||
.post("/daily/recordings/start", async (req) => {
|
.post("/daily/recordings/start", async (req, env) => {
|
||||||
const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1]
|
// Use server-side API key - never expose to client
|
||||||
|
const apiKey = env.DAILY_API_KEY
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return new Response(JSON.stringify({ error: 'No API key provided' }), {
|
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
||||||
status: 401,
|
status: 500,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -605,13 +654,14 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
.post("/daily/recordings/:recordingId/stop", async (req) => {
|
.post("/daily/recordings/:recordingId/stop", async (req, env) => {
|
||||||
const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1]
|
// Use server-side API key - never expose to client
|
||||||
|
const apiKey = env.DAILY_API_KEY
|
||||||
const { recordingId } = req.params
|
const { recordingId } = req.params
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return new Response(JSON.stringify({ error: 'No API key provided' }), {
|
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
||||||
status: 401,
|
status: 500,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue