Session 10: Internal auth refactor, prefetch cascade keys, Sentry, welcome email (1286 tests)

This commit is contained in:
Kev
2026-06-10 20:45:05 -04:00
parent b55dcbd614
commit e5c45ecc8e
22 changed files with 3837 additions and 94 deletions
+5 -13
View File
@@ -25,19 +25,11 @@ const { __helpers: gradingHelpers } = require('./grading');
const router = express.Router();
const espnLimiter = createLimiter(API_BUDGETS.espn);
const LOOPBACK_IPS = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
function requireInternal(req, res, next) {
const expected = process.env.VYNDR_INTERNAL_KEY;
if (!expected) return res.status(503).json({ error: 'Internal auth not configured' });
if (req.get('X-VYNDR-Internal-Key') !== expected) {
return res.status(401).json({ error: 'Invalid internal key' });
}
if (!LOOPBACK_IPS.has(req.ip || req.socket?.remoteAddress)) {
return res.status(403).json({ error: 'Origin not permitted' });
}
return next();
}
// Session 10 — uses src/middleware/internalAuth.js. /correct stays
// loopback-restricted because the morning sweep runs co-located with
// the API; n8n doesn't call this one.
const { requireInternalAuth } = require('../middleware/internalAuth');
const requireInternal = requireInternalAuth({ loopbackOnly: true });
async function fetchBoxScore(sportCfg, gameId) {
await espnLimiter.waitForToken();
+37 -30
View File
@@ -27,27 +27,22 @@ const clvTracker = require('../services/intelligence/clvTracker');
const accuracyTracker = require('../services/intelligence/accuracyTracker');
const weightAdjuster = require('../services/intelligence/weightAdjuster');
const { requireInternalAuth } = require('../middleware/internalAuth');
const router = express.Router();
const LOOPBACK_IPS = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
function requireInternal(req, res, next) {
const expected = process.env.VYNDR_INTERNAL_KEY;
if (!expected) {
// Refuse to serve if the secret isn't configured — better than
// accidentally exposing the endpoint with a default value.
return res.status(503).json({ error: 'Internal auth not configured' });
}
const provided = req.get('X-VYNDR-Internal-Key');
if (!provided || provided !== expected) {
return res.status(401).json({ error: 'Invalid internal key' });
}
const remoteIp = req.ip || req.socket?.remoteAddress;
if (!LOOPBACK_IPS.has(remoteIp)) {
return res.status(403).json({ error: 'Origin not permitted' });
}
return next();
}
// Session 10 — extracted into src/middleware/internalAuth.js. The two
// internal routes below keep slightly different policies:
// /resolve — loopback-only (called by the on-host poller)
// /pipeline — accepts off-host (called by n8n from another container,
// which is why the legacy loopback-only check broke it)
//
// `requireInternal` stays exported as `__helpers.requireInternal` for
// the existing test suite — it's a thin alias for the loopback-only
// variant so the resolution test (which spins up its own server on
// 127.0.0.1) behaves identically.
const requireInternal = requireInternalAuth({ loopbackOnly: true });
const requireInternalAnyOrigin = requireInternalAuth({ loopbackOnly: false });
// ---------------------------------------------------------------
// Box-score traversal — sport-specific shapes flattened into a uniform
@@ -412,22 +407,34 @@ router.post('/resolve', requireInternal, async (req, res) => {
const VALID_SPORTS = new Set(['nba', 'wnba', 'mlb', 'nfl', 'nhl', 'ncaab', 'ncaafb']);
router.post('/pipeline', requireInternal, async (req, res) => {
// Session 10 — `/pipeline` accepts off-host callers (n8n runs in a
// separate container). With a `sport` body field, runs that sport
// only; with an empty body, iterates every active sport. n8n's
// Morning Ops workflow sends an empty body; the per-sport workflows
// pass a specific sport. The legacy header (X-VYNDR-Internal-Key) and
// the new short form (x-internal-key) both authenticate.
router.post('/pipeline', requireInternalAnyOrigin, async (req, res) => {
const { sport, options } = req.body || {};
if (!sport || !VALID_SPORTS.has(sport)) {
if (sport && !VALID_SPORTS.has(sport)) {
return res.status(400).json({ error: 'sport must be one of: nba, wnba, mlb, nfl, nhl, ncaab, ncaafb' });
}
// Lazy-load the orchestrator so this route doesn't pay the require cost
// until it's actually invoked (and so unit tests of /resolve don't pull
// in the whole adapter graph).
const { runPipeline } = require('../services/intelligence/gradingOrchestrator');
try {
const summary = await runPipeline(sport, options || {});
return res.json(summary);
} catch (err) {
console.error('[VYNDR] Pipeline error:', err.message);
return res.status(503).json({ error: 'Pipeline run failed' });
const sportsToRun = sport ? [sport] : ['nba', 'wnba', 'mlb'];
const results = [];
for (const s of sportsToRun) {
try {
const summary = await runPipeline(s, options || {});
results.push({ sport: s, ...summary });
} catch (err) {
console.error('[VYNDR] Pipeline error:', s, err.message);
results.push({ sport: s, error: err.message });
}
}
// If a specific sport was requested, preserve the legacy single-object
// shape so existing callers (tests + the n8n per-sport workflows)
// don't break. Multi-sport runs return an array.
if (sport) return res.json(results[0]);
return res.json({ status: 'ok', timestamp: new Date().toISOString(), sports: results });
});
// Exported so server.js can wire it up with a larger body limit; also lets