@@ -30,6 +30,93 @@ if (typeof globalThis.Buffer === 'undefined') {
3030 globalThis . Buffer = BufferPolyfill ;
3131}
3232
33+ /**
34+ * Module-level storage for GitHub authentication token.
35+ * This is set by browser-specific code when GitHub OAuth is available.
36+ */
37+ let gitHubAuthToken : string | undefined ;
38+
39+ /**
40+ * Sets the GitHub authentication token to use for git protocol requests.
41+ * This is intended to be called by browser-specific initialization code
42+ * where GitHub OAuth is available.
43+ *
44+ * @param token The GitHub OAuth token, or undefined to clear it
45+ */
46+ export function setGitHubAuthToken ( token : string | undefined ) {
47+ gitHubAuthToken = token ;
48+ }
49+
50+ /**
51+ * Custom error class for GitHub authentication failures.
52+ */
53+ export class GitHubAuthenticationError extends Error {
54+ constructor ( public repoUrl : string , public status : number ) {
55+ super (
56+ `Authentication required to access private GitHub repository: ${ repoUrl } `
57+ ) ;
58+ this . name = 'GitHubAuthenticationError' ;
59+ }
60+ }
61+
62+ /**
63+ * Checks if a URL is a GitHub URL by parsing the hostname.
64+ * Handles both direct GitHub URLs and CORS-proxied URLs.
65+ *
66+ * @param url The URL to check
67+ * @returns true if the URL is definitively a GitHub URL, false otherwise
68+ */
69+ function isGitHubUrl ( url : string ) : boolean {
70+ try {
71+ const parsedUrl = new URL ( url ) ;
72+
73+ // Direct GitHub URL - check hostname
74+ if ( parsedUrl . hostname === 'github.com' ) {
75+ return true ;
76+ }
77+
78+ // CORS-proxied GitHub URL - the actual GitHub URL should be in the query string
79+ // Format: https://proxy.com/cors-proxy.php?https://github.com/...
80+ // We need to extract and validate the proxied URL's hostname
81+ const queryString = parsedUrl . search . substring ( 1 ) ; // Remove leading '?'
82+ if ( queryString ) {
83+ // Try to extract a URL from the query string
84+ // Match URLs that start with http:// or https://
85+ const urlMatch = queryString . match ( / ^ ( h t t p s ? : \/ \/ [ ^ \s & ] + ) / ) ;
86+ if ( urlMatch ) {
87+ try {
88+ const proxiedUrl = new URL ( urlMatch [ 1 ] ) ;
89+ if ( proxiedUrl . hostname === 'github.com' ) {
90+ return true ;
91+ }
92+ } catch {
93+ // Invalid proxied URL, ignore
94+ }
95+ }
96+ }
97+
98+ return false ;
99+ } catch {
100+ // If URL parsing fails, return false
101+ return false ;
102+ }
103+ }
104+
105+ /**
106+ * Adds GitHub authentication headers to a headers object if a token is available
107+ * and the URL is a GitHub URL.
108+ */
109+ function addGitHubAuthHeaders ( headers : HeadersInit , url : string ) : void {
110+ if ( gitHubAuthToken && isGitHubUrl ( url ) ) {
111+ // GitHub Git protocol requires Basic Auth with token as username and empty password
112+ const basicAuth = btoa ( `${ gitHubAuthToken } :` ) ;
113+ headers [ 'Authorization' ] = `Basic ${ basicAuth } ` ;
114+ // Tell CORS proxy to forward the Authorization header
115+ // Must be lowercase because the CORS proxy lowercases header names for comparison
116+ headers [ 'X-Cors-Proxy-Allowed-Request-Headers' ] = 'authorization' ;
117+ }
118+ }
119+
33120/**
34121 * Downloads specific files from a git repository.
35122 * It uses the git protocol over HTTP to fetch the files. It only uses
@@ -253,17 +340,33 @@ export async function listGitRefs(
253340 ] ) ) as any
254341 ) ;
255342
343+ const headers : HeadersInit = {
344+ Accept : 'application/x-git-upload-pack-advertisement' ,
345+ 'content-type' : 'application/x-git-upload-pack-request' ,
346+ 'Content-Length' : `${ packbuffer . length } ` ,
347+ 'Git-Protocol' : 'version=2' ,
348+ } ;
349+
350+ addGitHubAuthHeaders ( headers , repoUrl ) ;
351+
256352 const response = await fetch ( repoUrl + '/git-upload-pack' , {
257353 method : 'POST' ,
258- headers : {
259- Accept : 'application/x-git-upload-pack-advertisement' ,
260- 'content-type' : 'application/x-git-upload-pack-request' ,
261- 'Content-Length' : `${ packbuffer . length } ` ,
262- 'Git-Protocol' : 'version=2' ,
263- } ,
354+ headers,
264355 body : packbuffer as any ,
265356 } ) ;
266357
358+ if ( ! response . ok ) {
359+ if (
360+ ( response . status === 401 || response . status === 403 ) &&
361+ isGitHubUrl ( repoUrl )
362+ ) {
363+ throw new GitHubAuthenticationError ( repoUrl , response . status ) ;
364+ }
365+ throw new Error (
366+ `Failed to fetch git refs from ${ repoUrl } : ${ response . status } ${ response . statusText } `
367+ ) ;
368+ }
369+
267370 const refs : Record < string , string > = { } ;
268371 for await ( const line of parseGitResponseLines ( response ) ) {
269372 const spaceAt = line . indexOf ( ' ' ) ;
@@ -403,16 +506,32 @@ async function fetchWithoutBlobs(repoUrl: string, commitHash: string) {
403506 ] ) ) as any
404507 ) ;
405508
509+ const headers : HeadersInit = {
510+ Accept : 'application/x-git-upload-pack-advertisement' ,
511+ 'content-type' : 'application/x-git-upload-pack-request' ,
512+ 'Content-Length' : `${ packbuffer . length } ` ,
513+ } ;
514+
515+ addGitHubAuthHeaders ( headers , repoUrl ) ;
516+
406517 const response = await fetch ( repoUrl + '/git-upload-pack' , {
407518 method : 'POST' ,
408- headers : {
409- Accept : 'application/x-git-upload-pack-advertisement' ,
410- 'content-type' : 'application/x-git-upload-pack-request' ,
411- 'Content-Length' : `${ packbuffer . length } ` ,
412- } ,
519+ headers,
413520 body : packbuffer as any ,
414521 } ) ;
415522
523+ if ( ! response . ok ) {
524+ if (
525+ ( response . status === 401 || response . status === 403 ) &&
526+ isGitHubUrl ( repoUrl )
527+ ) {
528+ throw new GitHubAuthenticationError ( repoUrl , response . status ) ;
529+ }
530+ throw new Error (
531+ `Failed to fetch git objects from ${ repoUrl } : ${ response . status } ${ response . statusText } `
532+ ) ;
533+ }
534+
416535 const iterator = streamToIterator ( response . body ! ) ;
417536 const parsed = await parseUploadPackResponse ( iterator ) ;
418537 const packfile = Buffer . from ( ( await collect ( parsed . packfile ) ) as any ) ;
@@ -552,16 +671,32 @@ async function fetchObjects(url: string, objectHashes: string[]) {
552671 ] ) ) as any
553672 ) ;
554673
674+ const headers : HeadersInit = {
675+ Accept : 'application/x-git-upload-pack-advertisement' ,
676+ 'content-type' : 'application/x-git-upload-pack-request' ,
677+ 'Content-Length' : `${ packbuffer . length } ` ,
678+ } ;
679+
680+ addGitHubAuthHeaders ( headers , url ) ;
681+
555682 const response = await fetch ( url + '/git-upload-pack' , {
556683 method : 'POST' ,
557- headers : {
558- Accept : 'application/x-git-upload-pack-advertisement' ,
559- 'content-type' : 'application/x-git-upload-pack-request' ,
560- 'Content-Length' : `${ packbuffer . length } ` ,
561- } ,
684+ headers,
562685 body : packbuffer as any ,
563686 } ) ;
564687
688+ if ( ! response . ok ) {
689+ if (
690+ ( response . status === 401 || response . status === 403 ) &&
691+ isGitHubUrl ( url )
692+ ) {
693+ throw new GitHubAuthenticationError ( url , response . status ) ;
694+ }
695+ throw new Error (
696+ `Failed to fetch git objects from ${ url } : ${ response . status } ${ response . statusText } `
697+ ) ;
698+ }
699+
565700 const iterator = streamToIterator ( response . body ! ) ;
566701 const parsed = await parseUploadPackResponse ( iterator ) ;
567702 const packfile = Buffer . from ( ( await collect ( parsed . packfile ) ) as any ) ;
0 commit comments