Files
vyndr/src/routes/props.js
T
builtbykev 72a14dc4cd Build /api/props/top-graded server selector: rank with p_win, serve without it
New READ endpoint. No grade, ledger row, lock_line, or scoring write. Push
scoring untouched.

REVIEW ZERO CORRECTED THE PREMISE: the handler NEVER EXISTED in any commit
(searched git rev-list --all for a /top-graded definition in src/ — zero hits).
Not "removed" — the three axios callers (cheatsheetGenerator, gradeOfTheDay,
widget) and the Next proxy were written against a phantom endpoint, so those
three content generators have silently received [] for their entire life.
Contract recovered from the four consumers, not guessed: {props:[...]},
?sport=UPPERCASE (absent = all sports, which gradeOfTheDay relies on) + ?limit,
rows carrying player/stat/line/direction/sport/grade/confidence? plus the
player_name/stat_type aliases and game_id.

POPULATED-PATH RISK FOUND: the board's populated branch had never run in prod,
and dashboard/page.tsx:463 calls g.stat.replace(/_/g,' ') UNGUARDED (g.player
also feeds the row key, /scan URL and heading; sport must be UPPERCASE for
SportPill). toRow requires non-empty string player+stat and a finite line,
uppercases sport, and DROPS unrenderable rows — a shorter board beats a broken
one.

THE LEAK BOUNDARY (why this is server-side): the browser cannot rank on p_win
for all tiers because stripModelPrice deliberately withholds it from unentitled
tiers. Order of operations is
  read cache -> RANK with p_win (every tier) -> map rows incl. model fields
    -> stripModelPrice(rows, tier) -> serialize
so a free caller receives the paid RANKING without the paid VALUES. Tier comes
from resolveTierFromRequest, which FAILS CLOSED to 'free'. Cache-Control is
private under a bearer token, public otherwise (the /api/snapshot precedent).

ONE SHARED DEFINITION, no drift: new src/utils/gradeRanking.js
(takeablePWin/descNullsLast/rankGrades). heroPropService now imports
takeablePWin instead of its inline copy (behaviour unchanged — it was that
logic verbatim); the selector imports rankGrades; web/src/lib/slateAdapter
keeps its mirror (the browser cannot import src/, S25) and a test cross-checks
the two on identical fixtures (playerName.js precedent). Board is grade-first
("top GRADES"), hero is p_win-first ("top read") — they differ BY DESIGN and
agree within the leading tier.

HONEST LIMIT: the Next proxy (cachedBackendJson) sends no Authorization header
and caches under a shared key, so via the dashboard every viewer gets the
free-tier payload — correct order, no paid values. That is the SAFE behaviour;
forwarding auth into a shared cache is exactly how a paid payload leaks to
anonymous viewers. Per-tier delivery through the proxy needs a tier-keyed cache
and is not done here.

Verified on real prod snapshot data (anonymous path): MLB 8 props, WNBA 10,
0 paid-field leaks, render-contract safe on every row, sport uppercase.

Floor: 311 suites / 3882 tests green (18 new — leak test uses POPULATED p_win,
not today's nulls: entitled gets p_win and it drove the order, unentitled gets
a byte-identical order with all five MODEL_FIELDS absent and no trace in
JSON.stringify, while book/fair market facts survive). Web build exit 0.
Dashboard visual is auth-gated -> tagged for the Chrome audit, not faked.

Held: edge_pct rescale/retirement (Order B); board columns/contract unchanged;
tier-keyed proxy caching.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
2026-07-29 22:05:30 -04:00

124 lines
4.3 KiB
JavaScript

