import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { defineConfig, loadEnv } from 'vite'; import react from '@vitejs/plugin-react'; const DIR = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(DIR, '../..'); type BasicAuth = { username: string; password: string }; function stripTrailingSlashes(p: string): string { const out = p.replace(/\/+$/, ''); return out || '/'; } function readApiReadToken(env: Record): string | undefined { if (env.API_READ_TOKEN) return env.API_READ_TOKEN; const p = path.join(ROOT, 'tokens', 'read.json'); if (!fs.existsSync(p)) return undefined; try { const raw = fs.readFileSync(p, 'utf8'); const json = JSON.parse(raw) as { token?: string }; return json?.token ? String(json.token) : undefined; } catch { return undefined; } } function readApiAdminSecret(env: Record): string | undefined { if (Object.prototype.hasOwnProperty.call(env, 'API_PROXY_ADMIN_SECRET')) { const raw = String(env.API_PROXY_ADMIN_SECRET || '').trim(); return raw || undefined; } const p = path.join(ROOT, 'tokens', 'api.json'); if (!fs.existsSync(p)) return undefined; try { const raw = fs.readFileSync(p, 'utf8'); const json = JSON.parse(raw) as { adminSecret?: string }; return json?.adminSecret ? String(json.adminSecret) : undefined; } catch { return undefined; } } function parseBasicAuth(value: string | undefined): BasicAuth | undefined { const raw = String(value || '').trim(); if (!raw) return undefined; const idx = raw.indexOf(':'); if (idx <= 0) return undefined; const username = raw.slice(0, idx).trim(); const password = raw.slice(idx + 1); if (!username || !password) return undefined; return { username, password }; } function readProxyBasicAuth(env: Record): BasicAuth | undefined { const fromEnv = parseBasicAuth(env.API_PROXY_BASIC_AUTH); if (fromEnv) return fromEnv; const fileRaw = String(env.API_PROXY_BASIC_AUTH_FILE || '').trim(); if (!fileRaw) return undefined; const p = path.isAbsolute(fileRaw) ? fileRaw : path.join(ROOT, fileRaw); if (!fs.existsSync(p)) return undefined; try { const raw = fs.readFileSync(p, 'utf8'); const json = JSON.parse(raw) as { username?: string; password?: string }; const username = typeof json?.username === 'string' ? json.username.trim() : ''; const password = typeof json?.password === 'string' ? json.password : ''; if (!username || !password) return undefined; return { username, password }; } catch { return undefined; } } function parseUrl(v: string): URL | undefined { try { return new URL(v); } catch { return undefined; } } function inferUiProxyTarget(apiTarget: string): string | undefined { try { const u = new URL(apiTarget); const p = stripTrailingSlashes(u.pathname || '/'); if (!p.endsWith('/api')) return undefined; const basePath = p.slice(0, -'/api'.length) || '/'; u.pathname = basePath; u.search = ''; u.hash = ''; const out = u.toString(); return out.endsWith('/') ? out.slice(0, -1) : out; } catch { return undefined; } } function applyProxyBasicAuth(proxyReq: any) { if (!proxyBasicAuth) return false; const b64 = Buffer.from(`${proxyBasicAuth.username}:${proxyBasicAuth.password}`, 'utf8').toString('base64'); proxyReq.setHeader('Authorization', `Basic ${b64}`); return true; } function rewriteSetCookieForLocalDevHttp(proxyRes: any) { const v = proxyRes?.headers?.['set-cookie']; if (!v) return; const rewrite = (cookie: string) => { let out = cookie.replace(/;\s*secure\b/gi, ''); out = out.replace(/;\s*domain=[^;]+/gi, ''); out = out.replace(/;\s*samesite=none\b/gi, '; SameSite=Lax'); return out; }; proxyRes.headers['set-cookie'] = Array.isArray(v) ? v.map(rewrite) : rewrite(String(v)); } function readDevProxyAuthUser(env: Record): string | undefined { const raw = String(env.DEV_PROXY_AUTH_USER || env.VITE_DEV_AUTH_USER || '').trim(); return raw || undefined; } export default defineConfig(({ mode }) => { const env = { ...process.env, ...loadEnv(mode, DIR, '') } as Record; const apiReadToken = readApiReadToken(env); const apiAdminSecret = readApiAdminSecret(env); const proxyBasicAuth = readProxyBasicAuth(env); const devProxyAuthUser = readDevProxyAuthUser(env); const apiProxyTargetRaw = String(env.API_PROXY_TARGET || '').trim(); const apiProxyTarget = apiProxyTargetRaw || undefined; const apiProxyTargetUrl = apiProxyTarget ? parseUrl(apiProxyTarget) : undefined; const apiProxyTargetPath = stripTrailingSlashes(apiProxyTargetUrl?.pathname || '/'); const apiProxyTargetEndsWithApi = apiProxyTargetPath.endsWith('/api'); const apiProxyStripPrefixRaw = String(env.API_PROXY_STRIP_PREFIX || (apiProxyTargetEndsWithApi ? '/api' : '')).trim(); const apiProxyStripPrefix = apiProxyStripPrefixRaw ? stripTrailingSlashes(apiProxyStripPrefixRaw) : ''; const uiProxyTarget = env.FRONTEND_PROXY_TARGET || env.UI_PROXY_TARGET || env.AUTH_PROXY_TARGET || inferUiProxyTarget(apiProxyTarget) || (apiProxyTargetUrl && apiProxyTargetPath === '/' ? stripTrailingSlashes(apiProxyTargetUrl.toString()) : undefined); const graphqlProxyTarget = env.GRAPHQL_PROXY_TARGET || env.HASURA_PROXY_TARGET || uiProxyTarget; const graphqlProxyPathRaw = String(env.GRAPHQL_PROXY_PATH || '').trim(); const graphqlProxyPath = graphqlProxyPathRaw ? stripTrailingSlashes(graphqlProxyPathRaw) : ''; const portRaw = env.VITE_DEV_PORT || env.DEV_PORT || '5173'; const port = Number.parseInt(String(portRaw), 10); function applyProxyBasicAuth(proxyReq: any) { if (!proxyBasicAuth) return false; const b64 = Buffer.from(`${proxyBasicAuth.username}:${proxyBasicAuth.password}`, 'utf8').toString('base64'); proxyReq.setHeader('Authorization', `Basic ${b64}`); return true; } function applyDevProxyAuth(proxyReq: any) { if (!devProxyAuthUser) return false; proxyReq.setHeader('x-trade-user', devProxyAuthUser); return true; } const proxy: Record = { }; if (apiProxyTarget) { proxy['/api'] = { target: apiProxyTarget, changeOrigin: true, rewrite: (p: string) => { if (!apiProxyStripPrefix) return p; const rewritten = p.replace(new RegExp(`^${apiProxyStripPrefix}`), ''); return rewritten || '/'; }, configure: (p: any) => { p.on('proxyReq', (proxyReq: any) => { applyProxyBasicAuth(proxyReq); applyDevProxyAuth(proxyReq); if (apiAdminSecret) { proxyReq.setHeader('x-admin-secret', apiAdminSecret); return; } if (apiReadToken) proxyReq.setHeader('Authorization', `Bearer ${apiReadToken}`); }); }, }; } if (graphqlProxyTarget) { for (const prefix of ['/graphql', '/graphql-ws']) { proxy[prefix] = { target: graphqlProxyTarget, changeOrigin: true, ws: true, rewrite: (p: string) => (graphqlProxyPath ? p.replace(new RegExp(`^${prefix}`), graphqlProxyPath) : p), configure: (p: any) => { p.on('proxyReq', (proxyReq: any) => { applyProxyBasicAuth(proxyReq); applyDevProxyAuth(proxyReq); }); p.on('proxyReqWs', (proxyReq: any) => { applyProxyBasicAuth(proxyReq); applyDevProxyAuth(proxyReq); }); }, }; } } if (uiProxyTarget) { for (const prefix of ['/whoami', '/auth', '/logout']) { proxy[prefix] = { target: uiProxyTarget, changeOrigin: true, configure: (p: any) => { p.on('proxyReq', (proxyReq: any) => { applyProxyBasicAuth(proxyReq); applyDevProxyAuth(proxyReq); }); p.on('proxyRes', (proxyRes: any) => { rewriteSetCookieForLocalDevHttp(proxyRes); }); }, }; } } return { plugins: [react()], server: { port: Number.isInteger(port) && port > 0 ? port : 5173, strictPort: true, proxy, }, }; });