import { createHash } from "node:crypto"; import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { createRouterClient, ORPCError } from "@orpc/server"; import type { Actor } from "@rakazo/contracts"; import type { MediaRepos } from "@rakazo/db"; import { createMediaRepos } from "@rakazo/db"; import { Hono } from "hono"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import type { MediaTestDb, SeededMediaSpace } from "../../../packages/db/src/media-fixtures.js"; import { openMediaTestDb, seedMediaSpace, sourceItem, testBrandKit, } from "../../../packages/db/src/media-fixtures.js"; import { testProfile } from "../../../packages/media/src/test-fixtures.js"; import { createMediaRouter } from "./media.js"; import { mountMediaHttpRoutes } from "./media-http.js"; import type { MediaServiceCall } from "./media-test-fakes.js"; import { fakeMediaServices, sampleMediaInputs } from "./media-test-fakes.js"; import { MEDIA_UPLOAD_IDLE_MS } from "./media-uploads.js"; const databaseUrl = process.env.DATABASE_URL; const describePostgres = process.env.VERIFY_DATABASE && databaseUrl ? describe.sequential : describe.skip; type Space = { userId: string; spaceId: string }; function actorOf(space: Space, isDeploymentOwner = false): Actor { return { userId: space.userId, spaceId: space.spaceId, email: `${space.userId}@rakazo.test`, isDeploymentOwner, }; } function procedure(client: unknown, rpcPath: string) { let node = client; for (const key of rpcPath.split("/")) node = (node as Record)[key]; return node as (input: unknown) => Promise; } async function rpcCode(promise: Promise): Promise { try { await promise; } catch (error) { if (error instanceof ORPCError) return error.code; throw error; } return "OK"; } function sha256(bytes: Uint8Array): string { return createHash("sha256").update(bytes).digest("hex"); } describePostgres("media API authorization and uploads (PostgreSQL)", () => { let db: MediaTestDb; let repos: MediaRepos; let mediaRoot: string; let a: Space; let b: Space; let seededA: SeededMediaSpace; let seededB: SeededMediaSpace; let calls: MediaServiceCall[]; let renderOutputs: Map; let rpc: (actor: Actor) => unknown; let app: Hono; let currentActor: Actor | null; function setup(options: { now?: () => Date; chunkTimeoutMs?: number } = {}) { const fake = fakeMediaServices({ repos, mediaRoot, prisma: db.prisma, profiles: [testProfile()], renderOutputs, ...(options.now ? { now: options.now } : {}), }); calls = fake.calls; const router = createMediaRouter({ prisma: db.prisma, services: fake.services, dataDir: path.join(mediaRoot, ".."), }); rpc = (actor) => createRouterClient(router, { context: { actor } }); app = new Hono(); mountMediaHttpRoutes( app, { services: fake.services, ...(options.chunkTimeoutMs ? { chunkTimeoutMs: options.chunkTimeoutMs } : {}), }, async () => currentActor, ); } async function snapshot(spaceId: string) { const p = db.prisma; const where = { spaceId }; const orderBy = { id: "asc" as const }; return JSON.stringify( [ await p.mediaSource.findMany({ where, orderBy }), await p.mediaAsset.findMany({ where, orderBy }), await p.mediaUpload.findMany({ where, orderBy }), await p.mediaSegment.findMany({ where, orderBy }), await p.mediaAnalysis.findMany({ where, orderBy }), await p.mediaCollection.findMany({ where, orderBy }), await p.mediaCollectionItem.findMany({ where: { collection: where }, orderBy: [{ collectionId: "asc" }, { assetId: "asc" }], }), await p.mediaPerson.findMany({ where, orderBy }), await p.mediaFaceTrack.findMany({ where, orderBy }), await p.mediaMark.findMany({ where, orderBy }), await p.mediaBrandKit.findMany({ where, orderBy }), await p.mediaProject.findMany({ where, orderBy }), await p.mediaEditPlan.findMany({ where, orderBy }), await p.mediaRender.findMany({ where, orderBy }), await p.mediaPublication.findMany({ where, orderBy }), await p.mediaMetric.findMany({ where, orderBy }), await p.mediaMetricMapping.findMany({ where, orderBy }), ], (_key, value) => (typeof value === "bigint" ? value.toString() : value), ); } async function artifactIn(space: Space): Promise { const artifact = await db.prisma.artifact.create({ data: { spaceId: space.spaceId, userId: space.userId, name: "consent.pdf", mimeType: "application/pdf", size: 1, hash: "0".repeat(64), storageKey: `test/${space.spaceId}/consent.pdf`, }, }); return artifact.id; } /** A device item in `space` that waits for its original. */ async function remoteAsset(space: Space, index: number): Promise { const source = await procedure( rpc(actorOf(space)), "sources/registerClient", )({ kind: "ios_library", label: "Phone", deviceId: `device-${index}` }); const synced = await repos.upsertSourceItems(space.spaceId, (source as { id: string }).id, [ sourceItem(index), ]); const assetId = synced.changedAssetIds[0] as string; await repos.requestUpload(space.spaceId, [assetId]); return assetId; } function putChunk(uploadId: string, index: number, body: BodyInit, actor: Actor | null) { currentActor = actor; return app.request(`/api/media/uploads/${uploadId}/chunks/${index}`, { method: "PUT", body }); } beforeAll(async () => { db = openMediaTestDb(databaseUrl!); repos = createMediaRepos(db.prisma); const root = await mkdtemp(path.join(tmpdir(), "media-api-pg-")); mediaRoot = path.join(root, "media"); await mkdir(mediaRoot); renderOutputs = new Map(); a = await db.newUser(); b = await db.newUser(); seededA = await seedMediaSpace(repos, a.spaceId, a.userId); seededB = await seedMediaSpace(repos, b.spaceId, b.userId); setup(); }); afterAll(async () => { await db?.cleanup(); if (mediaRoot) await rm(path.dirname(mediaRoot), { recursive: true, force: true }); }); it("refuses another space's ids in every media procedure and leaves that space untouched", async () => { const before = await snapshot(a.spaceId); const foreignIds = Object.values(seededA); const intruder = rpc(actorOf(b)); const inputs = sampleMediaInputs(seededA, testBrandKit); for (const [rpcPath, sample] of Object.entries(inputs)) { if (sample.foreign === "dedicated") continue; const call = procedure(intruder, rpcPath)(sample.input); if (sample.foreign === "not_found") { expect(await rpcCode(call), rpcPath).toBe("NOT_FOUND"); continue; } const result = JSON.stringify(await call); for (const id of foreignIds) expect(result, rpcPath).not.toContain(id); } expect(await snapshot(a.spaceId)).toBe(before); // No service ever received the other space's ids. const leaked = calls.filter((call) => foreignIds.some((id) => JSON.stringify(call.input ?? null).includes(id)), ); expect(leaked).toEqual([]); expect(calls.every((call) => call.actor.spaceId === b.spaceId)).toBe(true); }); it("serves the same inputs inside the caller's own space", async () => { // Positive control: NOT_FOUND above comes from isolation, not from bad samples. const fabricated = new Set(["collections/merge", "people/merge"]); for (const [rpcPath, sample] of Object.entries(sampleMediaInputs(seededA, testBrandKit))) { if (sample.foreign !== "not_found" || fabricated.has(rpcPath)) continue; const space = await db.newUser(); const seeded = await seedMediaSpace(repos, space.spaceId, space.userId); const input = sampleMediaInputs(seeded, testBrandKit)[rpcPath]?.input; const code = await rpcCode(procedure(rpc(actorOf(space)), rpcPath)(input)); expect(code, rpcPath).not.toBe("NOT_FOUND"); } }, 120_000); it("refuses another space's records referenced from the caller's own ones", async () => { const own = rpc(actorOf(b)); const foreignArtifact = await artifactIn(a); const attempts: Array<[string, unknown]> = [ ["projects/update", { projectId: seededB.projectId, collectionIds: [seededA.collectionId] }], ["projects/update", { projectId: seededB.projectId, brandKitId: seededA.brandKitId }], [ "projects/create", { title: "Mixed", goal: "teaser", assetIds: [seededA.assetId], profileIds: ["example-short-vertical"], }, ], ["people/assignTrack", { trackId: seededB.trackId, personId: seededA.personId }], [ "marks/create", { kind: "tattoo", assetId: seededB.assetId, personId: seededA.personId, prompt: { tMs: 0 }, treatment: "blur", }, ], ["marks/update", { markId: seededB.markId, personId: seededA.personId }], [ "assets/setPaidOnly", { segmentIds: [seededB.segmentId, seededA.segmentId], paidOnly: true }, ], ["collections/merge", { collectionIds: [seededB.collectionId, seededA.collectionId] }], ["people/merge", { personIds: [seededB.personId, seededA.personId] }], ["brandKits/upsert", { kit: { ...testBrandKit, logoArtifactIds: [foreignArtifact] } }], [ "people/recordConsent", { personId: seededB.personId, consentArtifactId: foreignArtifact, consentRecordedAt: "2026-09-01T10:00:00.000Z", ageVerificationMethod: "document_on_file", ageVerifiedAt: "2026-09-01T10:00:00.000Z", }, ], ]; const before = await snapshot(a.spaceId); for (const [rpcPath, input] of attempts) { expect(await rpcCode(procedure(own, rpcPath)(input)), rpcPath).toBe("NOT_FOUND"); } expect(await snapshot(a.spaceId)).toBe(before); const person = await db.prisma.mediaPerson.findUniqueOrThrow({ where: { id: seededB.personId }, }); expect(person.consentRecordedAt).toBeNull(); }); it("records consent only as the signed-in member and ignores it on updates", async () => { const space = await db.newUser(); const seeded = await seedMediaSpace(repos, space.spaceId, space.userId); const client = rpc(actorOf(space)); await procedure( client, "people/update", )({ personId: seeded.personId, displayName: "Person 1", consentRecordedAt: "2026-09-01T10:00:00.000Z", ageVerificationMethod: "platform_verified", ageVerifiedAt: "2026-09-01T10:00:00.000Z", }); let person = await db.prisma.mediaPerson.findUniqueOrThrow({ where: { id: seeded.personId } }); expect(person.consentRecordedAt).toBeNull(); expect(person.ageVerifiedAt).toBeNull(); const artifactId = await artifactIn(space); await procedure( client, "people/recordConsent", )({ personId: seeded.personId, consentArtifactId: artifactId, consentRecordedAt: "2026-09-01T10:00:00.000Z", ageVerificationMethod: "platform_verified", ageVerifiedAt: "2026-09-01T10:00:00.000Z", }); person = await db.prisma.mediaPerson.findUniqueOrThrow({ where: { id: seeded.personId } }); expect(person.consentRecordedBy).toBe(space.userId); expect(person.consentArtifactId).toBe(artifactId); expect(person.ageVerificationMethod).toBe("platform_verified"); }); it("assembles a resumable chunked upload and hands it to ingest", async () => { const assetId = await remoteAsset(b, 101); const bytes = new TextEncoder().encode("0123456789"); const upload = await repos.beginUpload(b.spaceId, { assetId, userId: b.userId, sizeBytes: bytes.length, sha256: sha256(bytes), fileName: "clip.mp4", chunkBytes: 4, }); const owner = actorOf(b); expect((await putChunk(upload.id, 0, bytes.slice(0, 4), actorOf(a))).status).toBe(404); expect((await putChunk(upload.id, 0, bytes.slice(0, 4), null)).status).toBe(401); expect((await putChunk(upload.id, 3, bytes.slice(0, 2), owner)).status).toBe(400); expect((await putChunk(upload.id, 0, bytes.slice(0, 5), owner)).status).toBe(413); expect((await putChunk(upload.id, 0, bytes.slice(0, 3), owner)).status).toBe(400); // Out of order and repeated chunks are fine; the client resumes from status. expect((await putChunk(upload.id, 2, bytes.slice(8), owner)).status).toBe(200); expect((await putChunk(upload.id, 0, bytes.slice(0, 4), owner)).status).toBe(200); expect((await putChunk(upload.id, 0, bytes.slice(0, 4), owner)).status).toBe(200); const client = rpc(owner); const partial = (await procedure(client, "uploads/status")({ uploadId: upload.id })) as { receivedChunks: number[]; }; expect([...partial.receivedChunks].sort()).toEqual([0, 2]); expect(await rpcCode(procedure(client, "uploads/complete")({ uploadId: upload.id }))).toBe( "CONFLICT", ); expect((await putChunk(upload.id, 1, bytes.slice(4, 8), owner)).status).toBe(200); calls.length = 0; const done = (await procedure(client, "uploads/complete")({ uploadId: upload.id })) as { status: string; }; expect(done.status).toBe("complete"); const handed = calls.filter((call) => call.method === "uploadCompleted"); expect(handed).toHaveLength(1); const input = handed[0]?.input as { tempPath: string; sha256: string; assetId: string }; expect(input.assetId).toBe(assetId); expect(input.sha256).toBe(sha256(bytes)); expect(input.tempPath.startsWith(path.join(mediaRoot, b.spaceId, "uploads"))).toBe(true); expect(await readFile(input.tempPath, "utf8")).toBe("0123456789"); const asset = await repos.getAssetRecord(b.spaceId, assetId); expect(asset.storage).toBe("uploaded"); expect(asset.storagePath).toBe(`${b.spaceId}/uploads/${upload.id}/file`); // Completing again is idempotent and does not hand the file over twice. await procedure(client, "uploads/complete")({ uploadId: upload.id }); expect(calls.filter((call) => call.method === "uploadCompleted")).toHaveLength(1); expect((await putChunk(upload.id, 0, bytes.slice(0, 4), owner)).status).toBe(409); }); it("opens uploads through the RPC only for items that wait for their original", async () => { const client = rpc(actorOf(b)); const assetId = await remoteAsset(b, 102); const input = { assetId, sizeBytes: 10, sha256: "a".repeat(64), fileName: "clip.mp4" }; const first = (await procedure(client, "uploads/begin")(input)) as { id: string }; const resumed = (await procedure(client, "uploads/begin")(input)) as { id: string }; expect(resumed.id).toBe(first.id); const referenced = { ...input, assetId: seededB.assetId }; expect(await rpcCode(procedure(client, "uploads/begin")(referenced))).toBe("CONFLICT"); const tooLarge = { ...input, sizeBytes: 21 * 1024 * 1024 * 1024 }; expect(await rpcCode(procedure(client, "uploads/begin")(tooLarge))).toBe("BAD_REQUEST"); }); it("fails an upload whose bytes do not match the announced checksum", async () => { const assetId = await remoteAsset(b, 103); const bytes = new TextEncoder().encode("abcdef"); const upload = await repos.beginUpload(b.spaceId, { assetId, userId: b.userId, sizeBytes: bytes.length, sha256: "f".repeat(64), fileName: "clip.mp4", chunkBytes: 4, }); const owner = actorOf(b); await putChunk(upload.id, 0, bytes.slice(0, 4), owner); await putChunk(upload.id, 1, bytes.slice(4), owner); const client = rpc(owner); expect(await rpcCode(procedure(client, "uploads/complete")({ uploadId: upload.id }))).toBe( "BAD_REQUEST", ); const status = (await procedure(client, "uploads/status")({ uploadId: upload.id })) as { status: string; }; expect(status.status).toBe("failed"); const dir = path.join(mediaRoot, b.spaceId, "uploads", upload.id); await expect(stat(dir)).rejects.toMatchObject({ code: "ENOENT" }); }); it("expires an idle upload and starts a fresh one", async () => { const assetId = await remoteAsset(b, 104); const input = { assetId, sizeBytes: 10, sha256: "b".repeat(64), fileName: "clip.mp4" }; const first = (await procedure(rpc(actorOf(b)), "uploads/begin")(input)) as { id: string }; setup({ now: () => new Date(Date.now() + MEDIA_UPLOAD_IDLE_MS + 60_000) }); try { const response = await putChunk(first.id, 0, new Uint8Array(10), actorOf(b)); expect(response.status).toBe(409); const status = (await procedure( rpc(actorOf(b)), "uploads/status", )({ uploadId: first.id, })) as { status: string }; expect(status.status).toBe("failed"); const fresh = (await procedure(rpc(actorOf(b)), "uploads/begin")(input)) as { id: string }; expect(fresh.id).not.toBe(first.id); } finally { setup(); } }); it("drops a chunk that does not arrive in time", async () => { const assetId = await remoteAsset(b, 105); const upload = await repos.beginUpload(b.spaceId, { assetId, userId: b.userId, sizeBytes: 10, sha256: "c".repeat(64), fileName: "clip.mp4", }); setup({ chunkTimeoutMs: 50 }); try { const stalled = new ReadableStream({ start(controller) { controller.enqueue(new Uint8Array(2)); }, }); currentActor = actorOf(b); const response = await app.request(`/api/media/uploads/${upload.id}/chunks/0`, { method: "PUT", body: stalled, duplex: "half", } as RequestInit); expect(response.status).toBe(408); const status = await repos.uploadStatus(b.spaceId, upload.id); expect(status.receivedChunks).toEqual([]); } finally { setup(); } }); it("streams render files only inside the caller's space and render folder", async () => { const renderDir = path.join(mediaRoot, a.spaceId, "renders", seededA.renderId); await mkdir(renderDir, { recursive: true }); const video = path.join(renderDir, "video.mp4"); await writeFile(video, "0123456789"); renderOutputs.set(seededA.renderId, video); const url = `/api/media/renders/${seededA.renderId}/files/video.mp4`; currentActor = actorOf(a); const full = await app.request(url); expect(full.status).toBe(200); expect(await full.text()).toBe("0123456789"); const ranged = await app.request(url, { headers: { range: "bytes=2-4" } }); expect(ranged.status).toBe(206); expect(ranged.headers.get("content-range")).toBe("bytes 2-4/10"); expect(await ranged.text()).toBe("234"); const download = await app.request(`${url}?download=1`); expect(download.headers.get("content-disposition")).toContain("attachment"); // A render that has not passed its gate never leaves as a download. const queuedVideo = path.join(mediaRoot, a.spaceId, "renders", seededA.queuedRenderId, "v.mp4"); await mkdir(path.dirname(queuedVideo), { recursive: true }); await writeFile(queuedVideo, "x"); renderOutputs.set(seededA.queuedRenderId, queuedVideo); const queuedUrl = `/api/media/renders/${seededA.queuedRenderId}/files/v.mp4`; expect((await app.request(queuedUrl)).status).toBe(200); expect((await app.request(`${queuedUrl}?download=1`)).status).toBe(404); // A path the service reports outside the render folder is refused. const outside = path.join(mediaRoot, a.spaceId, "video.mp4"); await writeFile(outside, "secret"); renderOutputs.set(seededA.renderId, outside); expect((await app.request(url)).status).toBe(404); renderOutputs.set(seededA.renderId, video); currentActor = actorOf(b); expect((await app.request(url)).status).toBe(404); expect((await app.request(`/api/media/assets/${seededA.assetId}/thumb`)).status).toBe(404); expect((await app.request(`/api/media/bundles/${seededA.publicationId}`)).status).toBe(404); }); it("refuses a folder source to members who do not own the deployment", async () => { const folder = await mkdtemp(path.join(tmpdir(), "media-footage-")); try { const input = { label: "Footage", path: folder }; const member = procedure(rpc(actorOf(b)), "sources/createFolder")(input); expect(await rpcCode(member)).toBe("FORBIDDEN"); const created = (await procedure(rpc(actorOf(b, true)), "sources/createFolder")(input)) as { id: string; kind: string; }; expect(created.kind).toBe("folder"); const row = await db.prisma.mediaSource.findUniqueOrThrow({ where: { id: created.id } }); expect(row.ownerUserId).toBe(b.userId); } finally { await rm(folder, { recursive: true, force: true }); } }); });