feat(frontend): per-user DLOB source header

- Add cookie-based source selector (mevnode|drift)\n- Proxy sets x-hasura-dlob-source for HTTP + WS\n- Include same proxy script mount in prod overlay
This commit is contained in:
u1
2026-02-13 11:33:13 +01:00
parent 57433c7e75
commit 5f46d26037
4 changed files with 937 additions and 1 deletions

View File

@@ -37,6 +37,8 @@ const AUTH_SESSION_COOKIE = String(process.env.AUTH_SESSION_COOKIE || 'trade_ses
.trim()
.toLowerCase();
const AUTH_SESSION_TTL_SECONDS = Number.parseInt(process.env.AUTH_SESSION_TTL_SECONDS || '43200', 10); // 12h
const DLOB_SOURCE_COOKIE = String(process.env.DLOB_SOURCE_COOKIE || 'trade_dlob_source').trim() || 'trade_dlob_source';
const DLOB_SOURCE_DEFAULT = String(process.env.DLOB_SOURCE_DEFAULT || 'mevnode').trim() || 'mevnode';
function readJson(filePath) {
const raw = fs.readFileSync(filePath, 'utf8');
@@ -143,6 +145,29 @@ function safePathFromUrlPath(urlPath) {
return normalized;
}
function injectIndexHtml(html, { dlobSource, redirectPath }) {
const src = normalizeDlobSource(dlobSource) || 'mevnode';
const redirect = safeRedirectPath(redirectPath);
const hrefBase = `/prefs/dlob-source?redirect=${encodeURIComponent(redirect)}&set=`;
const styleActive = 'font-weight:700;text-decoration:underline;';
const styleInactive = 'font-weight:400;text-decoration:none;';
const snippet = `
<!-- trade: dlob source switch -->
<div style="position:fixed;right:12px;bottom:12px;z-index:2147483647;background:rgba(0,0,0,0.72);color:#fff;padding:8px 10px;border-radius:10px;font:12px/1.2 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;backdrop-filter:blur(6px);">
<span style="opacity:0.85;margin-right:6px;">DLOB</span>
<a href="${hrefBase}mevnode" style="color:#fff;${src === 'mevnode' ? styleActive : styleInactive}">mevnode</a>
<span style="opacity:0.6;margin:0 6px;">|</span>
<a href="${hrefBase}drift" style="color:#fff;${src === 'drift' ? styleActive : styleInactive}">drift</a>
</div>
`;
const bodyClose = /<\/body>/i;
if (bodyClose.test(html)) return html.replace(bodyClose, `${snippet}</body>`);
return `${html}\n${snippet}\n`;
}
function serveStatic(req, res) {
if (req.method !== 'GET' && req.method !== 'HEAD') {
send(res, 405, { 'content-type': 'text/plain; charset=utf-8' }, 'method_not_allowed');
@@ -171,6 +196,15 @@ function serveStatic(req, res) {
res.setHeader('content-type', contentTypeFor(filePath));
res.setHeader('cache-control', filePath.endsWith('index.html') ? 'no-cache' : 'public, max-age=31536000');
if (req.method === 'HEAD') return void res.end();
if (filePath.endsWith('index.html')) {
const html = fs.readFileSync(filePath, 'utf8');
const injected = injectIndexHtml(html, {
dlobSource: resolveDlobSource(req),
redirectPath: url.pathname + url.search,
});
res.end(injected);
return true;
}
fs.createReadStream(filePath).pipe(res);
return true;
} catch {
@@ -222,6 +256,43 @@ function readCookie(req, name) {
return null;
}
function normalizeDlobSource(value) {
const v = String(value ?? '')
.trim()
.toLowerCase();
if (v === 'mevnode') return 'mevnode';
if (v === 'drift') return 'drift';
return null;
}
function resolveDlobSource(req) {
const fromCookie = normalizeDlobSource(readCookie(req, DLOB_SOURCE_COOKIE));
if (fromCookie) return fromCookie;
return normalizeDlobSource(DLOB_SOURCE_DEFAULT) || 'mevnode';
}
function safeRedirectPath(value) {
const s = String(value ?? '').trim();
if (!s.startsWith('/')) return '/';
if (s.startsWith('//')) return '/';
return s.replace(/\r|\n/g, '');
}
function setDlobSourceCookie(res, { secure, dlobSource }) {
const src = normalizeDlobSource(dlobSource);
if (!src) return false;
const parts = [
`${DLOB_SOURCE_COOKIE}=${src}`,
'Path=/',
'SameSite=Lax',
'HttpOnly',
'Max-Age=31536000',
];
if (secure) parts.push('Secure');
res.setHeader('set-cookie', parts.join('; '));
return true;
}
function resolveAuthUser(req) {
const user = readHeader(req, AUTH_USER_HEADER) || readHeader(req, 'x-webauth-user');
const value = typeof user === 'string' ? user.trim() : '';
@@ -423,7 +494,7 @@ function withCors(res) {
res.setHeader('access-control-allow-methods', 'GET,POST,OPTIONS');
res.setHeader(
'access-control-allow-headers',
'content-type, authorization, x-hasura-admin-secret, x-hasura-role, x-hasura-user-id'
'content-type, authorization, x-hasura-admin-secret, x-hasura-role, x-hasura-user-id, x-hasura-dlob-source'
);
}
@@ -440,6 +511,8 @@ function proxyGraphqlHttp(req, res) {
const headers = stripHopByHopHeaders(req.headers);
headers.host = target.host;
delete headers['x-hasura-dlob-source'];
headers['x-hasura-dlob-source'] = resolveDlobSource(req);
const upstreamReq = lib.request(
{
@@ -499,6 +572,8 @@ function proxyGraphqlWs(req, socket, head) {
delete headers['content-length'];
delete headers['content-type'];
headers.host = target.host;
delete headers['x-hasura-dlob-source'];
headers['x-hasura-dlob-source'] = resolveDlobSource(req);
const lines = [];
lines.push(`GET ${target.pathname + target.search} HTTP/1.1`);
@@ -570,6 +645,25 @@ async function handler(req, res) {
return;
}
if (req.method === 'GET' && url.pathname === '/prefs/dlob-source') {
const set = url.searchParams.get('set');
if (!set) {
sendJson(res, 200, { ok: true, dlobSource: resolveDlobSource(req) });
return;
}
const ok = setDlobSourceCookie(res, { secure: isHttpsRequest(req), dlobSource: set });
if (!ok) {
sendJson(res, 400, { ok: false, error: 'invalid_dlob_source' });
return;
}
res.statusCode = 302;
res.setHeader('location', safeRedirectPath(url.searchParams.get('redirect') || '/'));
res.end();
return;
}
if (req.method === 'POST' && url.pathname === '/auth/login') {
if (AUTH_MODE === 'off' || AUTH_MODE === 'none' || AUTH_MODE === 'disabled') {
sendJson(res, 400, { ok: false, error: 'auth_disabled' });