From ca386283c58057996d1ba6de82abbc4b57d1ebd0 Mon Sep 17 00:00:00 2001 From: DarkSky <25152247+darkskygit@users.noreply.github.com> Date: Fri, 26 Dec 2025 09:41:16 +0800 Subject: [PATCH] feat: bump electron (#14151) --- .github/workflows/build-test.yml | 2 +- .github/workflows/release-desktop.yml | 14 ++- package.json | 2 +- .../src/app/effects/recording.ts | 29 ++++- .../frontend/apps/electron/forge.config.mjs | 100 +++++++++--------- packages/frontend/apps/electron/package.json | 2 +- .../apps/electron/scripts/make-env.ts | 2 + .../apps/electron/src/main/constants.ts | 4 +- .../apps/electron/src/main/protocol.ts | 82 +++++++++++--- .../electron/src/main/recording/feature.ts | 16 +++ .../apps/electron/src/main/recording/index.ts | 4 + .../src/main/security-restrictions.ts | 16 ++- .../core/src/modules/navigation/utils.ts | 1 + tools/cli/src/bundle.ts | 4 +- yarn.lock | 12 +-- 15 files changed, 203 insertions(+), 87 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index be7feca52..4add0443d 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -1351,7 +1351,7 @@ jobs: run: | sudo add-apt-repository universe sudo apt install -y libfuse2 elfutils flatpak flatpak-builder - flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo flatpak update # some flatpak deps need git protocol.file.allow git config --global protocol.file.allow always diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index f4732256e..2820bba2c 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -103,11 +103,13 @@ jobs: hard-link-nm: false nmHoistingLimits: workspaces enableScripts: false + - name: Build AFFiNE native uses: ./.github/actions/build-rust with: target: ${{ matrix.spec.target }} package: '@affine/native' + - uses: actions/download-artifact@v4 with: name: desktop-web @@ -128,7 +130,7 @@ jobs: run: | sudo add-apt-repository universe sudo apt install -y libfuse2 elfutils flatpak flatpak-builder - flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo flatpak update # some flatpak deps need git protocol.file.allow git config --global protocol.file.allow always @@ -165,7 +167,7 @@ jobs: mv packages/frontend/apps/electron/out/*/make/zip/linux/${{ matrix.spec.arch }}/*.zip ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.zip mv packages/frontend/apps/electron/out/*/make/*.AppImage ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.appimage mv packages/frontend/apps/electron/out/*/make/deb/${{ matrix.spec.arch }}/*.deb ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.deb - # mv packages/frontend/apps/electron/out/*/make/flatpak/*/*.flatpak ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.flatpak + mv packages/frontend/apps/electron/out/*/make/flatpak/*/*.flatpak ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.flatpak - uses: actions/attest-build-provenance@v2 if: ${{ matrix.spec.platform == 'darwin' }} @@ -181,6 +183,8 @@ jobs: ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.zip ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.appimage ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.deb + ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.flatpak + - name: Upload Artifact uses: actions/upload-artifact@v4 with: @@ -415,12 +419,6 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: desktop-web - path: web-static - - name: Zip web-static - run: zip -r web-static.zip web-static - name: Download Artifacts (macos-x64) uses: actions/download-artifact@v4 with: diff --git a/package.json b/package.json index f2b00033f..16f703a85 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@vitest/coverage-istanbul": "^3.2.4", "@vitest/ui": "^3.2.4", "cross-env": "^10.1.0", - "electron": "^35.0.0", + "electron": "^36.0.0", "eslint": "^9.16.0", "eslint-config-prettier": "^10.0.0", "eslint-import-resolver-typescript": "^4.0.0", diff --git a/packages/frontend/apps/electron-renderer/src/app/effects/recording.ts b/packages/frontend/apps/electron-renderer/src/app/effects/recording.ts index 87434d699..57bae90b8 100644 --- a/packages/frontend/apps/electron-renderer/src/app/effects/recording.ts +++ b/packages/frontend/apps/electron-renderer/src/app/effects/recording.ts @@ -14,11 +14,34 @@ import { getCurrentWorkspace, isAiEnabled } from './utils'; const logger = new DebugLogger('electron-renderer:recording'); +async function readRecordingFile(filepath: string) { + if (apis?.recording?.readRecordingFile) { + try { + return await apis.recording.readRecordingFile(filepath); + } catch (error) { + logger.error( + 'Failed to read recording file via IPC, fallback to fetch', + error + ); + } + } + + const fileUrl = new URL( + filepath, + location.origin.replace(/\.$/, 'local-file') + ); + const response = await fetch(fileUrl); + if (!response.ok) { + throw new Error( + `Failed to fetch recording file: ${response.status} ${response.statusText}` + ); + } + return response.arrayBuffer(); +} + async function saveRecordingBlob(blobEngine: BlobEngine, filepath: string) { logger.debug('Saving recording', filepath); - const opusBuffer = await fetch(new URL(filepath, location.origin)).then(res => - res.arrayBuffer() - ); + const opusBuffer = await readRecordingFile(filepath); const blob = new Blob([opusBuffer], { type: 'audio/mp4', }); diff --git a/packages/frontend/apps/electron/forge.config.mjs b/packages/frontend/apps/electron/forge.config.mjs index 30c3ef4e8..00937fce0 100644 --- a/packages/frontend/apps/electron/forge.config.mjs +++ b/packages/frontend/apps/electron/forge.config.mjs @@ -111,57 +111,60 @@ const makers = [ }, }, }, - !process.env.SKIP_BUNDLE && - false && { - name: '@electron-forge/maker-flatpak', - platforms: ['linux'], - /** @type {import('@electron-forge/maker-flatpak').MakerFlatpakConfig} */ - config: { - options: { - mimeType: linuxMimeTypes, - productName, - bin: productName, - id: fromBuildIdentifier(appIdMap), - icon: iconPngPath, // not working yet - branch: buildType, - files: [ - [ - './resources/affine.metainfo.xml', - '/usr/share/metainfo/affine.metainfo.xml', + !process.env.SKIP_BUNDLE && { + name: '@electron-forge/maker-flatpak', + platforms: ['linux'], + /** @type {import('@electron-forge/maker-flatpak').MakerFlatpakConfig} */ + config: { + options: { + mimeType: linuxMimeTypes, + productName, + bin: productName, + id: appIdMap[buildType], + icon: iconPngPath, // not working yet + branch: buildType, + runtime: 'org.freedesktop.Platform', + runtimeVersion: '25.08', + sdk: 'org.freedesktop.Sdk', + base: 'org.electronjs.Electron2.BaseApp', + baseVersion: '25.08', + files: [ + [ + './resources/affine.metainfo.xml', + '/usr/share/metainfo/affine.metainfo.xml', + ], + ], + modules: [ + { + name: 'zypak', + sources: [ + { + type: 'git', + url: 'https://github.com/refi64/zypak', + tag: 'v2025.09', + }, ], - ], - runtimeVersion: '25.08', - modules: [ - { - name: 'zypak', - sources: [ - { - type: 'git', - url: 'https://github.com/refi64/zypak', - tag: 'v2025.09', - }, - ], - }, - ], - finishArgs: [ - // Wayland/X11 Rendering - '--socket=wayland', - '--socket=x11', - '--share=ipc', - // Open GL - '--device=dri', - // Audio output - '--socket=pulseaudio', - // Read/write home directory access - '--filesystem=home', - // Allow communication with network - '--share=network', - // System notifications with libnotify - '--talk-name=org.freedesktop.Notifications', - ], - }, + }, + ], + finishArgs: [ + // Wayland/X11 Rendering + '--socket=wayland', + '--socket=x11', + '--share=ipc', + // Open GL + '--device=dri', + // Audio output + '--socket=pulseaudio', + // Read/write home directory access + '--filesystem=home', + // Allow communication with network + '--share=network', + // System notifications with libnotify + '--talk-name=org.freedesktop.Notifications', + ], }, }, + }, ].filter(Boolean); /** @@ -198,6 +201,7 @@ export default { }, ], executableName: productName, + ignore: [/\.map$/], asar: true, extendInfo: { NSAudioCaptureUsageDescription: diff --git a/packages/frontend/apps/electron/package.json b/packages/frontend/apps/electron/package.json index 1fdb9a62f..84a25fb9b 100644 --- a/packages/frontend/apps/electron/package.json +++ b/packages/frontend/apps/electron/package.json @@ -59,7 +59,7 @@ "builder-util-runtime": "^9.5.0", "cross-env": "^10.1.0", "debug": "^4.4.0", - "electron": "^35.0.0", + "electron": "^36.0.0", "electron-log": "^5.4.3", "electron-squirrel-startup": "1.0.1", "electron-window-state": "^5.0.3", diff --git a/packages/frontend/apps/electron/scripts/make-env.ts b/packages/frontend/apps/electron/scripts/make-env.ts index 70a62d25c..108d9242a 100644 --- a/packages/frontend/apps/electron/scripts/make-env.ts +++ b/packages/frontend/apps/electron/scripts/make-env.ts @@ -63,6 +63,8 @@ const { default: process.platform, }, }, + allowPositionals: true, + strict: false, }); log(`parsed args: arch=${arch}, platform=${platform}`); diff --git a/packages/frontend/apps/electron/src/main/constants.ts b/packages/frontend/apps/electron/src/main/constants.ts index f97cf4e0f..6d228df99 100644 --- a/packages/frontend/apps/electron/src/main/constants.ts +++ b/packages/frontend/apps/electron/src/main/constants.ts @@ -1,8 +1,8 @@ export const mainHost = '.'; export const anotherHost = 'another-host'; -export const mainWindowOrigin = `file://${mainHost}`; -export const anotherOrigin = `file://${anotherHost}`; +export const mainWindowOrigin = `assets://${mainHost}`; +export const anotherOrigin = `assets://${anotherHost}`; export const onboardingViewUrl = `${mainWindowOrigin}/onboarding`; export const shellViewUrl = `${mainWindowOrigin}/shell.html`; diff --git a/packages/frontend/apps/electron/src/main/protocol.ts b/packages/frontend/apps/electron/src/main/protocol.ts index e1ec02a7b..305eeb28f 100644 --- a/packages/frontend/apps/electron/src/main/protocol.ts +++ b/packages/frontend/apps/electron/src/main/protocol.ts @@ -12,16 +12,15 @@ protocol.registerSchemesAsPrivileged([ { scheme: 'assets', privileges: { - secure: false, + secure: true, + allowServiceWorkers: true, corsEnabled: true, supportFetchAPI: true, standard: true, bypassCSP: true, + stream: true, }, }, -]); - -protocol.registerSchemesAsPrivileged([ { scheme: 'file', privileges: { @@ -36,6 +35,17 @@ protocol.registerSchemesAsPrivileged([ ]); const webStaticDir = join(resourcesPath, 'web-static'); +const localWhiteListDirs = [ + path.resolve(app.getPath('sessionData')).toLowerCase(), + path.resolve(app.getPath('temp')).toLowerCase(), +]; + +function isPathInWhiteList(filepath: string) { + const lowerFilePath = filepath.toLowerCase(); + return localWhiteListDirs.some(whitelistDir => + lowerFilePath.startsWith(whitelistDir) + ); +} async function handleFileRequest(request: Request) { const urlObject = new URL(request.url); @@ -45,11 +55,14 @@ async function handleFileRequest(request: Request) { } const isAbsolutePath = urlObject.host !== '.'; + const isFontRequest = + urlObject.pathname && + /\.(woff2?|ttf|otf)$/i.test(urlObject.pathname.split('?')[0] ?? ''); // Redirect to webpack dev server if defined - if (process.env.DEV_SERVER_URL && !isAbsolutePath) { + if (process.env.DEV_SERVER_URL && !isAbsolutePath && !isFontRequest) { const devServerUrl = new URL( - urlObject.pathname, + `${urlObject.pathname}${urlObject.search}`, process.env.DEV_SERVER_URL ); return net.fetch(devServerUrl.toString(), request); @@ -83,14 +96,7 @@ async function handleFileRequest(request: Request) { filepath = path.resolve(filepath.replace(/^\//, '')); } // security check if the filepath is within app.getPath('sessionData') - const sessionDataPath = path - .resolve(app.getPath('sessionData')) - .toLowerCase(); - const tempPath = path.resolve(app.getPath('temp')).toLowerCase(); - if ( - !filepath.toLowerCase().startsWith(sessionDataPath) && - !filepath.toLowerCase().startsWith(tempPath) - ) { + if (urlObject.host !== 'local-file' || !isPathInWhiteList(filepath)) { throw new Error('Invalid filepath'); } } @@ -102,7 +108,20 @@ async function handleFileRequest(request: Request) { const corsWhitelist = [ /^(?:[a-zA-Z0-9-]+\.)*googlevideo\.com$/, /^(?:[a-zA-Z0-9-]+\.)*youtube\.com$/, + /^(?:[a-zA-Z0-9-]+\.)*youtube-nocookie\.com$/, + /^(?:[a-zA-Z0-9-]+\.)*gstatic\.com$/, + /^(?:[a-zA-Z0-9-]+\.)*googleapis\.com$/, + /^localhost(?::\d+)?$/, + /^127\.0\.0\.1(?::\d+)?$/, + /^insider\.affine\.pro$/, + /^app\.affine\.pro$/, ]; +const needRefererDomains = [ + /^(?:[a-zA-Z0-9-]+\.)*youtube\.com$/, + /^(?:[a-zA-Z0-9-]+\.)*youtube-nocookie\.com$/, + /^(?:[a-zA-Z0-9-]+\.)*googlevideo\.com$/, +]; +const defaultReferer = 'https://client.affine.local/'; export function registerProtocol() { protocol.handle('file', request => { @@ -159,6 +178,31 @@ export function registerProtocol() { delete responseHeaders['access-control-allow-headers']; delete responseHeaders['Access-Control-Allow-Origin']; delete responseHeaders['Access-Control-Allow-Headers']; + } else if ( + !needRefererDomains.some(domainRegex => domainRegex.test(hostname)) + ) { + if ( + !responseHeaders['access-control-allow-origin'] && + !responseHeaders['Access-Control-Allow-Origin'] + ) { + responseHeaders['Access-Control-Allow-Origin'] = ['*']; + } + if ( + !responseHeaders['access-control-allow-headers'] && + !responseHeaders['Access-Control-Allow-Headers'] + ) { + responseHeaders['Access-Control-Allow-Headers'] = [ + 'Origin, X-Requested-With, Content-Type, Accept, Authorization', + ]; + } + if ( + !responseHeaders['access-control-allow-methods'] && + !responseHeaders['Access-Control-Allow-Methods'] + ) { + responseHeaders['Access-Control-Allow-Methods'] = [ + 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', + ]; + } } // to allow url embedding, remove "x-frame-options", @@ -217,7 +261,7 @@ export function registerProtocol() { const url = new URL(details.url); (async () => { - // session cookies are set to file:// on production + // session cookies are set to assets:// on production // if sending request to the cloud, attach the session cookie (to affine cloud server) if ( url.protocol === 'http:' || @@ -235,6 +279,14 @@ export function registerProtocol() { delete details.requestHeaders['cookie']; details.requestHeaders['Cookie'] = cookieString; } + + const hostname = url.hostname; + const needReferer = needRefererDomains.some(regex => + regex.test(hostname) + ); + if (needReferer && !details.requestHeaders['Referer']) { + details.requestHeaders['Referer'] = defaultReferer; + } })() .catch(err => { logger.error('error handling before send headers', err); diff --git a/packages/frontend/apps/electron/src/main/recording/feature.ts b/packages/frontend/apps/electron/src/main/recording/feature.ts index adecc8352..f9b5a5b28 100644 --- a/packages/frontend/apps/electron/src/main/recording/feature.ts +++ b/packages/frontend/apps/electron/src/main/recording/feature.ts @@ -686,6 +686,22 @@ export async function getRawAudioBuffers( }; } +function assertRecordingFilepath(filepath: string) { + const normalizedPath = path.normalize(filepath); + const normalizedBase = path.normalize(SAVED_RECORDINGS_DIR + path.sep); + + if (!normalizedPath.toLowerCase().startsWith(normalizedBase.toLowerCase())) { + throw new Error('Invalid recording filepath'); + } + + return normalizedPath; +} + +export async function readRecordingFile(filepath: string) { + const normalizedPath = assertRecordingFilepath(filepath); + return fsp.readFile(normalizedPath); +} + export async function readyRecording(id: number, buffer: Buffer) { logger.info('readyRecording', id); diff --git a/packages/frontend/apps/electron/src/main/recording/index.ts b/packages/frontend/apps/electron/src/main/recording/index.ts index 6e836a884..2be0c0db6 100644 --- a/packages/frontend/apps/electron/src/main/recording/index.ts +++ b/packages/frontend/apps/electron/src/main/recording/index.ts @@ -19,6 +19,7 @@ import { handleBlockCreationFailed, handleBlockCreationSuccess, pauseRecording, + readRecordingFile, readyRecording, recordingStatus$, removeRecording, @@ -53,6 +54,9 @@ export const recordingHandlers = { getRawAudioBuffers: async (_, id: number, cursor?: number) => { return getRawAudioBuffers(id, cursor); }, + readRecordingFile: async (_, filepath: string) => { + return readRecordingFile(filepath); + }, // save the encoded recording buffer to the file system readyRecording: async (_, id: number, buffer: Uint8Array) => { return readyRecording(id, Buffer.from(buffer)); diff --git a/packages/frontend/apps/electron/src/main/security-restrictions.ts b/packages/frontend/apps/electron/src/main/security-restrictions.ts index ad0ce46ee..1cbf91a0d 100644 --- a/packages/frontend/apps/electron/src/main/security-restrictions.ts +++ b/packages/frontend/apps/electron/src/main/security-restrictions.ts @@ -1,5 +1,6 @@ import { app } from 'electron'; +import { anotherHost, mainHost } from './constants'; import { openExternalSafely } from './security/open-external'; const extractRedirectTarget = (rawUrl: string) => { @@ -33,7 +34,20 @@ const extractRedirectTarget = (rawUrl: string) => { app.on('web-contents-created', (_, contents) => { const isInternalUrl = (url: string) => { - return url.startsWith('file://.'); + try { + const parsed = new URL(url); + if ( + parsed.protocol === 'assets:' && + (parsed.hostname === mainHost || parsed.hostname === anotherHost) + ) { + return true; + } + if (parsed.protocol === 'file:' && parsed.hostname === mainHost) { + // legacy allowance for older file:// loads + return true; + } + } catch {} + return false; }; /** * Block navigation to origins not on the allowlist. diff --git a/packages/frontend/core/src/modules/navigation/utils.ts b/packages/frontend/core/src/modules/navigation/utils.ts index 7a49885ae..540e537e0 100644 --- a/packages/frontend/core/src/modules/navigation/utils.ts +++ b/packages/frontend/core/src/modules/navigation/utils.ts @@ -7,6 +7,7 @@ import queryString from 'query-string'; function maybeAffineOrigin(origin: string, baseUrl: string) { return ( origin.startsWith('file://') || + origin.startsWith('assets://') || origin.endsWith('affine.pro') || // stable/beta origin.endsWith('apple.getaffineapp.com') || // stable/beta origin.endsWith('affine.fail') || // canary diff --git a/tools/cli/src/bundle.ts b/tools/cli/src/bundle.ts index d1d8bdb86..2a1dd28eb 100644 --- a/tools/cli/src/bundle.ts +++ b/tools/cli/src/bundle.ts @@ -115,7 +115,9 @@ const defaultDevServerConfig: DevServerConfiguration = { overlay: process.env.DISABLE_DEV_OVERLAY === 'true' ? false : undefined, logging: process.env.CI ? 'none' : 'error', // see: https://webpack.js.org/configuration/dev-server/#websocketurl - webSocketURL: 'auto://0.0.0.0:8080/ws', + // must be an explicit ws/wss URL because custom protocols (e.g. assets://) + // cannot be used to construct WebSocket endpoints in Electron + webSocketURL: 'ws://0.0.0.0:8080/ws', }, historyApiFallback: { rewrites: [ diff --git a/yarn.lock b/yarn.lock index 4aa2b1d85..621539ec1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -583,7 +583,7 @@ __metadata: builder-util-runtime: "npm:^9.5.0" cross-env: "npm:^10.1.0" debug: "npm:^4.4.0" - electron: "npm:^35.0.0" + electron: "npm:^36.0.0" electron-log: "npm:^5.4.3" electron-squirrel-startup: "npm:1.0.1" electron-updater: "npm:^6.6.2" @@ -786,7 +786,7 @@ __metadata: "@vitest/coverage-istanbul": "npm:^3.2.4" "@vitest/ui": "npm:^3.2.4" cross-env: "npm:^10.1.0" - electron: "npm:^35.0.0" + electron: "npm:^36.0.0" eslint: "npm:^9.16.0" eslint-config-prettier: "npm:^10.0.0" eslint-import-resolver-typescript: "npm:^4.0.0" @@ -22392,16 +22392,16 @@ __metadata: languageName: node linkType: hard -"electron@npm:^35.0.0": - version: 35.7.5 - resolution: "electron@npm:35.7.5" +"electron@npm:^36.0.0": + version: 36.9.5 + resolution: "electron@npm:36.9.5" dependencies: "@electron/get": "npm:^2.0.0" "@types/node": "npm:^22.7.7" extract-zip: "npm:^2.0.1" bin: electron: cli.js - checksum: 10/f3757f0b6f21ddd5ebf8b83becdaa3e47dc13473e5375e6e6aee638d7029e73d08d6a4170b37ad8c594057da139e7cdf4c8b82b70c2c1ad0306abdcc274c5fee + checksum: 10/2c78ce37b6cdcf2388dc3f3932f2104f7bd529c51ee8d00e0201ce86089966309c0ba62c2045abc4dad81893ebe7f7070bb047dcb9c05366f31ddd7fb73790ca languageName: node linkType: hard