Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | 43x 43x 576x 576x 576x 576x 3x 576x 579x 562x 562x 561x 579x 579x 15x 15x 456x 1x 455x 455x 455x 43x | 'use strict';
/**
* SHY-0060 — server-side feature-access evaluation: turn an already-loaded
* user doc into a per-feature age-gating verdict.
*
* This is the glue between the stored user record and the pure gate engine
* (safety-gate.js): it extracts the two engine inputs — a VERIFIED age (or
* null) and an ISO alpha-2 region (or null) — from the user doc, then calls
* canAccess. It is pure (data in, verdict out): the caller loads the user doc
* once (endpoints already do) and passes the data in, so there is no extra
* Firestore read on the hot path.
*/
const { canAccess } = require('./safety-gate');
const { FEATURES } = require('./age-thresholds');
/**
* Calendar-aware whole-years age from a date-of-birth (ms epoch), computed
* in UTC. Mirrors the existing `isAtLeast18FromDob` age math exactly (so
* `isAtLeast18FromDob(dob) === ageFromDob(dob) >= 18`) and is the server
* analog of Kotlin DateUtils.calculateAge. The exact-calendar comparison
* avoids the ~6h/leap-window drift a `365.25 * MS_PER_DAY` approximation
* would put on the birthday boundary.
*
* @param {number} dateOfBirthMs ms epoch of the DOB
* @param {number} [nowMs=Date.now()] ms epoch of "now"
* @returns {number} whole years old
*/
function ageFromDob(dateOfBirthMs, nowMs = Date.now()) {
const today = new Date(nowMs);
const dob = new Date(dateOfBirthMs);
let age = today.getUTCFullYear() - dob.getUTCFullYear();
if (
today.getUTCMonth() < dob.getUTCMonth() ||
(today.getUTCMonth() === dob.getUTCMonth() && today.getUTCDate() < dob.getUTCDate())
) {
age -= 1;
}
return age;
}
/**
* The engine's "verified age" input: the user's age in whole years IFF the
* account is ID-verified (`ageVerified === true`) AND a usable DOB is on
* record; otherwise null (→ the engine's Reverify path). Matches the app's
* existing model where `ageVerified` gates the 18+ surfaces (PMs / gacha);
* an unverified — or verified-but-DOB-less (anomalous) — record is treated
* as unverified, the safe direction.
*/
function extractVerifiedAge(userData, nowMs) {
if (userData?.ageVerified !== true) return null;
const dob = userData.dateOfBirth;
if (typeof dob !== 'number' || !Number.isFinite(dob)) return null;
return ageFromDob(dob, nowMs);
}
/**
* The engine's region input: the user's `nationality` normalised to an
* uppercase ISO alpha-2 code, or null when it is absent/blank. Null means
* "region undetected" → the engine applies the conservative (strictest)
* threshold, so an undetected region never yields a more permissive gate
* than some stricter region would. Uppercasing guards the case-sensitive
* override lookup (stored `nationality` case is not guaranteed).
*/
function extractRegion(userData) {
const nat = userData?.nationality;
if (typeof nat !== 'string') return null;
const trimmed = nat.trim();
return trimmed.length > 0 ? trimmed.toUpperCase() : null;
}
/**
* Per-feature age-gating verdict for an already-loaded user doc.
*
* @param {Object} userData the `users/<id>` doc data
* @param {string} feature a Feature key (age-thresholds FEATURES)
* @param {number} [nowMs=Date.now()] clock for the age computation
* @returns {Object} a GateResult (see safety-gate.js)
* @throws if `feature` is not a known gated feature (fail-fast — a typo'd
* call-site constant must never silently mis-gate)
*/
function evaluateFeatureAccess(userData, feature, nowMs = Date.now()) {
if (!Object.hasOwn(FEATURES, feature)) {
throw new Error(`evaluateFeatureAccess: unknown feature "${feature}"`);
}
const verifiedAgeYears = extractVerifiedAge(userData, nowMs);
const region = extractRegion(userData);
return canAccess(feature, verifiedAgeYears, region);
}
module.exports = { ageFromDob, evaluateFeatureAccess, extractVerifiedAge, extractRegion };
|