Files
MikrotikManager/shared/api/mcp-artifact.ts

57 lines
2.9 KiB
TypeScript

import { z } from "zod"
import { getClaims, getToken } from "@/lib/auth"
import { ApiClientError, requestBlob } from "./http-client"
const receiptSchema = z.object({
artifactId: z.string().uuid(),
humanDownloadOnly: z.literal(true),
downloadPath: z.string().optional(),
})
/** Only structured receipts qualify; supplied paths never become navigation targets. */
export function humanArtifactReceipt(value: unknown) {
const parsed = receiptSchema.safeParse(value)
if (!parsed.success) return null
const path = `/api/mcp/artifacts/${parsed.data.artifactId}`
if (parsed.data.downloadPath !== undefined && parsed.data.downloadPath !== path) return null
return { artifactId: parsed.data.artifactId, path }
}
export async function downloadHumanArtifact(base: string, receipt: unknown, signal?: AbortSignal) {
const artifact = humanArtifactReceipt(receipt)
if (!artifact) throw new Error("Некорректная квитанция файла")
const claims = getClaims()
if (!getToken() || !claims?.sub || !claims.apps?.includes("mm")) {
throw new ApiClientError("Войдите в портал для скачивания файла", 401)
}
const response = await requestBlob(base, artifact.path, { signal, cache: "no-store", redirect: "error" })
const blob = await response.blob()
signal?.throwIfAborted()
const disposition = response.headers.get("Content-Disposition") ?? ""
const suggested = /filename="([a-zA-Z0-9._-]{1,200})"/.exec(disposition)?.[1]
const filename = suggested && suggested !== "." && suggested !== ".." ? suggested : `mcp-${artifact.artifactId}.bin`
const url = URL.createObjectURL(blob)
const link = document.createElement("a")
try {
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
} finally {
link.remove()
// Let the browser begin consuming the download before releasing the in-memory file.
setTimeout(() => URL.revokeObjectURL(url), 1000)
}
}
export function humanArtifactError(error: unknown): string {
if (error instanceof ApiClientError) {
if (error.status === 401) return "Войдите в портал для скачивания файла."
if (error.status === 403) return "Доступ к файлу отозван или файл принадлежит другому пользователю."
if (error.status === 404 || error.status === 410) return "Файл отсутствует или срок хранения истёк. Подготовьте новую выгрузку."
if (error.status === 409) return "Файл изменился или больше не соответствует квитанции. Подготовьте новую выгрузку."
}
// Do not render response bodies, filenames, credentials, or binary content as errors.
return "Не удалось скачать файл. Проверьте соединение и повторите попытку."
}