@@ -21,6 +21,8 @@ let pendingCliUrl: string | null = null
2121let pendingBootstrapToken : string | null = null
2222let showingLoadingScreen = false
2323let preloadingView : BrowserView | null = null
24+ const remoteWindowOrigins = new Map < number , Set < string > > ( )
25+ const insecureWindowOrigins = new Map < number , Set < string > > ( )
2426
2527if ( isMac ) {
2628 app . commandLine . appendSwitch ( "disable-spell-checking" )
@@ -93,8 +95,13 @@ function loadLoadingScreen(window: BrowserWindow) {
9395 } )
9496}
9597
96- function getAllowedRendererOrigins ( ) : string [ ] {
98+ function getAllowedRendererOrigins ( window ?: BrowserWindow | null ) : string [ ] {
9799 const origins = new Set < string > ( )
100+ if ( window ) {
101+ for ( const origin of remoteWindowOrigins . get ( window . id ) ?? [ ] ) {
102+ origins . add ( origin )
103+ }
104+ }
98105 const rendererCandidates = [ currentCliUrl , process . env . VITE_DEV_SERVER_URL , process . env . ELECTRON_RENDERER_URL ]
99106 for ( const candidate of rendererCandidates ) {
100107 if ( ! candidate ) {
@@ -109,13 +116,13 @@ function getAllowedRendererOrigins(): string[] {
109116 return Array . from ( origins )
110117}
111118
112- function shouldOpenExternally ( url : string ) : boolean {
119+ function shouldOpenExternally ( url : string , window ?: BrowserWindow | null ) : boolean {
113120 try {
114121 const parsed = new URL ( url )
115122 if ( parsed . protocol !== "http:" && parsed . protocol !== "https:" ) {
116123 return true
117124 }
118- const allowedOrigins = getAllowedRendererOrigins ( )
125+ const allowedOrigins = getAllowedRendererOrigins ( window )
119126 return ! allowedOrigins . includes ( parsed . origin )
120127 } catch {
121128 return false
@@ -128,21 +135,62 @@ function setupNavigationGuards(window: BrowserWindow) {
128135 }
129136
130137 window . webContents . setWindowOpenHandler ( ( { url } ) => {
131- if ( shouldOpenExternally ( url ) ) {
138+ if ( shouldOpenExternally ( url , window ) ) {
132139 handleExternal ( url )
133140 return { action : "deny" }
134141 }
135142 return { action : "allow" }
136143 } )
137144
138145 window . webContents . on ( "will-navigate" , ( event , url ) => {
139- if ( shouldOpenExternally ( url ) ) {
146+ if ( shouldOpenExternally ( url , window ) ) {
140147 event . preventDefault ( )
141148 handleExternal ( url )
142149 }
143150 } )
144151}
145152
153+ function setWindowAllowedOrigin ( window : BrowserWindow , url : string ) {
154+ try {
155+ const origin = new URL ( url ) . origin
156+ remoteWindowOrigins . set ( window . id , new Set ( [ origin ] ) )
157+ } catch ( error ) {
158+ console . warn ( "[cli] failed to store allowed origin" , url , error )
159+ }
160+ }
161+
162+ function clearWindowAllowedOrigin ( window : BrowserWindow ) {
163+ remoteWindowOrigins . delete ( window . id )
164+ }
165+
166+ function addWindowInsecureOrigin ( window : BrowserWindow , url : string ) {
167+ try {
168+ const origin = new URL ( url ) . origin
169+ insecureWindowOrigins . set ( window . id , new Set ( [ origin ] ) )
170+ } catch ( error ) {
171+ console . warn ( "[cli] failed to store insecure origin" , url , error )
172+ }
173+ }
174+
175+ function clearWindowInsecureOrigin ( window : BrowserWindow ) {
176+ insecureWindowOrigins . delete ( window . id )
177+ }
178+
179+ function isInsecureOriginAllowed ( url : string ) {
180+ try {
181+ const targetOrigin = new URL ( url ) . origin
182+ for ( const origins of insecureWindowOrigins . values ( ) ) {
183+ if ( origins . has ( targetOrigin ) ) {
184+ return true
185+ }
186+ }
187+ } catch {
188+ return false
189+ }
190+
191+ return false
192+ }
193+
146194let cachedPreloadPath : string | null = null
147195function getPreloadPath ( ) {
148196 if ( cachedPreloadPath && existsSync ( cachedPreloadPath ) ) {
@@ -207,25 +255,30 @@ function createWindow() {
207255 } ,
208256 } )
209257
210- setupNavigationGuards ( mainWindow )
258+ const window = mainWindow
259+
260+ setupNavigationGuards ( window )
211261
212262 if ( isMac ) {
213- mainWindow . webContents . session . setSpellCheckerEnabled ( false )
263+ window . webContents . session . setSpellCheckerEnabled ( false )
214264 }
215265
216266 showingLoadingScreen = true
217267 currentCliUrl = null
218- loadLoadingScreen ( mainWindow )
268+ clearWindowAllowedOrigin ( window )
269+ loadLoadingScreen ( window )
219270
220271 if ( process . env . NODE_ENV === "development" ) {
221- mainWindow . webContents . openDevTools ( { mode : "detach" } )
272+ window . webContents . openDevTools ( { mode : "detach" } )
222273 }
223274
224- createApplicationMenu ( mainWindow )
225- setupCliIPC ( mainWindow , cliManager )
275+ createApplicationMenu ( window )
276+ setupCliIPC ( window , cliManager )
226277
227- mainWindow . on ( "closed" , ( ) => {
278+ window . on ( "closed" , ( ) => {
228279 destroyPreloadingView ( )
280+ clearWindowAllowedOrigin ( window )
281+ clearWindowInsecureOrigin ( window )
229282 mainWindow = null
230283 currentCliUrl = null
231284 pendingCliUrl = null
@@ -322,10 +375,66 @@ function finalizeCliSwap(url: string) {
322375 return
323376 }
324377
378+ const window = mainWindow
325379 showingLoadingScreen = false
326380 currentCliUrl = url
381+ setWindowAllowedOrigin ( window , url )
327382 pendingCliUrl = null
328- mainWindow . loadURL ( url ) . catch ( ( error ) => console . error ( "[cli] failed to load CLI view:" , error ) )
383+ window . loadURL ( url ) . catch ( ( error ) => console . error ( "[cli] failed to load CLI view:" , error ) )
384+ }
385+
386+ function buildRemoteWindowTitle ( name : string , baseUrl : string ) {
387+ try {
388+ const parsed = new URL ( baseUrl )
389+ return `${ name } - ${ parsed . host } `
390+ } catch {
391+ return `${ name } - ${ baseUrl } `
392+ }
393+ }
394+
395+ function buildRemoteErrorHtml ( name : string , baseUrl : string , message : string ) {
396+ const escapedName = name . replace ( / [ & < > " ] / g, ( char ) => ( { "&" : "&" , "<" : "<" , ">" : ">" , '"' : """ } [ char ] ?? char ) )
397+ const escapedUrl = baseUrl . replace ( / [ & < > " ] / g, ( char ) => ( { "&" : "&" , "<" : "<" , ">" : ">" , '"' : """ } [ char ] ?? char ) )
398+ const escapedMessage = message . replace ( / [ & < > " ] / g, ( char ) => ( { "&" : "&" , "<" : "<" , ">" : ">" , '"' : """ } [ char ] ?? char ) )
399+ return `<!doctype html><html><head><meta charset="utf-8" /><title>${ escapedName } </title><style>body{margin:0;background:#111827;color:#f9fafb;font-family:Inter,system-ui,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:24px}main{max-width:560px;width:100%;background:rgba(17,24,39,.88);border:1px solid rgba(255,255,255,.08);border-radius:20px;padding:28px;box-shadow:0 25px 60px rgba(0,0,0,.45)}h1{margin:0 0 10px;font-size:1.5rem}p{margin:0 0 10px;color:#cbd5e1;line-height:1.5}code{display:block;margin-top:16px;padding:12px 14px;border-radius:12px;background:#0f172a;color:#bfdbfe;overflow:auto}</style></head><body><main><h1>${ escapedName } </h1><p>Could not connect to the remote server.</p><p>${ escapedMessage } </p><code>${ escapedUrl } </code></main></body></html>`
400+ }
401+
402+ async function openRemoteWindow ( payload : { id : string ; name : string ; baseUrl : string ; skipTlsVerify : boolean } ) {
403+ const targetUrl = new URL ( payload . baseUrl )
404+ const title = buildRemoteWindowTitle ( payload . name , payload . baseUrl )
405+ const window = new BrowserWindow ( {
406+ width : 1400 ,
407+ height : 900 ,
408+ minWidth : 800 ,
409+ minHeight : 600 ,
410+ backgroundColor : "#1a1a1a" ,
411+ icon : getIconPath ( ) ,
412+ title,
413+ webPreferences : {
414+ preload : getPreloadPath ( ) ,
415+ contextIsolation : true ,
416+ nodeIntegration : false ,
417+ spellcheck : ! isMac ,
418+ } ,
419+ } )
420+
421+ setWindowAllowedOrigin ( window , targetUrl . toString ( ) )
422+ if ( payload . skipTlsVerify ) {
423+ addWindowInsecureOrigin ( window , targetUrl . toString ( ) )
424+ }
425+
426+ setupNavigationGuards ( window )
427+ window . on ( "closed" , ( ) => {
428+ clearWindowAllowedOrigin ( window )
429+ clearWindowInsecureOrigin ( window )
430+ } )
431+
432+ try {
433+ await window . loadURL ( targetUrl . toString ( ) )
434+ } catch ( error ) {
435+ const message = error instanceof Error ? error . message : String ( error )
436+ await window . loadURL ( `data:text/html;charset=utf-8,${ encodeURIComponent ( buildRemoteErrorHtml ( payload . name , payload . baseUrl , message ) ) } ` )
437+ }
329438}
330439
331440let bootstrapExchangeInFlight = false
@@ -504,6 +613,17 @@ app.whenReady().then(() => {
504613 }
505614
506615 createWindow ( )
616+ ; ( mainWindow as BrowserWindow & { __codenomadOpenRemoteWindow ?: typeof openRemoteWindow } ) . __codenomadOpenRemoteWindow = openRemoteWindow
617+
618+ app . on ( "certificate-error" , ( event , _webContents , url , error , _certificate , callback ) => {
619+ if ( isInsecureOriginAllowed ( url ) ) {
620+ event . preventDefault ( )
621+ console . warn ( "[cli] allowing insecure remote certificate for" , url , error )
622+ callback ( true )
623+ return
624+ }
625+ callback ( false )
626+ } )
507627
508628 app . on ( "activate" , ( ) => {
509629 if ( BrowserWindow . getAllWindows ( ) . length === 0 ) {
0 commit comments