const path = require("node:path"); const { Blob } = require("node:buffer"); const { TextDecoder } = require("node:util"); const { HubApiError, createRepo, deleteFile, deleteRepo, downloadFile, listCommits, listDatasets, listFiles, listModels, listSpaces, modelInfo, pathsInfo, spaceInfo, uploadFile, whoAmI, datasetInfo, } = require("@huggingface/hub"); const HUB_URL = (process.env.HF_API_BASE || "https://huggingface.co").replace(/\/$/, ""); const MAX_DASHBOARD_ITEMS = 100; const MAX_COMMUNITY_ITEMS = 16; const MAX_FILE_LIST_ITEMS = 500; const MAX_FILE_PREVIEW_BYTES = 512 * 1024; const MAX_EDITABLE_FILE_BYTES = 256 * 1024; class HfApiError extends Error { constructor(statusCode, publicMessage, detail) { super(detail || publicMessage); this.name = "HfApiError"; this.statusCode = statusCode; this.publicMessage = publicMessage; } } function pluralFor(type) { return `${type}s`; } function repoRef(type, repoId) { return { type, name: repoId }; } function defaultTabFor(type) { if (type === "space") { return "app"; } if (type === "bucket") { return "files"; } return "overview"; } function hubRepoUrl(type, repoId) { if (type === "model") { return `${HUB_URL}/${repoId}`; } return `${HUB_URL}/${pluralFor(type)}/${repoId}`; } function spaceAppUrl(space) { if (!space.subdomain) { return hubRepoUrl("space", space.id); } return `https://${space.subdomain}.hf.space`; } function toIsoString(value) { if (!value) { return null; } if (value instanceof Date) { return Number.isNaN(value.getTime()) ? null : value.toISOString(); } const parsed = new Date(value); return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString(); } function splitRepoId(repoId) { const [owner, ...rest] = String(repoId || "").split("/"); return { owner: owner || "", name: rest.join("/"), }; } function normalizeRepoSegment(value, label) { const trimmed = String(value || "").trim(); if (!trimmed) { throw new HfApiError(400, `That ${label} is required.`); } if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(trimmed)) { throw new HfApiError(400, `That ${label} is invalid.`); } return trimmed; } function normalizeRepoIdInput(namespace, name) { return `${normalizeRepoSegment(namespace, "namespace")}/${normalizeRepoSegment(name, "name")}`; } function workspaceResourceUrl(type, repoId) { const { owner, name } = splitRepoId(repoId); return `/${pluralFor(type)}/${owner}/${name}/${defaultTabFor(type)}`; } function normalizeRemotePath(rawPath) { const value = String(rawPath || "") .trim() .replaceAll("\\", "/") .replace(/^\/+/, ""); if (!value) { return ""; } const normalized = path.posix.normalize(value); if (normalized === "." || normalized === "/") { return ""; } if (normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) { throw new HfApiError(400, "That file path is invalid."); } return normalized.replace(/^\/+/, ""); } function normalizeBranchName(branch) { const value = String(branch || "").trim(); if (!value) { return ""; } if (!/^[A-Za-z0-9._/-]+$/.test(value) || value.includes("..")) { throw new HfApiError(400, "That branch name is invalid."); } return value; } function dirname(remotePath) { const parent = path.posix.dirname(remotePath || ""); return parent === "." ? "" : parent; } async function collectAsync(generator, limit = Infinity) { const items = []; for await (const item of generator) { items.push(item); if (items.length >= limit) { break; } } return items; } function makeHubFetch(accessToken) { return async function hubFetch(input, init = {}) { const headers = new Headers(init.headers || {}); if (accessToken && !headers.has("Authorization")) { headers.set("Authorization", `Bearer ${accessToken}`); } return fetch(input, { ...init, headers, }); }; } async function parseJsonResponse(response) { if (!response) { return null; } const text = await response.text(); if (!text) { return null; } try { return JSON.parse(text); } catch (error) { return text; } } function toPermissionSummary(viewer, repoOwner) { const namespaces = new Set(viewer.namespaces || []); return { tokenRole: viewer.tokenRole, canAttemptWrite: viewer.tokenRole !== "read" && namespaces.has(repoOwner), repoOwner, }; } function summarizeViewer(identity) { const orgs = identity.type === "user" ? identity.orgs || [] : []; const namespaces = identity.type === "user" ? [identity.name, ...orgs.map((org) => org.name)] : [identity.name]; return { username: identity.name || identity.fullname || "hf-user", fullname: identity.fullname || identity.name || "Hugging Face user", avatarUrl: identity.avatarUrl || "", email: identity.email || "", type: identity.type || "user", isPro: Boolean(identity.isPro), tokenRole: identity.auth?.accessToken?.role || "read", tokenName: identity.auth?.accessToken?.displayName || "Access token", tokenCreatedAt: toIsoString(identity.auth?.accessToken?.createdAt), orgs: orgs.map((org) => ({ name: org.name, fullname: org.fullname || org.name, avatarUrl: org.avatarUrl || "", })), namespaces, profileUrl: identity.name ? `${HUB_URL}/${identity.name}` : HUB_URL, }; } function normalizeSpaceEntry(entry) { const repoName = entry.name || entry.id; const { owner, name } = splitRepoId(repoName); return { kind: "space", id: repoName, owner, name, title: entry.title || entry.cardData?.title || name, private: Boolean(entry.private), likes: entry.likes || 0, updatedAt: toIsoString(entry.updatedAt || entry.lastModified), createdAt: toIsoString(entry.createdAt), sdk: entry.sdk || entry.cardData?.sdk || null, runtimeStage: entry.runtime?.stage || null, subdomain: entry.subdomain || null, linkedModels: Array.isArray(entry.models) ? entry.models : [], linkedDatasets: Array.isArray(entry.datasets) ? entry.datasets : [], url: `/spaces/${owner}/${name}/app`, hubUrl: hubRepoUrl("space", repoName), }; } function normalizeModelEntry(entry) { const repoName = entry.name || entry.id; const { owner, name } = splitRepoId(repoName); return { kind: "model", id: repoName, owner, name, private: Boolean(entry.private), downloads: entry.downloads || 0, likes: entry.likes || 0, task: entry.task || entry.pipeline_tag || null, library: entry.library_name || null, updatedAt: toIsoString(entry.updatedAt || entry.lastModified), createdAt: toIsoString(entry.createdAt), url: `/models/${owner}/${name}/overview`, hubUrl: hubRepoUrl("model", repoName), }; } function normalizeDatasetEntry(entry) { const repoName = entry.name || entry.id; const { owner, name } = splitRepoId(repoName); return { kind: "dataset", id: repoName, owner, name, private: Boolean(entry.private), downloads: entry.downloads || 0, likes: entry.likes || 0, updatedAt: toIsoString(entry.updatedAt || entry.lastModified), createdAt: toIsoString(entry.createdAt), description: entry.description || entry.cardData?.description || "", url: `/datasets/${owner}/${name}/overview`, hubUrl: hubRepoUrl("dataset", repoName), }; } function namespaceValue(value) { if (!value) { return ""; } if (typeof value === "string") { return value; } if (typeof value === "object") { return String(value.name || value.id || value.slug || value.namespace || ""); } return ""; } function normalizeBucketRepoIdValue(value) { return String(value || "") .trim() .replace(/^buckets\//, "") .replace(/^bucket\//, ""); } function normalizeBucketEntry(entry, fallbackNamespace = "") { const rawName = normalizeBucketRepoIdValue( entry.name || entry.bucket || entry.slug || entry.bucketName || entry.repoName || "", ); const rawId = normalizeBucketRepoIdValue( entry.id || entry.repoId || entry.repo?.id || entry.path || (rawName.includes("/") ? rawName : ""), ); const namespace = namespaceValue(entry.namespace || entry.owner || entry.author || entry.repo?.owner || entry.createdBy) || String(fallbackNamespace || "").trim(); const bucketName = rawId ? splitRepoId(rawId).name : rawName.includes("/") ? splitRepoId(rawName).name : rawName; const id = rawId || (namespace && bucketName ? `${namespace}/${bucketName}` : rawName); const parsedId = splitRepoId(id); const owner = parsedId.name ? parsedId.owner : namespace || parsedId.owner; const name = parsedId.name || bucketName || entry.slug || ""; return { kind: "bucket", id, owner, name, private: Boolean(entry.private), size: Number(entry.size || entry.totalSize || entry.total_size || entry.usedStorage || entry.used_storage || 0), fileCount: Number( entry.fileCount || entry.totalFiles || entry.total_files || entry.objectCount || entry.object_count || 0, ), updatedAt: toIsoString(entry.updatedAt || entry.updated_at || entry.lastModified || entry.modifiedAt || entry.modified_at), createdAt: toIsoString(entry.createdAt || entry.created_at), region: entry.region || entry.location || entry.storageRegion || entry.storage_region || null, url: `/buckets/${owner}/${name}/files`, hubUrl: hubRepoUrl("bucket", id), }; } function compareByUpdatedDesc(a, b) { return (b.updatedAt || "").localeCompare(a.updatedAt || "") || a.id.localeCompare(b.id); } function uniqueById(items) { const seen = new Set(); return items.filter((item) => { if (seen.has(item.id)) { return false; } seen.add(item.id); return true; }); } function wrapHubError(error, fallbackMessage = "Hugging Face request failed.") { if (error instanceof HfApiError) { return error; } if (error instanceof HubApiError) { if (error.statusCode === 401) { return new HfApiError(401, "That Hugging Face access token is no longer valid.", error.message); } if (error.statusCode === 403) { return new HfApiError(403, "You do not have permission to do that with this access token.", error.message); } if (error.statusCode === 404) { return new HfApiError(404, "That Hugging Face resource was not found.", error.message); } return new HfApiError(502, fallbackMessage, error.message); } return new HfApiError(502, fallbackMessage, error && error.message ? error.message : String(error)); } async function safeList(fn) { try { return await fn(); } catch (error) { const wrapped = wrapHubError(error); if (wrapped.statusCode === 403 || wrapped.statusCode === 404) { return []; } throw wrapped; } } async function fetchJsonWithFallback(urls, accessToken) { const hubFetch = makeHubFetch(accessToken); let lastError = null; for (const url of urls) { try { const response = await hubFetch(url, { headers: { Accept: "application/json", }, }); if (!response.ok) { if (response.status === 404) { lastError = new HfApiError(404, "Not found."); continue; } throw new HfApiError( response.status, response.status === 403 ? "You do not have permission to access that Hugging Face resource." : "Hugging Face returned an unexpected response.", ); } return response.json(); } catch (error) { lastError = error; } } if (lastError) { throw wrapHubError(lastError); } throw new HfApiError(404, "Not found."); } async function hubJson(accessToken, url, init = {}, fallbackMessage = "Hugging Face request failed.") { const response = await makeHubFetch(accessToken)(url, init).catch((error) => { throw wrapHubError(error, fallbackMessage); }); const payload = await parseJsonResponse(response); if (!response.ok) { const detail = payload && typeof payload === "object" ? payload.error || payload.message || payload.detail || JSON.stringify(payload) : payload || response.statusText; throw wrapHubError(new HfApiError(response.status, fallbackMessage, detail), fallbackMessage); } return payload; } async function optionalHubJson(accessToken, url, init = {}, fallbackMessage = "Hugging Face request failed.") { try { return await hubJson(accessToken, url, init, fallbackMessage); } catch (error) { const wrapped = wrapHubError(error, fallbackMessage); if (wrapped.statusCode === 403 || wrapped.statusCode === 404) { return null; } throw wrapped; } } function extractRows(payload, keys = []) { if (Array.isArray(payload)) { return payload; } for (const key of keys) { if (Array.isArray(payload?.[key])) { return payload[key]; } } return []; } function looksLikeSpaceConfigKey(key) { const text = String(key || "").trim(); return /^[A-Za-z_][A-Za-z0-9_]*$/.test(text) && (text.includes("_") || /\d/.test(text) || text === text.toUpperCase()); } function hasSpaceConfigFields(value) { if (!value || typeof value !== "object" || Array.isArray(value)) { return false; } return [ "key", "name", "id", "description", "helper", "help", "value", "content", "rawValue", "updatedAt", "updated_at", "lastModified", "hidden", "masked", ].some((field) => field in value); } function extractSpaceConfigRows(payload, key) { const rows = extractRows(payload, [key, "items"]); if (rows.length) { return rows; } const containers = [payload?.[key], payload?.items, payload].filter( (entry, index, items) => entry && typeof entry === "object" && !Array.isArray(entry) && items.indexOf(entry) === index, ); for (const container of containers) { const objectRows = Object.entries(container) .filter(([entryKey, value]) => { if (Array.isArray(value)) { return false; } if (hasSpaceConfigFields(value)) { return true; } return looksLikeSpaceConfigKey(entryKey) && (value === null || ["string", "number", "boolean"].includes(typeof value)); }) .map(([entryKey, value]) => value && typeof value === "object" && !Array.isArray(value) ? { ...value, key: value.key || value.name || value.id || entryKey, } : { key: entryKey, value, }, ); if (objectRows.length) { return objectRows; } } return []; } function normalizeVisibility(value, isPrivate = false) { const normalized = String(value || "").trim().toLowerCase(); if (normalized === "public" || normalized === "private" || normalized === "protected") { return normalized; } return isPrivate ? "private" : "public"; } function buildNativeLinks(type, repoId) { const repoUrl = hubRepoUrl(type, repoId); const settingsUrl = `${repoUrl}/settings`; return { repoUrl, settingsUrl, analyticsUrl: settingsUrl, communityUrl: type === "bucket" ? repoUrl : `${repoUrl}/discussions`, storageUrl: settingsUrl, storageOverviewUrl: `${HUB_URL}/settings/repositories`, webhooksUrl: `${HUB_URL}/settings/webhooks`, xetUrl: settingsUrl, contributionsUrl: type === "bucket" ? repoUrl : `${repoUrl}/discussions`, }; } function normalizeSpaceConfigEntry(entry, kind) { const key = String(entry?.key || entry?.name || entry?.id || entry?.env || ""); return { key, description: entry?.description || entry?.helper || entry?.help || entry?.comment || "", value: kind === "variable" ? entry?.value ?? entry?.content ?? entry?.rawValue ?? entry?.default ?? null : null, updatedAt: toIsoString(entry?.updatedAt || entry?.updated_at || entry?.lastModified || entry?.createdAt), hidden: kind === "secret" || entry?.hidden === true || entry?.masked === true || entry?.value === undefined, }; } function normalizeWebhookEntry(entry) { const watched = Array.isArray(entry?.watched) ? entry.watched : Array.isArray(entry?.watchedItems) ? entry.watchedItems : []; return { id: String(entry?.id || entry?._id || ""), url: entry?.url || entry?.endpoint || "", domains: Array.isArray(entry?.domains) ? entry.domains : [], disabled: Boolean(entry?.disabled), watched: watched .map((item) => ({ type: String(item?.type || ""), name: String(item?.name || item?.id || ""), })) .filter((item) => item.type && item.name), }; } function filterWebhooksForResource(webhooks, type, repoId, owner) { return webhooks.filter((webhook) => webhook.watched.some( (item) => (item.type === type && item.name === repoId) || (item.type === "user" && item.name === owner) || (item.type === "org" && item.name === owner), ), ); } function normalizeAttachedBucket(entry) { const bucketId = String( entry?.bucket || entry?.bucketId || entry?.id || entry?.repoId || entry?.storageBucket || "", ); return { bucketId, mountPath: entry?.mountPath || entry?.mount_path || entry?.path || entry?.targetPath || "", mode: entry?.mode || entry?.type || "", }; } async function listSpaceVariables(accessToken, repoId) { const payload = await optionalHubJson( accessToken, `${HUB_URL}/api/spaces/${repoId}/variables`, { headers: { Accept: "application/json", }, }, "Couldn't load that Space's variables.", ); return extractSpaceConfigRows(payload, "variables") .map((entry) => normalizeSpaceConfigEntry(entry, "variable")) .filter((entry) => entry.key) .sort((a, b) => a.key.localeCompare(b.key)); } async function listSpaceSecrets(accessToken, repoId) { const payload = await optionalHubJson( accessToken, `${HUB_URL}/api/spaces/${repoId}/secrets`, { headers: { Accept: "application/json", }, }, "Couldn't load that Space's secrets.", ); return extractSpaceConfigRows(payload, "secrets") .map((entry) => normalizeSpaceConfigEntry(entry, "secret")) .filter((entry) => entry.key) .sort((a, b) => a.key.localeCompare(b.key)); } async function getSpaceRuntimeDetails(accessToken, repoId) { return optionalHubJson( accessToken, `${HUB_URL}/api/spaces/${repoId}/runtime`, { headers: { Accept: "application/json", }, }, "Couldn't load that Space runtime.", ); } async function listAccountWebhooks(accessToken) { const payload = await optionalHubJson( accessToken, `${HUB_URL}/api/settings/webhooks`, { headers: { Accept: "application/json", }, }, "Couldn't load your Hugging Face webhooks.", ); return extractRows(payload, ["webhooks", "items"]).map(normalizeWebhookEntry); } async function upsertSpaceConfigEntry(accessToken, repoId, kind, params) { const key = normalizeRepoSegment(params.key, `${kind} key`).toUpperCase(); const value = String(params.value || ""); const description = String(params.description || ""); if (!value) { throw new HfApiError(400, `That ${kind} value is required.`); } await hubJson( accessToken, `${HUB_URL}/api/spaces/${repoId}/${kind === "secret" ? "secrets" : "variables"}`, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ key, name: key, value, description, }), }, `Couldn't save that Space ${kind}.`, ); return { ok: true, key }; } async function getViewer(accessToken) { try { const identity = await whoAmI({ accessToken, hubUrl: HUB_URL, }); return summarizeViewer(identity); } catch (error) { throw wrapHubError(error, "Couldn't read your Hugging Face account details."); } } async function listOwnedSpaces(accessToken, namespaces, query = "") { const trimmedQuery = String(query || "").trim(); const results = await Promise.all( namespaces.map((owner) => safeList(async () => collectAsync( listSpaces({ accessToken, hubUrl: HUB_URL, search: { owner, ...(trimmedQuery ? { query: trimmedQuery } : {}), }, additionalFields: ["author", "createdAt", "runtime", "subdomain", "tags", "models", "datasets"], }), MAX_DASHBOARD_ITEMS, ), ), ), ); return uniqueById(results.flat().map(normalizeSpaceEntry)).sort(compareByUpdatedDesc); } async function listOwnedModels(accessToken, namespaces, query = "") { const trimmedQuery = String(query || "").trim(); const results = await Promise.all( namespaces.map((owner) => safeList(async () => collectAsync( listModels({ accessToken, hubUrl: HUB_URL, search: { owner, ...(trimmedQuery ? { query: trimmedQuery } : {}), }, additionalFields: ["author", "createdAt", "library_name", "sha", "spaces"], }), MAX_DASHBOARD_ITEMS, ), ), ), ); return uniqueById(results.flat().map(normalizeModelEntry)).sort(compareByUpdatedDesc); } async function listOwnedDatasets(accessToken, namespaces, query = "") { const trimmedQuery = String(query || "").trim(); const results = await Promise.all( namespaces.map((owner) => safeList(async () => collectAsync( listDatasets({ accessToken, hubUrl: HUB_URL, search: { owner, ...(trimmedQuery ? { query: trimmedQuery } : {}), }, additionalFields: ["author", "createdAt", "description", "sha", "tags"], }), MAX_DASHBOARD_ITEMS, ), ), ), ); return uniqueById(results.flat().map(normalizeDatasetEntry)).sort(compareByUpdatedDesc); } function extractBucketRows(payload) { return extractRows(payload, ["buckets", "items", "data", "results"]); } function filterBucketsForNamespace(rows, namespace = "") { const trimmedNamespace = String(namespace || "").trim(); const normalizedRows = rows .map((entry) => normalizeBucketEntry(entry, trimmedNamespace)) .filter((bucket) => bucket.id && bucket.owner && bucket.name); if (!trimmedNamespace) { return normalizedRows; } return normalizedRows.filter((bucket) => bucket.owner === trimmedNamespace); } async function listBucketsForNamespace(accessToken, namespace, includeViewerFallback = false) { const urls = []; if (namespace) { urls.push(`${HUB_URL}/api/buckets?namespace=${encodeURIComponent(namespace)}`); urls.push(`${HUB_URL}/api/buckets?author=${encodeURIComponent(namespace)}`); urls.push(`${HUB_URL}/api/buckets?owner=${encodeURIComponent(namespace)}`); } if (includeViewerFallback || !namespace) { urls.push(`${HUB_URL}/api/buckets`); } let lastError = null; for (const url of urls) { try { const payload = await hubJson( accessToken, url, { headers: { Accept: "application/json", }, }, "Couldn't load your buckets.", ); const rows = filterBucketsForNamespace(extractBucketRows(payload), namespace); if (rows.length) { return rows; } } catch (error) { const wrapped = wrapHubError(error, "Couldn't load your buckets."); if (wrapped.statusCode !== 403 && wrapped.statusCode !== 404) { lastError = wrapped; } } } if (lastError) { throw lastError; } return []; } async function listOwnedBuckets(accessToken, viewer, query = "") { const trimmedQuery = String(query || "").trim().toLowerCase(); const bucketGroups = await Promise.all( (viewer.namespaces || []).map((namespace) => safeList(async () => listBucketsForNamespace(accessToken, namespace, namespace === viewer.username)), ), ); const buckets = uniqueById(bucketGroups.flat()).sort(compareByUpdatedDesc); if (!trimmedQuery) { return buckets; } return buckets.filter((bucket) => { const haystack = `${bucket.owner}/${bucket.name}`.toLowerCase(); return haystack.includes(trimmedQuery); }); } async function getOwnedResourceIndex(accessToken, viewer, query = "") { const [spaces, models, datasets, buckets] = await Promise.all([ listOwnedSpaces(accessToken, viewer.namespaces, query), listOwnedModels(accessToken, viewer.namespaces, query), listOwnedDatasets(accessToken, viewer.namespaces, query), listOwnedBuckets(accessToken, viewer, query), ]); return { spaces, models, datasets, buckets }; } function searchResultsFromIndex(index, query) { const trimmedQuery = String(query || "").trim().toLowerCase(); if (!trimmedQuery) { return { total: 0, query: "", groups: [], }; } const groups = [ { type: "space", label: "Spaces", items: index.spaces }, { type: "model", label: "Models", items: index.models }, { type: "dataset", label: "Datasets", items: index.datasets }, { type: "bucket", label: "Buckets", items: index.buckets }, ] .map((group) => ({ ...group, items: group.items .filter((item) => `${item.owner}/${item.name} ${item.title || ""} ${item.description || ""}` .toLowerCase() .includes(trimmedQuery), ) .slice(0, 8), })) .filter((group) => group.items.length > 0); return { total: groups.reduce((sum, group) => sum + group.items.length, 0), query: trimmedQuery, groups, }; } function parseRoute(requestedPath) { const url = new URL(requestedPath || "/", "http://hf-home.local"); const segments = url.pathname.split("/").filter(Boolean); const search = url.searchParams; if (segments.length === 0) { return { kind: "dashboard", pathname: url.pathname, search }; } if (segments[0] === "settings") { return { kind: "settings", pathname: url.pathname, search }; } const collectionKinds = new Map([ ["spaces", "space"], ["models", "model"], ["datasets", "dataset"], ["buckets", "bucket"], ]); const collectionType = collectionKinds.get(segments[0]); if (!collectionType) { return { kind: "notFound", pathname: url.pathname, search }; } if (segments.length === 1) { return { kind: "collection", resourceType: collectionType, pathname: url.pathname, search, }; } const owner = segments[1]; const name = segments[2]; if (!owner || !name) { return { kind: "notFound", pathname: url.pathname, search }; } const repoId = `${owner}/${name}`; const defaultTab = collectionType === "space" ? "app" : collectionType === "bucket" ? "files" : "overview"; const tab = segments[3] || defaultTab; return { kind: "resource", resourceType: collectionType, owner, name, repoId, tab, branch: search.get("branch") || "", path: search.get("path") || "", pathname: url.pathname, search, }; } async function listRepoRefs(accessToken, type, repoId) { const urls = [ `${HUB_URL}/api/${pluralFor(type)}/${repoId}/refs`, `${HUB_URL}/api/${pluralFor(type)}/${repoId}/refs?include_prs=1`, ]; try { const payload = await fetchJsonWithFallback(urls, accessToken); const branches = Array.isArray(payload.branches) ? payload.branches : []; const tags = Array.isArray(payload.tags) ? payload.tags : []; const pullRequests = Array.isArray(payload.pullRequests) ? payload.pullRequests : Array.isArray(payload.prs) ? payload.prs : []; const normalizeRef = (ref) => ({ name: ref.name || ref.branch || ref.tag || "", ref: ref.ref || "", targetCommit: ref.targetCommit || ref.target_commit || ref.commit || ref.sha || ref.oid || null, }); return { branches: branches.map(normalizeRef).filter((branch) => branch.name), tags: tags.map(normalizeRef).filter((tag) => tag.name), pullRequests: pullRequests.map(normalizeRef).filter((pull) => pull.name), }; } catch (error) { return { branches: [{ name: "main", ref: "refs/heads/main", targetCommit: null }], tags: [], pullRequests: [], unavailable: true, }; } } async function getRecentCommits(accessToken, type, repoId, revision) { try { return ( await collectAsync( listCommits({ accessToken, hubUrl: HUB_URL, repo: repoRef(type, repoId), ...(revision ? { revision } : {}), }), MAX_COMMUNITY_ITEMS, ) ).map((commit) => ({ id: commit.oid, title: commit.title, message: commit.message, date: toIsoString(commit.date), authors: commit.authors || [], })); } catch (error) { return []; } } async function getRepoDiscussions(accessToken, type, repoId) { const urls = [ `${HUB_URL}/api/${pluralFor(type)}/${repoId}/discussions?p=0`, `${HUB_URL}/api/${pluralFor(type)}/${repoId}/discussions`, ]; try { const payload = await fetchJsonWithFallback(urls, accessToken); const rows = Array.isArray(payload) ? payload : Array.isArray(payload.discussions) ? payload.discussions : Array.isArray(payload.items) ? payload.items : []; return rows.slice(0, MAX_COMMUNITY_ITEMS).map((item) => ({ number: item.num || item.number || item.id || null, title: item.title || item.subject || "Untitled discussion", author: item.author?.name || item.author || item.user || "unknown", status: item.status || item.state || "open", type: item.type || (item.isPullRequest ? "pull_request" : "discussion"), createdAt: toIsoString(item.createdAt || item.created_at), updatedAt: toIsoString(item.updatedAt || item.updated_at || item.lastModified), url: item.url || `${hubRepoUrl(type, repoId)}/discussions/${item.num || item.number || ""}`, })); } catch (error) { return []; } } function formatPathEntry(entry) { const name = entry.path.split("/").pop() || entry.path; return { name, path: entry.path, type: entry.type, size: Number(entry.size || 0), oid: entry.oid || null, xetHash: entry.xetHash || null, uploadedAt: toIsoString(entry.uploadedAt), lastCommit: entry.lastCommit ? { id: entry.lastCommit.id, title: entry.lastCommit.title, date: toIsoString(entry.lastCommit.date), } : null, lfs: entry.lfs || null, securityFileStatus: entry.securityFileStatus || null, }; } function fileLooksText(bytes) { if (bytes.length === 0) { return true; } let suspicious = 0; const sample = bytes.subarray(0, Math.min(bytes.length, 1024)); for (const byte of sample) { if (byte === 0) { return false; } if (byte < 9 || (byte > 13 && byte < 32)) { suspicious += 1; } } return suspicious / sample.length < 0.08; } async function loadSelectedFile(accessToken, type, repoId, filePath, revision, metadata) { try { const blob = await downloadFile({ accessToken, hubUrl: HUB_URL, repo: repoRef(type, repoId), path: filePath, ...(type === "bucket" ? {} : { revision }), }); if (!blob) { return null; } const cappedBlob = blob.slice(0, MAX_FILE_PREVIEW_BYTES); const buffer = Buffer.from(await cappedBlob.arrayBuffer()); const textFile = fileLooksText(buffer); const content = textFile ? new TextDecoder("utf8").decode(buffer) : ""; return { path: filePath, size: blob.size, binary: !textFile, editable: textFile && blob.size <= MAX_EDITABLE_FILE_BYTES, truncated: blob.size > MAX_FILE_PREVIEW_BYTES, content, oid: metadata?.oid || null, uploadedAt: toIsoString(metadata?.uploadedAt), lastCommit: metadata?.lastCommit ? { id: metadata.lastCommit.id, title: metadata.lastCommit.title, date: toIsoString(metadata.lastCommit.date), } : null, downloadUrl: `/api/file/download?type=${encodeURIComponent(type)}` + `&repoId=${encodeURIComponent(repoId)}` + `&path=${encodeURIComponent(filePath)}` + (revision ? `&branch=${encodeURIComponent(revision)}` : ""), }; } catch (error) { return null; } } function buildBreadcrumbs(resourceType, owner, name, currentPath, branch) { const crumbs = [ { label: `${owner}/${name}`, href: `/${pluralFor(resourceType)}/${owner}/${name}/files` + (branch ? `?branch=${encodeURIComponent(branch)}` : ""), }, ]; if (!currentPath) { return crumbs; } const segments = currentPath.split("/").filter(Boolean); let running = ""; for (const segment of segments) { running = running ? `${running}/${segment}` : segment; crumbs.push({ label: segment, href: `/${pluralFor(resourceType)}/${owner}/${name}/files?path=${encodeURIComponent(running)}` + (branch ? `&branch=${encodeURIComponent(branch)}` : ""), }); } return crumbs; } async function buildFilesView(accessToken, type, owner, name, requestedBranch, requestedPath, branches) { const repoId = `${owner}/${name}`; const branchNames = new Set((branches || []).map((branch) => branch.name)); const branch = type === "bucket" ? "" : requestedBranch && branchNames.has(requestedBranch) ? requestedBranch : branches.find((entry) => entry.name === "main")?.name || branches[0]?.name || "main"; const safePath = normalizeRemotePath(requestedPath); let currentPath = ""; let selectedFile = null; let selectionInfo = null; if (safePath) { try { const details = await pathsInfo({ accessToken, hubUrl: HUB_URL, repo: repoRef(type, repoId), paths: [safePath], expand: true, ...(type === "bucket" ? {} : { revision: branch }), }); selectionInfo = details[0] || null; if (selectionInfo?.type === "directory") { currentPath = safePath; } else if (selectionInfo?.type === "file") { currentPath = dirname(safePath); } } catch (error) { currentPath = dirname(safePath); } } const rawEntries = await collectAsync( listFiles({ accessToken, hubUrl: HUB_URL, repo: repoRef(type, repoId), path: currentPath || undefined, recursive: false, expand: true, ...(type === "bucket" ? {} : { revision: branch }), }), MAX_FILE_LIST_ITEMS, ); const entries = rawEntries .map(formatPathEntry) .sort((left, right) => { if (left.type !== right.type) { return left.type === "directory" ? -1 : 1; } return left.name.localeCompare(right.name); }); if (selectionInfo?.type === "file") { selectedFile = await loadSelectedFile(accessToken, type, repoId, safePath, branch, selectionInfo); } return { branch, branches, currentPath, requestedPath: safePath, breadcrumbs: buildBreadcrumbs(type, owner, name, currentPath, branch), entries, selectedFile, branchUnavailable: type === "bucket", }; } async function getSpaceDetail(accessToken, viewer, owner, name, tab, branch, requestedPath) { const repoId = `${owner}/${name}`; const info = await spaceInfo({ accessToken, hubUrl: HUB_URL, name: repoId, additionalFields: [ "author", "cardData", "createdAt", "datasets", "models", "resourceGroup", "runtime", "sha", "subdomain", "tags", "usedStorage", ], }).catch((error) => { throw wrapHubError(error, "Couldn't load that Space."); }); const refs = await listRepoRefs(accessToken, "space", repoId); const activeTab = new Set(["app", "files", "community", "settings"]).has(tab) ? tab : "app"; const files = activeTab === "files" ? await buildFilesView(accessToken, "space", owner, name, branch, requestedPath, refs.branches) : null; const commits = await getRecentCommits(accessToken, "space", repoId, files?.branch || refs.branches[0]?.name || "main"); const discussions = activeTab === "community" ? await getRepoDiscussions(accessToken, "space", repoId) : []; const runtime = activeTab === "settings" ? (await getSpaceRuntimeDetails(accessToken, repoId)) || info.runtime || null : info.runtime || null; const nativeLinks = buildNativeLinks("space", repoId); const settings = activeTab === "settings" ? await (async () => { const [variables, secrets, webhooks, buckets] = await Promise.all([ safeList(async () => listSpaceVariables(accessToken, repoId)), safeList(async () => listSpaceSecrets(accessToken, repoId)), safeList(async () => listAccountWebhooks(accessToken)), safeList(async () => listBucketsForNamespace(accessToken, owner, owner === viewer.username)), ]); const attachedBuckets = Array.isArray(runtime?.volumes) ? runtime.volumes.map(normalizeAttachedBucket).filter((entry) => entry.bucketId || entry.mountPath) : []; return { ...nativeLinks, visibility: normalizeVisibility(info.visibility, info.private), usedStorage: Number(info.usedStorage || runtime?.usedStorage || 0) || 0, resourceGroup: info.resourceGroup || null, variables, secrets, webhooks: filterWebhooksForResource(webhooks, "space", repoId, owner), buckets, attachedBuckets, supportsVisibility: true, supportsMove: true, supportsDelete: true, }; })() : { ...nativeLinks, visibility: normalizeVisibility(info.visibility, info.private), usedStorage: Number(info.usedStorage || 0) || 0, resourceGroup: info.resourceGroup || null, variables: [], secrets: [], webhooks: [], buckets: [], attachedBuckets: [], supportsVisibility: true, supportsMove: true, supportsDelete: true, }; return { kind: "space", title: info.title || info.cardData?.title || name, resourceType: "space", owner, name, id: repoId, tab: activeTab, private: Boolean(info.private), likes: info.likes || 0, sdk: info.sdk || info.cardData?.sdk || null, runtime, createdAt: toIsoString(info.createdAt), updatedAt: toIsoString(info.updatedAt || info.lastModified), sha: info.sha || null, subdomain: info.subdomain || null, appUrl: spaceAppUrl(info), hubUrl: hubRepoUrl("space", repoId), tags: Array.isArray(info.tags) ? info.tags : [], linkedModels: Array.isArray(info.models) ? info.models : [], linkedDatasets: Array.isArray(info.datasets) ? info.datasets : [], permissions: toPermissionSummary(viewer, owner), branches: refs.branches, files, community: { commits, discussions, nativeUrl: `${hubRepoUrl("space", repoId)}/discussions`, }, live: { logsUrl: `/api/spaces/${owner}/${name}/logs/stream?kind=container`, buildLogsUrl: `/api/spaces/${owner}/${name}/logs/stream?kind=build`, eventsUrl: `/api/spaces/${owner}/${name}/events/stream`, metricsUrl: `/api/spaces/${owner}/${name}/metrics/stream`, updatesUrl: `/api/resources/space/${owner}/${name}/updates/stream` + (files?.branch ? `?branch=${encodeURIComponent(files.branch)}` : ""), }, settings: { ...settings, sdk: info.sdk || info.cardData?.sdk || null, runtime, appUrl: spaceAppUrl(info), }, }; } async function getModelDetail(accessToken, viewer, owner, name, tab, branch, requestedPath) { const repoId = `${owner}/${name}`; const info = await modelInfo({ accessToken, hubUrl: HUB_URL, name: repoId, additionalFields: [ "author", "cardData", "createdAt", "library_name", "pipeline_tag", "sha", "spaces", "tags", ], }).catch((error) => { throw wrapHubError(error, "Couldn't load that model."); }); const refs = await listRepoRefs(accessToken, "model", repoId); const activeTab = new Set(["overview", "files", "community", "settings"]).has(tab) ? tab : "overview"; const files = activeTab === "files" ? await buildFilesView(accessToken, "model", owner, name, branch, requestedPath, refs.branches) : null; const commits = await getRecentCommits(accessToken, "model", repoId, files?.branch || refs.branches[0]?.name || "main"); const discussions = activeTab === "community" ? await getRepoDiscussions(accessToken, "model", repoId) : []; return { kind: "model", resourceType: "model", owner, name, id: repoId, title: repoId, tab: activeTab, private: Boolean(info.private), likes: info.likes || 0, downloads: info.downloads || 0, task: info.pipeline_tag || null, library: info.library_name || null, createdAt: toIsoString(info.createdAt), updatedAt: toIsoString(info.updatedAt || info.lastModified), sha: info.sha || null, tags: Array.isArray(info.tags) ? info.tags : [], spaces: Array.isArray(info.spaces) ? info.spaces : [], hubUrl: hubRepoUrl("model", repoId), permissions: toPermissionSummary(viewer, owner), branches: refs.branches, files, overview: { description: info.cardData?.description || "", downloads: info.downloads || 0, likes: info.likes || 0, task: info.pipeline_tag || null, library: info.library_name || null, }, community: { commits, discussions, nativeUrl: `${hubRepoUrl("model", repoId)}/discussions`, }, live: { updatesUrl: `/api/resources/model/${owner}/${name}/updates/stream` + (files?.branch ? `?branch=${encodeURIComponent(files.branch)}` : ""), }, settings: { ...buildNativeLinks("model", repoId), visibility: normalizeVisibility(info.visibility, info.private), resourceGroup: info.resourceGroup || null, usedStorage: Number(info.usedStorage || 0) || 0, supportsVisibility: true, supportsMove: true, supportsDelete: true, }, }; } async function getDatasetDetail(accessToken, viewer, owner, name, tab, branch, requestedPath) { const repoId = `${owner}/${name}`; const info = await datasetInfo({ accessToken, hubUrl: HUB_URL, name: repoId, additionalFields: ["author", "createdAt", "description", "files", "sha", "tags"], }).catch((error) => { throw wrapHubError(error, "Couldn't load that dataset."); }); const refs = await listRepoRefs(accessToken, "dataset", repoId); const activeTab = new Set(["overview", "files", "community", "settings"]).has(tab) ? tab : "overview"; const files = activeTab === "files" ? await buildFilesView(accessToken, "dataset", owner, name, branch, requestedPath, refs.branches) : null; const commits = await getRecentCommits(accessToken, "dataset", repoId, files?.branch || refs.branches[0]?.name || "main"); const discussions = activeTab === "community" ? await getRepoDiscussions(accessToken, "dataset", repoId) : []; return { kind: "dataset", resourceType: "dataset", owner, name, id: repoId, title: repoId, tab: activeTab, private: Boolean(info.private), likes: info.likes || 0, downloads: info.downloads || 0, createdAt: toIsoString(info.createdAt), updatedAt: toIsoString(info.updatedAt || info.lastModified), sha: info.sha || null, description: info.description || "", tags: Array.isArray(info.tags) ? info.tags : [], hubUrl: hubRepoUrl("dataset", repoId), permissions: toPermissionSummary(viewer, owner), branches: refs.branches, files, overview: { description: info.description || "", downloads: info.downloads || 0, likes: info.likes || 0, }, community: { commits, discussions, nativeUrl: `${hubRepoUrl("dataset", repoId)}/discussions`, }, live: { updatesUrl: `/api/resources/dataset/${owner}/${name}/updates/stream` + (files?.branch ? `?branch=${encodeURIComponent(files.branch)}` : ""), }, settings: { ...buildNativeLinks("dataset", repoId), visibility: normalizeVisibility(info.visibility, info.private), resourceGroup: info.resourceGroup || null, usedStorage: Number(info.usedStorage || 0) || 0, supportsVisibility: true, supportsMove: true, supportsDelete: true, }, }; } async function getBucketInfo(accessToken, repoId) { const directPayload = await optionalHubJson( accessToken, `${HUB_URL}/api/buckets/${repoId}`, { headers: { Accept: "application/json", }, }, "Couldn't load that bucket.", ); if (directPayload) { return normalizeBucketEntry(directPayload); } const { owner, name } = splitRepoId(repoId); const buckets = await safeList(async () => listBucketsForNamespace(accessToken, owner, true)); const matched = buckets.find((bucket) => bucket.id === repoId) || buckets.find((bucket) => bucket.owner === owner && bucket.name === name); if (matched) { return matched; } return { kind: "bucket", id: repoId, owner, name, private: true, size: 0, fileCount: 0, updatedAt: null, createdAt: null, region: null, url: `/buckets/${owner}/${name}/files`, hubUrl: hubRepoUrl("bucket", repoId), }; } async function getBucketActivity(accessToken, repoId, currentPath = "") { const rows = await collectAsync( listFiles({ accessToken, hubUrl: HUB_URL, repo: repoRef("bucket", repoId), recursive: true, path: currentPath || undefined, expand: true, }), MAX_FILE_LIST_ITEMS, ); return rows .map(formatPathEntry) .filter((entry) => entry.type === "file") .sort((left, right) => (right.uploadedAt || "").localeCompare(left.uploadedAt || "")) .slice(0, MAX_COMMUNITY_ITEMS); } async function getBucketDetail(accessToken, viewer, owner, name, tab, requestedPath) { const repoId = `${owner}/${name}`; const info = await getBucketInfo(accessToken, repoId).catch((error) => { throw wrapHubError(error, "Couldn't load that bucket."); }); const activeTab = new Set(["files", "settings"]).has(tab) ? tab : "files"; const files = activeTab === "files" ? await buildFilesView(accessToken, "bucket", owner, name, "", requestedPath, []) : null; const activity = await getBucketActivity(accessToken, repoId, files?.currentPath || ""); const derivedFileCount = info.fileCount || activity.length || files?.entries.filter((entry) => entry.type === "file").length || 0; const derivedSize = info.size || activity.reduce((sum, entry) => sum + Number(entry.size || 0), 0); return { kind: "bucket", resourceType: "bucket", owner, name, id: repoId, title: repoId, tab: activeTab, private: Boolean(info.private), size: derivedSize, fileCount: derivedFileCount, createdAt: info.createdAt, updatedAt: info.updatedAt, region: info.region || null, hubUrl: hubRepoUrl("bucket", repoId), permissions: toPermissionSummary(viewer, owner), files, activity, live: { updatesUrl: `/api/resources/bucket/${owner}/${name}/updates/stream` + (files?.currentPath ? `?path=${encodeURIComponent(files.currentPath)}` : ""), }, settings: { ...buildNativeLinks("bucket", repoId), visibility: normalizeVisibility(info.visibility, info.private), storageType: "Mutable storage bucket", branchUnavailable: true, usedStorage: Number(derivedSize || 0) || 0, supportsVisibility: false, supportsMove: false, supportsDelete: true, }, }; } function collectionTitle(resourceType) { switch (resourceType) { case "space": return "Your Spaces"; case "model": return "Your Models"; case "dataset": return "Your Datasets"; case "bucket": return "Your Buckets"; default: return "Your Resources"; } } function summarizeResourceIndex(index) { return { spaces: index.spaces.length, models: index.models.length, datasets: index.datasets.length, buckets: index.buckets.length, }; } async function getCollectionDetail(accessToken, viewer, resourceType) { const index = await getOwnedResourceIndex(accessToken, viewer); const items = resourceType === "space" ? index.spaces : resourceType === "model" ? index.models : resourceType === "dataset" ? index.datasets : index.buckets; return { kind: "collection", resourceType, title: collectionTitle(resourceType), items, summary: summarizeResourceIndex(index), }; } async function getDashboardDetail(accessToken, viewer) { const index = await getOwnedResourceIndex(accessToken, viewer); return { kind: "dashboard", title: `${viewer.fullname}'s workspace`, summary: summarizeResourceIndex(index), resources: index, }; } async function getSettingsDetail(accessToken, viewer) { const index = await getOwnedResourceIndex(accessToken, viewer); return { kind: "settings", title: "Account settings", account: viewer, summary: summarizeResourceIndex(index), resources: { spaces: index.spaces.slice(0, 4), models: index.models.slice(0, 4), datasets: index.datasets.slice(0, 4), buckets: index.buckets.slice(0, 4), }, links: { profileUrl: viewer.profileUrl, webhooksUrl: `${HUB_URL}/settings/webhooks`, repositoriesUrl: `${HUB_URL}/settings/repositories`, tokensUrl: `${HUB_URL}/settings/tokens`, }, }; } async function getPageData(accessToken, requestedPath) { const viewer = await getViewer(accessToken); const route = parseRoute(requestedPath); try { let page; if (route.kind === "dashboard") { page = await getDashboardDetail(accessToken, viewer); } else if (route.kind === "settings") { page = await getSettingsDetail(accessToken, viewer); } else if (route.kind === "collection") { page = await getCollectionDetail(accessToken, viewer, route.resourceType); } else if (route.kind === "resource") { if (route.resourceType === "space") { page = await getSpaceDetail(accessToken, viewer, route.owner, route.name, route.tab, route.branch, route.path); } else if (route.resourceType === "model") { page = await getModelDetail(accessToken, viewer, route.owner, route.name, route.tab, route.branch, route.path); } else if (route.resourceType === "dataset") { page = await getDatasetDetail(accessToken, viewer, route.owner, route.name, route.tab, route.branch, route.path); } else { page = await getBucketDetail(accessToken, viewer, route.owner, route.name, route.tab, route.path); } } else { page = { kind: "notFound", title: "Not found", }; } return { viewer, route, page, }; } catch (error) { throw wrapHubError(error, "Couldn't load that page from Hugging Face."); } } async function getSearchResults(accessToken, requestedPath, query) { const viewer = await getViewer(accessToken); const index = await getOwnedResourceIndex(accessToken, viewer, query); const results = searchResultsFromIndex(index, query); return { viewer, route: parseRoute(requestedPath), ...results, }; } async function getResourceUpdates(accessToken, type, repoId, options = {}) { const safeBranch = type === "bucket" ? "" : normalizeBranchName(options.branch || ""); const safePath = normalizeRemotePath(options.path || ""); if (type === "bucket") { const activity = await getBucketActivity(accessToken, repoId, safePath); return { kind: "bucket", signature: activity.map((entry) => `${entry.path}:${entry.uploadedAt || ""}`).join("|"), items: activity, }; } const commits = await getRecentCommits(accessToken, type, repoId, safeBranch || "main"); return { kind: "repo", signature: commits.map((commit) => commit.id).join("|"), items: commits, }; } async function getDownloadBlob(accessToken, params) { const type = params.type; const repoId = params.repoId; const filePath = normalizeRemotePath(params.path); const branch = type === "bucket" ? "" : normalizeBranchName(params.branch || "main"); try { const blob = await downloadFile({ accessToken, hubUrl: HUB_URL, repo: repoRef(type, repoId), path: filePath, ...(type === "bucket" ? {} : { revision: branch }), }); if (!blob) { throw new HfApiError(404, "That file was not found."); } return { blob, fileName: filePath.split("/").pop() || "download", contentType: blob.type || "application/octet-stream", }; } catch (error) { throw wrapHubError(error, "Couldn't download that file."); } } function toCommitMessage(action, filePath) { return `${action} ${filePath} from HF Home`; } async function updateTextFile(accessToken, params) { const type = params.type; const repoId = params.repoId; const filePath = normalizeRemotePath(params.path); const branch = type === "bucket" ? "" : normalizeBranchName(params.branch || "main"); const content = String(params.content || ""); if (Buffer.byteLength(content, "utf8") > MAX_EDITABLE_FILE_BYTES) { throw new HfApiError(413, "That file is too large to edit in this interface."); } try { const result = await uploadFile({ accessToken, hubUrl: HUB_URL, repo: repoRef(type, repoId), branch: branch || undefined, parentCommit: params.parentCommit || undefined, file: { path: filePath, content: new Blob([content], { type: "text/plain;charset=utf-8" }), }, commitTitle: toCommitMessage("Update", filePath), commitDescription: "Updated from the authenticated HF Home interface.", }); return { ok: true, commit: result?.commit || null, }; } catch (error) { throw wrapHubError(error, "Couldn't save that file."); } } async function removeFile(accessToken, params) { const type = params.type; const repoId = params.repoId; const filePath = normalizeRemotePath(params.path); const branch = type === "bucket" ? "" : normalizeBranchName(params.branch || "main"); try { const result = await deleteFile({ accessToken, hubUrl: HUB_URL, repo: repoRef(type, repoId), path: filePath, branch: branch || undefined, parentCommit: params.parentCommit || undefined, commitTitle: toCommitMessage("Delete", filePath), commitDescription: "Deleted from the authenticated HF Home interface.", }); return { ok: true, commit: result?.commit || null, }; } catch (error) { throw wrapHubError(error, "Couldn't delete that file."); } } async function performSpaceAction(accessToken, repoId, action) { const safeAction = String(action || "").trim(); const allowed = new Set(["pause", "restart", "factory-restart"]); if (!allowed.has(safeAction)) { throw new HfApiError(400, "That Space action is not supported."); } const targetUrl = safeAction === "pause" ? `${HUB_URL}/api/spaces/${repoId}/pause` : `${HUB_URL}/api/spaces/${repoId}/restart${safeAction === "factory-restart" ? "?factory=true" : ""}`; const response = await makeHubFetch(accessToken)(targetUrl, { method: "POST", headers: { Accept: "application/json", }, }).catch((error) => { throw wrapHubError(error, "Couldn't reach Hugging Face for that Space action."); }); if (!response.ok) { throw wrapHubError(new HfApiError(response.status, "Space action failed.")); } try { return await response.json(); } catch (error) { return { ok: true }; } } async function createResource(accessToken, params) { const type = String(params.type || "").trim(); if (!["space", "model", "dataset", "bucket"].includes(type)) { throw new HfApiError(400, "That resource type is not supported."); } const repoId = normalizeRepoIdInput(params.namespace, params.name); const visibility = String(params.visibility || "").trim().toLowerCase(); const isPrivate = visibility === "private"; try { const created = await createRepo({ accessToken, hubUrl: HUB_URL, repo: repoRef(type, repoId), private: isPrivate, ...(type === "space" ? { sdk: String(params.sdk || "gradio").trim() || "gradio" } : {}), }); const createdRepoId = created?.id || repoId; return { ok: true, repoId: createdRepoId, url: workspaceResourceUrl(type, createdRepoId), }; } catch (error) { throw wrapHubError(error, "Couldn't create that resource."); } } async function moveResource(accessToken, params) { const type = String(params.type || "").trim(); if (!["space", "model", "dataset", "bucket"].includes(type)) { throw new HfApiError(400, "That resource type is not supported."); } const fromId = normalizeRepoIdInput(...Object.values(splitRepoId(params.fromId || ""))); const toId = normalizeRepoIdInput(...Object.values(splitRepoId(params.toId || ""))); const targetUrl = type === "bucket" ? `${HUB_URL}/api/buckets/move` : `${HUB_URL}/api/repos/move`; await hubJson( accessToken, targetUrl, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ type, repoType: type, repo_type: type, fromId, from_id: fromId, fromRepo: fromId, toId, to_id: toId, toRepo: toId, }), }, "Couldn't rename or transfer that resource.", ); return { ok: true, repoId: toId, url: workspaceResourceUrl(type, toId), }; } async function updateResourceVisibility(accessToken, params) { const type = String(params.type || "").trim(); if (!["space", "model", "dataset"].includes(type)) { throw new HfApiError(400, "Visibility changes are only supported for Spaces, models, and datasets."); } const repoId = normalizeRepoIdInput(...Object.values(splitRepoId(params.repoId || ""))); const visibility = normalizeVisibility(params.visibility, params.private); const payload = { private: visibility === "private", visibility, }; await hubJson( accessToken, `${HUB_URL}/api/${pluralFor(type)}/${repoId}/settings`, { method: "PUT", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify(payload), }, "Couldn't update that resource's visibility.", ); return { ok: true, visibility, }; } async function deleteResource(accessToken, params) { const type = String(params.type || "").trim(); if (!["space", "model", "dataset", "bucket"].includes(type)) { throw new HfApiError(400, "That resource type is not supported."); } const repoId = normalizeRepoIdInput(...Object.values(splitRepoId(params.repoId || ""))); try { await deleteRepo({ accessToken, hubUrl: HUB_URL, repo: repoRef(type, repoId), }); return { ok: true }; } catch (error) { throw wrapHubError(error, "Couldn't delete that resource."); } } async function saveSpaceSecret(accessToken, repoId, params) { return upsertSpaceConfigEntry(accessToken, repoId, "secret", params); } async function saveSpaceVariable(accessToken, repoId, params) { return upsertSpaceConfigEntry(accessToken, repoId, "variable", params); } function buildSpaceStreamUrl(repoId, kind) { if (kind === "events") { return `${HUB_URL}/api/spaces/${repoId}/events`; } if (kind === "metrics") { return `${HUB_URL}/api/spaces/${repoId}/metrics`; } if (kind === "build") { return `${HUB_URL}/api/spaces/${repoId}/logs/build`; } return `${HUB_URL}/api/spaces/${repoId}/logs/run`; } module.exports = { HUB_URL, HfApiError, buildSpaceStreamUrl, createResource, deleteResource, getDownloadBlob, getPageData, getResourceUpdates, getSearchResults, getViewer, moveResource, normalizeBranchName, normalizeRemotePath, performSpaceAction, removeFile, saveSpaceSecret, saveSpaceVariable, updateTextFile, updateResourceVisibility, wrapHubError, };