const express = require('express');
const { requireAuth } = require('../middleware/auth');
const { checkGracePeriod } = require('../middleware/gracePeriod');
const { getSupabaseServiceClient } = require('../utils/supabase');
const { createRateLimit } = require('../middleware/rateLimit');
const { resolveTierFromRequest } = require('../utils/requestTier');
const { getTopGraded } = require('../services/topGradedService');
const router = express.Router();
/**
* GET /top-graded — the dashboard TOP GRADES board (specs/top-graded-selector.md).
*
* PUBLIC + tier-aware, NOT `requireAuth`: this board must keep serving anonymous
* callers (it is the free funnel), so entitlement is resolved best-effort and
* FAILS CLOSED to 'free'. Ranking happens server-side WITH `p_win` for every
* tier; `stripModelPrice` then removes the paid signal for unentitled callers —
* so a free caller gets the CORRECT ORDER without the paid VALUES.
*
* A thin slate is `200 { props: [] }`, never a 404 and never filler: the board
* renders its own honest empty state, and a 404 is what left it blank for months.
*
* Cache-Control follows the /api/snapshot precedent — `private` once a bearer
* token is present, so a CDN can never hand a paid payload to an anonymous viewer.
*/
router.get('/top-graded', createRateLimit({ windowMs: 60_000, max: 60 }), async (req, res) => {
const deps = router.__deps || {};
const sportParam = req.query.sport ? String(req.query.sport) : null;
try {
const tier = await (deps.resolveTier || resolveTierFromRequest)(req);
const out = await (deps.getTopGraded || getTopGraded)({
sport: sportParam,
limit: req.query.limit,
tier,
...(deps.cacheGet ? { cacheGet: deps.cacheGet } : {}),
});
res.set('Cache-Control', req.headers.authorization ? 'private, max-age=60' : 'public, max-age=60');
return res.json(out);
} catch (err) {
console.error('[props/top-graded]', err.message);
// Honest empty — the board's empty state, never a 404.
return res.status(200).json({ props: [], sport: sportParam ? sportParam.toUpperCase() : null, updated_at: null });
}
});
// Test seam — lets route tests inject without Redis/Supabase (slips.js precedent).
router.__internals = { setDeps: (d) => { router.__deps = d; } };
// GET /joint-history — joint outcome history and phi coefficient
router.get('/joint-history', requireAuth, checkGracePeriod, async (req, res) => {
const { player_a, stat_a, player_b, stat_b } = req.query;
// Block free tier
if (!req.user.tier || req.user.tier === 'free') {
return res.status(403).json({
error: 'Joint history requires Analyst or Desk tier',
upgrade_url: '/pricing',
});
}
if (!player_a || !stat_a || !player_b || !stat_b) {
return res.status(400).json({
error: 'Required query params: player_a, stat_a, player_b, stat_b',
});
}
try {
const supabase = getSupabaseServiceClient();
const { data, error } = await supabase
.from('joint_outcomes')
.select('*')
.eq('player_a', player_a)
.eq('stat_a', stat_a)
.eq('player_b', player_b)
.eq('stat_b', stat_b);
if (error) throw error;
if (!data || data.length === 0) {
return res.json({
player_a,
stat_a,
player_b,
stat_b,
sample_size: 0,
phi_coefficient: null,
outcomes: [],
});
}
// Calculate phi coefficient from joint outcomes
let both_hit = 0, a_only = 0, b_only = 0, neither = 0;
for (const row of data) {
if (row.a_hit && row.b_hit) both_hit++;
else if (row.a_hit && !row.b_hit) a_only++;
else if (!row.a_hit && row.b_hit) b_only++;
else neither++;
}
const n = data.length;
const num = (both_hit * neither) - (a_only * b_only);
const denom = Math.sqrt(
(both_hit + a_only) * (b_only + neither) *
(both_hit + b_only) * (a_only + neither)
);
const phi = denom === 0 ? 0 : num / denom;
res.json({
player_a,
stat_a,
player_b,
stat_b,
sample_size: n,
phi_coefficient: Math.round(phi * 1000) / 1000,
outcomes: data,
});
} catch (err) {
console.error('[props/joint-history]', err.message);
res.status(503).json({ error: 'Service temporarily unavailable' });
}
});
module.exports = router;