hf / src /hf-api.js
incognitolm
more update
7b2596d
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,
};