Session 31: Code audit + security review — NFL MARKET_MAP gap fixed, npm audit 0 vulns (1695 tests)

- Add NFL keys to oddsNormalizer.MARKET_MAP (defensive; same silent-zero
  class as the Session 30 MLB bug) + NFL surface test
- npm audit fix: ws/qs + Supabase transitives, 7 vulns -> 0 (semver-safe)
- Audit findings documented in BUILD-STATE: grades cache has no writer,
  NFL/NHL not wired end-to-end, rate limiting only on /analyze, tests
  mutate a tracked jsonl, leaked GitHub PAT in origin remote (rotate)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-06-14 23:48:40 -04:00
parent a3351e2135
commit 2ba3958c7a
4 changed files with 186 additions and 33 deletions
+70
View File
@@ -4,6 +4,76 @@
2026-06-14 2026-06-14
## Current Phase ## Current Phase
AUDIT v31.0 — Full code audit + security review + cleanup (Session 31)
## Session 31 (2026-06-14) — SHIPPED (audit)
Full-codebase audit, not a feature build. Validated 30 sessions of
assumptions across the API→normalizer→cache→route→proxy→frontend chain.
Backend 1694 → **1695 tests** (+1), 137 suites, zero regressions. Web
build clean. `npm audit`: 7 vulns (3 high, 4 moderate) → **0**.
### FIXES APPLIED
1. **MARKET_MAP NFL gap (silent-failure class, same family as the MLB
bug Session 30 fixed).** `oddsNormalizer.MARKET_MAP` had ZERO NFL
keys — when NFL wires up (season approaching) every NFL prop would
normalize to nothing. Added defensive mappings for both odds-api
`_yds` and `_yards` spellings → internal stat_types aligned with
`config/statFilters.js` (passing_yards/rushing_yards/receiving_yards/
interceptions + pass/rush/reception TDs, receptions, anytime_td,
kicking_points). Additive/zero-risk (only fires when those markets
are returned). +1 explicit NFL surface test.
2. **`npm audit fix`** — resolved ws (uninitialized memory disclosure)
+ qs (DoS) + transitive Supabase deps, all semver-safe. 0 vulns.
### SECURITY AUDIT (Ryan Montgomery)
- Stripe webhook: ✅ verified via `stripe.webhooks.constructEvent`
(`stripeService.constructWebhookEvent`, signature + 400 on failure).
- CORS: ✅ allowlist (localhost, vyndr.app, *.vercel.app preview),
`FRONTEND_ORIGINS` override — NOT open `*`.
- Internal routes: ✅ `requireInternalAuth` via `router.use` on the whole
`/api/internal` router (VYNDR_INTERNAL_KEY).
- Hardcoded secrets in SOURCE: ✅ none (only doc-comment references).
- npm audit: ✅ 0 after fix.
- Supabase service-role: scoped to `src/utils/supabase.js` (backend-only;
Express enforces its own auth) — acceptable.
- 🔴 **CRITICAL (operator action): live GitHub PAT (`ghp_…`) embedded in
the `origin` remote URL in `.git/config`.** Not in tracked source (not
committed/pushed) but exposed on disk. ROTATE the token and scrub the
URL (use a credential helper). Not auto-fixed — needs rotation.
### FINDINGS DOCUMENTED (deliberately left)
- **`grades:{sport}` cache has NO writer.** `contentTemplateService`
collector reads it ("when present"), so content slate/POTD never reach
`dataLevel: 'full'` in prod — degrades to lines/schedule. Graceful by
design; wiring a grades-cache writer is feature work. Severity: medium.
- **NFL/NHL props not wired end-to-end.** `oddsService.SPORT_KEYS` has no
nfl/nhl; `proplineAdapter.MARKETS.nfl/nhl` are empty. Not a current
silent failure (nothing expects them yet). NFL MARKET_MAP now ready;
full wiring is feature work. NHL: no product support anywhere (absent
from statFilters/streaks) — left entirely.
- **Inbound rate limiting only on `/api/analyze`.** Public cached
endpoints (/odds, /schedule, /gamelines, /streaks) have no inbound
throttle. Real risk bounded (Redis-cached, no per-hit upstream cost).
Recommend adding the existing `middleware/rateLimit` to public routers.
- **Tests mutate a tracked file** (`data/training/resolutions-2026-06.jsonl`
gets resolution rows appended on every run → spurious git diff).
Reverted the artifact; logging path should write to a temp/ignored
location under test. Severity: low (hygiene).
- Cache keys VERIFIED aligned: oddsService writes `odds:{sport}:{UTC}`;
content `getBestLines` reads the same. No mismatches found.
- Edge cases already covered: PropLine unsupported-sport→null,
error→null, all-3-keys-exhausted→null, no-keys→null, and
PropLine-error→odds-api fallback all have tests.
### Files modified
- `src/utils/oddsNormalizer.js` (NFL MARKET_MAP keys)
- `tests/unit/oddsNormalizer.test.js` (NFL surface test)
- `package-lock.json` (npm audit fix)
---
## Previous Phase
SHIP BUILD v30.0 — Provider backbone: PropLine 3-key adapter, MLB Stats API, ESPN summary (Session 30) SHIP BUILD v30.0 — Provider backbone: PropLine 3-key adapter, MLB Stats API, ESPN summary (Session 30)
## Session 30 (2026-06-14) — SHIPPED ## Session 30 (2026-06-14) — SHIPPED
+62 -33
View File
@@ -2337,9 +2337,9 @@
} }
}, },
"node_modules/anymatch/node_modules/picomatch": { "node_modules/anymatch/node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -2385,14 +2385,40 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.13.6", "version": "1.18.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.11", "follow-redirects": "^1.16.0",
"form-data": "^4.0.5", "form-data": "^4.0.5",
"proxy-from-env": "^1.1.0" "https-proxy-agent": "^5.0.1",
"proxy-from-env": "^2.1.0"
}
},
"node_modules/axios/node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/axios/node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
} }
}, },
"node_modules/babel-jest": { "node_modules/babel-jest": {
@@ -2551,9 +2577,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "2.0.2", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -3592,9 +3618,9 @@
} }
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.11", "version": "1.16.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@@ -5505,9 +5531,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "8.3.0", "version": "8.4.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@@ -5522,9 +5548,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "4.0.3", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -5612,10 +5638,13 @@
} }
}, },
"node_modules/proxy-from-env": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"license": "MIT" "license": "MIT",
"engines": {
"node": ">=10"
}
}, },
"node_modules/pure-rand": { "node_modules/pure-rand": {
"version": "7.0.1", "version": "7.0.1",
@@ -5635,9 +5664,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.15.0", "version": "6.15.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
"integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"side-channel": "^1.1.0" "side-channel": "^1.1.0"
@@ -6352,9 +6381,9 @@
} }
}, },
"node_modules/test-exclude/node_modules/brace-expansion": { "node_modules/test-exclude/node_modules/brace-expansion": {
"version": "1.1.12", "version": "1.1.15",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -6765,9 +6794,9 @@
} }
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.19.0", "version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
+23
View File
@@ -35,6 +35,29 @@ const MARKET_MAP = {
pitcher_earned_runs: 'earned_runs', pitcher_earned_runs: 'earned_runs',
pitcher_hits_allowed: 'hits_allowed', pitcher_hits_allowed: 'hits_allowed',
pitcher_outs: 'outs', pitcher_outs: 'outs',
// NFL props (Session 31 audit) — defensive mapping added BEFORE NFL is
// fully wired into the props flow, so it can't repeat the MLB silent-zero
// bug (MARKET_MAP dropping every market → props normalize to nothing).
// The Odds API + PropLine use the abbreviated `_yds` keys; the spec's
// `_yards` spellings are mapped too so either form survives. Internal
// stat_type names align with config/statFilters.js (passing_yards,
// rushing_yards, receiving_yards, interceptions).
player_pass_yds: 'passing_yards',
player_pass_yards: 'passing_yards',
player_pass_tds: 'pass_tds',
player_pass_completions: 'pass_completions',
player_pass_attempts: 'pass_attempts',
player_pass_interceptions: 'interceptions',
player_rush_yds: 'rushing_yards',
player_rush_yards: 'rushing_yards',
player_rush_attempts: 'rush_attempts',
player_rush_tds: 'rush_tds',
player_receptions: 'receptions',
player_reception_yds: 'receiving_yards',
player_receiving_yards: 'receiving_yards',
player_reception_tds: 'reception_tds',
player_anytime_td: 'anytime_td',
player_kicking_points: 'kicking_points',
// Soccer props — World Cup 2026 + permanent league support. // Soccer props — World Cup 2026 + permanent league support.
// odds-api keys verified against soccer_fifa_world_cup market list. // odds-api keys verified against soccer_fifa_world_cup market list.
// 'assists' is shared with NBA — sport context discriminates downstream. // 'assists' is shared with NBA — sport context discriminates downstream.
+31
View File
@@ -121,6 +121,37 @@ describe('oddsNormalizer', () => {
} }
}); });
it('exposes the NFL market keys added in the Session 31 audit', () => {
// Defensive mapping landed before NFL is fully wired so it can't
// repeat the MLB silent-zero bug. Both odds-api `_yds` and the
// `_yards` spellings must resolve, and internal names align with
// config/statFilters.js (passing/rushing/receiving_yards, interceptions).
expect(MARKET_MAP.player_pass_yds).toBe('passing_yards');
expect(MARKET_MAP.player_pass_yards).toBe('passing_yards');
expect(MARKET_MAP.player_rush_yds).toBe('rushing_yards');
expect(MARKET_MAP.player_reception_yds).toBe('receiving_yards');
expect(MARKET_MAP.player_receiving_yards).toBe('receiving_yards');
expect(MARKET_MAP.player_receptions).toBe('receptions');
expect(MARKET_MAP.player_pass_interceptions).toBe('interceptions');
expect(MARKET_MAP.player_anytime_td).toBe('anytime_td');
// End-to-end: an NFL market normalizes to a real prop, not zero.
const event = makeEvent({
bookmakers: [
makeBookmaker('draftkings', [
makeMarket('player_pass_yds', [
makeOutcome('Over', 'Patrick Mahomes', -110, 275.5),
makeOutcome('Under', 'Patrick Mahomes', -110, 275.5),
]),
]),
],
});
const result = normalizeProps([event]);
expect(result).toHaveLength(1);
expect(result[0].stat_type).toBe('passing_yards');
expect(result[0].player).toBe('Patrick Mahomes');
});
it('handles missing/null odds gracefully (skips incomplete outcomes)', () => { it('handles missing/null odds gracefully (skips incomplete outcomes)', () => {
const event = makeEvent({ const event = makeEvent({
bookmakers: [ bookmakers: [