Session 7d: Audit fixes - rate limiting, error leak, parallel parlays, analyze cache, bundle analyzer

This commit is contained in:
Kev
2026-06-10 03:12:20 -04:00
parent d954e4d952
commit 6f4a353de9
18 changed files with 913 additions and 72 deletions
+33 -2
View File
@@ -1,8 +1,39 @@
const express = require('express');
const { analyzeProp } = require('../services/propAnalyzer');
const { cacheGet, cacheSet } = require('../utils/redis');
const { createRateLimit } = require('../middleware/rateLimit');
const router = express.Router();
// SEC-1 (Session 7d): /prop and /batch are public — both proxy to the
// prop analyzer which makes upstream API calls. Cap to 10 requests per
// IP per minute so a single bad actor can't burn analyzer credits.
const analyzeLimit = createRateLimit({ windowMs: 60_000, max: 10 });
router.use(analyzeLimit);
// PERF-1 (Session 7d): cache analyzeProp results in Redis. Same prop hit
// twice within 60s reuses the previous analysis instead of re-doing the
// upstream chain. 60s is short enough that line moves still surface.
const ANALYZE_TTL_SECONDS = 60;
function analyzeCacheKey(prop) {
const sport = (prop.sport || 'nba').toLowerCase();
const player = String(prop.player || '').trim().toLowerCase();
const stat = String(prop.stat_type || '').trim().toLowerCase();
const line = Number(prop.line);
const direction = String(prop.direction || '').toLowerCase();
return `analyze:${sport}:${player}:${stat}:${line}:${direction}`;
}
async function cachedAnalyze(prop) {
const key = analyzeCacheKey(prop);
const cached = await cacheGet(key);
if (cached) return { ...cached, _cache: 'HIT' };
const result = await analyzeProp(prop);
// cacheSet swallows failures (degraded Redis) — analysis still flows
// even when the cache is down.
await cacheSet(key, result, ANALYZE_TTL_SECONDS);
return { ...result, _cache: 'MISS' };
}
const VALID_STAT_TYPES = new Set([
'points', 'rebounds', 'assists', 'threes', 'blocks',
'steals', 'pra', 'turnovers',
@@ -32,7 +63,7 @@ router.post('/prop', async (req, res) => {
}
try {
const result = await analyzeProp(req.body);
const result = await cachedAnalyze(req.body);
return res.json(result);
} catch (err) {
if (err.response && err.response.status === 404) {
@@ -61,7 +92,7 @@ router.post('/batch', async (req, res) => {
}
try {
const result = await analyzeProp(prop);
const result = await cachedAnalyze(prop);
results.push(result);
} catch (err) {
results.push({
+7 -2
View File
@@ -182,7 +182,11 @@ router.post('/', async (req, res) => {
try {
svg = renderer.buildSvg(type, format, payload);
} catch (err) {
return res.status(400).json({ error: 'render failed', detail: err.message });
// SEC-2 (Session 7d): don't echo err.message to public callers — the
// SVG renderer may surface file paths or upstream library detail.
// Log to stderr for ops, return a generic 400 to the caller.
console.error('[VYNDR] shareCard render failed:', err?.message);
return res.status(400).json({ error: 'render failed' });
}
// Optional SVG-only mode (no rasterization)
@@ -203,7 +207,8 @@ router.post('/', async (req, res) => {
res.set('X-Degraded', 'svg-fallback');
return res.send(svg);
}
return res.status(500).json({ error: 'rasterize failed', detail: err.message });
console.error('[VYNDR] shareCard rasterize failed:', err?.message);
return res.status(500).json({ error: 'rasterize failed' });
}
// Write cache (best-effort; ignore failures so the response still flies)