fbcb00b7b1
PHASE 0.5 GATE — the three checks, and one correction.
`fairLine` does not exist. Zero hits across src/ and web/src. Option A as
written had no referent, but it resolves better than feared: `fair_odds` is
already a real de-vigged American price on every graded snapshot row, so there
is nothing to derive.
Gate 1 (is it a price): PASS. fair_odds is American odds from
impliedProbToAmerican inside devigTwoWay; fair_prob is the probability. Both
distinct from `line`, the stat threshold.
Gate 2 (numeric match): PASS, 8/8 exact. Recomputed fair_odds and fair_prob
independently from the stored raw over/under prices; every value matched the
stored one to the integer and to 3dp. Same de-vig, same numbers the component
was proven against.
Gate 3 (poison independence): PASS, and proven on the quarantined cohort
itself. devigTwoWay's inputs are (over_odds, under_odds) — market prices
only, no model term is reachable. The 8 rows recomputed above are all
wrong_opponent_grade rows, and their fair prices reproduce exactly from the
market. The poison is in the grade, not the price. Quarantine therefore
suppresses the MODEL leg only; the fair leg stands, as designed.
THE LEAK WAS REAL AND ALREADY LIVE. GET /api/snapshot/:sport is public and
unauthenticated, and it was serving model_odds, p_win, ev_pct, value and
takeable to anonymous callers on every graded row — 25 of 25 on the live wnba
board. The Session-66 gate on /api/analyze was bypassed entirely by this
endpoint.
The strip covers more than model_odds, because model_odds is not the only way
to read the model price: p_win IS the price in another base, and ev_pct is
INVERTIBLE — ev is a function of p_win and book_odds, and book_odds is public,
so leaving ev behind hands the price over. All five model-derived fields go.
book_odds, fair_odds, fair_prob, overround and devig_method stay on every tier:
the fair leg is never the paywall. Rows that keep a book+fair pair are stamped
model_price_locked so a gated price is never mistaken for a missing one.
Tier comes from resolveTierFromRequest, which reads a bearer token when one is
present and otherwise returns 'free'. It FAILS CLOSED on every error path, so a
resolution failure can only ever withhold the price. The response now varies by
entitlement, so the /:sport handler downgrades Cache-Control to private for
authenticated callers and the browser proxy forwards the bearer token —
otherwise a CDN could hand a paid payload to an anonymous viewer, or every
request would look anonymous and paid users would lose the leg.
READ CARD — a manual scan carries no market. The request is {player, stat,
line, direction}, so the engine has no over/under prices to de-vig and
book_odds/fair_odds are legitimately absent from its response; that is why the
triplet was hidden there. lookupSnapshotPrices recovers them from the
pre-graded snapshot via the same cache-only read this route already performs
for locked odds and team. The join is exact on player + stat + line + side
(fair_odds is side-specific), and returns nothing unless book and fair are BOTH
present — a user-chosen line the board never graded has no market attached, so
the triplet stays hidden rather than borrowing another line's price.
FAIR-LEG ABSENCE, measured before shipping: 636 graded rows, 636 with book,
636 with fair, 0 one-sided. Absence rate 0.0%. The hero number is not a
sometimes-number on current data.
Tests 3556 passed / 291 suites, web build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
149 lines
6.3 KiB
JavaScript
149 lines
6.3 KiB
JavaScript
/* ============================================================
|
|
Session 67 — the model price must never leave the server for an unentitled
|
|
viewer, and the FAIR leg must never be gated.
|
|
|
|
`GET /api/snapshot/:sport` is PUBLIC. Session 66 gated model_odds on
|
|
/api/analyze; this endpoint bypassed that gate and served the model price
|
|
(and p_win, and an invertible ev_pct) to anonymous callers on every graded
|
|
row. These tests lock the fix shut.
|
|
============================================================ */
|
|
|
|
const { stripModelPrice, MODEL_FIELDS, MARKET_FIELDS, entitledToModelPrice } = require('../../src/utils/snapshotGating');
|
|
const { resolveTierFromRequest } = require('../../src/utils/requestTier');
|
|
|
|
// A real shape, from a live /api/snapshot/mlb row.
|
|
const ROW = Object.freeze({
|
|
player_name: 'Josh Bell', stat_type: 'hits', line: 0.5, direction: 'over', grade: 'B',
|
|
book_odds: -210, fair_odds: -173, fair_prob: 0.633, overround: 0.068, devig_method: 'multiplicative',
|
|
model_odds: -311, p_win: 0.757, ev_pct: 11.7, value: false, takeable: false,
|
|
projection: 0.8, archetype: 'DRIVER',
|
|
});
|
|
|
|
describe('stripModelPrice — free/anonymous never receive the model price', () => {
|
|
it('removes EVERY model-derived field for free', () => {
|
|
const [out] = stripModelPrice([ROW], 'free');
|
|
for (const f of MODEL_FIELDS) expect(out[f]).toBeUndefined();
|
|
});
|
|
|
|
it('removes them for anonymous / unknown / garbage tiers (fails closed)', () => {
|
|
for (const tier of [undefined, null, '', 'anonymous', 'nonsense', 'FREE ']) {
|
|
const [out] = stripModelPrice([ROW], tier);
|
|
expect(out.model_odds).toBeUndefined();
|
|
expect(out.p_win).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('strips ev_pct too — it is INVERTIBLE back to p_win', () => {
|
|
// ev is a function of (p_win, book_odds); book_odds is public, so leaving
|
|
// ev behind hands over the model price in a different base.
|
|
const [out] = stripModelPrice([ROW], 'free');
|
|
expect(out.ev_pct).toBeUndefined();
|
|
expect(MODEL_FIELDS).toContain('ev_pct');
|
|
});
|
|
|
|
it('KEEPS every market field — the fair leg is never the paywall', () => {
|
|
const [out] = stripModelPrice([ROW], 'free');
|
|
for (const f of MARKET_FIELDS) expect(out[f]).toBe(ROW[f]);
|
|
expect(out.fair_odds).toBe(-173);
|
|
});
|
|
|
|
it('flags the lock so a gated price is not mistaken for a missing one', () => {
|
|
const [out] = stripModelPrice([ROW], 'free');
|
|
expect(out.model_price_locked).toBe(true);
|
|
});
|
|
|
|
it('does NOT flag a lock on a row with no price story to lock onto', () => {
|
|
const [out] = stripModelPrice([{ ...ROW, book_odds: null, fair_odds: null }], 'free');
|
|
expect(out.model_price_locked).toBeUndefined();
|
|
});
|
|
|
|
it('analyst and desk receive the full triplet, untouched', () => {
|
|
for (const tier of ['analyst', 'desk']) {
|
|
const [out] = stripModelPrice([ROW], tier);
|
|
expect(out.model_odds).toBe(-311);
|
|
expect(out.p_win).toBe(0.757);
|
|
expect(out.ev_pct).toBe(11.7);
|
|
expect(out.model_price_locked).toBeUndefined();
|
|
}
|
|
expect(entitledToModelPrice('analyst')).toBe(true);
|
|
expect(entitledToModelPrice('free')).toBe(false);
|
|
});
|
|
|
|
it('never mutates the input rows', () => {
|
|
const input = [{ ...ROW }];
|
|
stripModelPrice(input, 'free');
|
|
expect(input[0].model_odds).toBe(-311);
|
|
});
|
|
|
|
it('survives junk rows without throwing', () => {
|
|
expect(() => stripModelPrice([null, undefined, 'x', 7], 'free')).not.toThrow();
|
|
expect(stripModelPrice(null, 'free')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('resolveTierFromRequest — a public endpoint that still gates', () => {
|
|
const noSupabase = { getSupabaseServiceClient: () => null };
|
|
|
|
it('anonymous (no header) is free', async () => {
|
|
expect(await resolveTierFromRequest({ headers: {} }, noSupabase)).toBe('free');
|
|
});
|
|
|
|
it('a malformed or empty bearer is free', async () => {
|
|
expect(await resolveTierFromRequest({ headers: { authorization: 'Basic x' } }, noSupabase)).toBe('free');
|
|
expect(await resolveTierFromRequest({ headers: { authorization: 'Bearer ' } }, noSupabase)).toBe('free');
|
|
});
|
|
|
|
it('FAILS CLOSED — a thrown resolver yields free, never an entitled tier', async () => {
|
|
const boom = { getSupabaseServiceClient: () => { throw new Error('supabase down'); } };
|
|
expect(await resolveTierFromRequest({ headers: { authorization: 'Bearer abc' } }, boom)).toBe('free');
|
|
});
|
|
|
|
it('an invalid token is free', async () => {
|
|
const bad = {
|
|
getSupabaseServiceClient: () => ({
|
|
auth: { getUser: async () => ({ data: null, error: new Error('bad token') }) },
|
|
}),
|
|
};
|
|
expect(await resolveTierFromRequest({ headers: { authorization: 'Bearer abc' } }, bad)).toBe('free');
|
|
});
|
|
|
|
it('a valid token resolves the real tier', async () => {
|
|
const good = {
|
|
getSupabaseServiceClient: () => ({
|
|
auth: { getUser: async () => ({ data: { user: { id: 'u1' } }, error: null }) },
|
|
from: () => ({ select: () => ({ eq: () => ({ single: async () => ({ data: { tier: 'desk' } }) }) }) }),
|
|
}),
|
|
};
|
|
expect(await resolveTierFromRequest({ headers: { authorization: 'Bearer good' } }, good)).toBe('desk');
|
|
});
|
|
});
|
|
|
|
describe('the route wiring', () => {
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const src = fs.readFileSync(path.join(__dirname, '..', '..', 'src', 'routes', 'snapshot.js'), 'utf8');
|
|
|
|
it('gates BOTH response paths (snapshot and the grades fallback)', () => {
|
|
const gated = src.match(/grades: gate\(/g) || [];
|
|
expect(gated.length).toBe(2);
|
|
});
|
|
|
|
it('never shared-caches a response that varies by entitlement', () => {
|
|
// A CDN handing a paid payload to an anonymous viewer would defeat the gate.
|
|
expect(src).toMatch(/req\.headers\.authorization \? 'private, max-age=30' : 'public, max-age=30'/);
|
|
// Scoped to the /:sport handler — /summary carries counts only (no price
|
|
// data), so it stays legitimately public-cacheable.
|
|
const bySport = src.slice(src.indexOf("router.get('/:sport'"));
|
|
expect(bySport).not.toMatch(/res\.set\('Cache-Control', 'public, max-age=30'\)/);
|
|
});
|
|
|
|
it('the browser proxy forwards the bearer token', () => {
|
|
const proxy = fs.readFileSync(
|
|
path.join(__dirname, '..', '..', 'web', 'src', 'app', 'api', 'snapshot', '[sport]', 'route.ts'),
|
|
'utf8',
|
|
);
|
|
expect(proxy).toMatch(/Authorization: auth/);
|
|
expect(proxy).toMatch(/private, max-age=30/);
|
|
});
|
|
});
|