| 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, |
| }; |
| } |
|
|