Integrate group files: nullable room for uploads, archive class, claim idempotency, upload route

Wires the three parallel group-cloud tracks to one surface and fixes the
seams between them:

- chat_media_archive.room_id becomes nullable (direct web uploads have no
  Matrix room yet — the contract's null-event pattern extends to the room);
  core GroupFile.roomId and ArchiveEntryInput.roomId follow.
- mime_class gains 'archive' (zip) across the 029 CHECK, validation
  MIME_CLASSES and the bot classifier — one vocabulary everywhere.
- registerArchiveEntry claim keys are null-safe for room-less uploads.
- New: POST /files/groups/:groupId/upload — FileInterceptor (20 MiB) streams
  through magic-byte sniffing, library-safe naming, oCIS upload and the
  metadata index, behind group-membership ACL (Owner/Admin bypass).
- safeFileName/sniffMime promoted to packages/core (file-names.ts,
  file-mime.ts) so the bot and the api share one implementation; the bot's
  local modules become re-export shims; @types/node added to core.

Verified: api typecheck clean, group-files 22/22, bot 74/74, web typecheck
clean; 029 re-applied clean on a fresh scratch DB (room_id nullable,
archive class, claim_key unique all confirmed in the catalog).
This commit is contained in:
glass beadian 2026-10-11 10:09:24 +00:00
parent 688a2ce996
commit e7e703c105
21 changed files with 473 additions and 227 deletions

View file

@ -13,11 +13,13 @@
* `any`, and DB CHECK constraints (029) are coarse backstops only.
*/
import { BadRequestException } from "@nestjs/common";
import { MIME_CLASSES } from "@tactical-media-broadcast-hub/core";
import type {
FileRetention,
GroupFile,
GroupFileMapping,
MatrixAttachmentCandidate,
MimeClass,
SharedFileOrigin,
} from "@tactical-media-broadcast-hub/core";
@ -29,8 +31,7 @@ export type RetentionMode = (typeof RETENTION_MODES)[number];
export const ORIGIN_KINDS = ["matrix", "bridge", "upload", "pad-export"] as const;
export type OriginKind = (typeof ORIGIN_KINDS)[number];
export const MIME_CLASSES = ["image", "video", "audio", "document", "other"] as const;
export type MimeClass = (typeof MIME_CLASSES)[number];
export { MIME_CLASSES, type MimeClass };
export const FILE_STATUSES = ["available", "revoked", "expired"] as const;
export type FileStatus = (typeof FILE_STATUSES)[number];