import { ORPCError } from "@orpc/server"; import type { MediaRuntime } from "@rakazo/adapter-kit"; import { type EncryptedSecretStore, loadMediaSettings, MediaJobRejectedError, MediaSettingsInvalidError, mediaSettingsView, saveMediaSettings, } from "@rakazo/adapters"; import type { Actor, MediaJobStartInput, MediaSettingsUpdate } from "@rakazo/contracts"; import type { PrismaClient } from "@rakazo/db"; import { resolveThreadTarget } from "./thread-target.js"; export interface MediaRouteDeps { prisma: PrismaClient; secrets: EncryptedSecretStore; media?: MediaRuntime; } /** Media backends run on the deployment owner's own server, so only they configure them. */ function requireDeploymentOwner(actor: Actor) { if (!actor.isDeploymentOwner) throw new ORPCError("FORBIDDEN"); } function requireMedia(deps: MediaRouteDeps): MediaRuntime { if (!deps.media) throw new ORPCError("NOT_IMPLEMENTED"); return deps.media; } export async function getMediaSettings(deps: MediaRouteDeps, actor: Actor) { requireDeploymentOwner(actor); return mediaSettingsView(await loadMediaSettings(deps)); } export async function updateMediaSettings( deps: MediaRouteDeps, actor: Actor, input: MediaSettingsUpdate, ) { requireDeploymentOwner(actor); try { return await saveMediaSettings(deps, input); } catch (error) { if (error instanceof MediaSettingsInvalidError) { throw new ORPCError("BAD_REQUEST", { message: error.message }); } throw error; } } /** * Starts an operation a user picked on a card (export, transcribe, edit) in the same conversation. * The thread is authorized like every thread procedure; the runtime checks that the operation's * source artifacts belong to the actor, and the job can only ever read the actor's artifacts. */ export async function startMediaJob(deps: MediaRouteDeps, actor: Actor, input: MediaJobStartInput) { const media = requireMedia(deps); const target = await resolveThreadTarget(deps.prisma, actor, input); const source = await deps.prisma.message.findFirst({ where: { id: input.messageId, threadId: target.threadId }, select: { botId: true }, }); if (!source) throw new ORPCError("NOT_FOUND"); const bots = target.kind === "bot" ? [target.botId] : [ ...(target.leadBotId && target.memberBotIds.includes(target.leadBotId) ? [target.leadBotId] : []), ...target.memberBotIds, ]; const botId = source.botId && bots.includes(source.botId) ? source.botId : bots[0]; if (!botId) throw new ORPCError("NOT_FOUND"); try { return await media.start({ operation: input.operation, input: input.input, spaceId: actor.spaceId, userId: actor.userId, botId, groupId: target.kind === "group" ? target.groupId : null, threadId: target.threadId, runId: null, }); } catch (error) { if (error instanceof MediaJobRejectedError) { throw new ORPCError("BAD_REQUEST", { message: error.message }); } throw error; } } async function ownedJobId(deps: MediaRouteDeps, actor: Actor, jobId: string): Promise { const job = await deps.prisma.mediaJob.findFirst({ where: { id: jobId, spaceId: actor.spaceId, userId: actor.userId }, select: { id: true }, }); if (!job) throw new ORPCError("NOT_FOUND"); return job.id; } export async function getMediaJob(deps: MediaRouteDeps, actor: Actor, jobId: string) { const media = requireMedia(deps); const snapshot = await media.get(await ownedJobId(deps, actor, jobId)); if (!snapshot) throw new ORPCError("NOT_FOUND"); return snapshot; } export async function cancelMediaJob(deps: MediaRouteDeps, actor: Actor, jobId: string) { const media = requireMedia(deps); const id = await ownedJobId(deps, actor, jobId); await media.cancel(id); const snapshot = await media.get(id); if (!snapshot) throw new ORPCError("NOT_FOUND"); return snapshot; }