/** * earth-now.co * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * File: apps/ingest/src/archive.ts * Purpose: Raw payload archive — every ingested byte is kept (with sha256) for future re-fitting; S3-shaped interface, local FS for now */ import { createHash } from "node:crypto"; import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; export interface ArchiveOptions { /** Archive root. Defaults to $RAW_ARCHIVE_DIR, then "data/raw". */ dir?: string; /** Timestamp override for deterministic tests. */ now?: Date; } export interface ArchiveResult { rawPath: string; checksumPath: string; sha256: string; } /** * Write `${dir}/${sourceId}/${timestamp}.raw` plus a sibling `.sha256` checksum file. * We keep ALL raw data for re-fitting; when this moves to S3 only this module changes. */ export async function archiveRaw( sourceId: string, payload: string | Uint8Array, options: ArchiveOptions = {}, ): Promise { if (!/^[a-z0-9_-]+$/i.test(sourceId)) throw new Error(`archiveRaw: invalid sourceId '${sourceId}' (path-safe [a-z0-9_-] only)`); const dir = options.dir ?? process.env.RAW_ARCHIVE_DIR ?? "data/raw"; const now = options.now ?? new Date(); // ISO timestamp made filename-safe: 2026-08-09T12:00:00.000Z → 2026-08-09T12-00-00-000Z const stamp = now.toISOString().replace(/[:.]/g, "-"); const targetDir = join(dir, sourceId); await mkdir(targetDir, { recursive: true }); const data = typeof payload === "string" ? Buffer.from(payload, "utf8") : Buffer.from(payload); const sha256 = createHash("sha256").update(data).digest("hex"); const fileName = `${stamp}.raw`; const rawPath = join(targetDir, fileName); const checksumPath = `${rawPath}.sha256`; await writeFile(rawPath, data); await writeFile(checksumPath, `${sha256} ${fileName}\n`, "utf8"); return { rawPath, checksumPath, sha256 }; }