import type { MimeClass } from "@tactical-media-broadcast-hub/core"; /** * One manifest entry = one `chat_media_archive` row for the room. Metadata * only; payloads stay in oCIS until the exporter pulls them over WebDAV. * Field names follow the index columns; nothing here can identify file bytes. */ export interface ManifestEntry { eventId: string; mxcUri: string; originalName: string; safeName: string; mime: string; mimeClass: MimeClass; byteSize: number; /** Lowercase hex SHA-256 of verified payload bytes; the restore script re-verifies it. */ sha256: string; sender: string; /** UTC ISO-8601, from the Matrix event timestamp. */ createdAt: string; ocisResourceId: string; /** False once redacted/retention marks the row unavailable. */ available: boolean; } /** Static bundle metadata; `files` carries the per-row entries. */ export interface BundleManifest { format: "chat-media-archive-bundle"; version: 1; roomId: string; workspaceId: string; groupId: string; /** Bundle build time (UTC ISO-8601). */ exportedAt: string; /** Object key the ciphertext was uploaded to, filled in after the PUT. */ objectKey: string | null; /** bech32 `age1...` recipients the file key was wrapped for. */ recipients: string[]; fileCount: number; /** Sum of ManifestEntry.byteSize. */ totalBytes: number; files: ManifestEntry[]; } /** Manifest + payload bytes in memory; the exporter packs and encrypts this. */ export interface BundlePayload { manifest: BundleManifest; /** Entry path inside the tar → payload bytes. Keyed by `files/` style paths. */ payloads: Map; } export type IndexedFile = ManifestEntry & { id: string }; /** Read side of `chat_media_archive` for exports; faked in tests. */ export interface ArchiveIndexPort { /** * Every archive row for the room, oldest first, including rows already * marked unavailable (the manifest records their state; unavailable rows * contribute no payload — see `buildBundle`). */ listRoom(workspaceId: string, roomId: string): Promise; } export const MANIFEST_PATH = "manifest.json"; export const PAYLOAD_DIR = "files"; /** Deterministic, collision-free tar path for one archive row's payload. */ export function payloadPath(entry: ManifestEntry): string { return `${PAYLOAD_DIR}/${entry.eventId}/${entry.safeName}`; } export function newBundleId(): string { // Timetap + randomness: readable object keys, no crypto requirement. return `${Date.now().toString(36)}${Math.floor(Math.random() * 0x1_000_000).toString(36).padStart(4, "0")}`; }