Session 7h: Stripe products, tier config, scan limits, response gating, free tier
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Tier access matrix — single source of truth for what each tier unlocks.
|
||||
*
|
||||
* The tier set matches the DB CHECK constraint in migrations 001 + 011:
|
||||
* tier IN ('free', 'analyst', 'desk')
|
||||
*
|
||||
* Free is the top of the funnel. Users see the grade (the hook) but the
|
||||
* reasoning + kill-condition details stay locked. Analyst opens the
|
||||
* intelligence. Desk adds engine2 (LLM) and portfolio tracking.
|
||||
*
|
||||
* `api_access` is FALSE on every tier and must stay false. VYNDR is a
|
||||
* consumer product; the proprietary engine is never exposed externally.
|
||||
*
|
||||
* `scans_per_day` is the per-tier daily limit. The scan-limit middleware
|
||||
* reads this and 429s on overflow. Anonymous callers fall back to the
|
||||
* 'free' bucket, IP-keyed.
|
||||
*/
|
||||
|
||||
const TIERS = Object.freeze({
|
||||
free: Object.freeze({
|
||||
scans_per_day: 3,
|
||||
grade_visible: true,
|
||||
reasoning_visible: false, // blurred — frontend renders tier-locked
|
||||
kill_conditions_detail: false, // count only, not codes/reasons
|
||||
kill_conditions_count: true,
|
||||
alerts: false,
|
||||
portfolio: false,
|
||||
engine2: false,
|
||||
stat_dashboard: true,
|
||||
api_access: false,
|
||||
}),
|
||||
analyst: Object.freeze({
|
||||
scans_per_day: 15,
|
||||
grade_visible: true,
|
||||
reasoning_visible: true,
|
||||
kill_conditions_detail: true,
|
||||
kill_conditions_count: true,
|
||||
alerts: true,
|
||||
portfolio: false,
|
||||
engine2: false,
|
||||
stat_dashboard: true,
|
||||
api_access: false,
|
||||
}),
|
||||
desk: Object.freeze({
|
||||
scans_per_day: Infinity,
|
||||
grade_visible: true,
|
||||
reasoning_visible: true,
|
||||
kill_conditions_detail: true,
|
||||
kill_conditions_count: true,
|
||||
alerts: true,
|
||||
portfolio: true,
|
||||
engine2: true, // LLM deep analysis
|
||||
stat_dashboard: true,
|
||||
api_access: false,
|
||||
}),
|
||||
});
|
||||
|
||||
const VALID_TIERS = Object.freeze(Object.keys(TIERS));
|
||||
|
||||
function getTier(tierName) {
|
||||
const key = String(tierName || 'free').toLowerCase();
|
||||
return TIERS[key] || TIERS.free;
|
||||
}
|
||||
|
||||
function getScanLimit(tierName) {
|
||||
return getTier(tierName).scans_per_day;
|
||||
}
|
||||
|
||||
function canAccess(tierName, feature) {
|
||||
const t = getTier(tierName);
|
||||
if (!(feature in t)) return false;
|
||||
return !!t[feature];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TIERS,
|
||||
VALID_TIERS,
|
||||
getTier,
|
||||
getScanLimit,
|
||||
canAccess,
|
||||
};
|
||||
Reference in New Issue
Block a user