T-HUB/apps/api/src/video/video.roundtrip.test.ts
glass beadian 0a1c3b392b feat: direct-to-R2 Files uploads (client-sealed SPENC2) + chunked video dropzone + multi-file queue
For scale: payload bytes go browser → R2 → browser; the api handles keys,
authz and metadata only, so a cheap box can serve thousands of users.

- SPENC2 frame (browser-sealed, P-256 ECDH → HKDF wrap of a per-file CEK,
  10 MiG AES-GCM segments): storage.ts gains the reader (magic-branched
  getObject), presignPut/headObject, openDirectHeader; sealForDirect is the
  reference sealer for cross-implementation tests
- Files API: GET /files/direct/config, POST /files/direct/init (presigned
  PUT), POST /files/direct/complete (HEAD-gated commit), POST
  /files/:id/direct-download (presigned GET + CEK ticket)
- web: lib/direct-storage (WebCrypto seal/open), lib/files-direct (flows),
  dropzone rewritten with a multi-file sequential queue (per-file status,
  direct-first with server fallback), file download decrypts in-browser for
  direct objects (falls back to streaming)
- video dropzone wired to the chunked ingest API (>10 MiB → init/chunk/
  complete), fixing the Cloudflare ~100 MB body cap for large videos
- DIRECT_EC_PRIVATE_KEY (P-256, PKCS8 b64) wired through compose; cross-impl
  roundtrip tests prove web-seal ↔ server-open byte compatibility
2026-10-16 08:12:11 +00:00

47 lines
2.6 KiB
TypeScript

import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
import { keyForHash, sha256, seal, open, type StorageService, directStorageDisabled } from "../storage/storage.js";
import { probeVideoFile } from "./ffprobe.js";
import { serviceWithDatabase, workspaceId } from "./test-fixtures.js";
// Opt in with a generated/real MP4; the normal suite needs no ffmpeg installation.
const fixture = process.env.VIDEO_ROUNDTRIP_FIXTURE;
describe.skipIf(fixture === undefined)("real ffprobe roundtrip (encrypted in-memory storage)", () => {
it("uploads actual MP4 bytes, decrypts the stored source, and persists real probe metadata", async () => {
if (fixture === undefined) throw new Error("VIDEO_ROUNDTRIP_FIXTURE is required");
const bytes = await readFile(fixture);
const objects = new Map<string, Buffer>();
const kek = Buffer.alloc(32, 7);
const storage: StorageService = {
putContent: async (source, contentType) => {
const hash = sha256(source);
const key = keyForHash(hash, contentType);
const deduplicated = objects.has(key);
if (!deduplicated) objects.set(key, seal(source, kek));
return { key, hash, sizeBytes: source.length, contentType, deduplicated };
},
getObject: async (key) => {
const encrypted = objects.get(key);
if (encrypted === undefined) throw new Error("missing object");
const source = open(encrypted, kek);
expect(key).toBe(keyForHash(sha256(source), "video/mp4"));
return source;
},
hasContent: async (hash, contentType) => objects.has(keyForHash(hash, contentType)),
presign: async () => { throw new Error("must decrypt, not presign"); },
...directStorageDisabled(),
};
const { service } = serviceWithDatabase({}, probeVideoFile, storage);
const uploaded = await service.uploadVideoBytes(workspaceId, bytes, "video/mp4");
const status = await service.getVideoStatus(uploaded.videoAsset.id, workspaceId);
expect(status.processingJob.state).toBe("complete");
expect(status.videoAsset).toMatchObject({ durationMs: 1000, width: 320, height: 180, frameRate: 25 });
expect(status.videoAsset.codec).toBeDefined();
expect(objects.get(status.videoAsset.storageKey)?.equals(bytes)).toBe(false);
expect(await storage.getObject(status.videoAsset.storageKey)).toEqual(bytes);
const again = await storage.putContent(bytes, "video/mp4");
expect(again).toMatchObject({ key: status.videoAsset.storageKey, deduplicated: true });
console.info("Real ffprobe status:", JSON.stringify(status));
});
});