fix(web): avoid CSP errors when downloading skills

This commit is contained in:
vsxd 2026-03-16 21:03:37 +08:00
parent 316a86628c
commit 330f64cedd
2 changed files with 20 additions and 39 deletions

View file

@ -37,9 +37,8 @@ export { ApiError }
export const WEB_API_PREFIX = '/api/web'
export type DownloadedFile = {
blob: Blob
fileName?: string
export type SkillDownloadRequest = {
url: string
}
type RuntimeConfig = {
@ -278,20 +277,6 @@ function ensureTrailingSlash(value: string): string {
return value.endsWith('/') ? value : `${value}/`
}
function parseDownloadFileName(contentDisposition: string | null): string | undefined {
if (!contentDisposition) {
return undefined
}
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i)
if (utf8Match) {
return decodeURIComponent(utf8Match[1])
}
const basicMatch = contentDisposition.match(/filename="?([^";]+)"?/i)
return basicMatch?.[1]
}
export async function getCurrentUser(): Promise<User | null> {
try {
const user = await unwrap<User>(client.GET('/api/v1/auth/me', {
@ -442,23 +427,25 @@ export const accountApi = {
}
export const skillDownloadApi = {
async downloadVersion(namespace: string, slug: string, version: string): Promise<DownloadedFile> {
async downloadVersion(namespace: string, slug: string, version: string): Promise<SkillDownloadRequest> {
const cleanNamespace = namespace.startsWith('@') ? namespace.slice(1) : namespace
const response = await fetch(
withBaseUrl(`${WEB_API_PREFIX}/skills/${cleanNamespace}/${slug}/versions/${version}/download`),
{
headers: withRequestHeaders(),
},
)
const requestUrl = withBaseUrl(`${WEB_API_PREFIX}/skills/${cleanNamespace}/${slug}/versions/${version}/download`)
const url = typeof requestUrl === 'string' ? requestUrl : requestUrl.toString()
if (!response.ok) {
const response = await fetch(requestUrl, {
headers: withRequestHeaders(),
redirect: 'manual',
})
if (response.type === 'opaqueredirect') {
return { url }
}
if (!response.ok && response.status !== 0 && response.status !== 302) {
throw new ApiError(`HTTP ${response.status}`, response.status)
}
return {
blob: await response.blob(),
fileName: parseDownloadFileName(response.headers.get('content-disposition')),
}
return { url }
},
}

View file

@ -137,15 +137,12 @@ export function SkillDetailPage() {
const submitPromotionMutation = useSubmitPromotion()
const reportMutation = useSubmitSkillReport(namespace, slug)
const triggerBrowserDownload = (blob: Blob, fileName: string) => {
const objectUrl = window.URL.createObjectURL(blob)
const triggerBrowserDownload = (url: string) => {
const link = document.createElement('a')
link.href = objectUrl
link.download = fileName
link.href = url
document.body.appendChild(link)
link.click()
link.remove()
window.setTimeout(() => window.URL.revokeObjectURL(objectUrl), 0)
}
const handleDownload = async () => {
@ -158,11 +155,8 @@ export function SkillDetailPage() {
}
try {
const downloadedFile = await skillDownloadApi.downloadVersion(namespace, slug, selectedVersionEntry.version)
triggerBrowserDownload(
downloadedFile.blob,
downloadedFile.fileName ?? `${slug}-${selectedVersionEntry.version}.zip`,
)
const downloadRequest = await skillDownloadApi.downloadVersion(namespace, slug, selectedVersionEntry.version)
triggerBrowserDownload(downloadRequest.url)
incrementSkillDownloadCount(queryClient, { namespace, slug })
queryClient.invalidateQueries({ queryKey: ['skills', namespace, slug] })
queryClient.invalidateQueries({ queryKey: ['skills', 'my'] })