Download an asset
Take a download URL from a result, resolve it, fetch the bytes, and verify them.
You do not download an asset by ID. Every asset from folder browse or search already carries a downloadUrl. This guide fetches the file behind that URL. It assumes you completed the authentication flow.
Fetch the bytes
Most results return a CI HUB proxy downloadUrl (it contains /api/v1/assets/download); provider-direct URL forms carry $...$ placeholders instead. The client library's download() resolves every form and attaches the right credentials by itself. On the HTTP path, keep the cihubSig parameter and send both tokens for proxy URLs, and resolve the other forms with the steps in Asset URLs.
import { writeFile } from 'node:fs/promises'
import { basename } from 'node:path'
async function downloadAsset(asset: { id: string; name: string; downloadUrl: string }) {
const response = await tokens.withDamAuth('dropbox', (cihubToken, damToken) =>
client.download({ url: asset.downloadUrl, damToken, cihubToken }),
)
const bytes = Buffer.from(await response.arrayBuffer())
// asset.name is provider-supplied; strip path components so it can't escape the target directory.
const fileName = basename(asset.name ?? '').replace(/[/\\]/g, '') || asset.id
await writeFile(fileName, bytes)
return bytes
}import { writeFile } from 'node:fs/promises'
import { basename } from 'node:path'
const authHeaders = {
Authorization: `Bearer ${ciHubAccessToken}`,
'provider-authorization': `Bearer ${damProviderToken}`,
}
async function downloadAsset(asset: { id: string; name: string; downloadUrl: string }) {
const isProxy = asset.downloadUrl.includes('/api/v1/assets/download')
// Proxy URLs keep the cihubSig parameter and carry both tokens. Provider-direct
// URLs are resolved per Asset URLs and must never receive the CI HUB tokens;
// resolvePlaceholders is the resolution described on that page.
const url = isProxy ? asset.downloadUrl : resolvePlaceholders(asset.downloadUrl)
const response = await fetch(url, isProxy ? { headers: authHeaders } : undefined)
if (!response.ok) {
// These media URLs reply with a bare HTTP status, not the error envelope.
throw new Error(`download failed: HTTP ${response.status}`)
}
const bytes = Buffer.from(await response.arrayBuffer())
// asset.name is provider-supplied; strip path components so it can't escape the target directory.
const fileName = basename(asset.name ?? '').replace(/[/\\]/g, '') || asset.id
await writeFile(fileName, bytes)
return bytes
}Get a ready URL instead of bytes
To hand a URL to an <img> tag or a browser download rather than stream the bytes yourself, append noRedirect=true to a CI HUB proxy downloadUrl (the /api/v1/assets/download form; provider-direct URLs do not support it). The call returns { "downloadUrl": "..." } pointing at the file. This is a raw HTTP call whichever idiom you integrate with; the client library has no wrapper for it.
async function resolveDownloadUrl(asset: { downloadUrl: string }) {
const url = new URL(asset.downloadUrl)
url.searchParams.set('noRedirect', 'true')
const response = await fetch(url, { headers: authHeaders })
if (!response.ok) {
// The download endpoint replies with a plain status and short text body, not the JSON envelope.
const detail = await response.text()
throw new Error(`Download URL resolution failed (${response.status}): ${detail || response.statusText}`)
}
const { downloadUrl } = await response.json()
return downloadUrl
}Verify the bytes
Some providers include a content hash on the asset. When present, hash the bytes you received and compare. The field name and algorithm are provider-specific, so read them from the asset's fields rather than assuming; treat the check as optional integrity. See Asset model.
import { createHash } from 'node:crypto'
function verify(bytes: Buffer, expectedHash: string, algorithm = 'sha256') {
const actual = createHash(algorithm).update(bytes).digest('hex')
if (actual !== expectedHash) {
throw new Error('hash mismatch: the downloaded bytes do not match the asset hash')
}
}