From 2ba3958c7a8c5cdbfef1d642fddc42171ad65aeb Mon Sep 17 00:00:00 2001 From: Kev Date: Sun, 14 Jun 2026 23:48:40 -0400 Subject: [PATCH] =?UTF-8?q?Session=2031:=20Code=20audit=20+=20security=20r?= =?UTF-8?q?eview=20=E2=80=94=20NFL=20MARKET=5FMAP=20gap=20fixed,=20npm=20a?= =?UTF-8?q?udit=200=20vulns=20(1695=20tests)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- BUILD-STATE.md | 70 +++++++++++++++++++++++ package-lock.json | 95 ++++++++++++++++++++----------- src/utils/oddsNormalizer.js | 23 ++++++++ tests/unit/oddsNormalizer.test.js | 31 ++++++++++ 4 files changed, 186 insertions(+), 33 deletions(-) diff --git a/BUILD-STATE.md b/BUILD-STATE.md index 6978234..05729cd 100755 --- a/BUILD-STATE.md +++ b/BUILD-STATE.md @@ -4,6 +4,76 @@ 2026-06-14 ## 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) ## Session 30 (2026-06-14) — SHIPPED diff --git a/package-lock.json b/package-lock.json index 09d7968..86c3c7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2337,9 +2337,9 @@ } }, "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -2385,14 +2385,40 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz", + "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "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": { @@ -2551,9 +2577,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { @@ -3592,9 +3618,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -5505,9 +5531,9 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", @@ -5522,9 +5548,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -5612,10 +5638,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/pure-rand": { "version": "7.0.1", @@ -5635,9 +5664,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -6352,9 +6381,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -6765,9 +6794,9 @@ } }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/src/utils/oddsNormalizer.js b/src/utils/oddsNormalizer.js index bac634f..47c91f2 100644 --- a/src/utils/oddsNormalizer.js +++ b/src/utils/oddsNormalizer.js @@ -35,6 +35,29 @@ const MARKET_MAP = { pitcher_earned_runs: 'earned_runs', pitcher_hits_allowed: 'hits_allowed', 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. // odds-api keys verified against soccer_fifa_world_cup market list. // 'assists' is shared with NBA — sport context discriminates downstream. diff --git a/tests/unit/oddsNormalizer.test.js b/tests/unit/oddsNormalizer.test.js index 7b1e89d..fccc10b 100644 --- a/tests/unit/oddsNormalizer.test.js +++ b/tests/unit/oddsNormalizer.test.js @@ -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)', () => { const event = makeEvent({ bookmakers: [