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 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | 44x 44x 44x 44x 44x 44x 98x 718x 625x 93x 93x 93x 86x 7x 7x 93x 74x 74x 27x 44x | 'use strict';
/**
* SHY-0060 — the default-OFF operator feature flag that gates ALL
* per-feature age-gating enforcement.
*
* The engine (age-thresholds.js + safety-gate.js) computes verdicts
* unconditionally, but no call site ACTS on a verdict unless this flag is
* switched ON. The flag ships OFF: the engine lands inert in production and
* is activated later, once the final threshold values + legal/T&S sign-offs
* are in. Turning it on is an operator toggle, not a redeploy.
*
* Storage mirrors the repo's remote-config idiom (autoModEnabled in
* `config/moderation`, maintenanceMode in `config/app`): a boolean field
* `ageGatingEnabled` on the Firestore `config/safety` doc, operator-writable
* via the existing admin `PUT /config/safety` endpoint.
*
* Read cost: enforcement checks this flag on every gated action (DM, gift,
* gacha, voice-join). A 60s process-cache (the alertManager idiom) collapses
* that to ~1 Firestore read/min regardless of traffic — required by the $0
* hosting constraint — at the cost of a bounded ≤60s lag on an operator flip.
*/
const log = require('../utils/log');
/** Mirrors alertManager's CONFIG_CACHE_TTL — 60s remote-config freshness. */
const CONFIG_CACHE_TTL_MS = 60 * 1000;
const DEFAULT_SAFETY_CONFIG_DOC = 'config/safety';
// The doc the flag is read from. A `let` (not const) purely so real-emulator
// tests can redirect it to an isolated per-file doc — Jest runs test FILES in
// parallel workers that share ONE emulator, so multiple files toggling the
// single production `config/safety` doc would race. Production never touches
// the setter; it always reads config/safety.
let safetyConfigDoc = DEFAULT_SAFETY_CONFIG_DOC;
// Process-scoped cache. `null` = never successfully loaded (distinct from a
// loaded `false`), so a cold process reads through instead of serving a
// phantom OFF. Jest workers are separate processes, so this never bleeds
// across workers; within a file, __resetAgeGatingFlagCache() isolates tests.
let cachedEnabled = null;
let cacheLoadedAt = 0;
/**
* Pure decision: does the raw `config/safety` doc data (or null when the
* section is absent) mean age-gating enforcement is switched ON?
*
* STRICT by design — only a real boolean `true` enables gating. A mistyped
* operator value ("true", 1, "on") reads as OFF, the safe default, so a
* fat-fingered config value can never silently flip a SAFETY gate.
*
* @param {Object|null|undefined} configData the `config/safety` doc data
* @returns {boolean}
*/
function resolveAgeGatingEnabled(configData) {
return configData?.ageGatingEnabled === true;
}
/**
* Is age-gating enforcement switched ON right now? Reads `config/safety`
* through a 60s process-cache and applies the strict resolver above.
*
* @param {FirebaseFirestore.Firestore} db Admin-SDK Firestore handle
* @param {number} [nowMs=Date.now()] injectable clock (for deterministic
* TTL tests — a plain value, not a mocked timer)
* @returns {Promise<boolean>}
*/
async function isAgeGatingEnabled(db, nowMs = Date.now()) {
if (cachedEnabled !== null && nowMs - cacheLoadedAt < CONFIG_CACHE_TTL_MS) {
return cachedEnabled;
}
try {
const snap = await db.doc(safetyConfigDoc).get();
cachedEnabled = resolveAgeGatingEnabled(snap.exists ? snap.data() : null);
cacheLoadedAt = nowMs;
} catch (err) {
// A config-read failure must NEVER 500 a gated request — fail SAFE to
// OFF (no enforcement). We keep any prior cached value and deliberately
// do NOT bump cacheLoadedAt, so the next call retries the read rather
// than pinning the error-default for a full TTL window.
//
// Not integration-tested against the real emulator: a genuine Firestore
// read failure (network-unreachable / deadline-exceeded) can't be
// induced deterministically mid-suite without bouncing the shared stack.
// The safe-default VALUE is exhaustively pinned by the resolver's
// null-data unit tests; this branch only routes a thrown read to it.
log.error('age-gating-flag', 'config/safety read failed; defaulting OFF', {
error: err?.message,
});
if (cachedEnabled === null) cachedEnabled = false;
}
return cachedEnabled;
}
/** Test-only: clear the process cache so a test observes a cold read. */
function __resetAgeGatingFlagCache() {
cachedEnabled = null;
cacheLoadedAt = 0;
}
/**
* Test-only: redirect the flag read to an isolated doc so parallel Jest
* workers don't race on the single production `config/safety`. Call with no
* argument to restore the production default (do this in afterAll).
*/
function __setSafetyConfigDocForTests(docPath = DEFAULT_SAFETY_CONFIG_DOC) {
safetyConfigDoc = docPath;
}
module.exports = {
resolveAgeGatingEnabled,
isAgeGatingEnabled,
__resetAgeGatingFlagCache,
__setSafetyConfigDocForTests,
};
|