import fs from 'node:fs'; import path from 'node:path'; import { smsSitesConfigSchema, type SmsSitesConfig } from '@dynamic-sites/shared'; import { logger } from '../logger.js'; const REPO_ROOT = process.env.REPO_ROOT || '.'; const CONFIG_PATH = path.join(REPO_ROOT, 'config/sms-sites.json'); let cachedConfig: SmsSitesConfig | null = null; let configLoadedAt = 0; const CONFIG_TTL_MS = 60_000; // Reload every minute function loadConfig(): SmsSitesConfig | null { const now = Date.now(); if (cachedConfig && now - configLoadedAt < CONFIG_TTL_MS) return cachedConfig; try { const raw = fs.readFileSync(CONFIG_PATH, 'utf-8'); const parsed = JSON.parse(raw); const result = smsSitesConfigSchema.safeParse(parsed); if (!result.success) { logger.warn({ event: 'sms.config_invalid', errors: result.error.message }, 'Invalid SMS sites config'); return cachedConfig; } cachedConfig = result.data; configLoadedAt = now; return cachedConfig; } catch (err) { logger.warn({ event: 'sms.config_missing', error: (err as Error).message }, 'Could not load SMS sites config'); return cachedConfig; } } /** * Check if a phone number is one of our own system numbers (the Telnyx numbers we send from). * Used to filter out Telnyx delivery receipts / echo of our own sent messages. */ export function isOwnNumber(phone: string): boolean { const config = loadConfig(); if (!config) return false; return config.sites.some(site => site.phoneNumber === phone); } /** * Check if a sender is authorized to make edits for a given site. * Returns the matching site config if authorized, or null. */ export function findAuthorizedSite(from: string, to: string): { siteId: string; repoRoot: string } | null { const config = loadConfig(); if (!config) return null; const site = config.sites.find(site => site.phoneNumber === to && site.allowedSenders.includes(from) ); if (!site) return null; return { siteId: site.siteId, repoRoot: site.repoRoot || '.' }; }