signup / src /lib /huggingface.ts
burtenshaw's picture
burtenshaw HF Staff
Deploy Next.js signup Space
654b283 verified
raw
history blame
6.42 kB
import { createRepo, repoExists, uploadFile, whoAmI } from "@huggingface/hub";
import type { NextRequest } from "next/server";
import { z } from "zod";
import type { SignupSession } from "./session";
const DEFAULT_HUB_URL = "https://huggingface.co";
const OAUTH_SCOPE = "openid profile email";
const EVENT_NAME = "Humanity's Last Hackathon";
const EVENT_OBJECTIVE = "Write Mac Metal kernels with Codex.";
const tokenResponseSchema = z.object({
access_token: z.string().min(1),
expires_in: z.number().int().positive(),
scope: z.string().min(1),
token_type: z.string().min(1),
});
const userInfoSchema = z.object({
sub: z.string().min(1),
name: z.string(),
preferred_username: z.string().min(1),
email: z.string().email().optional(),
email_verified: z.boolean().optional(),
picture: z.string().url(),
profile: z.string().url(),
website: z.string().url().optional(),
isPro: z.boolean(),
});
export interface SignupRecord {
schemaVersion: 2;
event: {
name: string;
objective: string;
};
signedUpAt: string;
source: {
type: "huggingface-oauth";
scope: string;
};
participant: {
hf: {
id: string;
username: string;
fullName: string;
email: string;
emailVerified: boolean;
avatarUrl: string;
profileUrl: string;
website: string | null;
isPro: boolean;
};
};
}
function getHubUrl(): string {
return (process.env.HF_HUB_URL?.trim() || DEFAULT_HUB_URL).replace(/\/+$/, "");
}
function requireEnv(name: string): string {
const value = process.env[name]?.trim();
if (!value) {
throw new Error(`Missing ${name}.`);
}
return value;
}
export function getAppUrl(request?: NextRequest): string {
const configured = process.env.APP_URL?.trim();
if (configured) {
return configured.replace(/\/+$/, "");
}
if (!request) {
return "http://localhost:3000";
}
const forwardedProto = request.headers.get("x-forwarded-proto");
const forwardedHost = request.headers.get("x-forwarded-host") ?? request.headers.get("host");
if (forwardedProto && forwardedHost) {
return `${forwardedProto}://${forwardedHost}`;
}
return new URL(request.url).origin;
}
export function getOAuthRedirectUri(request?: NextRequest): string {
return `${getAppUrl(request)}/api/auth/huggingface/callback`;
}
export function createOAuthAuthorizationUrl(params: {
state: string;
codeChallenge: string;
request?: NextRequest;
}): string {
const search = new URLSearchParams({
client_id: requireEnv("HF_OAUTH_CLIENT_ID"),
redirect_uri: getOAuthRedirectUri(params.request),
response_type: "code",
scope: OAUTH_SCOPE,
state: params.state,
code_challenge: params.codeChallenge,
code_challenge_method: "S256",
});
return `${getHubUrl()}/oauth/authorize?${search.toString()}`;
}
export async function exchangeCodeForToken(params: {
code: string;
codeVerifier: string;
redirectUri: string;
}) {
const response = await fetch(`${getHubUrl()}/oauth/token`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "authorization_code",
code: params.code,
redirect_uri: params.redirectUri,
client_id: requireEnv("HF_OAUTH_CLIENT_ID"),
client_secret: requireEnv("HF_OAUTH_CLIENT_SECRET"),
code_verifier: params.codeVerifier,
}),
});
if (!response.ok) {
throw new Error(`Hugging Face token exchange failed with status ${response.status}.`);
}
return tokenResponseSchema.parse(await response.json());
}
export async function fetchUserInfo(accessToken: string) {
const response = await fetch(`${getHubUrl()}/oauth/userinfo`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
if (!response.ok) {
throw new Error(`Hugging Face userinfo failed with status ${response.status}.`);
}
return userInfoSchema.parse(await response.json());
}
export async function fetchViewer(accessToken: string) {
const viewer = await whoAmI({ accessToken });
if (viewer.type !== "user") {
throw new Error("Expected a user account from Hugging Face OAuth.");
}
return viewer;
}
export function createSignupRecord(params: { session: SignupSession }): SignupRecord {
const signedUpAt = new Date().toISOString();
return {
schemaVersion: 2,
event: {
name: EVENT_NAME,
objective: EVENT_OBJECTIVE,
},
signedUpAt,
source: {
type: "huggingface-oauth",
scope: OAUTH_SCOPE,
},
participant: {
hf: {
id: params.session.id,
username: params.session.username,
fullName: params.session.name,
email: params.session.email,
emailVerified: params.session.emailVerified,
avatarUrl: params.session.avatarUrl,
profileUrl: params.session.profileUrl,
website: params.session.website,
isPro: params.session.isPro,
},
},
};
}
function createDatasetReadme(): string {
return `---
tags:
- private
- registrations
- hackathon
---
# Humanity's Last Hackathon Signups
Private registration dataset for the Humanity's Last Hackathon landing page.
- Event: ${EVENT_NAME}
- Objective: ${EVENT_OBJECTIVE}
- Source: simple Hugging Face OAuth signup app
Each participant record is stored at \`signups/<hf-user-id>.json\`.
`;
}
export async function storeSignupRecord(record: SignupRecord) {
const repo = {
type: "dataset" as const,
name: requireEnv("HF_DATASET_REPO"),
};
const accessToken = requireEnv("HF_DATASET_WRITE_TOKEN");
const filePath = `signups/${record.participant.hf.id}.json`;
if (!(await repoExists({ repo, accessToken }))) {
await createRepo({
repo,
accessToken,
private: true,
files: [
{
path: "README.md",
content: new Blob([createDatasetReadme()], { type: "text/markdown" }),
},
],
});
}
await uploadFile({
repo,
accessToken,
file: {
path: filePath,
content: new Blob([`${JSON.stringify(record, null, 2)}\n`], {
type: "application/json",
}),
},
commitTitle: `Register ${record.participant.hf.username} for Humanity's Last Hackathon`,
commitDescription: "Write or update a signup record from the Hugging Face OAuth signup app.",
});
return {
repoName: repo.name,
path: filePath,
};
}