fix(app): replace CORS Access-Control-Allow-Origin * with explicit localhost origin
Add setCorsHeaders() and handleCorsOptions() helpers in server/dev-auth.ts. Replace wildcard CORS origin with http://localhost:5173 in all Vite plugins and claude-proxy.ts. Include Authorization in allowed CORS headers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
cc7d9fc19e
commit
4dc9588c8a
@@ -3,7 +3,7 @@ import { createServer } from 'http'
|
||||
import { readFileSync, existsSync } from 'fs'
|
||||
import { resolve, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { validateDevAuth } from './dev-auth.js'
|
||||
import { validateDevAuth, handleCorsOptions } from './dev-auth.js'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
@@ -209,7 +209,7 @@ async function streamOpenRouterProxy(
|
||||
res: import('http').ServerResponse,
|
||||
): Promise<void> {
|
||||
if (!OPENROUTER_API_KEY) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' })
|
||||
res.writeHead(500, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': 'http://localhost:5173' })
|
||||
res.end(JSON.stringify({ error: 'OPENROUTER_API_KEY not configured on server' }))
|
||||
return
|
||||
}
|
||||
@@ -218,7 +218,7 @@ async function streamOpenRouterProxy(
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Origin': 'http://localhost:5173',
|
||||
'X-Accel-Buffering': 'no',
|
||||
})
|
||||
|
||||
@@ -280,12 +280,7 @@ async function streamOpenRouterProxy(
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204, {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
})
|
||||
res.end()
|
||||
handleCorsOptions(res)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -316,7 +311,7 @@ const server = createServer((req, res) => {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Origin': 'http://localhost:5173',
|
||||
'X-Accel-Buffering': 'no',
|
||||
})
|
||||
streamViaAnthropicApi(model, system, messages ?? [], res)
|
||||
@@ -378,7 +373,7 @@ const server = createServer((req, res) => {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Origin': 'http://localhost:5173',
|
||||
'X-Accel-Buffering': 'no',
|
||||
})
|
||||
|
||||
@@ -437,7 +432,7 @@ const server = createServer((req, res) => {
|
||||
console.error('[proxy] Parse error:', err)
|
||||
res.writeHead(400, {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Origin': 'http://localhost:5173',
|
||||
})
|
||||
res.end(JSON.stringify({ error: String(err) }))
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { IncomingMessage, ServerResponse } from 'http'
|
||||
|
||||
const DEV_TOKEN = process.env.VITE_DEV_API_TOKEN ?? ''
|
||||
|
||||
/** Validate Authorization header. Returns true if authorized, false if rejected (response already sent). */
|
||||
/** Validate Authorization header. Returns true if authorized, false if rejected (response already sent). */
|
||||
export function validateDevAuth(req: IncomingMessage, res: ServerResponse): boolean {
|
||||
if (!DEV_TOKEN) return true // No token configured, skip auth
|
||||
@@ -16,3 +17,20 @@ export function validateDevAuth(req: IncomingMessage, res: ServerResponse): bool
|
||||
res.end(JSON.stringify({ error: 'Unauthorized' }))
|
||||
return false
|
||||
}
|
||||
|
||||
const ALLOWED_ORIGIN = 'http://localhost:5173'
|
||||
|
||||
/** Set CORS headers with explicit localhost origin instead of wildcard. */
|
||||
export function setCorsHeaders(res: ServerResponse): void {
|
||||
res.setHeader('Access-Control-Allow-Origin', ALLOWED_ORIGIN)
|
||||
}
|
||||
|
||||
/** Write CORS preflight response. */
|
||||
export function handleCorsOptions(res: ServerResponse): void {
|
||||
res.writeHead(204, {
|
||||
'Access-Control-Allow-Origin': ALLOWED_ORIGIN,
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
})
|
||||
res.end()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Plugin } from 'vite'
|
||||
import type { Connect } from 'vite'
|
||||
import { validateDevAuth } from './server/dev-auth'
|
||||
import { validateDevAuth, setCorsHeaders } from './server/dev-auth'
|
||||
|
||||
export interface MusicSearchResult {
|
||||
source: 'wavlake'
|
||||
@@ -237,7 +237,7 @@ function createSearchMiddleware() {
|
||||
const cached = getCached(key)
|
||||
if (cached !== undefined) {
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
setCorsHeaders(res)
|
||||
res.setHeader('Cache-Control', 'public, max-age=3600')
|
||||
res.setHeader('X-Cache', 'HIT')
|
||||
res.end(JSON.stringify(cached ?? { error: 'No results on Wavlake' }))
|
||||
@@ -250,7 +250,7 @@ function createSearchMiddleware() {
|
||||
setCache(key, result)
|
||||
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
setCorsHeaders(res)
|
||||
res.setHeader('Cache-Control', 'public, max-age=3600')
|
||||
res.end(JSON.stringify(result ?? { error: 'No results on Wavlake' }))
|
||||
} catch (err) {
|
||||
@@ -279,7 +279,7 @@ function createRankingsMiddleware() {
|
||||
const cached = rankingsCache.get(cacheKey)
|
||||
if (cached && Date.now() - cached.ts < RANKINGS_TTL) {
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
setCorsHeaders(res)
|
||||
res.setHeader('Cache-Control', 'public, max-age=600')
|
||||
res.setHeader('X-Cache', 'HIT')
|
||||
res.end(JSON.stringify(cached.data))
|
||||
@@ -291,7 +291,7 @@ function createRankingsMiddleware() {
|
||||
rankingsCache.set(cacheKey, { data: results, ts: Date.now() })
|
||||
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
setCorsHeaders(res)
|
||||
res.setHeader('Cache-Control', 'public, max-age=600')
|
||||
res.end(JSON.stringify(results))
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Plugin } from 'vite'
|
||||
import type { Connect } from 'vite'
|
||||
import Parser from 'rss-parser'
|
||||
import { validateDevAuth } from './server/dev-auth'
|
||||
import { validateDevAuth, setCorsHeaders } from './server/dev-auth'
|
||||
|
||||
export interface RssArticle {
|
||||
title: string
|
||||
@@ -140,7 +140,7 @@ function createRssMiddleware() {
|
||||
try {
|
||||
const articles = await fetchRssFromUrls(safe)
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
setCorsHeaders(res)
|
||||
res.setHeader('Cache-Control', 'public, max-age=300')
|
||||
res.end(JSON.stringify({ articles }))
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Plugin } from 'vite'
|
||||
import type { Connect } from 'vite'
|
||||
import { loadEnv } from 'vite'
|
||||
import { validateDevAuth } from './server/dev-auth'
|
||||
import { validateDevAuth, setCorsHeaders } from './server/dev-auth'
|
||||
|
||||
const TMDB_POSTER = 'https://image.tmdb.org/t/p/w342'
|
||||
const TMDB_BACKDROP = 'https://image.tmdb.org/t/p/w780'
|
||||
@@ -40,7 +40,7 @@ function createTmdbSearchMiddleware(tmdbKey: string | undefined, type: 'movie' |
|
||||
const posterUrl = first?.poster_path ? `${TMDB_POSTER}${first.poster_path}` : null
|
||||
const backdropUrl = first?.backdrop_path ? `${TMDB_BACKDROP}${first.backdrop_path}` : null
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
setCorsHeaders(res)
|
||||
res.setHeader('Cache-Control', 'public, max-age=86400')
|
||||
res.end(JSON.stringify({ posterUrl, backdropUrl }))
|
||||
} catch (err) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Plugin } from 'vite'
|
||||
import type { Connect } from 'vite'
|
||||
import { loadEnv } from 'vite'
|
||||
import { search as searchDuckDuckGo } from 'duck-duck-scrape'
|
||||
import { validateDevAuth } from './server/dev-auth'
|
||||
import { validateDevAuth, setCorsHeaders } from './server/dev-auth'
|
||||
|
||||
export interface WebSearchResult {
|
||||
title: string
|
||||
@@ -95,7 +95,7 @@ function createWebSearchMiddleware(searxUrl: string | undefined, braveApiKey: st
|
||||
|
||||
const sendResults = (results: WebSearchResult[]) => {
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
setCorsHeaders(res)
|
||||
res.setHeader('Cache-Control', 'public, max-age=300')
|
||||
res.end(JSON.stringify({ results }))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user