Session 10: Internal auth refactor, prefetch cascade keys, Sentry, welcome email (1286 tests)
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user