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
47 lines
2.6 KiB
TypeScript
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));
|
|
});
|
|
});
|