← back to Crankd
lib/compliance.js
61 lines
'use strict';
const env = (k, d) => (process.env[k] ?? d);
const boolEnv = (k, d = false) => {
const v = env(k);
if (v == null) return d;
return /^(1|true|yes|on)$/i.test(String(v));
};
// FCC Feb 2024 ruling: AI-generated voice calls under TCPA require prior express
// consent and the recipient must be told they're hearing an AI voice.
function aiDisclosurePreamble(owner = env('OWNER_LEGAL_NAME', 'the caller')) {
return (
`Heads up — this is an A I generated voice call from ${owner}. ` +
`You opted in to receive this. Press one to continue, two to stop these forever, ` +
`or hang up at any time.`
);
}
function inQuietHours(nowDate = new Date(), tz = 'America/Los_Angeles') {
const start = parseInt(env('QUIET_HOURS_START', '08'), 10);
const end = parseInt(env('QUIET_HOURS_END', '21'), 10);
const hour = parseInt(
new Intl.DateTimeFormat('en-US', { hour: 'numeric', hour12: false, timeZone: tz })
.format(nowDate),
10
);
return hour < start || hour >= end;
}
// Pre-flight: returns { ok, reasons[] }. Every reason is a hard block — no soft pass.
function preflight({ recipient, opt_in_record, has_recording_consent, dnc_clean }) {
const reasons = [];
if (boolEnv('COMPLIANCE_STEVE_ONLY_MODE', true)) {
const allowed = (env('STEVE_ALLOWED_NUMBERS', '') || '').split(',').map(s => s.trim()).filter(Boolean);
if (!allowed.includes(recipient)) reasons.push('steve_only_mode_recipient_not_allowlisted');
}
if (boolEnv('COMPLIANCE_REQUIRE_OPT_IN', true)) {
if (!opt_in_record || !opt_in_record.confirmed_at) reasons.push('no_opt_in_record');
}
if (boolEnv('COMPLIANCE_DNC_SCRUB_REQUIRED', true)) {
if (!dnc_clean) reasons.push('dnc_scrub_not_run_or_failed');
}
if (boolEnv('COMPLIANCE_TIME_OF_DAY_GATE', true)) {
if (inQuietHours()) reasons.push('inside_quiet_hours');
}
if (boolEnv('COMPLIANCE_BOTH_PARTY_CONSENT_FOR_RECORDING', true)) {
// Recording is OPTIONAL per call; only enforce if a recording was requested.
if (has_recording_consent === false) reasons.push('recording_requested_without_both_party_consent');
}
return { ok: reasons.length === 0, reasons };
}
module.exports = { aiDisclosurePreamble, inQuietHours, preflight, boolEnv };