chat-ui-agent-demo / src /lib /server /agent /AgentPushAuth.ts
Mike0021's picture
Deploy verified chat and coding agent UX (part 2)
fae625a verified
Raw History Blame Contribute Delete
3.63 kB
import { whoAmI } from "@huggingface/hub";
import { config } from "$lib/server/config";
import type { HuggingFacePushTarget } from "./AgentDeliverables";
import type { Workspace } from "$lib/types/Workspace";
const NAMESPACE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
const SERVICE_REPO_PATTERN =
/^(space|model|dataset):([A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*)$/;
type AgentPushConfigName =
"AGENT_HF_TOKEN" | "HF_TOKEN" | "AGENT_SANDBOX_NAMESPACE" | "AGENT_SERVICE_PUSH_REPOS";
export class AgentPushAuthError extends Error {
constructor(
message: string,
readonly status: number
) {
super(message);
this.name = "AgentPushAuthError";
}
}
export interface ResolveAgentPushCredentialOptions {
userToken?: string;
workspace: Pick<Workspace, "repo">;
target: HuggingFacePushTarget;
configValue?: (name: AgentPushConfigName) => string;
resolveServiceOwner?: (accessToken: string) => Promise<string>;
}
/**
* A user's OAuth token retains normal Hub permissions. The service token is a
* narrower fallback: it can create from a blank workspace, or update the exact
* current HF source only when an operator explicitly allow-lists that repo.
* Both paths remain confined to the configured (or token-owned) namespace.
*/
export async function resolveAgentPushCredential({
userToken,
workspace,
target,
configValue = readConfigValue,
resolveServiceOwner = resolveWhoAmIOwner,
}: ResolveAgentPushCredentialOptions): Promise<string> {
if (userToken) return userToken;
const serviceToken = configValue("AGENT_HF_TOKEN") || configValue("HF_TOKEN");
if (!serviceToken) {
throw new AgentPushAuthError("Sign in with Hugging Face to push this workspace", 401);
}
const creatingFromBlank = workspace.repo.kind === "blank" && target.create === true;
const serviceRepoKey = `${target.repoType}:${target.repoId}`;
const updatingAllowlistedSource =
workspace.repo.kind === "hf" &&
target.create === false &&
workspace.repo.ref === target.repoId &&
workspace.repo.repoType === target.repoType &&
parseServicePushRepos(configValue("AGENT_SERVICE_PUSH_REPOS")).has(serviceRepoKey);
if (!creatingFromBlank && !updatingAllowlistedSource) {
throw new AgentPushAuthError(
"Service credentials may only create from a blank workspace or update an explicitly allowed source repository",
403
);
}
const requestedNamespace = target.repoId.slice(0, target.repoId.indexOf("/"));
let allowedNamespace = configValue("AGENT_SANDBOX_NAMESPACE").trim();
if (!allowedNamespace) {
try {
allowedNamespace = (await resolveServiceOwner(serviceToken)).trim();
} catch {
throw new AgentPushAuthError("Unable to authorize the service repository namespace", 503);
}
}
if (
!NAMESPACE_PATTERN.test(allowedNamespace) ||
requestedNamespace.toLowerCase() !== allowedNamespace.toLowerCase()
) {
throw new AgentPushAuthError(
"Anonymous pushes are limited to the configured service namespace",
403
);
}
return serviceToken;
}
export function parseServicePushRepos(value: string): Set<string> {
const entries = value
.split(",")
.map((entry) => entry.trim())
.filter(Boolean);
return new Set(entries.filter((entry) => SERVICE_REPO_PATTERN.test(entry)));
}
function readConfigValue(name: AgentPushConfigName): string {
return config[name];
}
async function resolveWhoAmIOwner(accessToken: string): Promise<string> {
const identity = await whoAmI({ accessToken });
return identity.name;
}
/** Route-facing service object keeps policy testable without exposing credentials. */
export const agentPushAuth = { resolveCredential: resolveAgentPushCredential };