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

@ -44,8 +44,9 @@ CREATE TABLE chat_media_archive (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE ON UPDATE RESTRICT,
group_id uuid NOT NULL REFERENCES groups(id) ON DELETE CASCADE ON UPDATE RESTRICT,
-- Immutable Matrix room ID, never an alias. Tenant-pinned below.
room_id text NOT NULL CHECK (btrim(room_id) <> '' AND room_id ~ '^!'),
-- Matrix room ID when the file arrived through a room; NULL for direct
-- uploads/exports that have not been announced into one yet. Tenant-pinned.
room_id text CHECK (btrim(room_id) = '' OR room_id ~ '^!'),
-- NULL for direct uploads/pad exports until a room announcement supplies one.
event_id text CHECK (event_id IS NULL OR btrim(event_id) <> ''),
origin text NOT NULL CHECK (origin IN ('matrix','bridge','upload','pad-export')),
@ -58,8 +59,8 @@ CREATE TABLE chat_media_archive (
-- Normalized safe display filename (app-side sanitizeName).
safe_name text NOT NULL CHECK (btrim(safe_name) <> '' AND char_length(safe_name) <= 255),
mime text NOT NULL CHECK (mime <> ''),
-- Coarse class derived from mime (image/video/audio/document/other).
mime_class text NOT NULL CHECK (mime_class IN ('image','video','audio','document','other')),
-- Coarse class derived from mime (image/video/audio/document/archive/other).
mime_class text NOT NULL CHECK (mime_class IN ('image','video','audio','document','archive','other')),
byte_size bigint NOT NULL CHECK (byte_size >= 0),
-- SHA-256 of verified payload bytes, lowercase hex. Never an S3 ETag.
sha256 text NOT NULL CHECK (sha256 ~ '^[0-9a-f]{64}$'),