From 80240e774900bf3137a36e90e07fadcf25fbc768 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Sun, 2 Nov 2025 22:45:31 +0100 Subject: [PATCH 1/7] Add support for private github repos via git:directory --- .../storage/src/lib/git-sparse-checkout.ts | 167 ++++++++++++++++-- .../github-private-repo-auth-modal/index.tsx | 56 ++++++ .../website/src/components/layout/index.tsx | 4 + .../github/acquire-oauth-token-if-needed.tsx | 17 +- .../playground/website/src/github/state.ts | 8 + .../src/lib/state/redux/boot-site-client.ts | 7 + .../website/src/lib/state/redux/slice-ui.ts | 8 +- packages/playground/website/vite.oauth.ts | 2 +- 8 files changed, 245 insertions(+), 24 deletions(-) create mode 100644 packages/playground/website/src/components/github-private-repo-auth-modal/index.tsx diff --git a/packages/playground/storage/src/lib/git-sparse-checkout.ts b/packages/playground/storage/src/lib/git-sparse-checkout.ts index a977fc33bf..c8dfd52c09 100644 --- a/packages/playground/storage/src/lib/git-sparse-checkout.ts +++ b/packages/playground/storage/src/lib/git-sparse-checkout.ts @@ -30,6 +30,93 @@ if (typeof globalThis.Buffer === 'undefined') { globalThis.Buffer = BufferPolyfill; } +/** + * Module-level storage for GitHub authentication token. + * This is set by browser-specific code when GitHub OAuth is available. + */ +let gitHubAuthToken: string | undefined; + +/** + * Sets the GitHub authentication token to use for git protocol requests. + * This is intended to be called by browser-specific initialization code + * where GitHub OAuth is available. + * + * @param token The GitHub OAuth token, or undefined to clear it + */ +export function setGitHubAuthToken(token: string | undefined) { + gitHubAuthToken = token; +} + +/** + * Custom error class for GitHub authentication failures. + */ +export class GitHubAuthenticationError extends Error { + constructor(public repoUrl: string, public status: number) { + super( + `Authentication required to access private GitHub repository: ${repoUrl}` + ); + this.name = 'GitHubAuthenticationError'; + } +} + +/** + * Checks if a URL is a GitHub URL by parsing the hostname. + * Handles both direct GitHub URLs and CORS-proxied URLs. + * + * @param url The URL to check + * @returns true if the URL is definitively a GitHub URL, false otherwise + */ +function isGitHubUrl(url: string): boolean { + try { + const parsedUrl = new URL(url); + + // Direct GitHub URL - check hostname + if (parsedUrl.hostname === 'github.com') { + return true; + } + + // CORS-proxied GitHub URL - the actual GitHub URL should be in the query string + // Format: https://proxy.com/cors-proxy.php?https://github.com/... + // We need to extract and validate the proxied URL's hostname + const queryString = parsedUrl.search.substring(1); // Remove leading '?' + if (queryString) { + // Try to extract a URL from the query string + // Match URLs that start with http:// or https:// + const urlMatch = queryString.match(/^(https?:\/\/[^\s&]+)/); + if (urlMatch) { + try { + const proxiedUrl = new URL(urlMatch[1]); + if (proxiedUrl.hostname === 'github.com') { + return true; + } + } catch { + // Invalid proxied URL, ignore + } + } + } + + return false; + } catch { + // If URL parsing fails, return false + return false; + } +} + +/** + * Adds GitHub authentication headers to a headers object if a token is available + * and the URL is a GitHub URL. + */ +function addGitHubAuthHeaders(headers: HeadersInit, url: string): void { + if (gitHubAuthToken && isGitHubUrl(url)) { + // GitHub Git protocol requires Basic Auth with token as username and empty password + const basicAuth = btoa(`${gitHubAuthToken}:`); + headers['Authorization'] = `Basic ${basicAuth}`; + // Tell CORS proxy to forward the Authorization header + // Must be lowercase because the CORS proxy lowercases header names for comparison + headers['X-Cors-Proxy-Allowed-Request-Headers'] = 'authorization'; + } +} + /** * Downloads specific files from a git repository. * It uses the git protocol over HTTP to fetch the files. It only uses @@ -253,17 +340,33 @@ export async function listGitRefs( ])) as any ); + const headers: HeadersInit = { + Accept: 'application/x-git-upload-pack-advertisement', + 'content-type': 'application/x-git-upload-pack-request', + 'Content-Length': `${packbuffer.length}`, + 'Git-Protocol': 'version=2', + }; + + addGitHubAuthHeaders(headers, repoUrl); + const response = await fetch(repoUrl + '/git-upload-pack', { method: 'POST', - headers: { - Accept: 'application/x-git-upload-pack-advertisement', - 'content-type': 'application/x-git-upload-pack-request', - 'Content-Length': `${packbuffer.length}`, - 'Git-Protocol': 'version=2', - }, + headers, body: packbuffer as any, }); + if (!response.ok) { + if ( + (response.status === 401 || response.status === 403) && + isGitHubUrl(repoUrl) + ) { + throw new GitHubAuthenticationError(repoUrl, response.status); + } + throw new Error( + `Failed to fetch git refs from ${repoUrl}: ${response.status} ${response.statusText}` + ); + } + const refs: Record = {}; for await (const line of parseGitResponseLines(response)) { const spaceAt = line.indexOf(' '); @@ -403,16 +506,32 @@ async function fetchWithoutBlobs(repoUrl: string, commitHash: string) { ])) as any ); + const headers: HeadersInit = { + Accept: 'application/x-git-upload-pack-advertisement', + 'content-type': 'application/x-git-upload-pack-request', + 'Content-Length': `${packbuffer.length}`, + }; + + addGitHubAuthHeaders(headers, repoUrl); + const response = await fetch(repoUrl + '/git-upload-pack', { method: 'POST', - headers: { - Accept: 'application/x-git-upload-pack-advertisement', - 'content-type': 'application/x-git-upload-pack-request', - 'Content-Length': `${packbuffer.length}`, - }, + headers, body: packbuffer as any, }); + if (!response.ok) { + if ( + (response.status === 401 || response.status === 403) && + isGitHubUrl(repoUrl) + ) { + throw new GitHubAuthenticationError(repoUrl, response.status); + } + throw new Error( + `Failed to fetch git objects from ${repoUrl}: ${response.status} ${response.statusText}` + ); + } + const iterator = streamToIterator(response.body!); const parsed = await parseUploadPackResponse(iterator); const packfile = Buffer.from((await collect(parsed.packfile)) as any); @@ -552,16 +671,32 @@ async function fetchObjects(url: string, objectHashes: string[]) { ])) as any ); + const headers: HeadersInit = { + Accept: 'application/x-git-upload-pack-advertisement', + 'content-type': 'application/x-git-upload-pack-request', + 'Content-Length': `${packbuffer.length}`, + }; + + addGitHubAuthHeaders(headers, url); + const response = await fetch(url + '/git-upload-pack', { method: 'POST', - headers: { - Accept: 'application/x-git-upload-pack-advertisement', - 'content-type': 'application/x-git-upload-pack-request', - 'Content-Length': `${packbuffer.length}`, - }, + headers, body: packbuffer as any, }); + if (!response.ok) { + if ( + (response.status === 401 || response.status === 403) && + isGitHubUrl(url) + ) { + throw new GitHubAuthenticationError(url, response.status); + } + throw new Error( + `Failed to fetch git objects from ${url}: ${response.status} ${response.statusText}` + ); + } + const iterator = streamToIterator(response.body!); const parsed = await parseUploadPackResponse(iterator); const packfile = Buffer.from((await collect(parsed.packfile)) as any); diff --git a/packages/playground/website/src/components/github-private-repo-auth-modal/index.tsx b/packages/playground/website/src/components/github-private-repo-auth-modal/index.tsx new file mode 100644 index 0000000000..12f9bd1153 --- /dev/null +++ b/packages/playground/website/src/components/github-private-repo-auth-modal/index.tsx @@ -0,0 +1,56 @@ +import { Modal } from '../modal'; +import { useAppDispatch } from '../../lib/state/redux/store'; +import { setActiveModal } from '../../lib/state/redux/slice-ui'; +import { Icon } from '@wordpress/components'; +import { GitHubIcon } from '../../github/github'; +import css from '../../github/github-oauth-guard/style.module.css'; + +const OAUTH_FLOW_URL = 'oauth.php?redirect=1'; + +export function GitHubPrivateRepoAuthModal() { + const dispatch = useAppDispatch(); + + // Remove the modal parameter from the redirect URI + // so it doesn't persist after OAuth completes + const redirectUrl = new URL(window.location.href); + redirectUrl.searchParams.delete('modal'); + + const urlParams = new URLSearchParams(); + urlParams.set('redirect_uri', redirectUrl.toString()); + const oauthUrl = `${OAUTH_FLOW_URL}&${urlParams.toString()}`; + + return ( + dispatch(setActiveModal(null))} + > +
+

+ This blueprint requires access to a private GitHub + repository. +

+

+ To continue, please connect your GitHub account with + WordPress Playground. +

+ +

+ + + Connect your GitHub account + +

+

+ + Your access token is stored only in memory and will be + cleared when you close this tab. + +

+
+
+ ); +} diff --git a/packages/playground/website/src/components/layout/index.tsx b/packages/playground/website/src/components/layout/index.tsx index 9523bd0946..4ef23807bd 100644 --- a/packages/playground/website/src/components/layout/index.tsx +++ b/packages/playground/website/src/components/layout/index.tsx @@ -31,6 +31,7 @@ import { PreviewPRModal } from '../../github/preview-pr'; import { MissingSiteModal } from '../missing-site-modal'; import { RenameSiteModal } from '../rename-site-modal'; import { SaveSiteModal } from '../save-site-modal'; +import { GitHubPrivateRepoAuthModal } from '../github-private-repo-auth-modal'; acquireOAuthTokenIfNeeded(); @@ -41,6 +42,7 @@ export const modalSlugs = { IMPORT_FORM: 'import-form', GITHUB_IMPORT: 'github-import', GITHUB_EXPORT: 'github-export', + GITHUB_PRIVATE_REPO_AUTH: 'github-private-repo-auth', PREVIEW_PR_WP: 'preview-pr-wordpress', PREVIEW_PR_GUTENBERG: 'preview-pr-gutenberg', MISSING_SITE_PROMPT: 'missing-site-prompt', @@ -216,6 +218,8 @@ function Modals(blueprint: BlueprintV1Declaration) { return ; } else if (currentModal === modalSlugs.SAVE_SITE) { return ; + } else if (currentModal === modalSlugs.GITHUB_PRIVATE_REPO_AUTH) { + return ; } if (query.get('gh-ensure-auth') === 'yes') { diff --git a/packages/playground/website/src/github/acquire-oauth-token-if-needed.tsx b/packages/playground/website/src/github/acquire-oauth-token-if-needed.tsx index d7a3080dd1..cd2a01d286 100644 --- a/packages/playground/website/src/github/acquire-oauth-token-if-needed.tsx +++ b/packages/playground/website/src/github/acquire-oauth-token-if-needed.tsx @@ -1,5 +1,6 @@ import { setOAuthToken, oAuthState } from './state'; import { oauthCode } from './github-oauth-guard'; +import { setGitHubAuthToken } from '@wp-playground/storage'; export async function acquireOAuthTokenIfNeeded() { if (!oauthCode) { @@ -25,15 +26,21 @@ export async function acquireOAuthTokenIfNeeded() { }); const body = await response.json(); setOAuthToken(body.access_token); + setGitHubAuthToken(body.access_token); + + // Remove the ?code=... from the URL and clean up any modal state + const url = new URL(window.location.href); + url.searchParams.delete('code'); + url.searchParams.delete('modal'); + // Keep the hash (it contains the blueprint) + + // Reload the page to retry the blueprint with the new token + // This is necessary because the blueprint failed before we had the token + window.location.href = url.toString(); } finally { oAuthState.value = { ...oAuthState.value, isAuthorizing: false, }; } - - // Remove the ?code=... from the URL - const url = new URL(window.location.href); - url.searchParams.delete('code'); - window.history.replaceState(null, '', url.toString()); } diff --git a/packages/playground/website/src/github/state.ts b/packages/playground/website/src/github/state.ts index bb7b1eb521..3e9069b3a7 100644 --- a/packages/playground/website/src/github/state.ts +++ b/packages/playground/website/src/github/state.ts @@ -1,4 +1,5 @@ import { signal } from '@preact/signals-react'; +import { setGitHubAuthToken } from '@wp-playground/storage'; export interface GitHubOAuthState { token?: string; @@ -16,6 +17,11 @@ export const oAuthState = signal({ token: shouldStoreToken ? localStorage.getItem(TOKEN_KEY) || '' : '', }); +// Initialize the git-sparse-checkout module with the token if it exists +if (oAuthState.value.token) { + setGitHubAuthToken(oAuthState.value.token); +} + export function setOAuthToken(token?: string) { if (shouldStoreToken) { localStorage.setItem(TOKEN_KEY, token || ''); @@ -24,4 +30,6 @@ export function setOAuthToken(token?: string) { ...oAuthState.value, token, }; + // Also update the token in the git-sparse-checkout module + setGitHubAuthToken(token); } diff --git a/packages/playground/website/src/lib/state/redux/boot-site-client.ts b/packages/playground/website/src/lib/state/redux/boot-site-client.ts index 7f5e87e613..ca0639b3d7 100644 --- a/packages/playground/website/src/lib/state/redux/boot-site-client.ts +++ b/packages/playground/website/src/lib/state/redux/boot-site-client.ts @@ -198,6 +198,13 @@ export function bootSiteClient( (e as any).originalErrorClassName === 'ArtifactExpiredError' ) { dispatch(setActiveSiteError('github-artifact-expired')); + } else if ( + (e as any).name === 'GitHubAuthenticationError' || + (e as any).originalErrorClassName === + 'GitHubAuthenticationError' || + (e as any).cause?.name === 'GitHubAuthenticationError' + ) { + dispatch(setActiveModal(modalSlugs.GITHUB_PRIVATE_REPO_AUTH)); } else { dispatch(setActiveSiteError('site-boot-failed')); dispatch(setActiveModal(modalSlugs.ERROR_REPORT)); diff --git a/packages/playground/website/src/lib/state/redux/slice-ui.ts b/packages/playground/website/src/lib/state/redux/slice-ui.ts index 0e4e23419a..2e45e7b3d9 100644 --- a/packages/playground/website/src/lib/state/redux/slice-ui.ts +++ b/packages/playground/website/src/lib/state/redux/slice-ui.ts @@ -34,10 +34,13 @@ const initialState: UIState = { * Don't show certain modals after a page refresh. * The save-site and error-report modals should only be triggered by user actions, * not by loading a URL with the modal parameter. + * The github-private-repo-auth modal should only be triggered by authentication errors, + * not by loading a URL with the modal parameter. */ activeModal: query.get('modal') === 'error-report' || - query.get('modal') === 'save-site' + query.get('modal') === 'save-site' || + query.get('modal') === 'github-private-repo-auth' ? null : query.get('modal') || null, offline: !navigator.onLine, @@ -122,7 +125,8 @@ export const listenToOnlineOfflineEventsMiddleware: Middleware = */ if ( query.get('modal') === 'error-report' || - query.get('modal') === 'save-site' + query.get('modal') === 'save-site' || + query.get('modal') === 'github-private-repo-auth' ) { setTimeout(() => { store.dispatch(uiSlice.actions.setActiveModal(null)); diff --git a/packages/playground/website/vite.oauth.ts b/packages/playground/website/vite.oauth.ts index ed665fdda5..b2ea337504 100644 --- a/packages/playground/website/vite.oauth.ts +++ b/packages/playground/website/vite.oauth.ts @@ -18,7 +18,7 @@ export const oAuthMiddleware = async ( if (query.get('redirect') === '1') { const params: Record = { client_id: CLIENT_ID!, - scope: 'public_repo', + scope: 'repo', }; if (query.has('redirect_uri')) { params.redirect_uri = query.get('redirect_uri')!; From 07c3a4d3dc564f134ab263e5e3bd0f037b20e4c1 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Sun, 2 Nov 2025 23:10:18 +0100 Subject: [PATCH 2/7] Fix type errors --- .../storage/src/lib/git-sparse-checkout.ts | 63 +++++++++---------- 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/packages/playground/storage/src/lib/git-sparse-checkout.ts b/packages/playground/storage/src/lib/git-sparse-checkout.ts index c8dfd52c09..03b22115d6 100644 --- a/packages/playground/storage/src/lib/git-sparse-checkout.ts +++ b/packages/playground/storage/src/lib/git-sparse-checkout.ts @@ -103,18 +103,20 @@ function isGitHubUrl(url: string): boolean { } /** - * Adds GitHub authentication headers to a headers object if a token is available - * and the URL is a GitHub URL. + * Returns GitHub authentication headers if a token is available and the URL is a GitHub URL. */ -function addGitHubAuthHeaders(headers: HeadersInit, url: string): void { +function getGitHubAuthHeaders(url: string): Record { if (gitHubAuthToken && isGitHubUrl(url)) { // GitHub Git protocol requires Basic Auth with token as username and empty password const basicAuth = btoa(`${gitHubAuthToken}:`); - headers['Authorization'] = `Basic ${basicAuth}`; - // Tell CORS proxy to forward the Authorization header - // Must be lowercase because the CORS proxy lowercases header names for comparison - headers['X-Cors-Proxy-Allowed-Request-Headers'] = 'authorization'; + return { + Authorization: `Basic ${basicAuth}`, + // Tell CORS proxy to forward the Authorization header + // Must be lowercase because the CORS proxy lowercases header names for comparison + 'X-Cors-Proxy-Allowed-Request-Headers': 'authorization', + }; } + return {}; } /** @@ -340,18 +342,15 @@ export async function listGitRefs( ])) as any ); - const headers: HeadersInit = { - Accept: 'application/x-git-upload-pack-advertisement', - 'content-type': 'application/x-git-upload-pack-request', - 'Content-Length': `${packbuffer.length}`, - 'Git-Protocol': 'version=2', - }; - - addGitHubAuthHeaders(headers, repoUrl); - const response = await fetch(repoUrl + '/git-upload-pack', { method: 'POST', - headers, + headers: { + Accept: 'application/x-git-upload-pack-advertisement', + 'content-type': 'application/x-git-upload-pack-request', + 'Content-Length': `${packbuffer.length}`, + 'Git-Protocol': 'version=2', + ...getGitHubAuthHeaders(repoUrl), + }, body: packbuffer as any, }); @@ -506,17 +505,14 @@ async function fetchWithoutBlobs(repoUrl: string, commitHash: string) { ])) as any ); - const headers: HeadersInit = { - Accept: 'application/x-git-upload-pack-advertisement', - 'content-type': 'application/x-git-upload-pack-request', - 'Content-Length': `${packbuffer.length}`, - }; - - addGitHubAuthHeaders(headers, repoUrl); - const response = await fetch(repoUrl + '/git-upload-pack', { method: 'POST', - headers, + headers: { + Accept: 'application/x-git-upload-pack-advertisement', + 'content-type': 'application/x-git-upload-pack-request', + 'Content-Length': `${packbuffer.length}`, + ...getGitHubAuthHeaders(repoUrl), + }, body: packbuffer as any, }); @@ -671,17 +667,14 @@ async function fetchObjects(url: string, objectHashes: string[]) { ])) as any ); - const headers: HeadersInit = { - Accept: 'application/x-git-upload-pack-advertisement', - 'content-type': 'application/x-git-upload-pack-request', - 'Content-Length': `${packbuffer.length}`, - }; - - addGitHubAuthHeaders(headers, url); - const response = await fetch(url + '/git-upload-pack', { method: 'POST', - headers, + headers: { + Accept: 'application/x-git-upload-pack-advertisement', + 'content-type': 'application/x-git-upload-pack-request', + 'Content-Length': `${packbuffer.length}`, + ...getGitHubAuthHeaders(url), + }, body: packbuffer as any, }); From a2c992369a2e88c12b56319fa94fb571e9d432cf Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Sun, 2 Nov 2025 23:22:55 +0100 Subject: [PATCH 3/7] Better check for github URLs in a cors proxy url --- .../storage/src/lib/git-sparse-checkout.ts | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/playground/storage/src/lib/git-sparse-checkout.ts b/packages/playground/storage/src/lib/git-sparse-checkout.ts index 03b22115d6..f9c1113eca 100644 --- a/packages/playground/storage/src/lib/git-sparse-checkout.ts +++ b/packages/playground/storage/src/lib/git-sparse-checkout.ts @@ -36,6 +36,15 @@ if (typeof globalThis.Buffer === 'undefined') { */ let gitHubAuthToken: string | undefined; +/** + * Known CORS proxy URL prefixes used by WordPress Playground. + * Keep up this synced with packages/playground/website-extras/vite.config.ts + */ +const KNOWN_CORS_PROXY_URLS = [ + 'https://wordpress-playground-cors-proxy.net/?', + 'http://127.0.0.1:5263/cors-proxy.php?', +]; + /** * Sets the GitHub authentication token to use for git protocol requests. * This is intended to be called by browser-specific initialization code @@ -61,7 +70,7 @@ export class GitHubAuthenticationError extends Error { /** * Checks if a URL is a GitHub URL by parsing the hostname. - * Handles both direct GitHub URLs and CORS-proxied URLs. + * Handles both direct GitHub URLs and CORS-proxied GitHub URLs. * * @param url The URL to check * @returns true if the URL is definitively a GitHub URL, false otherwise @@ -70,40 +79,32 @@ function isGitHubUrl(url: string): boolean { try { const parsedUrl = new URL(url); - // Direct GitHub URL - check hostname if (parsedUrl.hostname === 'github.com') { return true; } - // CORS-proxied GitHub URL - the actual GitHub URL should be in the query string - // Format: https://proxy.com/cors-proxy.php?https://github.com/... - // We need to extract and validate the proxied URL's hostname - const queryString = parsedUrl.search.substring(1); // Remove leading '?' - if (queryString) { - // Try to extract a URL from the query string - // Match URLs that start with http:// or https:// - const urlMatch = queryString.match(/^(https?:\/\/[^\s&]+)/); - if (urlMatch) { + for (const proxyUrl of KNOWN_CORS_PROXY_URLS) { + if (url.startsWith(proxyUrl)) { + const proxiedUrl = url.substring(proxyUrl.length); try { - const proxiedUrl = new URL(urlMatch[1]); - if (proxiedUrl.hostname === 'github.com') { - return true; - } + const proxiedParsedUrl = new URL(proxiedUrl); + return proxiedParsedUrl.hostname === 'github.com'; } catch { - // Invalid proxied URL, ignore + return false; } } } return false; } catch { - // If URL parsing fails, return false return false; } } /** * Returns GitHub authentication headers if a token is available and the URL is a GitHub URL. + * + * @param url The URL to check */ function getGitHubAuthHeaders(url: string): Record { if (gitHubAuthToken && isGitHubUrl(url)) { From fbb6873c5dc151ee4952ee164d5aefdb51884094 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Mon, 3 Nov 2025 06:59:48 +0100 Subject: [PATCH 4/7] Show the github url --- .../github-private-repo-auth-modal/index.tsx | 39 +++++++++++++++++-- .../src/lib/state/redux/boot-site-client.ts | 14 ++++++- .../website/src/lib/state/redux/slice-ui.ts | 8 ++++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/packages/playground/website/src/components/github-private-repo-auth-modal/index.tsx b/packages/playground/website/src/components/github-private-repo-auth-modal/index.tsx index 12f9bd1153..f785a9b69c 100644 --- a/packages/playground/website/src/components/github-private-repo-auth-modal/index.tsx +++ b/packages/playground/website/src/components/github-private-repo-auth-modal/index.tsx @@ -1,5 +1,5 @@ import { Modal } from '../modal'; -import { useAppDispatch } from '../../lib/state/redux/store'; +import { useAppDispatch, useAppSelector } from '../../lib/state/redux/store'; import { setActiveModal } from '../../lib/state/redux/slice-ui'; import { Icon } from '@wordpress/components'; import { GitHubIcon } from '../../github/github'; @@ -7,8 +7,34 @@ import css from '../../github/github-oauth-guard/style.module.css'; const OAUTH_FLOW_URL = 'oauth.php?redirect=1'; +function extractRepoName(url: string): string { + try { + // Handle CORS-proxied URLs - extract the actual GitHub URL + const corsProxyPrefixes = [ + 'https://wordpress-playground-cors-proxy.net/?', + 'http://127.0.0.1:5263/cors-proxy.php?', + ]; + let githubUrl = url; + for (const prefix of corsProxyPrefixes) { + if (url.startsWith(prefix)) { + githubUrl = url.substring(prefix.length); + break; + } + } + + // Extract owner/repo from GitHub URL + const match = githubUrl.match(/github\.com\/([^\/]+\/[^\/]+)/); + return match ? match[1] : url; + } catch { + return url; + } +} + export function GitHubPrivateRepoAuthModal() { const dispatch = useAppDispatch(); + const repoUrl = useAppSelector((state) => state.ui.githubAuthRepoUrl); + + const displayRepoName = repoUrl ? extractRepoName(repoUrl) : ''; // Remove the modal parameter from the redirect URI // so it doesn't persist after OAuth completes @@ -27,11 +53,16 @@ export function GitHubPrivateRepoAuthModal() {

This blueprint requires access to a private GitHub - repository. + repository: +

+

+ + github.com/{displayRepoName} +

- To continue, please connect your GitHub account with - WordPress Playground. + If you have a GitHub account with access to this repository, + you can connect it to continue.

diff --git a/packages/playground/website/src/lib/state/redux/boot-site-client.ts b/packages/playground/website/src/lib/state/redux/boot-site-client.ts index ca0639b3d7..94418ca657 100644 --- a/packages/playground/website/src/lib/state/redux/boot-site-client.ts +++ b/packages/playground/website/src/lib/state/redux/boot-site-client.ts @@ -17,7 +17,11 @@ import { setupPostMessageRelay } from '@php-wasm/web'; import { startPlaygroundWeb } from '@wp-playground/client'; import type { PlaygroundClient } from '@wp-playground/remote'; import { getRemoteUrl } from '../../config'; -import { setActiveModal, setActiveSiteError } from './slice-ui'; +import { + setActiveModal, + setActiveSiteError, + setGitHubAuthRepoUrl, +} from './slice-ui'; import type { PlaygroundDispatch, PlaygroundReduxState } from './store'; import { selectSiteBySlug } from './slice-sites'; // @ts-ignore @@ -204,6 +208,14 @@ export function bootSiteClient( 'GitHubAuthenticationError' || (e as any).cause?.name === 'GitHubAuthenticationError' ) { + // Extract repo URL from the error + const repoUrl = + (e as any).repoUrl || + (e as any).cause?.repoUrl || + undefined; + if (repoUrl) { + dispatch(setGitHubAuthRepoUrl(repoUrl)); + } dispatch(setActiveModal(modalSlugs.GITHUB_PRIVATE_REPO_AUTH)); } else { dispatch(setActiveSiteError('site-boot-failed')); diff --git a/packages/playground/website/src/lib/state/redux/slice-ui.ts b/packages/playground/website/src/lib/state/redux/slice-ui.ts index 2e45e7b3d9..1f0b62043b 100644 --- a/packages/playground/website/src/lib/state/redux/slice-ui.ts +++ b/packages/playground/website/src/lib/state/redux/slice-ui.ts @@ -17,6 +17,7 @@ export interface UIState { error?: SiteError; }; activeModal: string | null; + githubAuthRepoUrl?: string; offline: boolean; siteManagerIsOpen: boolean; siteManagerSection: SiteManagerSection; @@ -88,6 +89,12 @@ const uiSlice = createSlice({ state.activeModal = action.payload; }, + setGitHubAuthRepoUrl: ( + state, + action: PayloadAction + ) => { + state.githubAuthRepoUrl = action.payload; + }, setOffline: (state, action: PayloadAction) => { state.offline = action.payload; }, @@ -139,6 +146,7 @@ export const listenToOnlineOfflineEventsMiddleware: Middleware = export const { setActiveModal, setActiveSiteError, + setGitHubAuthRepoUrl, setOffline, setSiteManagerOpen, setSiteManagerSection, From 4175a9866145e228966ead4dce654362e2d49978 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Mon, 3 Nov 2025 08:04:27 +0100 Subject: [PATCH 5/7] Remove superfluous backslashes --- .../src/components/github-private-repo-auth-modal/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playground/website/src/components/github-private-repo-auth-modal/index.tsx b/packages/playground/website/src/components/github-private-repo-auth-modal/index.tsx index f785a9b69c..aa83d04ffd 100644 --- a/packages/playground/website/src/components/github-private-repo-auth-modal/index.tsx +++ b/packages/playground/website/src/components/github-private-repo-auth-modal/index.tsx @@ -23,7 +23,7 @@ function extractRepoName(url: string): string { } // Extract owner/repo from GitHub URL - const match = githubUrl.match(/github\.com\/([^\/]+\/[^\/]+)/); + const match = githubUrl.match(/github\.com\/([^/]+\/[^/]+)/); return match ? match[1] : url; } catch { return url; From 49721f5b34f133ad9978d1965b8051ae57cc77ef Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Wed, 5 Nov 2025 09:40:57 +0100 Subject: [PATCH 6/7] Rename authentication error to GitAuthenticationError and avoid Github specific code in the git library --- .../blueprints/src/lib/v1/compile.ts | 12 ++ .../blueprints/src/lib/v1/resources.ts | 32 +++- .../client/src/blueprints-v1-handler.ts | 2 + packages/playground/client/src/index.ts | 4 + .../storage/src/lib/git-sparse-checkout.ts | 164 ++++++------------ .../github/acquire-oauth-token-if-needed.tsx | 2 - .../playground/website/src/github/state.ts | 8 - .../src/lib/state/redux/boot-site-client.ts | 8 +- 8 files changed, 101 insertions(+), 131 deletions(-) diff --git a/packages/playground/blueprints/src/lib/v1/compile.ts b/packages/playground/blueprints/src/lib/v1/compile.ts index b2b3b7bb89..bb200035dd 100644 --- a/packages/playground/blueprints/src/lib/v1/compile.ts +++ b/packages/playground/blueprints/src/lib/v1/compile.ts @@ -83,6 +83,10 @@ export interface CompileBlueprintV1Options { * A filesystem to use for the blueprint. */ streamBundledFile?: StreamBundledFile; + /** + * Additional headers to pass to git operations. + */ + gitAdditionalHeaders?: Record; /** * Additional steps to add to the blueprint. */ @@ -142,6 +146,7 @@ function compileBlueprintJson( onBlueprintValidated = () => {}, corsProxy, streamBundledFile, + gitAdditionalHeaders, additionalSteps, }: CompileBlueprintV1Options = {} ): CompiledBlueprintV1 { @@ -321,6 +326,7 @@ function compileBlueprintJson( totalProgressWeight, corsProxy, streamBundledFile, + gitAdditionalHeaders, }) ); @@ -514,6 +520,10 @@ interface CompileStepArgsOptions { * A filesystem to use for the "blueprint" resource type. */ streamBundledFile?: StreamBundledFile; + /** + * Additional headers to pass to git operations. + */ + gitAdditionalHeaders?: Record; } /** @@ -532,6 +542,7 @@ function compileStep( totalProgressWeight, corsProxy, streamBundledFile, + gitAdditionalHeaders, }: CompileStepArgsOptions ): { run: CompiledV1Step; step: S; resources: Array> } { const stepProgress = rootProgressTracker.stage( @@ -546,6 +557,7 @@ function compileStep( semaphore, corsProxy, streamBundledFile, + gitAdditionalHeaders, }); } args[key] = value; diff --git a/packages/playground/blueprints/src/lib/v1/resources.ts b/packages/playground/blueprints/src/lib/v1/resources.ts index 675ca531f5..2c926b4b3a 100644 --- a/packages/playground/blueprints/src/lib/v1/resources.ts +++ b/packages/playground/blueprints/src/lib/v1/resources.ts @@ -157,12 +157,14 @@ export abstract class Resource { progress, corsProxy, streamBundledFile, + gitAdditionalHeaders, }: { /** Optional semaphore to limit concurrent downloads */ semaphore?: Semaphore; progress?: ProgressTracker; corsProxy?: string; streamBundledFile?: StreamBundledFile; + gitAdditionalHeaders?: Record; } ): Resource { let resource: Resource; @@ -185,6 +187,7 @@ export abstract class Resource { case 'git:directory': resource = new GitDirectoryResource(ref, progress, { corsProxy, + additionalHeaders: gitAdditionalHeaders, }); break; case 'literal:directory': @@ -556,12 +559,18 @@ export class UrlResource extends FetchResource { */ export class GitDirectoryResource extends Resource { private reference: GitDirectoryReference; - private options?: { corsProxy?: string }; + private options?: { + corsProxy?: string; + additionalHeaders?: Record; + }; constructor( reference: GitDirectoryReference, _progress?: ProgressTracker, - options?: { corsProxy?: string } + options?: { + corsProxy?: string; + additionalHeaders?: Record; + } ) { super(); this.reference = reference; @@ -574,11 +583,19 @@ export class GitDirectoryResource extends Resource { ? `${this.options.corsProxy}${this.reference.url}` : this.reference.url; - const commitHash = await resolveCommitHash(repoUrl, { - value: this.reference.ref, - type: this.reference.refType ?? 'infer', - }); - const allFiles = await listGitFiles(repoUrl, commitHash); + const commitHash = await resolveCommitHash( + repoUrl, + { + value: this.reference.ref, + type: this.reference.refType ?? 'infer', + }, + this.options?.additionalHeaders + ); + const allFiles = await listGitFiles( + repoUrl, + commitHash, + this.options?.additionalHeaders + ); const requestedPath = (this.reference.path ?? '').replace(/^\/+/, ''); const filesToClone = listDescendantFiles(allFiles, requestedPath); @@ -588,6 +605,7 @@ export class GitDirectoryResource extends Resource { filesToClone, { withObjects: this.reference['.git'], + additionalHeaders: this.options?.additionalHeaders, } ); let files = checkout.files; diff --git a/packages/playground/client/src/blueprints-v1-handler.ts b/packages/playground/client/src/blueprints-v1-handler.ts index d989091cd1..cb60d9a43c 100644 --- a/packages/playground/client/src/blueprints-v1-handler.ts +++ b/packages/playground/client/src/blueprints-v1-handler.ts @@ -21,6 +21,7 @@ export class BlueprintsV1Handler { onBlueprintValidated, onBlueprintStepCompleted, corsProxy, + gitAdditionalHeaders, mounts, sapiName, scope, @@ -72,6 +73,7 @@ export class BlueprintsV1Handler { onStepCompleted: onBlueprintStepCompleted, onBlueprintValidated, corsProxy, + gitAdditionalHeaders, }); await runBlueprintV1Steps(compiled, playground); } diff --git a/packages/playground/client/src/index.ts b/packages/playground/client/src/index.ts index 4d97aca96a..5312980aed 100644 --- a/packages/playground/client/src/index.ts +++ b/packages/playground/client/src/index.ts @@ -85,6 +85,10 @@ export interface StartPlaygroundOptions { * your Blueprint to replace all cross-origin URLs with the proxy URL. */ corsProxy?: string; + /** + * Additional headers to pass to git operations. + */ + gitAdditionalHeaders?: Record; /** * The version of the SQLite driver to use. * Defaults to the latest development version. diff --git a/packages/playground/storage/src/lib/git-sparse-checkout.ts b/packages/playground/storage/src/lib/git-sparse-checkout.ts index f9c1113eca..d91cc138a8 100644 --- a/packages/playground/storage/src/lib/git-sparse-checkout.ts +++ b/packages/playground/storage/src/lib/git-sparse-checkout.ts @@ -31,95 +31,17 @@ if (typeof globalThis.Buffer === 'undefined') { } /** - * Module-level storage for GitHub authentication token. - * This is set by browser-specific code when GitHub OAuth is available. + * Custom error class for git authentication failures. */ -let gitHubAuthToken: string | undefined; - -/** - * Known CORS proxy URL prefixes used by WordPress Playground. - * Keep up this synced with packages/playground/website-extras/vite.config.ts - */ -const KNOWN_CORS_PROXY_URLS = [ - 'https://wordpress-playground-cors-proxy.net/?', - 'http://127.0.0.1:5263/cors-proxy.php?', -]; - -/** - * Sets the GitHub authentication token to use for git protocol requests. - * This is intended to be called by browser-specific initialization code - * where GitHub OAuth is available. - * - * @param token The GitHub OAuth token, or undefined to clear it - */ -export function setGitHubAuthToken(token: string | undefined) { - gitHubAuthToken = token; -} - -/** - * Custom error class for GitHub authentication failures. - */ -export class GitHubAuthenticationError extends Error { +export class GitAuthenticationError extends Error { constructor(public repoUrl: string, public status: number) { super( - `Authentication required to access private GitHub repository: ${repoUrl}` + `Authentication required to access private repository: ${repoUrl}` ); - this.name = 'GitHubAuthenticationError'; + this.name = 'GitAuthenticationError'; } } -/** - * Checks if a URL is a GitHub URL by parsing the hostname. - * Handles both direct GitHub URLs and CORS-proxied GitHub URLs. - * - * @param url The URL to check - * @returns true if the URL is definitively a GitHub URL, false otherwise - */ -function isGitHubUrl(url: string): boolean { - try { - const parsedUrl = new URL(url); - - if (parsedUrl.hostname === 'github.com') { - return true; - } - - for (const proxyUrl of KNOWN_CORS_PROXY_URLS) { - if (url.startsWith(proxyUrl)) { - const proxiedUrl = url.substring(proxyUrl.length); - try { - const proxiedParsedUrl = new URL(proxiedUrl); - return proxiedParsedUrl.hostname === 'github.com'; - } catch { - return false; - } - } - } - - return false; - } catch { - return false; - } -} - -/** - * Returns GitHub authentication headers if a token is available and the URL is a GitHub URL. - * - * @param url The URL to check - */ -function getGitHubAuthHeaders(url: string): Record { - if (gitHubAuthToken && isGitHubUrl(url)) { - // GitHub Git protocol requires Basic Auth with token as username and empty password - const basicAuth = btoa(`${gitHubAuthToken}:`); - return { - Authorization: `Basic ${basicAuth}`, - // Tell CORS proxy to forward the Authorization header - // Must be lowercase because the CORS proxy lowercases header names for comparison - 'X-Cors-Proxy-Allowed-Request-Headers': 'authorization', - }; - } - return {}; -} - /** * Downloads specific files from a git repository. * It uses the git protocol over HTTP to fetch the files. It only uses @@ -157,14 +79,21 @@ export async function sparseCheckout( filesPaths: string[], options?: { withObjects?: boolean; + additionalHeaders?: Record; } ): Promise { - const treesPack = await fetchWithoutBlobs(repoUrl, commitHash); + const treesPack = await fetchWithoutBlobs( + repoUrl, + commitHash, + options?.additionalHeaders + ); const objects = await resolveObjects(treesPack.idx, commitHash, filesPaths); const blobOids = filesPaths.map((path) => objects[path].oid); const blobsPack = - blobOids.length > 0 ? await fetchObjects(repoUrl, blobOids) : null; + blobOids.length > 0 + ? await fetchObjects(repoUrl, blobOids, options?.additionalHeaders) + : null; const fetchedPaths: Record = {}; await Promise.all( @@ -267,9 +196,14 @@ const FULL_SHA_REGEX = /^[0-9a-f]{40}$/i; */ export async function listGitFiles( repoUrl: string, - commitHash: string + commitHash: string, + additionalHeaders?: Record ): Promise { - const treesPack = await fetchWithoutBlobs(repoUrl, commitHash); + const treesPack = await fetchWithoutBlobs( + repoUrl, + commitHash, + additionalHeaders + ); const rootTree = await resolveAllObjects(treesPack.idx, commitHash); if (!rootTree?.object) { return []; @@ -285,13 +219,17 @@ export async function listGitFiles( * @param ref The branch name or commit hash. * @returns The commit hash. */ -export async function resolveCommitHash(repoUrl: string, ref: GitRef) { +export async function resolveCommitHash( + repoUrl: string, + ref: GitRef, + additionalHeaders?: Record +) { const parsed = await parseGitRef(repoUrl, ref); if (parsed.resolvedOid) { return parsed.resolvedOid; } - const oid = await fetchRefOid(repoUrl, parsed.refname); + const oid = await fetchRefOid(repoUrl, parsed.refname, additionalHeaders); if (!oid) { throw new Error(`Git ref "${parsed.refname}" not found at ${repoUrl}`); } @@ -329,7 +267,8 @@ function gitTreeToFileTree(tree: GitTree): GitFileTree[] { */ export async function listGitRefs( repoUrl: string, - fullyQualifiedBranchPrefix: string + fullyQualifiedBranchPrefix: string, + additionalHeaders?: Record ) { const packbuffer = Buffer.from( (await collect([ @@ -350,17 +289,14 @@ export async function listGitRefs( 'content-type': 'application/x-git-upload-pack-request', 'Content-Length': `${packbuffer.length}`, 'Git-Protocol': 'version=2', - ...getGitHubAuthHeaders(repoUrl), + ...additionalHeaders, }, body: packbuffer as any, }); if (!response.ok) { - if ( - (response.status === 401 || response.status === 403) && - isGitHubUrl(repoUrl) - ) { - throw new GitHubAuthenticationError(repoUrl, response.status); + if (response.status === 401 || response.status === 403) { + throw new GitAuthenticationError(repoUrl, response.status); } throw new Error( `Failed to fetch git refs from ${repoUrl}: ${response.status} ${response.statusText}` @@ -479,8 +415,12 @@ async function parseGitRef( } } -async function fetchRefOid(repoUrl: string, refname: string) { - const refs = await listGitRefs(repoUrl, refname); +async function fetchRefOid( + repoUrl: string, + refname: string, + additionalHeaders?: Record +) { + const refs = await listGitRefs(repoUrl, refname, additionalHeaders); const candidates = [refname, `${refname}^{}`]; for (const candidate of candidates) { const sanitized = candidate.trim(); @@ -491,7 +431,11 @@ async function fetchRefOid(repoUrl: string, refname: string) { return null; } -async function fetchWithoutBlobs(repoUrl: string, commitHash: string) { +async function fetchWithoutBlobs( + repoUrl: string, + commitHash: string, + additionalHeaders?: Record +) { const packbuffer = Buffer.from( (await collect([ GitPktLine.encode( @@ -512,17 +456,14 @@ async function fetchWithoutBlobs(repoUrl: string, commitHash: string) { Accept: 'application/x-git-upload-pack-advertisement', 'content-type': 'application/x-git-upload-pack-request', 'Content-Length': `${packbuffer.length}`, - ...getGitHubAuthHeaders(repoUrl), + ...additionalHeaders, }, body: packbuffer as any, }); if (!response.ok) { - if ( - (response.status === 401 || response.status === 403) && - isGitHubUrl(repoUrl) - ) { - throw new GitHubAuthenticationError(repoUrl, response.status); + if (response.status === 401 || response.status === 403) { + throw new GitAuthenticationError(repoUrl, response.status); } throw new Error( `Failed to fetch git objects from ${repoUrl}: ${response.status} ${response.statusText}` @@ -655,7 +596,11 @@ async function resolveObjects( } // Request oid for each resolvedRef -async function fetchObjects(url: string, objectHashes: string[]) { +async function fetchObjects( + url: string, + objectHashes: string[], + additionalHeaders?: Record +) { const packbuffer = Buffer.from( (await collect([ ...objectHashes.map((objectHash) => @@ -674,17 +619,14 @@ async function fetchObjects(url: string, objectHashes: string[]) { Accept: 'application/x-git-upload-pack-advertisement', 'content-type': 'application/x-git-upload-pack-request', 'Content-Length': `${packbuffer.length}`, - ...getGitHubAuthHeaders(url), + ...additionalHeaders, }, body: packbuffer as any, }); if (!response.ok) { - if ( - (response.status === 401 || response.status === 403) && - isGitHubUrl(url) - ) { - throw new GitHubAuthenticationError(url, response.status); + if (response.status === 401 || response.status === 403) { + throw new GitAuthenticationError(url, response.status); } throw new Error( `Failed to fetch git objects from ${url}: ${response.status} ${response.statusText}` diff --git a/packages/playground/website/src/github/acquire-oauth-token-if-needed.tsx b/packages/playground/website/src/github/acquire-oauth-token-if-needed.tsx index cd2a01d286..286b5aa670 100644 --- a/packages/playground/website/src/github/acquire-oauth-token-if-needed.tsx +++ b/packages/playground/website/src/github/acquire-oauth-token-if-needed.tsx @@ -1,6 +1,5 @@ import { setOAuthToken, oAuthState } from './state'; import { oauthCode } from './github-oauth-guard'; -import { setGitHubAuthToken } from '@wp-playground/storage'; export async function acquireOAuthTokenIfNeeded() { if (!oauthCode) { @@ -26,7 +25,6 @@ export async function acquireOAuthTokenIfNeeded() { }); const body = await response.json(); setOAuthToken(body.access_token); - setGitHubAuthToken(body.access_token); // Remove the ?code=... from the URL and clean up any modal state const url = new URL(window.location.href); diff --git a/packages/playground/website/src/github/state.ts b/packages/playground/website/src/github/state.ts index 3e9069b3a7..bb7b1eb521 100644 --- a/packages/playground/website/src/github/state.ts +++ b/packages/playground/website/src/github/state.ts @@ -1,5 +1,4 @@ import { signal } from '@preact/signals-react'; -import { setGitHubAuthToken } from '@wp-playground/storage'; export interface GitHubOAuthState { token?: string; @@ -17,11 +16,6 @@ export const oAuthState = signal({ token: shouldStoreToken ? localStorage.getItem(TOKEN_KEY) || '' : '', }); -// Initialize the git-sparse-checkout module with the token if it exists -if (oAuthState.value.token) { - setGitHubAuthToken(oAuthState.value.token); -} - export function setOAuthToken(token?: string) { if (shouldStoreToken) { localStorage.setItem(TOKEN_KEY, token || ''); @@ -30,6 +24,4 @@ export function setOAuthToken(token?: string) { ...oAuthState.value, token, }; - // Also update the token in the git-sparse-checkout module - setGitHubAuthToken(token); } diff --git a/packages/playground/website/src/lib/state/redux/boot-site-client.ts b/packages/playground/website/src/lib/state/redux/boot-site-client.ts index 94418ca657..3ad88d66ea 100644 --- a/packages/playground/website/src/lib/state/redux/boot-site-client.ts +++ b/packages/playground/website/src/lib/state/redux/boot-site-client.ts @@ -27,6 +27,7 @@ import { selectSiteBySlug } from './slice-sites'; // @ts-ignore import { corsProxyUrl } from 'virtual:cors-proxy-url'; import { modalSlugs } from '../../../components/layout'; +import { createGitHubAuthHeaders } from '../../github/git-auth-helpers'; export function bootSiteClient( siteSlug: string, @@ -155,6 +156,7 @@ export function bootSiteClient( : [], shouldInstallWordPress: !isWordPressInstalled, corsProxy: corsProxyUrl, + gitAdditionalHeaders: createGitHubAuthHeaders(), }); // @TODO: Remove backcompat code after 2024-12-01. @@ -203,10 +205,10 @@ export function bootSiteClient( ) { dispatch(setActiveSiteError('github-artifact-expired')); } else if ( - (e as any).name === 'GitHubAuthenticationError' || + (e as any).name === 'GitAuthenticationError' || (e as any).originalErrorClassName === - 'GitHubAuthenticationError' || - (e as any).cause?.name === 'GitHubAuthenticationError' + 'GitAuthenticationError' || + (e as any).cause?.name === 'GitAuthenticationError' ) { // Extract repo URL from the error const repoUrl = From 06b1ae5ec2a1ff56cb11b52fd8d2e3e83239d749 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Wed, 5 Nov 2025 09:59:15 +0100 Subject: [PATCH 7/7] Add missing git-auth-helpers --- .../website/src/github/git-auth-helpers.ts | 35 +++++++++++++++++++ .../src/lib/state/redux/boot-site-client.ts | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 packages/playground/website/src/github/git-auth-helpers.ts diff --git a/packages/playground/website/src/github/git-auth-helpers.ts b/packages/playground/website/src/github/git-auth-helpers.ts new file mode 100644 index 0000000000..676efb3552 --- /dev/null +++ b/packages/playground/website/src/github/git-auth-helpers.ts @@ -0,0 +1,35 @@ +import { oAuthState } from './state'; + +const KNOWN_CORS_PROXY_URLS = [ + 'https://playground.wordpress.net/cors-proxy.php?', + 'https://wordpress-playground-cors-proxy.net/?', + 'http://127.0.0.1:5263/cors-proxy.php?', +]; + +export function isGitHubUrl(url: string): boolean { + if (url.includes('github.com')) { + return true; + } + for (const corsProxyUrl of KNOWN_CORS_PROXY_URLS) { + if ( + url.startsWith(corsProxyUrl) && + url.substring(corsProxyUrl.length).includes('github.com') + ) { + return true; + } + } + return false; +} + +export function createGitHubAuthHeaders(): Record { + const token = oAuthState.value.token; + if (!token) { + return {}; + } + + return { + Authorization: `Basic ${btoa(`${token}:`)}`, + // Tell the CORS proxy to forward the Authorization header + 'X-Cors-Proxy-Allowed-Request-Headers': 'Authorization', + }; +} diff --git a/packages/playground/website/src/lib/state/redux/boot-site-client.ts b/packages/playground/website/src/lib/state/redux/boot-site-client.ts index 3ad88d66ea..96263d76b4 100644 --- a/packages/playground/website/src/lib/state/redux/boot-site-client.ts +++ b/packages/playground/website/src/lib/state/redux/boot-site-client.ts @@ -27,7 +27,7 @@ import { selectSiteBySlug } from './slice-sites'; // @ts-ignore import { corsProxyUrl } from 'virtual:cors-proxy-url'; import { modalSlugs } from '../../../components/layout'; -import { createGitHubAuthHeaders } from '../../github/git-auth-helpers'; +import { createGitHubAuthHeaders } from '../../../github/git-auth-helpers'; export function bootSiteClient( siteSlug: string,