From 2cd06cd3010c40dc67db7c5161ac18ef55a79794 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 26 Nov 2024 12:45:53 +0100 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=92=A5=20Fix=20avatar=20name=20&=20us?= =?UTF-8?q?ername=20mixup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/hub/src/lib/oauth-handle-redirect.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/hub/src/lib/oauth-handle-redirect.ts b/packages/hub/src/lib/oauth-handle-redirect.ts index 771b6d439..cab892db4 100644 --- a/packages/hub/src/lib/oauth-handle-redirect.ts +++ b/packages/hub/src/lib/oauth-handle-redirect.ts @@ -161,6 +161,7 @@ export async function oauthHandleRedirect(opts?: { hubUrl?: string }): Promise ({ id: org.sub, - name: org.name, + name: org.preferred_username, fullname: org.name, isEnterprise: org.isEnterprise, canPay: org.canPay, From ef546f7e7befc6d67157044555f9fc73a67c043c Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 26 Nov 2024 14:20:29 +0100 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=92=A5=20Actually=20use=20raw=20API?= =?UTF-8?q?=20result?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/hub/src/lib/oauth-handle-redirect.ts | 83 ++++++------------- 1 file changed, 24 insertions(+), 59 deletions(-) diff --git a/packages/hub/src/lib/oauth-handle-redirect.ts b/packages/hub/src/lib/oauth-handle-redirect.ts index cab892db4..7a10d9e58 100644 --- a/packages/hub/src/lib/oauth-handle-redirect.ts +++ b/packages/hub/src/lib/oauth-handle-redirect.ts @@ -1,28 +1,31 @@ import { HUB_URL } from "../consts"; import { createApiError } from "../error"; +export interface UserInfo { + sub: string; + name: string; + preferred_username: string; + email_verified?: boolean; + email?: string; + picture: string; + website?: string; + isPro: boolean; + canPay?: boolean; + orgs?: Array<{ + sub: string; + name: string; + picture: string; + preferred_username: string; + isEnterprise: boolean; + canPay?: boolean; + roleInOrg?: string; + }>; +} + export interface OAuthResult { accessToken: string; accessTokenExpiresAt: Date; - userInfo: { - id: string; - name: string; - fullname: string; - email?: string; - emailVerified?: boolean; - avatarUrl: string; - websiteUrl?: string; - isPro: boolean; - canPay?: boolean; - orgs: Array<{ - id: string; - name: string; - isEnterprise: boolean; - canPay?: boolean; - avatarUrl: string; - roleInOrg?: string; - }>; - }; + userInfo: UserInfo; /** * State passed to the OAuth provider in the original request to the OAuth provider. */ @@ -147,50 +150,12 @@ export async function oauthHandleRedirect(opts?: { hubUrl?: string }): Promise; - } = await userInfoRes.json(); + const userInfo: UserInfo = await userInfoRes.json(); return { accessToken: token.access_token, accessTokenExpiresAt, - userInfo: { - id: userInfo.sub, - name: userInfo.preferred_username, - fullname: userInfo.name, - email: userInfo.email, - emailVerified: userInfo.email_verified, - avatarUrl: userInfo.picture, - websiteUrl: userInfo.website, - isPro: userInfo.isPro, - orgs: - userInfo.orgs?.map((org) => ({ - id: org.sub, - name: org.preferred_username, - fullname: org.name, - isEnterprise: org.isEnterprise, - canPay: org.canPay, - avatarUrl: org.picture, - roleInOrg: org.roleInOrg, - })) ?? [], - }, + userInfo: userInfo, state: parsedState.state, scope: token.scope, }; From 91ab7534303e1816c779fa66c8a71ccddcd8efc8 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 26 Nov 2024 14:28:18 +0100 Subject: [PATCH 3/9] =?UTF-8?q?=E2=9C=A8=20Also=20work=20in=20node=20conte?= =?UTF-8?q?xt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/hub/src/lib/oauth-handle-redirect.ts | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/packages/hub/src/lib/oauth-handle-redirect.ts b/packages/hub/src/lib/oauth-handle-redirect.ts index 7a10d9e58..cf86f44f9 100644 --- a/packages/hub/src/lib/oauth-handle-redirect.ts +++ b/packages/hub/src/lib/oauth-handle-redirect.ts @@ -42,12 +42,21 @@ export interface OAuthResult { * There is also a helper function {@link oauthHandleRedirectIfPresent}, which will call `oauthHandleRedirect` if the URL contains an oauth code * in the query parameters and return `false` otherwise. */ -export async function oauthHandleRedirect(opts?: { hubUrl?: string }): Promise { - if (typeof window === "undefined") { - throw new Error("oauthHandleRedirect is only available in the browser"); +export async function oauthHandleRedirect(opts?: { + /** + * The URL of the hub. Defaults to {@link HUB_URL}. + */ + hubUrl?: string; + /** + * The URL to analyze. Defaults to `window.location.href`. + */ + redirectedUrl?: string; +}): Promise { + if (typeof window === "undefined" && !opts?.redirectedUrl) { + throw new Error("oauthHandleRedirect is only available in the browser, unless you provide a redirectedUrl"); } - const searchParams = new URLSearchParams(window.location.search); + const searchParams = new URLSearchParams(opts?.redirectedUrl ?? window.location.search); const [error, errorDescription] = [searchParams.get("error"), searchParams.get("error_description")]; @@ -173,12 +182,23 @@ export async function oauthHandleRedirect(opts?: { hubUrl?: string }): Promise { - if (typeof window === "undefined") { - throw new Error("oauthHandleRedirect is only available in the browser"); +export async function oauthHandleRedirectIfPresent(opts?: { + /** + * The URL of the hub. Defaults to {@link HUB_URL}. + */ + hubUrl?: string; + /** + * The URL to analyze. Defaults to `window.location.href`. + */ + redirectedUrl?: string; +}): Promise { + if (typeof window === "undefined" && !opts?.redirectedUrl) { + throw new Error( + "oauthHandleRedirectIfPresent is only available in the browser, unless you provide a redirectedUrl" + ); } - const searchParams = new URLSearchParams(window.location.search); + const searchParams = new URLSearchParams(opts?.redirectedUrl ?? window.location.search); if (searchParams.has("error")) { return oauthHandleRedirect(opts); From 0ee5ffaa32eb6e75e8583e9dcf87134c08349844 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 26 Nov 2024 15:05:24 +0100 Subject: [PATCH 4/9] =?UTF-8?q?=E2=9C=A8=20Other=20features=20to=20be=20av?= =?UTF-8?q?ailable=20in=20node=20context?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/hub/src/lib/oauth-handle-redirect.ts | 45 +++++++++++++++-- packages/hub/src/lib/oauth-login-url.ts | 48 ++++++++++++++++--- 2 files changed, 81 insertions(+), 12 deletions(-) diff --git a/packages/hub/src/lib/oauth-handle-redirect.ts b/packages/hub/src/lib/oauth-handle-redirect.ts index cf86f44f9..e887c60b0 100644 --- a/packages/hub/src/lib/oauth-handle-redirect.ts +++ b/packages/hub/src/lib/oauth-handle-redirect.ts @@ -48,12 +48,31 @@ export async function oauthHandleRedirect(opts?: { */ hubUrl?: string; /** - * The URL to analyze. Defaults to `window.location.href`. + * The URL to analyze. + * + * @default window.location.href */ redirectedUrl?: string; + /** + * nonce generated by oauthLoginUrl + * + * @default localStorage.getItem("huggingface.co:oauth:nonce") + */ + nonce?: string; + /** + * codeVerifier generated by oauthLoginUrl + * + * @default localStorage.getItem("huggingface.co:oauth:code_verifier") + */ + codeVerifier?: string; }): Promise { if (typeof window === "undefined" && !opts?.redirectedUrl) { - throw new Error("oauthHandleRedirect is only available in the browser, unless you provide a redirectedUrl"); + throw new Error("oauthHandleRedirect is only available in the browser, unless you provide redirectedUrl"); + } + if (typeof localStorage === "undefined" && (!opts?.nonce || !opts?.codeVerifier)) { + throw new Error( + "oauthHandleRedirect requires localStorage to be available, unless you provide nonce and codeVerifier" + ); } const searchParams = new URLSearchParams(opts?.redirectedUrl ?? window.location.search); @@ -188,16 +207,32 @@ export async function oauthHandleRedirectIfPresent(opts?: { */ hubUrl?: string; /** - * The URL to analyze. Defaults to `window.location.href`. + * The URL to analyze. + * + * @default window.location.href */ redirectedUrl?: string; + /** + * nonce generated by oauthLoginUrl + * + * @default localStorage.getItem("huggingface.co:oauth:nonce") + */ + nonce?: string; + /** + * codeVerifier generated by oauthLoginUrl + * + * @default localStorage.getItem("huggingface.co:oauth:code_verifier") + */ + codeVerifier?: string; }): Promise { if (typeof window === "undefined" && !opts?.redirectedUrl) { + throw new Error("oauthHandleRedirect is only available in the browser, unless you provide redirectedUrl"); + } + if (typeof localStorage === "undefined" && (!opts?.nonce || !opts?.codeVerifier)) { throw new Error( - "oauthHandleRedirectIfPresent is only available in the browser, unless you provide a redirectedUrl" + "oauthHandleRedirect requires localStorage to be available, unless you provide nonce and codeVerifier" ); } - const searchParams = new URLSearchParams(opts?.redirectedUrl ?? window.location.search); if (searchParams.has("error")) { diff --git a/packages/hub/src/lib/oauth-login-url.ts b/packages/hub/src/lib/oauth-login-url.ts index 9067ba994..2f3728296 100644 --- a/packages/hub/src/lib/oauth-login-url.ts +++ b/packages/hub/src/lib/oauth-login-url.ts @@ -64,9 +64,24 @@ export async function oauthLoginUrl(opts?: { * State to pass to the OAuth provider, which will be returned in the call to `oauthLogin` after the redirect. */ state?: string; + /** + * If provided, will be filled with the code verifier and nonce used for the OAuth flow, + * instead of using localStorage. + * + * When calling {@link `oauthHandleRedirectIfPresent`} or {@link `oauthHandleRedirect`} you will need to provide the same values. + */ + localStorage?: { + codeVerifier?: string; + nonce?: string; + }; }): Promise { - if (typeof window === "undefined") { - throw new Error("oauthLogin is only available in the browser"); + if (typeof window === "undefined" && (!opts?.redirectUrl || !opts?.clientId)) { + throw new Error("oauthLogin is only available in the browser, unless you provide clientId and redirectUrl"); + } + if (typeof localStorage === "undefined" && !opts?.localStorage) { + throw new Error( + "oauthLogin requires localStorage to be available in the context, unless you provide a localStorage empty object as argument" + ); } const hubUrl = opts?.hubUrl || HUB_URL; @@ -91,18 +106,37 @@ export async function oauthLoginUrl(opts?: { // Two random UUIDs concatenated together, because min length is 43 and max length is 128 const newCodeVerifier = globalThis.crypto.randomUUID() + globalThis.crypto.randomUUID(); - localStorage.setItem("huggingface.co:oauth:nonce", newNonce); - localStorage.setItem("huggingface.co:oauth:code_verifier", newCodeVerifier); + if (opts?.localStorage) { + if (opts.localStorage.codeVerifier !== undefined && opts.localStorage.codeVerifier !== null) { + throw new Error( + "localStorage.codeVerifier must be a initially set to null or undefined, and will be filled by oauthLoginUrl" + ); + } + if (opts.localStorage.nonce !== undefined && opts.localStorage.nonce !== null) { + throw new Error( + "localStorage.nonce must be a initially set to null or undefined, and will be filled by oauthLoginUrl" + ); + } + opts.localStorage.codeVerifier = newCodeVerifier; + opts.localStorage.nonce = newNonce; + } else { + localStorage.setItem("huggingface.co:oauth:nonce", newNonce); + localStorage.setItem("huggingface.co:oauth:code_verifier", newCodeVerifier); + } - const redirectUri = opts?.redirectUrl || window.location.href; + const redirectUri = opts?.redirectUrl || (typeof window !== "undefined" ? window.location.href : undefined); + if (!redirectUri) { + throw new Error("Missing redirectUrl"); + } const state = JSON.stringify({ nonce: newNonce, redirectUri, state: opts?.state, }); - // @ts-expect-error window.huggingface is defined inside static Spaces. - const variables: Record | null = window?.huggingface?.variables ?? null; + const variables: Record | null = + // @ts-expect-error window.huggingface is defined inside static Spaces. + typeof window !== "undefined" ? window.huggingface?.variables ?? null : null; const clientId = opts?.clientId || variables?.OAUTH_CLIENT_ID; From be017493ec64739347b0bf6ad971b28bccfb00f4 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 26 Nov 2024 15:27:54 +0100 Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=92=A1=20Comment=20OAuth=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/hub/src/lib/oauth-handle-redirect.ts | 67 ++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/packages/hub/src/lib/oauth-handle-redirect.ts b/packages/hub/src/lib/oauth-handle-redirect.ts index e887c60b0..b26034c46 100644 --- a/packages/hub/src/lib/oauth-handle-redirect.ts +++ b/packages/hub/src/lib/oauth-handle-redirect.ts @@ -2,23 +2,88 @@ import { HUB_URL } from "../consts"; import { createApiError } from "../error"; export interface UserInfo { + /** + * OpenID Connect field. Unique identifier for the user, even in case of rename. + */ sub: string; + /** + * OpenID Connect field. The user's full name. + */ name: string; + /** + * OpenID Connect field. The user's username. + */ preferred_username: string; + /** + * OpenID Connect field, available if scope "email" was granted. + */ email_verified?: boolean; + /** + * OpenID Connect field, available if scope "email" was granted. + */ email?: string; + /** + * OpenID Connect field. The user's profile picture URL. + */ picture: string; + /** + * OpenID Connect field. The user's website URL. + */ website?: string; + + /** + * Hugging Face field. Whether the user is a pro user. + */ isPro: boolean; + /** + * Hugging Face field. Whether the user has a payment method set up. Needs "read-billing" scope. + */ canPay?: boolean; + /** + * Hugging Face field. The user's orgs + */ orgs?: Array<{ + /** + * OpenID Connect field. Unique identifier for the org. + */ sub: string; + /** + * OpenID Connect field. The org's full name. + */ name: string; - picture: string; + /** + * OpenID Connect field. The org's username. + */ preferred_username: string; + /** + * OpenID Connect field. The org's profile picture URL. + */ + picture: string; + + /** + * Hugging Face field. Whether the org is an enterprise org. + */ isEnterprise: boolean; + /** + * Hugging Face field. Whether the org has a payment method set up. Needs "read-billing" scope, and the user needs to approve access to the org in the OAuth page. + */ canPay?: boolean; + /** + * Hugging Face field. The user's role in the org. The user needs to approve access to the org in the OAuth page. + */ roleInOrg?: string; + /** + * HuggingFace field. When the user granted the oauth app access to the org, but didn't complete SSO. + * + * Should never happen directly after the oauth flow. + */ + pendingSSO?: boolean; + /** + * HuggingFace field. When the user granted the oauth app access to the org, but didn't complete MFA. + * + * Should never happen directly after the oauth flow. + */ + missingMFA?: boolean; }>; } From e86174058d3550be44935faf490488b1d1bb6fa6 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 26 Nov 2024 15:58:33 +0100 Subject: [PATCH 6/9] =?UTF-8?q?=E2=9C=85=20Add=20oauth=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/hub/src/error.ts | 3 + packages/hub/src/lib/create-repo.ts | 3 + .../hub/src/lib/oauth-handle-redirect.spec.ts | 55 +++++++++++++++++++ packages/hub/src/lib/oauth-handle-redirect.ts | 22 ++++++-- packages/hub/src/test/consts.ts | 1 + packages/hub/vitest-browser.config.mts | 2 + 6 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 packages/hub/src/lib/oauth-handle-redirect.spec.ts diff --git a/packages/hub/src/error.ts b/packages/hub/src/error.ts index f0a3e33c4..0da5b2dd9 100644 --- a/packages/hub/src/error.ts +++ b/packages/hub/src/error.ts @@ -15,6 +15,9 @@ export async function createApiError( if (response.headers.get("Content-Type")?.startsWith("application/json")) { const json = await response.json(); error.message = json.error || json.message || error.message; + if (json.error_description) { + error.message = error.message ? error.message + `: ${json.error_description}` : json.error_description; + } error.data = json; } else { error.data = { message: await response.text() }; diff --git a/packages/hub/src/lib/create-repo.ts b/packages/hub/src/lib/create-repo.ts index 4005844d6..c0323dc11 100644 --- a/packages/hub/src/lib/create-repo.ts +++ b/packages/hub/src/lib/create-repo.ts @@ -9,6 +9,9 @@ import { toRepoId } from "../utils/toRepoId"; export async function createRepo( params: { repo: RepoDesignation; + /** + * If unset, will follow the organization's default setting. (typically public, except for some Enterprise organizations) + */ private?: boolean; license?: string; /** diff --git a/packages/hub/src/lib/oauth-handle-redirect.spec.ts b/packages/hub/src/lib/oauth-handle-redirect.spec.ts new file mode 100644 index 000000000..8b27477d5 --- /dev/null +++ b/packages/hub/src/lib/oauth-handle-redirect.spec.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { TEST_COOKIE, TEST_HUB_URL } from "../test/consts"; +import { oauthLoginUrl } from "./oauth-login-url"; +import { oauthHandleRedirect } from "./oauth-handle-redirect"; + +describe("oauthHandleRedirect", () => { + it("should work", async () => { + const localStorage = { + nonce: undefined, + codeVerifier: undefined, + }; + const url = await oauthLoginUrl({ + clientId: "dummy-app", + redirectUrl: "http://localhost:3000", + localStorage, + hubUrl: TEST_HUB_URL, + }); + const resp = await fetch(url, { + method: "POST", + headers: { + Cookie: `token=${TEST_COOKIE}`, + }, + redirect: "manual", + }); + if (resp.status !== 303) { + throw new Error(`Failed to fetch url ${url}: ${resp.status} ${resp.statusText}`); + } + const location = resp.headers.get("Location"); + if (!location) { + throw new Error(`No location header in response`); + } + const result = await oauthHandleRedirect({ + redirectedUrl: location, + codeVerifier: localStorage.codeVerifier, + nonce: localStorage.nonce, + hubUrl: TEST_HUB_URL, + }); + + if (!result) { + throw new Error("Expected result to be defined"); + } + expect(result.accessToken).toBeInstanceOf(String); + expect(result.accessTokenExpiresAt).toBeInstanceOf(Date); + expect(result.accessTokenExpiresAt).toBeGreaterThan(Date.now()); + expect(result.scope).toBeInstanceOf(String); + expect(result.userInfo).toEqual({ + sub: "62f264b9f3c90f4b6514a269", + name: "@huggingface/hub CI bot", + preferred_username: "hub.js", + email_verified: true, + email: "elitt@huggingface.co", + picture: "https://huggingface.co/hub.js", + }); + }); +}); diff --git a/packages/hub/src/lib/oauth-handle-redirect.ts b/packages/hub/src/lib/oauth-handle-redirect.ts index b26034c46..8714b5999 100644 --- a/packages/hub/src/lib/oauth-handle-redirect.ts +++ b/packages/hub/src/lib/oauth-handle-redirect.ts @@ -140,7 +140,13 @@ export async function oauthHandleRedirect(opts?: { ); } - const searchParams = new URLSearchParams(opts?.redirectedUrl ?? window.location.search); + const redirectedUrl = opts?.redirectedUrl ?? window.location.href; + const searchParams = + URL.parse !== undefined ? URL.parse(redirectedUrl)?.searchParams : new URL(redirectedUrl).searchParams; + + if (!searchParams) { + throw new Error("Failed to parse redirected URL: " + redirectedUrl); + } const [error, errorDescription] = [searchParams.get("error"), searchParams.get("error_description")]; @@ -149,17 +155,17 @@ export async function oauthHandleRedirect(opts?: { } const code = searchParams.get("code"); - const nonce = localStorage.getItem("huggingface.co:oauth:nonce"); + const nonce = opts?.nonce ?? localStorage.getItem("huggingface.co:oauth:nonce"); if (!code) { - throw new Error("Missing oauth code from query parameters in redirected URL"); + throw new Error("Missing oauth code from query parameters in redirected URL: " + redirectedUrl); } if (!nonce) { throw new Error("Missing oauth nonce from localStorage"); } - const codeVerifier = localStorage.getItem("huggingface.co:oauth:code_verifier"); + const codeVerifier = opts?.codeVerifier ?? localStorage.getItem("huggingface.co:oauth:code_verifier"); if (!codeVerifier) { throw new Error("Missing oauth code_verifier from localStorage"); @@ -215,8 +221,12 @@ export async function oauthHandleRedirect(opts?: { }).toString(), }); - localStorage.removeItem("huggingface.co:oauth:code_verifier"); - localStorage.removeItem("huggingface.co:oauth:nonce"); + if (!opts?.codeVerifier) { + localStorage.removeItem("huggingface.co:oauth:code_verifier"); + } + if (!opts?.nonce) { + localStorage.removeItem("huggingface.co:oauth:nonce"); + } if (!tokenRes.ok) { throw await createApiError(tokenRes); diff --git a/packages/hub/src/test/consts.ts b/packages/hub/src/test/consts.ts index 297ea01d0..6b8b7983d 100644 --- a/packages/hub/src/test/consts.ts +++ b/packages/hub/src/test/consts.ts @@ -1,3 +1,4 @@ export const TEST_HUB_URL = "https://hub-ci.huggingface.co"; export const TEST_USER = "hub.js"; export const TEST_ACCESS_TOKEN = "hf_hub.js"; +export const TEST_COOKIE = "huggingface-hub.js-cookie"; diff --git a/packages/hub/vitest-browser.config.mts b/packages/hub/vitest-browser.config.mts index db22fb67c..2c21588af 100644 --- a/packages/hub/vitest-browser.config.mts +++ b/packages/hub/vitest-browser.config.mts @@ -8,6 +8,8 @@ export default defineConfig({ "src/lib/cache-management.spec.ts", "src/lib/download-file-to-cache-dir.spec.ts", "src/lib/snapshot-download.spec.ts", + // Because we use redirect: "manual" in the test + "src/lib/oauth-handle-redirect.spec.ts", ], }, }); From c7cb4bb06b954f48f152d5190644156ccb891bb6 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 26 Nov 2024 16:04:43 +0100 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=9A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/hub/src/lib/oauth-handle-redirect.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/hub/src/lib/oauth-handle-redirect.ts b/packages/hub/src/lib/oauth-handle-redirect.ts index 8714b5999..f4274906c 100644 --- a/packages/hub/src/lib/oauth-handle-redirect.ts +++ b/packages/hub/src/lib/oauth-handle-redirect.ts @@ -141,12 +141,13 @@ export async function oauthHandleRedirect(opts?: { } const redirectedUrl = opts?.redirectedUrl ?? window.location.href; - const searchParams = - URL.parse !== undefined ? URL.parse(redirectedUrl)?.searchParams : new URL(redirectedUrl).searchParams; - - if (!searchParams) { - throw new Error("Failed to parse redirected URL: " + redirectedUrl); - } + const searchParams = (() => { + try { + return new URL(redirectedUrl).searchParams; + } catch (err) { + throw new Error("Failed to parse redirected URL: " + redirectedUrl); + } + })(); const [error, errorDescription] = [searchParams.get("error"), searchParams.get("error_description")]; From 0bf77c6b70ef1b087fe96a390930f9199c65881f Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Wed, 27 Nov 2024 12:53:53 +0100 Subject: [PATCH 8/9] =?UTF-8?q?=E2=9C=85=20Fix=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hub/src/lib/oauth-handle-redirect.spec.ts | 15 ++++++++++----- packages/hub/src/lib/oauth-handle-redirect.ts | 4 ++++ packages/hub/src/lib/oauth-login-url.ts | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/hub/src/lib/oauth-handle-redirect.spec.ts b/packages/hub/src/lib/oauth-handle-redirect.spec.ts index 8b27477d5..638493580 100644 --- a/packages/hub/src/lib/oauth-handle-redirect.spec.ts +++ b/packages/hub/src/lib/oauth-handle-redirect.spec.ts @@ -13,6 +13,7 @@ describe("oauthHandleRedirect", () => { clientId: "dummy-app", redirectUrl: "http://localhost:3000", localStorage, + scopes: "openid profile email", hubUrl: TEST_HUB_URL, }); const resp = await fetch(url, { @@ -39,17 +40,21 @@ describe("oauthHandleRedirect", () => { if (!result) { throw new Error("Expected result to be defined"); } - expect(result.accessToken).toBeInstanceOf(String); + expect(result.accessToken).toEqual(expect.any(String)); expect(result.accessTokenExpiresAt).toBeInstanceOf(Date); - expect(result.accessTokenExpiresAt).toBeGreaterThan(Date.now()); - expect(result.scope).toBeInstanceOf(String); + expect(result.accessTokenExpiresAt.getTime()).toBeGreaterThan(Date.now()); + expect(result.scope).toEqual(expect.any(String)); expect(result.userInfo).toEqual({ sub: "62f264b9f3c90f4b6514a269", name: "@huggingface/hub CI bot", preferred_username: "hub.js", email_verified: true, - email: "elitt@huggingface.co", - picture: "https://huggingface.co/hub.js", + email: "eliott@huggingface.co", + isPro: false, + picture: expect.stringContaining("/avatars/934b830e9fdaa879487852f79eef7165.svg"), + profile: "https://hub-ci.huggingface.co/hub.js", + website: "https://github.com/huggingface/hub.js", + orgs: [], }); }); }); diff --git a/packages/hub/src/lib/oauth-handle-redirect.ts b/packages/hub/src/lib/oauth-handle-redirect.ts index f4274906c..89bbb0b60 100644 --- a/packages/hub/src/lib/oauth-handle-redirect.ts +++ b/packages/hub/src/lib/oauth-handle-redirect.ts @@ -26,6 +26,10 @@ export interface UserInfo { * OpenID Connect field. The user's profile picture URL. */ picture: string; + /** + * OpenID Connect field. The user's profile URL. + */ + profile: string; /** * OpenID Connect field. The user's website URL. */ diff --git a/packages/hub/src/lib/oauth-login-url.ts b/packages/hub/src/lib/oauth-login-url.ts index 2f3728296..9b8bca3f3 100644 --- a/packages/hub/src/lib/oauth-login-url.ts +++ b/packages/hub/src/lib/oauth-login-url.ts @@ -40,7 +40,7 @@ export async function oauthLoginUrl(opts?: { clientId?: string; hubUrl?: string; /** - * OAuth scope, a list of space separate scopes. + * OAuth scope, a list of space-separated scopes. * * For static Spaces, you can omit this and it will be loaded from the Space config, as long as `hf_oauth: true` is present in the README.md's metadata. * For other Spaces, it is available to the backend in the OAUTH_SCOPES environment variable, as long as `hf_oauth: true` is present in the README.md's metadata. From 91dc0cc3296739660c9c95163ca1fbe3004578db Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Fri, 29 Nov 2024 16:28:48 +0100 Subject: [PATCH 9/9] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20try=20full=20url=20now?= =?UTF-8?q?=20for=20avatar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/hub/src/lib/oauth-handle-redirect.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hub/src/lib/oauth-handle-redirect.spec.ts b/packages/hub/src/lib/oauth-handle-redirect.spec.ts index 638493580..f06f6a40e 100644 --- a/packages/hub/src/lib/oauth-handle-redirect.spec.ts +++ b/packages/hub/src/lib/oauth-handle-redirect.spec.ts @@ -51,7 +51,7 @@ describe("oauthHandleRedirect", () => { email_verified: true, email: "eliott@huggingface.co", isPro: false, - picture: expect.stringContaining("/avatars/934b830e9fdaa879487852f79eef7165.svg"), + picture: "https://hub-ci.huggingface.co/avatars/934b830e9fdaa879487852f79eef7165.svg", profile: "https://hub-ci.huggingface.co/hub.js", website: "https://github.com/huggingface/hub.js", orgs: [],