import * as sentry from '@sentry/electron' ; import * as dotenv from 'dotenv' ; import * as electron from 'electron' ; import { autoUpdater } from 'electron-updater' ; import * as path from 'path' ; import { URL , URLSearchParams } from 'url' ; import * as menu from './menu' ; const app = electron . app ; const ipcMain = electron . ipcMain ; const shell = electron . shell ; dotenv . config ( { path : path . join ( __dirname , '.env' ) } ) ; const debugMode = process . env . OUTLINE_DEBUG === 'true' ; const IMAGES_BASENAME = `${ path . join ( __dirname . replace ( 'app.asar' , 'app.asar.unpacked' ) , 'server_manager' , 'web_app' ) }` ; const sentryDsn = process . env . SENTRY_DSN ; if ( sentryDsn ) { sentry . init ( { dsn : sentryDsn , release : electron . app . getVersion ( ) , maxBreadcrumbs : 100 , beforeBreadcrumb : ( breadcrumb ) => { if ( breadcrumb . category === 'console' ) { if ( breadcrumb . level === sentry . Severity . Debug ) { return null ; } } return breadcrumb ; } } ) ; } console . info ( `Outline Manager is starting` ) ; interface IpcEvent { returnValue : { } ; } function createMainWindow ( ) { const win = new electron . BrowserWindow ( { width : 800 , height : 1024 , minWidth : 600 , minHeight : 768 , maximizable : false , icon : path . join ( __dirname , 'web_app' , 'ui_components' , 'icons' , 'launcher.png' ) , webPreferences : { nodeIntegration : false , preload : path . join ( __dirname , 'preload.js' ) , nativeWindowOpen : true , webviewTag : false } } ) ; const webAppUrl = getWebAppUrl ( ) ; win . loadURL ( webAppUrl ) ; const handleNavigation = ( event , url ) => { try { const parsed = new URL ( url ) ; if ( parsed . protocol === 'http:' || parsed . protocol === 'https:' || parsed . protocol === 'macappstore:' ) { shell . openExternal ( url ) ; } else { console . warn ( `Refusing to open URL with protocol "${ parsed . protocol }"` ) ; } } catch ( e ) { console . warn ( 'Could not parse URL: ' + url ) ; } event . preventDefault ( ) ; } ; win . webContents . on ( 'will-navigate' , ( event , url ) => { handleNavigation ( event , url ) ; } ) ; win . webContents . on ( 'new-window' , ( event , url ) => { handleNavigation ( event , url ) ; } ) ; win . webContents . on ( 'did-finish-load' , ( ) => { if ( ! debugMode ) { autoUpdater . checkForUpdates ( ) ; } } ) ; return win ; } function getWebAppUrl ( ) { const queryParams = new URLSearchParams ( ) ; queryParams . set ( 'version' , electron . app . getVersion ( ) ) ; if ( process . env . SB_IMAGE ) { queryParams . set ( 'image' , process . env . SB_IMAGE ) ; console . log ( `Will install Shadowbox from ${ process . env . SB_IMAGE } Docker image` ) ; } if ( process . env . SB_METRICS_URL ) { queryParams . set ( 'metricsUrl' , process . env . SB_METRICS_URL ) ; console . log ( `Will use metrics url ${ process . env . SB_METRICS_URL }` ) ; } if ( sentryDsn ) { queryParams . set ( 'sentryDsn' , sentryDsn ) ; } if ( debugMode ) { queryParams . set ( 'outlineDebugMode' , 'true' ) ; console . log ( 'Enabling Outline debug mode' ) ; } const webAppUrl = new URL ( 'outline://web_app/index.html' ) ; webAppUrl . search = queryParams . toString ( ) ; const webAppUrlString = webAppUrl . toString ( ) ; console . log ( 'Launching web app from ' + webAppUrlString ) ; return webAppUrlString ; } function workaroundDigitalOceanApiCors ( ) { const headersFilter = { urls : [ 'https://api.digitalocean.com/*' ] } ; electron . session . defaultSession . webRequest . onHeadersReceived ( headersFilter , ( details , callback ) => { if ( details . method === 'OPTIONS' ) { details . responseHeaders [ 'access-control-allow-origin' ] = [ 'outline://web_app' ] ; if ( details . statusCode === 403 ) { details . statusCode = 200 ; details . statusLine = 'HTTP/1.1 200' ; details . responseHeaders [ 'status' ] = [ '200' ] ; details . responseHeaders [ 'access-control-allow-headers' ] = [ '*' ] ; details . responseHeaders [ 'access-control-allow-credentials' ] = [ 'true' ] ; details . responseHeaders [ 'access-control-allow-methods' ] = [ 'GET' , 'POST' , 'PUT' , 'PATCH' , 'DELETE' , 'OPTIONS' ] ; details . responseHeaders [ 'access-control-expose-headers' ] = [ 'RateLimit-Limit' , 'RateLimit-Remaining' , 'RateLimit-Reset' , 'Total' , 'Link' ] ; details . responseHeaders [ 'access-control-max-age' ] = [ '86400' ] ; } } callback ( details ) ; } ) ; } function main ( ) { let mainWindow ; electron . protocol . registerSchemesAsPrivileged ( [ { scheme : 'outline' , privileges : { standard : true , secure : true } } ] ) ; if ( ! app . requestSingleInstanceLock ( ) ) { console . log ( 'another instance is running - exiting' ) ; app . quit ( ) ; } app . on ( 'second-instance' , ( ) => { if ( mainWindow ) { if ( mainWindow . isMinimized ( ) ) { mainWindow . restore ( ) ; } mainWindow . focus ( ) ; } } ) ; app . on ( 'ready' , ( ) => { const menuTemplate = menu . getMenuTemplate ( debugMode ) ; if ( menuTemplate . length > 0 ) { electron . Menu . setApplicationMenu ( electron . Menu . buildFromTemplate ( menuTemplate ) ) ; } workaroundDigitalOceanApiCors ( ) ; const registered = electron . protocol . registerFileProtocol ( 'outline' , ( request , callback ) => { const appPath = new URL ( request . url ) . pathname ; const filesystemPath = path . join ( __dirname , 'server_manager/web_app' , appPath ) ; callback ( filesystemPath ) ; } ) ; if ( ! registered ) { throw new Error ( 'Failed to register outline protocol' ) ; } mainWindow = createMainWindow ( ) ; } ) ; const UPDATE_DOWNLOADED_EVENT = 'update-downloaded' ; autoUpdater . on ( UPDATE_DOWNLOADED_EVENT , ( ev , info ) => { if ( mainWindow ) { mainWindow . webContents . send ( UPDATE_DOWNLOADED_EVENT ) ; } } ) ; const trustedFingerprints = new Set < string > ( ) ; ipcMain . on ( 'trust-certificate' , ( event , fingerprint ) => { trustedFingerprints . add ( `sha256/${ fingerprint }` ) ; event . returnValue = true ; } ) ; app . on ( 'certificate-error' , ( event , webContents , url , error , certificate , callback ) => { event . preventDefault ( ) ; callback ( trustedFingerprints . has ( certificate . fingerprint ) ) ; } ) ; ipcMain . on ( 'bring-to-front' , ( event ) => { if ( mainWindow . isMinimized ( ) ) { mainWindow . restore ( ) ; } mainWindow . focus ( ) ; } ) ; ipcMain . on ( 'open-image' , ( event , basename ) => { const p = path . join ( IMAGES_BASENAME , basename ) ; if ( ! shell . openPath ( p ) ) { console . error ( `could not open image at ${ p }` ) ; } } ) ; app . on ( 'activate' , ( ) => { if ( ! mainWindow ) { mainWindow = createMainWindow ( ) ; mainWindow . on ( 'closed' , ( ) => { mainWindow = null ; } ) ; } } ) ; app . on ( 'window-all-closed' , ( ) => { app . quit ( ) ; } ) ; } main ( ) ;