import { mkdir, mkdtemp, rm, symlink } 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 { MediaToolInputSchemas, mediaRpcShapes } from "@rakazo/contracts"; import type { MediaRepos, PrismaClient } from "@rakazo/db"; import { InvalidMediaInputError, IsolationError, MediaStateConflictError } from "@rakazo/db"; import { PathOutsideRootError } from "@rakazo/media/node"; import { Hono } from "hono"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { createMediaRouter, MEDIA_NOT_CONFIGURED, mediaRpcError, unconfiguredCapabilities, validateMediaFolder, } from "./media.js"; import { mountMediaHttpRoutes, parseByteRange } from "./media-http.js"; import type { MediaSampleIds } from "./media-test-fakes.js"; import { fakeMediaServices, mediaProcedurePaths, sampleMediaInputs } from "./media-test-fakes.js"; import { expectedChunkBytes, uploadChunkCount } from "./media-uploads.js"; const member: Actor = { userId: "user-member", spaceId: "space-a", email: "member@rakazo.test", isDeploymentOwner: false, }; const owner: Actor = { ...member, userId: "user-owner", isDeploymentOwner: true }; const ids = Object.fromEntries( [ "sourceId", "assetId", "secondAssetId", "segmentId", "trackId", "personId", "markId", "collectionId", "brandKitId", "projectId", "planId", "renderId", "publicationId", "mappingId", "uploadId", ].map((key) => [key, `id-${key.toLowerCase()}`]), ) as unknown as MediaSampleIds; const kit = { name: "Kit", colors: { primary: "#101010", secondary: "#202020", accent: "#303030", text: "#f0f0f0", background: "#000000", }, }; function client(router: ReturnType, actor: Actor | null) { return createRouterClient(router, { context: { actor } }); } function procedure(target: unknown, rpcPath: string) { let node: unknown = target; 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"; } /** Repositories that fail the test when reached. */ function untouchedRepos(): MediaRepos { return new Proxy({} as MediaRepos, { get: (_target, key) => () => { throw new Error(`repository ${String(key)} must not be called`); }, }); } const prismaStub = {} as PrismaClient; describe("media router without a configured sidecar", () => { const router = createMediaRouter({ prisma: prismaStub, services: null, dataDir: "/data" }); const inputs = sampleMediaInputs(ids, kit); it("has a sample input for every media procedure", () => { expect(Object.keys(inputs).sort()).toEqual(mediaProcedurePaths().sort()); }); it("reports media as not configured so every surface keeps it hidden", async () => { const capabilities = await client(router, member).capabilities({}); expect(capabilities).toEqual(unconfiguredCapabilities()); expect(capabilities.configured).toBe(false); expect( Object.values(capabilities.operations).every( (op) => !(op as { available: boolean }).available, ), ).toBe(true); }); it("refuses every other procedure", async () => { for (const [rpcPath, sample] of Object.entries(inputs)) { if (rpcPath === "capabilities") continue; const promise = procedure(client(router, member), rpcPath)(sample.input); await expect(promise, rpcPath).rejects.toMatchObject({ code: "PRECONDITION_FAILED", message: MEDIA_NOT_CONFIGURED, }); } }); it("requires a signed-in member for every procedure", async () => { for (const [rpcPath, sample] of Object.entries(inputs)) { const code = await rpcCode(procedure(client(router, null), rpcPath)(sample.input)); expect(code, rpcPath).toBe("UNAUTHORIZED"); } }); it("answers the byte routes with 404 and anonymous callers with 401", async () => { const app = new Hono(); let actor: Actor | null = member; mountMediaHttpRoutes(app, { services: null }, async () => actor); const requests = [ new Request("http://api/api/media/uploads/u1/chunks/0", { method: "PUT", body: "abcd" }), new Request("http://api/api/media/renders/r1/files/video.mp4"), new Request("http://api/api/media/assets/a1/thumb"), new Request("http://api/api/media/assets/a1/proxy"), new Request("http://api/api/media/bundles/p1"), ]; for (const request of requests) { expect((await app.request(request.clone())).status, request.url).toBe(404); } actor = null; for (const request of requests) { expect((await app.request(request.clone())).status, request.url).toBe(401); } }); }); describe("media router permissions", () => { let root: string; beforeAll(async () => { root = await mkdtemp(path.join(tmpdir(), "media-api-")); }); afterAll(async () => { await rm(root, { recursive: true, force: true }); }); it("lets only the deployment owner add a folder source", async () => { const created: unknown[] = []; const repos = new Proxy({} as MediaRepos, { get: (_target, key) => { if (key === "createSource") { return async (_spaceId: string, input: unknown) => { created.push(input); return { id: "src-1", kind: "folder", label: "Footage", status: "idle", assetCount: 0, lastSyncedAt: null, error: null, createdAt: "2026-10-01T00:00:00.000Z", }; }; } return () => { throw new Error(`repository ${String(key)} must not be called`); }; }, }); const { services, calls } = fakeMediaServices({ repos, mediaRoot: path.join(root, "media") }); const footage = path.join(root, "footage"); await mkdir(footage); const router = createMediaRouter({ prisma: prismaStub, services, dataDir: path.join(root, "data"), appRoot: path.join(root, "app"), }); const denied = client(router, member).sources.createFolder({ label: "Footage", path: footage }); await expect(denied).rejects.toMatchObject({ code: "FORBIDDEN" }); expect(created).toEqual([]); expect(calls).toEqual([]); await client(router, owner).sources.createFolder({ label: "Footage", path: footage }); expect(created).toEqual([ { kind: "folder", label: "Footage", ownerUserId: owner.userId, config: { path: footage } }, ]); expect(calls.map((call) => `${call.service}.${call.method}`)).toEqual(["ingest.requestSync"]); }); it("accepts only a real, specific folder apart from evOo's own directories", async () => { const dataDir = path.join(root, "data"); const appRoot = path.join(root, "app"); const folder = path.join(root, "clips"); await mkdir(path.join(dataDir, "media"), { recursive: true }); await mkdir(path.join(appRoot, "packages"), { recursive: true }); await mkdir(folder, { recursive: true }); await symlink(folder, path.join(root, "clips-link")); const forbidden = { dataDir, appRoot }; await expect(validateMediaFolder(folder, forbidden)).resolves.toBe(folder); const rejected = [ "clips", path.join(root, "missing"), path.join(root, "clips-link"), "/", dataDir, path.join(dataDir, "media"), root, path.join(appRoot, "packages"), ]; for (const candidate of rejected) { await expect(validateMediaFolder(candidate, forbidden), candidate).rejects.toBeInstanceOf( InvalidMediaInputError, ); } }); }); describe("consent stays with the signed-in user", () => { it("never accepts consent or age fields from the agent's people tool", () => { const schema = MediaToolInputSchemas.media_people; expect(schema.shape.action.options).toEqual(["list", "update", "merge"]); const parsed = schema.parse({ action: "update", personId: "person-1", consentArtifactId: "artifact-1", consentRecordedAt: "2026-09-01T00:00:00.000Z", ageVerificationMethod: "platform_verified", ageVerifiedAt: "2026-09-01T00:00:00.000Z", }); expect(Object.keys(parsed).sort()).toEqual(["action", "personId"]); }); it("drops consent fields sent to people.update", async () => { const patches: unknown[] = []; const repos = new Proxy({} as MediaRepos, { get: (_target, key) => { if (key === "updatePerson") { return async (_spaceId: string, _personId: string, patch: unknown) => { patches.push(patch); return {}; }; } return () => { throw new Error(`repository ${String(key)} must not be called`); }; }, }); const { services } = fakeMediaServices({ repos, mediaRoot: "/unused" }); const router = createMediaRouter({ prisma: prismaStub, services, dataDir: "/data" }); await procedure( client(router, member), "people/update", )({ personId: "person-1", displayName: "Person 1", consentRecordedAt: "2026-09-01T00:00:00.000Z", ageVerificationMethod: "platform_verified", ageVerifiedAt: "2026-09-01T00:00:00.000Z", }).catch(() => undefined); expect(patches).toHaveLength(1); expect(Object.entries(patches[0] as object).filter(([, value]) => value !== undefined)).toEqual( [["displayName", "Person 1"]], ); }); it("marks recordConsent as the only user-only write and keeps it out of the services", () => { const people = mediaRpcShapes.people as Record; expect(Object.keys(people)).toContain("recordConsent"); const { services } = fakeMediaServices({ repos: untouchedRepos(), mediaRoot: "/unused" }); const serviceMethods = (["ingest", "privacy", "render", "marketing"] as const).flatMap((name) => Object.keys(services[name] as unknown as Record), ); expect(serviceMethods.filter((method) => /consent|age/i.test(method))).toEqual([]); }); }); describe("media error mapping", () => { it("never reveals whether a foreign id exists", () => { expect((mediaRpcError(new IsolationError()) as ORPCError).code).toBe( "NOT_FOUND", ); expect( (mediaRpcError(new PathOutsideRootError("../x")) as ORPCError).code, ).toBe("BAD_REQUEST"); expect( (mediaRpcError(new MediaStateConflictError("busy")) as ORPCError).code, ).toBe("CONFLICT"); const other = new Error("boom"); expect(mediaRpcError(other)).toBe(other); }); }); describe("upload and streaming helpers", () => { it("splits uploads into full chunks plus a shorter last one", () => { const row = { sizeBytes: 10n, chunkBytes: 4 }; expect(uploadChunkCount(row)).toBe(3); expect([0, 1, 2].map((index) => expectedChunkBytes(row, index))).toEqual([4, 4, 2]); expect(uploadChunkCount({ sizeBytes: 8n, chunkBytes: 4 })).toBe(2); expect(expectedChunkBytes({ sizeBytes: 8n, chunkBytes: 4 }, 1)).toBe(4); }); it("parses single byte ranges and refuses unsatisfiable ones", () => { expect(parseByteRange(undefined, 100)).toBeNull(); expect(parseByteRange("bytes=0-9", 100)).toEqual({ start: 0, end: 9 }); expect(parseByteRange("bytes=90-", 100)).toEqual({ start: 90, end: 99 }); expect(parseByteRange("bytes=-10", 100)).toEqual({ start: 90, end: 99 }); expect(parseByteRange("bytes=50-500", 100)).toEqual({ start: 50, end: 99 }); expect(parseByteRange("bytes=0-1,5-6", 100)).toBeNull(); expect(parseByteRange("bytes=100-", 100)).toBe("unsatisfiable"); expect(parseByteRange("bytes=9-3", 100)).toBe("unsatisfiable"); expect(parseByteRange("items=0-1", 100)).toBe("unsatisfiable"); expect(parseByteRange("bytes=-0", 100)).toBe("unsatisfiable"); }); });