T-HUB/packages/core/src/shared-files.ts
glass beadian 4bf90ffe25 Complete files UI rewiring to the deployed api surface; expose mimeClass
page.tsx now calls the real backend: GET /files with groupId/name/mimeClass/
cursor/limit (origin filter dropped — the api has no origin param), shares
tracked as ShareCreated (revoke by id), retention mapped to the orthogonal
legalHold model, downloads by item.name, samples reshaped to the wire
contract.

GroupFile gains mimeClass (the indexed coarse class the row icon and the
filter depend on): core contract, service row mapper, parseGroupFile guard,
test fixture, UI types — one vocabulary end to end. parseGroupFile's roomId
becomes null-tolerant to match direct-upload rows (mapping input stays
strict).

Verified: core build clean, api typecheck 0 errors + group-files 22/22,
web typecheck 0 errors.
2026-10-11 11:08:08 +00:00

114 lines
3.9 KiB
TypeScript

/** Shared file/index contracts. Payloads are remote; these records contain metadata only. */
/** A share expiry never deletes a file. UTC ISO-8601 for timed retention. */
export type FileRetention = { legalHold: boolean } & (
| { mode: "keep"; expiresAt?: never }
| { mode: "until"; expiresAt: string }
);
export interface GroupFileMapping {
workspaceId: string;
groupId: string;
/** Immutable Matrix room ID, never an alias or display name. */
roomId: string;
spaceId: string;
folderId: string;
archivePolicy: "disabled" | "unencrypted-archive";
retention: FileRetention;
}
export type SharedFileOrigin =
| { kind: "matrix"; mxcUri: string }
| { kind: "bridge"; mxcUri: string; providerId: string; remoteEventId?: string }
| { kind: "upload" }
| { kind: "pad-export"; padId: string; revision: string };
import type { MimeClass } from "./file-mime.js";
export interface GroupFile {
id: string;
workspaceId: string;
groupId: string;
/** Matrix room when the file arrived through one; null for direct uploads. */
roomId: string | null;
senderId: string;
/** Null for a direct upload/export until a link is posted in the room. */
eventId: string | null;
origin: SharedFileOrigin;
name: string;
mimeType: string;
/** Coarse class derived from the payload (image/video/audio/document/archive/other). */
mimeClass: MimeClass;
sizeBytes: number;
/** SHA-256 of verified payload bytes, lowercase hex; never an S3 ETag. */
checksum: { algorithm: "sha256"; value: string };
createdAt: string;
indexedAt: string;
file: { spaceId: string; resourceId: string; version: string };
/** Authenticated library link by default; never a persisted presigned R2 URL. */
shareLink: { url: string; access: "members" | "public"; expiresAt: string | null };
retention: FileRetention;
status: "available" | "revoked" | "expired";
}
export interface MatrixAttachmentCandidate {
roomId: string;
eventId: string;
senderId: string;
eventType: string;
msgtype: string;
/** Unknown state must fail closed, including on initial sync. */
roomEncryption: "unencrypted" | "encrypted" | "unknown";
encryptedFile: boolean;
redacted: boolean;
mxcUri: string;
}
/** Eligibility only; the adapter must also check membership/ACL, quotas and actual bytes. */
export function canArchiveAttachment(
mapping: GroupFileMapping,
event: MatrixAttachmentCandidate,
): boolean {
return mapping.archivePolicy === "unencrypted-archive"
&& mapping.roomId === event.roomId
&& event.roomEncryption === "unencrypted"
&& !event.encryptedFile
&& !event.redacted
&& event.eventType === "m.room.message"
&& ["m.file", "m.image", "m.audio", "m.video"].includes(event.msgtype)
&& /^mxc:\/\/[^/\s]+\/[^/\s?#]+$/.test(event.mxcUri);
}
/** Collision-free replay identity; DB uniqueness is scoped to workspace/room/event. */
export function attachmentArchiveKey(workspaceId: string, roomId: string, eventId: string): string {
return JSON.stringify([workspaceId, roomId, eventId]);
}
/** Future adapter boundary, not a running bot or public API. Commit before advancing sync. */
export interface HubArchivePort {
archive(mapping: GroupFileMapping, event: MatrixAttachmentCandidate): Promise<GroupFile>;
revoke(workspaceId: string, roomId: string, eventId: string): Promise<void>;
}
export interface CollaborativeDocument {
fileId: string;
mimeType: "application/vnd.oasis.opendocument.text";
editor: "wopi";
/** Authenticated launch URL; WOPI access tokens are short-lived and not indexed. */
launchUrl: string;
}
/** Etherpad/HedgeDoc-compatible link/export seam. No pad text or revisions in Hub DB. */
export interface SharedPad {
id: string;
workspaceId: string;
groupId: string;
roomId: string;
service: "etherpad-compatible" | "hedgedoc-compatible";
url: string;
payloadStore: "r2";
retention: FileRetention;
}
export interface PadExportPort {
exportRevision(pad: SharedPad, revision: string): Promise<GroupFile>;
}