import { ZodError } from "zod"; import { CompleteFileUploadResponse, CreateFileUploadRequest, CreateFileUploadResponse, FileAsset, FileListRequest, FileListResponse, FileDownloadUrlResponse, RETAINED_OUTPUT_DEFAULT_PAGE_BYTES, RETAINED_OUTPUT_MAX_PAGE_BYTES, RetainedArtifactMetadataSchema, retainedArtifactReferenceFromFile, retainedGeneratedImageReferenceFromFile, retainedGeneratedVideoReferenceFromFile, retainedScreenshotReferenceFromFile, VideoArtifactPlaybackSource, resolveRetainedOutputRange, type RetainedArtifactMetadata, type RetainedOutputUnavailableReason, } from "@opengeni/contracts"; import { withSessionRlsActorContext, claimFileUploadCleanup, completeFileUploadCleanup, completeFileUpload, createFileUpload, getFileUpload, getGeneratedImageArtifact, recordAuditEvent, getGeneratedVideoArtifact, getFilesForSubject, listFilesForSubject, getRetainedFileArtifact, getRetainedScreenshotArtifact, requireFileForSubject, type RetainedFileArtifact, type GeneratedImageArtifact, type GeneratedVideoArtifact, type RetainedScreenshotArtifact, } from "@opengeni/db"; import type { Context, Hono } from "hono"; import { HTTPException } from "hono/http-exception"; import { fileOwnerContextForAccess, requireAccessGrantAuthorization, grantHasAgentAttemptAuthority, requireAccessGrant, requireLiveAgentAttemptAuthorization, requireSessionAuthorization, SessionAuthorizationDeniedError, SessionAuthorizationUnavailableError, } from "@opengeni/core"; import { recordWorkspaceUsage, requireLimit } from "@opengeni/core"; import type { ApiRouteDeps } from "@opengeni/core"; import { retryWhileMissing } from "@opengeni/storage"; import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; import { buildFilesMcpServer } from "../mcp/files"; import { mcpOAuthAuthenticateHeader, resolveMcpOAuthRouteAccess } from "../mcp-oauth"; import { withAccessGrantSessionRlsContext } from "../access-grant-rls"; import { buildWorkspaceToolGatewayMcpServer, prepareMcpOAuthWorkspaceToolGateway, } from "../workspace-tool-gateway"; export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void { const { db, objectStorage } = deps; const fileRequestAuthority = new WeakMap(); // This scope covers metadata, original bytes, range reads and finalization. // A service/agent subject string alone cannot unlock a personal attachment. for (const path of [ "/v1/workspaces/:workspaceId/files", "/v1/workspaces/:workspaceId/files/*", "/v1/workspaces/:workspaceId/artifacts/*", ]) { app.use(path, async (c, next) => { const permission = c.req.path.includes("/files/uploads") ? "files:upload" : "files:read"; const access = await requireAccessGrantAuthorization( c, deps, c.req.param("workspaceId") ?? "", permission, ); const actor = await fileOwnerContextForAccess(deps, access, permission); fileRequestAuthority.set(c.req.raw, { owner: actor.privateFileOwnerSubjectId ?? null, agent: access.grant.principalKind === "agent_attempt", }); return withSessionRlsActorContext(actor, next); }); } app.all("/v1/workspaces/:workspaceId/mcp/files", async (c) => { const workspaceId = c.req.param("workspaceId"); let oauthAccess: Awaited>; try { oauthAccess = await resolveMcpOAuthRouteAccess(deps, c.req.raw, workspaceId); } catch (error) { if (error instanceof HTTPException && error.status === 401) { c.header("www-authenticate", mcpOAuthAuthenticateHeader(deps, new URL(c.req.url).pathname)); } throw error; } if (oauthAccess) { return await withAccessGrantSessionRlsContext(deps, oauthAccess.grant, async () => { const prepared = await prepareMcpOAuthWorkspaceToolGateway( deps, oauthAccess.grant, oauthAccess.allowedToolIdentities, ); const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true, }); const server = buildWorkspaceToolGatewayMcpServer( prepared, oauthAccess.grant, deps.observability, ); try { await server.connect(transport); return await transport.handleRequest(c.req.raw); } finally { await Promise.allSettled([server.close(), prepared.close()]); } }); } let access: Awaited>; try { access = await requireAccessGrantAuthorization(c, deps, workspaceId, "files:read"); } catch (error) { if (deps.settings.mcpOauthEnabled && error instanceof HTTPException && error.status === 401) { c.header("www-authenticate", mcpOAuthAuthenticateHeader(deps, new URL(c.req.url).pathname)); } throw error; } const { grant } = access; return await withAccessGrantSessionRlsContext(deps, grant, async () => { const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true, }); const server = buildFilesMcpServer(deps, access); await server.connect(transport); return await transport.handleRequest(c.req.raw); }); }); app.post("/v1/workspaces/:workspaceId/files/uploads", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireAccessGrant(c, deps, workspaceId, "files:upload"); if (!objectStorage) { throw new HTTPException(503, { message: "object storage is not configured", }); } const payload = CreateFileUploadRequest.parse(await c.req.json()); const privateOwner = fileRequestAuthority.get(c.req.raw)?.owner ?? null; const personal = payload.scope === "personal" || (fileRequestAuthority.get(c.req.raw)?.agent && privateOwner !== null); if (personal && !privateOwner) throw new HTTPException(403, { message: "Personal uploads require the authenticated owner" }); await requireLimit(deps, { accountId: grant.accountId, workspaceId, action: "file:upload", quantity: payload.sizeBytes, }); if (payload.sizeBytes > objectStorage.maxSinglePutSizeBytes) { throw new HTTPException(413, { message: `file exceeds single PUT limit of ${objectStorage.maxSinglePutSizeBytes} bytes`, }); } const fileId = crypto.randomUUID(); const safeFilename = sanitizeFilename(payload.filename); const objectKey = `workspaces/${workspaceId}/files/${fileId}/original/${safeFilename}`; const signed = await objectStorage.createPutUrl({ key: objectKey, contentType: payload.contentType, ...(payload.sha256 ? { sha256: payload.sha256 } : {}), }); const upload = await createFileUpload(db, { accountId: grant.accountId, workspaceId, fileId, privateOwnerSubjectId: personal ? privateOwner : null, filename: payload.filename, safeFilename, contentType: payload.contentType, sizeBytes: payload.sizeBytes, sha256: payload.sha256 ?? null, bucket: objectStorage.bucket, objectKey, expiresAt: signed.expiresAt, }); // Every principal-facing signed URL issuance is a // metadata-only audit fact. Awaited before the URL leaves the platform: // no recorded fact, no bearer capability. Never the URL or object key. await recordAuditEvent(db, { accountId: grant.accountId, workspaceId, subjectId: grant.subjectId, action: "file.signed_upload.issued", targetType: "workspace_file", targetId: fileId, metadata: { fileId, contentType: payload.contentType, sizeBytes: payload.sizeBytes, expiresAt: signed.expiresAt.toISOString(), }, }); return c.json( CreateFileUploadResponse.parse({ fileId: upload.file.id, uploadId: upload.uploadId, putUrl: signed.url, requiredHeaders: signed.requiredHeaders, expiresAt: upload.expiresAt, maxSizeBytes: objectStorage.maxSinglePutSizeBytes, }), 201, ); }); app.post("/v1/workspaces/:workspaceId/files/uploads/:uploadId/complete", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireAccessGrant(c, deps, workspaceId, "files:upload"); if (!objectStorage) { throw new HTTPException(503, { message: "object storage is not configured", }); } const upload = await getFileUpload(db, workspaceId, c.req.param("uploadId")); if (!upload) { throw new HTTPException(404, { message: "file upload not found" }); } const recordUploadedFileUsage = async (file: typeof upload.file): Promise => { await recordWorkspaceUsage(deps, { accountId: grant.accountId, workspaceId, subjectId: grant.subjectId, eventType: "file.uploaded", quantity: file.sizeBytes, unit: "byte", sourceResourceType: "file", sourceResourceId: file.id, idempotencyKey: `file.uploaded:${workspaceId}:${file.id}`, }); }; const completeAndRecordUsage = async (): Promise => { let file: typeof upload.file; try { file = await completeFileUpload(db, workspaceId, upload.id); } catch (error) { // HEAD runs outside the DB transaction. If the expiry reaper claims the // row in that window, report the durable state instead of leaking an // internal 500. A concurrent successful finalize remains a normal // idempotent completion and still repairs usage below. const current = await getFileUpload(db, workspaceId, upload.id); if (current?.status === "completed" && current.file.status === "ready") { file = current.file; } else if (current && current.status !== "pending") { throw new HTTPException(409, { message: `file upload is ${publicFileUploadStatus(current.status)}`, }); } else { throw error; } } await recordUploadedFileUsage(file); return file; }; const rejectAndCleanObject = async ( status: 409 | 422, message: string, terminalStatus: "failed" | "expired", ): Promise => { const claim = await claimFileUploadCleanup(db, { workspaceId, uploadId: upload.id, fileId: upload.file.id, }); // A concurrent valid finalize may have committed while this request was // checking expiry/provider metadata. Never delete that ready object's // key; preserve normal idempotent finalize and repair usage instead. if (claim.outcome === "completed") { await recordUploadedFileUsage(claim.file); return claim.file; } if (claim.outcome === "unavailable") { throw new HTTPException(409, { message: `file upload is ${publicFileUploadStatus(claim.status)}`, }); } try { await objectStorage.deleteObject(upload.file.objectKey); } catch (error) { // The row remains cleanup_pending and the file non-ready. The global // reaper can reclaim the idempotent delete after its claim timeout. deps.observability?.warn( "file upload rejection cleanup failed; claim remains reclaimable", { workspaceId, fileId: upload.file.id, uploadId: upload.id, error: error instanceof Error ? error.message : String(error), }, ); throw new HTTPException(status, { message }); } const settled = await completeFileUploadCleanup(db, { accountId: grant.accountId, workspaceId, uploadId: upload.id, fileId: upload.file.id, terminalStatus, }); if (!settled) { throw new HTTPException(409, { message: "file upload cleanup claim was superseded", }); } throw new HTTPException(status, { message }); }; // A client can lose the response after the server commits completion, or two // tabs can race the same completion request. A ready object is durable and // safe to return again. Re-enter the row-locked DB finalize and retry the // idempotent usage write before returning: the previous request may have // died after completion committed but before `file.uploaded` was recorded. if (upload.status === "completed" && upload.file.status === "ready") { const file = await completeAndRecordUsage(); return c.json(CompleteFileUploadResponse.parse({ file })); } if (upload.status !== "pending") { throw new HTTPException(409, { message: `file upload is ${publicFileUploadStatus(upload.status)}`, }); } if (upload.expiresAt.getTime() < Date.now()) { const file = await rejectAndCleanObject(409, "file upload has expired", "expired"); return c.json(CompleteFileUploadResponse.parse({ file })); } const head = await retryWhileMissing(async () => { if (!(await objectStorage.fileExists(upload.file))) return null; return await objectStorage.headFile(upload.file); }); if (!head) { throw new HTTPException(409, { message: "uploaded object is not available" }); } if (Number(head.ContentLength ?? -1) !== upload.file.sizeBytes) { const file = await rejectAndCleanObject( 422, "uploaded object size does not match file metadata", "failed", ); return c.json(CompleteFileUploadResponse.parse({ file })); } if ( upload.file.contentType && head.ContentType && head.ContentType !== upload.file.contentType ) { const file = await rejectAndCleanObject( 422, "uploaded object content type does not match file metadata", "failed", ); return c.json(CompleteFileUploadResponse.parse({ file })); } if (upload.file.sha256 && head.Metadata?.sha256 !== upload.file.sha256) { const file = await rejectAndCleanObject( 422, "uploaded object checksum metadata does not match file metadata", "failed", ); return c.json(CompleteFileUploadResponse.parse({ file })); } const file = await completeAndRecordUsage(); return c.json(CompleteFileUploadResponse.parse({ file })); }); app.get("/v1/workspaces/:workspaceId/files", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireAccessGrant(c, deps, workspaceId, "files:read"); const parsed = FileListRequest.safeParse({ ...c.req.query(), ...(c.req.query("limit") !== undefined ? { limit: Number(c.req.query("limit")) } : {}), }); if (!parsed.success) throw new HTTPException(422, { message: "Invalid file list request" }); c.header("cache-control", "private, no-store"); return c.json( FileListResponse.parse( await listFilesForSubject(db, { accountId: grant.accountId, workspaceId, subjectId: grant.subjectId, ...parsed.data, }).catch((error: unknown) => { if (error instanceof ZodError) throw new HTTPException(422, { message: "Invalid file cursor" }); throw error; }), ), ); }); app.get("/v1/workspaces/:workspaceId/files/:fileId", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireAccessGrant(c, deps, workspaceId, "files:read"); const file = await requireFileForSubject(db, { accountId: grant.accountId, workspaceId, subjectId: grant.subjectId, fileId: c.req.param("fileId"), }).catch(() => null); if (!file) { throw new HTTPException(404, { message: "file not found" }); } return c.json(FileAsset.parse(file)); }); app.get("/v1/workspaces/:workspaceId/artifacts/:artifactId", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireAccessGrant(c, deps, workspaceId, "files:read"); const artifactId = retainedArtifactId(c.req.param("artifactId")); const artifact = await getWorkspaceArtifact(db, { accountId: grant.accountId, workspaceId, subjectId: await fileAuthoritySubjectIdForGrant(deps, grant), artifactId, }); if (!artifact) { return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404); } return c.json(artifact.metadata); }); app.get("/v1/workspaces/:workspaceId/artifacts/:artifactId/content", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireAccessGrant(c, deps, workspaceId, "files:read"); const artifactId = retainedArtifactId(c.req.param("artifactId")); const artifact = await getWorkspaceArtifact(db, { accountId: grant.accountId, workspaceId, subjectId: await fileAuthoritySubjectIdForGrant(deps, grant), artifactId, }); if (!artifact) { return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404); } return await serveRetainedArtifactContent( c, objectStorage, artifactId, artifact.file, artifact.metadata, ); }); app.post("/v1/workspaces/:workspaceId/artifacts/:artifactId/playback-source", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireAccessGrant(c, deps, workspaceId, "files:read"); if (!objectStorage) { throw new HTTPException(503, { message: "object storage is not configured" }); } const artifactId = retainedArtifactId(c.req.param("artifactId")); const retained = await getGeneratedVideoArtifact(db, workspaceId, artifactId); if (!retained || retained.artifact.deletedAt) { throw new HTTPException(404, { message: "generated video not found" }); } if (retained.file.status !== "ready") { throw new HTTPException(409, { message: `generated video is ${retained.file.status}` }); } const file = generatedVideoFileAsset(retained.file); const videoPresent = await retryWhileMissing(async () => (await objectStorage.fileExists(file)) ? true : null, ); if (!videoPresent) { throw new HTTPException(410, { message: "generated video bytes are unavailable" }); } const signed = await objectStorage.createGetUrl({ key: file.objectKey }); await recordAuditEvent(db, { accountId: grant.accountId, workspaceId, subjectId: grant.subjectId, action: "file.signed_url.issued", targetType: "retained_artifact", targetId: artifactId, metadata: { artifactId, kind: "video_playback", expiresAt: signed.expiresAt.toISOString(), }, }); return c.json( VideoArtifactPlaybackSource.parse({ schemaVersion: 1, artifactId, url: signed.url, expiresAt: signed.expiresAt.toISOString(), contentType: "video/mp4", sizeBytes: file.sizeBytes, sha256: file.sha256, acceptRanges: "bytes", }), ); }); // These two retained-screenshot routes are registered before the session // route module, so its `/sessions/:sessionId/*` authorization middleware // does not cover them: an agent attempt must pass the same core seam here // (private sessions, Slack ownership, and the 0426 agent-access scope) so a // known artifact id cannot read into a tree the attempt may not reach. const requireAgentSessionAccess = async ( c: Context, grant: Awaited>, ): Promise => { if (!grantHasAgentAttemptAuthority(grant)) return; try { await requireSessionAuthorization(deps, grant, { sessionId: c.req.param("sessionId") ?? "", operation: "session.read", surface: "http", }); } catch (error) { if (error instanceof SessionAuthorizationDeniedError) { throw new HTTPException(404, { message: "session not found", cause: error }); } if (error instanceof SessionAuthorizationUnavailableError) { throw new HTTPException(503, { message: "session authorization is unavailable", cause: error, }); } throw error; } }; app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/artifacts/:artifactId", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireAccessGrant(c, deps, workspaceId, "files:read"); await requireAgentSessionAccess(c, grant); const artifactId = retainedArtifactId(c.req.param("artifactId")); const artifact = await getRetainedScreenshotArtifact( db, workspaceId, c.req.param("sessionId"), artifactId, ); if (!artifact) { return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404); } return c.json(retainedScreenshotMetadata(artifact)); }); app.get( "/v1/workspaces/:workspaceId/sessions/:sessionId/artifacts/:artifactId/content", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireAccessGrant(c, deps, workspaceId, "files:read"); await requireAgentSessionAccess(c, grant); const artifactId = retainedArtifactId(c.req.param("artifactId")); const artifact = await getRetainedScreenshotArtifact( db, workspaceId, c.req.param("sessionId"), artifactId, ); if (!artifact) { return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404); } return await serveRetainedArtifactContent( c, objectStorage, artifactId, artifact.file, retainedScreenshotMetadata(artifact), ); }, ); app.post("/v1/workspaces/:workspaceId/files/:fileId/download-url", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireAccessGrant(c, deps, workspaceId, "files:read"); if (!objectStorage) { throw new HTTPException(503, { message: "object storage is not configured", }); } const file = await requireFileForSubject(db, { accountId: grant.accountId, workspaceId, subjectId: grant.subjectId, fileId: c.req.param("fileId"), }).catch(() => null); if (!file) { throw new HTTPException(404, { message: "file not found" }); } if (file.status !== "ready") { throw new HTTPException(409, { message: `file is ${file.status}` }); } const present = await retryWhileMissing(async () => (await objectStorage.fileExists(file)) ? true : null, ); if (!present) { throw new HTTPException(410, { message: "file bytes are unavailable" }); } const signed = await objectStorage.createGetUrl({ key: file.objectKey }); await recordAuditEvent(db, { accountId: grant.accountId, workspaceId, subjectId: grant.subjectId, action: "file.signed_url.issued", targetType: "workspace_file", targetId: file.id, metadata: { fileId: file.id, kind: "download", expiresAt: signed.expiresAt.toISOString(), }, }); return c.json( FileDownloadUrlResponse.parse({ url: signed.url, expiresAt: signed.expiresAt.toISOString(), }), ); }); } async function serveRetainedArtifactContent( c: Context, objectStorage: ApiRouteDeps["objectStorage"], artifactId: string, file: RetainedFileArtifact["file"], metadata: RetainedArtifactMetadata, ) { if (!metadata.available) { return c.json(metadata, retainedArtifactUnavailableStatus(metadata.reason)); } if (!objectStorage) { return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 503); } const rangeHeader = c.req.header("range"); const range = resolveRetainedOutputRange( rangeHeader, metadata.originalBytes, rangeHeader ? RETAINED_OUTPUT_MAX_PAGE_BYTES : RETAINED_OUTPUT_DEFAULT_PAGE_BYTES, ); if (range.kind === "invalid") { return c.json( { message: "invalid retained artifact byte range", reason: range.reason, maxRangeBytes: RETAINED_OUTPUT_MAX_PAGE_BYTES, }, 400, ); } if (range.kind === "unsatisfiable") { return c.json( { message: "retained artifact byte range is not satisfiable", reason: range.reason, }, 416, { "Accept-Ranges": "bytes", "Content-Range": range.contentRange, "Cache-Control": "private, no-store", }, ); } const headers = { "Accept-Ranges": range.acceptRanges, "Cache-Control": "private, no-store", "Content-Length": String(range.length), "Content-Type": metadata.contentType, "X-Content-Type-Options": "nosniff", ...(range.contentRange ? { "Content-Range": range.contentRange } : {}), }; if (range.kind === "empty") { const present = await retryWhileMissing(async () => (await objectStorage.fileExists(file)) ? true : null, ); if (!present) { return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410); } return c.body(null, 200, headers); } const bytes = await retryWhileMissing(async () => objectStorage.getFileRange(file, { start: range.start, end: range.end, }), ); if (!bytes) { return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410); } if (bytes.byteLength !== range.length) { throw new HTTPException(502, { message: "object storage returned an invalid byte range", }); } return c.body(new Uint8Array(bytes), range.status, headers); } export function sanitizeFilename(filename: string): string { const trimmed = filename.trim().replace(/[/\\]/g, "_"); const safe = trimmed .replace(/[^A-Za-z0-9._ -]+/g, "_") .replace(/\s+/g, " ") .trim(); return safe || "file"; } /** Keep the cleanup lease internal; clients only consume terminal upload states. */ function publicFileUploadStatus(status: string): string { return status === "cleanup_pending" ? "failed" : status; } function retainedArtifactId(value: string): string { const parsed = FileAsset.shape.id.safeParse(value); if (!parsed.success) { throw new HTTPException(404, { message: "artifact not found" }); } return parsed.data; } function retainedArtifactUnavailable( artifactId: string, reason: RetainedOutputUnavailableReason, ): RetainedArtifactMetadata { return RetainedArtifactMetadataSchema.parse({ available: false, artifactId, reason, }); } function retainedArtifactMetadata(artifact: RetainedFileArtifact): RetainedArtifactMetadata { const reference = retainedArtifactReferenceFromFile(artifact.file); if (reference) return reference; const { file, uploadStatus, uploadExpiresAt } = artifact; if (file.status === "deleted") { return retainedArtifactUnavailable(file.id, "deleted"); } if ( file.status === "expired" || uploadStatus === "expired" || (uploadStatus === "pending" && uploadExpiresAt !== null && uploadExpiresAt.getTime() < Date.now()) ) { return retainedArtifactUnavailable(file.id, "expired"); } if (file.status === "failed" || uploadStatus === "failed" || uploadStatus === "cleanup_pending") { return retainedArtifactUnavailable(file.id, "failed"); } if (file.status === "pending_upload" || uploadStatus === "pending") { return retainedArtifactUnavailable(file.id, "pending"); } return retainedArtifactUnavailable(file.id, "unsupported"); } async function getWorkspaceArtifact( db: ApiRouteDeps["db"], input: { accountId: string; workspaceId: string; subjectId: string | null; artifactId: string; }, ): Promise<{ file: RetainedFileArtifact["file"]; metadata: RetainedArtifactMetadata } | null> { const generated = await getGeneratedImageArtifact(db, input.workspaceId, input.artifactId); if (generated) { return { file: generated.file, metadata: retainedGeneratedImageMetadata(generated) }; } const generatedVideo = await getGeneratedVideoArtifact(db, input.workspaceId, input.artifactId); if (generatedVideo && !generatedVideo.artifact.deletedAt) { const file = generatedVideoFileAsset(generatedVideo.file); return { file, metadata: retainedGeneratedVideoMetadata({ artifact: generatedVideo.artifact, file, }), }; } const artifact = await getRetainedFileArtifact(db, input.workspaceId, input.artifactId); if (!artifact) return null; const [authorizedFile] = await getFilesForSubject(db, { accountId: input.accountId, workspaceId: input.workspaceId, subjectId: input.subjectId, fileIds: [artifact.file.id], }); if (!authorizedFile) return null; const authorizedArtifact = { ...artifact, file: authorizedFile }; return { file: authorizedFile, metadata: retainedArtifactMetadata(authorizedArtifact), }; } /** Human and stable service grants already carry their immutable subject. * Agent tokens carry only a technical worker subject, so resolve the exact * live attempt and use the initiating human frozen on its durable turn. */ export async function fileAuthoritySubjectIdForGrant( deps: Pick, grant: import("@opengeni/contracts").AccessGrant, ): Promise { if (grant.principalKind !== "agent_attempt") return grant.subjectId; const sessionId = grant.metadata?.["sessionId"]; if (typeof sessionId !== "string") { throw new HTTPException(404, { message: "file not found" }); } try { const actor = await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId); return actor.initiatingHumanSubjectId; } catch (error) { if (error instanceof SessionAuthorizationDeniedError) { throw new HTTPException(404, { message: "file not found", cause: error }); } throw error; } } function generatedVideoFileAsset( file: NonNullable>>["file"], ): RetainedFileArtifact["file"] { return FileAsset.parse({ ...file, createdAt: file.createdAt.toISOString(), updatedAt: file.updatedAt.toISOString(), }); } function retainedGeneratedVideoMetadata(artifact: { artifact: GeneratedVideoArtifact; file: RetainedFileArtifact["file"]; }): RetainedArtifactMetadata { const reference = retainedGeneratedVideoReferenceFromFile({ ...artifact.file, id: artifact.artifact.id, width: artifact.artifact.width, height: artifact.artifact.height, updatedAt: artifact.artifact.readyAt.toISOString(), }); return reference ?? retainedArtifactUnavailable(artifact.artifact.id, "failed"); } function retainedGeneratedImageMetadata( artifact: GeneratedImageArtifact, ): RetainedArtifactMetadata { if (artifact.status === "ready") { const reference = retainedGeneratedImageReferenceFromFile({ ...artifact.file, width: artifact.width, height: artifact.height, }); if (reference) return reference; } return retainedArtifactUnavailable(artifact.artifactId, "pending"); } function retainedScreenshotMetadata( artifact: RetainedScreenshotArtifact, ): RetainedArtifactMetadata { if ( artifact.status === "ready" && artifact.sessionId && artifact.retentionExpiresAt.getTime() > Date.now() ) { const reference = retainedScreenshotReferenceFromFile({ ...artifact.file, sessionId: artifact.sessionId, width: artifact.width, height: artifact.height, expiresAt: artifact.retentionExpiresAt.toISOString(), }); if (reference) return reference; } if (artifact.status === "deleted") { return retainedArtifactUnavailable(artifact.artifactId, "deleted"); } if (artifact.status === "expired" || artifact.retentionExpiresAt.getTime() <= Date.now()) { return retainedArtifactUnavailable(artifact.artifactId, "expired"); } if (artifact.status === "pending" || artifact.status === "reconciling") { return retainedArtifactUnavailable(artifact.artifactId, "pending"); } return retainedArtifactUnavailable(artifact.artifactId, "failed"); } function retainedArtifactUnavailableStatus( reason: RetainedOutputUnavailableReason, ): 404 | 409 | 410 | 422 { switch (reason) { case "deleted": return 404; case "expired": case "missing_storage": return 410; case "unsupported": case "not_retained": case "storage_write_failed": case "quota_exceeded": case "invalid_content": case "oversized": return 422; case "pending": case "failed": return 409; } }