@@ -52,13 +52,15 @@ export function getGithubPkceAvailability(): GithubPkceAvailability {
5252 // GitHub Pages needs an OAuth proxy (GitHub's token endpoint is not CORS-enabled).
5353 if ( isGithubPagesHost ( ) && ! getOauthProxyUrl ( ) ) return { supported : false , reason : 'unsupportedRuntime' } ;
5454
55- // IMPORTANT (GitHub Pages):
56- // adapter-static writes ` auth/callback/index.html `, which requires a trailing slash when accessed directly.
57- // Without it, GitHub Pages falls back to `404 .html`, and the callback runtime won't load reliably .
58- const redirectUri = new URL ( `${ base } /auth/callback/ ` , window . location . origin ) . toString ( ) ;
55+ // Use a stable redirectUri (no trailing slash) to match typical GitHub OAuth app settings.
56+ // GitHub Pages will serve `404.html` for `/ auth/callback`, and SvelteKit can still route it
57+ // as long as asset paths are base-aware (see `app/src/app .html`) .
58+ const redirectUri = new URL ( `${ base } /auth/callback` , window . location . origin ) . toString ( ) ;
5959 return { supported : true , clientId, redirectUri } ;
6060}
6161
62+ export type GithubPkceCodePayload = { code : string ; state : string } ;
63+
6264export function getGithubManualTokenAllowed ( ) {
6365 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
6466 return Boolean ( dev ) || Boolean ( publicEnv . PUBLIC_GITHUB_ALLOW_MANUAL_TOKEN ) ;
@@ -105,9 +107,10 @@ export async function startGithubPkceLoginPopup(): Promise<GithubPkceStartResult
105107 const popup = openCenteredPopup ( authorizeUrl . toString ( ) , 'github-login' , 520 , 640 ) ;
106108 if ( ! popup ) return { ok : false , errorKey : 'errors.githubPopupBlocked' } ;
107109
108- // Best-effort: close popup from the opener side once the token is delivered.
109- // (Callback page may be unable to close itself depending on browser policies.)
110- attachPopupAutoClose ( popup ) ;
110+ // Best-effort: complete the OAuth callback from the opener side.
111+ // This is more reliable on GitHub Pages because `/auth/callback` may be served via `404.html` fallback,
112+ // and GitHub's COOP headers can sever `window.opener` in the popup.
113+ attachPopupCallbackCoordinator ( popup , availability . redirectUri ) ;
111114 return { ok : true } ;
112115}
113116
@@ -232,11 +235,20 @@ function openCenteredPopup(url: string, name: string, width: number, height: num
232235 return window . open ( url , name , `width=${ width } ,height=${ height } ,left=${ left } ,top=${ top } ` ) ;
233236}
234237
235- function attachPopupAutoClose ( popup : Window ) {
238+ function attachPopupCallbackCoordinator ( popup : Window , redirectUri : string ) {
236239 if ( ! browser ) return ;
237240
238241 const channelName = 'neoxk:github-oauth' ;
239242 const channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel ( channelName ) ;
243+ const notifyError = ( payload : { errorKey : string ; values ?: Record < string , string > } ) => {
244+ try {
245+ const outbound = new BroadcastChannel ( channelName ) ;
246+ outbound . postMessage ( { type : 'github-oauth-error' , ...payload } ) ;
247+ outbound . close ( ) ;
248+ } catch {
249+ // ignore
250+ }
251+ } ;
240252
241253 let done = false ;
242254 const closePopup = ( ) => {
@@ -251,39 +263,60 @@ function attachPopupAutoClose(popup: Window) {
251263 if ( done ) return ;
252264 done = true ;
253265 window . removeEventListener ( 'message' , handleMessage ) ;
254- window . removeEventListener ( 'storage' , handleStorage ) ;
255266 channel ?. removeEventListener ( 'message' , handleChannelMessage as any ) ;
256267 channel ?. close ( ) ;
257268 clearInterval ( interval ) ;
258269 clearTimeout ( timeout ) ;
259270 } ;
260271
261- const onToken = ( token : unknown ) => {
262- const value = typeof token === 'string' ? token . trim ( ) : '' ;
272+ const deliverToken = ( token : string ) => {
273+ const value = token . trim ( ) ;
263274 if ( ! value ) return ;
275+ try {
276+ const outbound = new BroadcastChannel ( channelName ) ;
277+ outbound . postMessage ( { type : 'github-token' , token : value } ) ;
278+ outbound . close ( ) ;
279+ } catch {
280+ // ignore
281+ }
282+ } ;
283+
284+ const onCallback = async ( payload : { code : string ; state : string } ) => {
285+ if ( done ) return ;
286+ const code = String ( payload . code || '' ) . trim ( ) ;
287+ const state = String ( payload . state || '' ) . trim ( ) ;
288+ if ( ! code || ! state ) return ;
289+
290+ const callbackUrl = new URL ( redirectUri ) ;
291+ callbackUrl . searchParams . set ( 'code' , code ) ;
292+ callbackUrl . searchParams . set ( 'state' , state ) ;
293+
294+ const result = await completeGithubPkceCallback ( callbackUrl ) ;
295+ if ( ! result . ok ) {
296+ notifyError ( { errorKey : result . errorKey , values : result . values } ) ;
297+ return ;
298+ }
299+ deliverToken ( result . token ) ;
264300 closePopup ( ) ;
265- // A second close attempt helps on some browsers.
266301 setTimeout ( closePopup , 100 ) ;
267302 cleanup ( ) ;
268303 } ;
269304
270305 const handleMessage = ( event : MessageEvent ) => {
271306 if ( event . origin !== window . location . origin ) return ;
272- if ( event . data ?. type === 'github-token' && event . data . token ) onToken ( event . data . token ) ;
273- } ;
274-
275- const handleStorage = ( event : StorageEvent ) => {
276- if ( event . key !== 'githubToken' ) return ;
277- if ( event . newValue ) onToken ( event . newValue ) ;
307+ if ( event . data ?. type === 'github-oauth-code' && event . data . code && event . data . state ) {
308+ void onCallback ( { code : event . data . code , state : event . data . state } ) ;
309+ }
278310 } ;
279311
280312 const handleChannelMessage = ( event : MessageEvent ) => {
281313 const data = ( event as MessageEvent ) . data as any ;
282- if ( data ?. type === 'github-token' && data . token ) onToken ( data . token ) ;
314+ if ( data ?. type === 'github-oauth-code' && data . code && data . state ) {
315+ void onCallback ( { code : data . code , state : data . state } ) ;
316+ }
283317 } ;
284318
285319 window . addEventListener ( 'message' , handleMessage ) ;
286- window . addEventListener ( 'storage' , handleStorage ) ;
287320 channel ?. addEventListener ( 'message' , handleChannelMessage as any ) ;
288321
289322 const interval = window . setInterval ( ( ) => {
@@ -292,11 +325,17 @@ function attachPopupAutoClose(popup: Window) {
292325 cleanup ( ) ;
293326 return ;
294327 }
328+
329+ // If the popup navigates back to our origin, try to read its URL and complete the callback here.
295330 try {
296- const token = localStorage . getItem ( 'githubToken' ) ;
297- if ( token ) onToken ( token ) ;
331+ const href = popup . location . href ;
332+ if ( ! href ) return ;
333+ const url = new URL ( href ) ;
334+ const code = url . searchParams . get ( 'code' ) ;
335+ const state = url . searchParams . get ( 'state' ) ;
336+ if ( code && state ) void onCallback ( { code, state } ) ;
298337 } catch {
299- // ignore
338+ // ignore (cross-origin until it returns to our site)
300339 }
301340 } , 350 ) ;
302341
0 commit comments