T-HUB/apps/archive-bot/src/archive-index.ts

75 lines
2.6 KiB
TypeScript

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/<eventId>` style paths. */
payloads: Map<string, Uint8Array>;
}
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<IndexedFile[]>;
}
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")}`;
}