export function assertBatchTimeBounds(batch, { now = new Date(), maxFutureSkewSeconds = 300 } = {}) { const acceptedAt = validDate(now, "intake_policy_clock_invalid"); const maxFutureAt = new Date(acceptedAt.getTime() + Number(maxFutureSkewSeconds) * 1000); if (!Number.isInteger(maxFutureSkewSeconds) || maxFutureSkewSeconds < 0) { throw intakePolicyError("intake_policy_future_skew_invalid"); } const receivedAt = validDate(batch?.batch?.receivedAt, "batch_received_at_invalid"); if (receivedAt > maxFutureAt) throw intakePolicyError("batch_received_at_too_far_in_future"); for (const fact of batch?.facts || []) { const observedAt = validDate(fact?.observedAt, "fact_observed_at_invalid"); if (observedAt > maxFutureAt) throw intakePolicyError("fact_observed_at_too_far_in_future"); } } /** * Retention is based on the server's acceptance time, never on provider or L2 * timestamps supplied in a batch. */ export function rawRetentionExpiry({ now = new Date(), rawRetentionDays }) { const acceptedAt = validDate(now, "intake_policy_clock_invalid"); if (!Number.isInteger(rawRetentionDays) || rawRetentionDays < 1) { throw intakePolicyError("raw_retention_days_invalid"); } return new Date(acceptedAt.getTime() + rawRetentionDays * 24 * 60 * 60 * 1000); } export function intakePolicyError(code) { return Object.assign(new Error(code), { status: 422, code }); } function validDate(value, code) { const parsed = value instanceof Date ? new Date(value.getTime()) : new Date(String(value ?? "")); if (Number.isNaN(parsed.getTime())) throw intakePolicyError(code); return parsed; }