From 6064d96ae7fe3e9dbbeb80f5bf9d7f391a1d1c3e Mon Sep 17 00:00:00 2001 From: Silberengel Date: Thu, 16 Apr 2026 21:57:42 +0200 Subject: [PATCH] bug-fix electron --- electron/main.cjs | 88 +++++++++++++++++---------------- electron/preload.cjs | 22 +++++---- package-lock.json | 4 +- package.json | 5 +- src/lib/electron-aware-fetch.ts | 5 +- 5 files changed, 67 insertions(+), 57 deletions(-) diff --git a/electron/main.cjs b/electron/main.cjs index 22464c5a..51e72263 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -1,9 +1,8 @@ 'use strict' -const { app, BrowserWindow, ipcMain, shell, Menu, session } = require('electron') +const { app, BrowserWindow, ipcMain, shell, Menu, session, net } = require('electron') const fs = require('fs') const http = require('http') -const https = require('https') const path = require('path') /** True when running from source (`electron .`); false when packaged. */ @@ -209,6 +208,16 @@ function relaxCorsForRendererSubresources() { callback({ cancel: false, responseHeaders: details.responseHeaders }) return } + // Main-process `net.fetch` (translate / LanguageTool IPC) has no `webContentsId` — do not rewrite + // response headers; the consumer is Node/Chromium fetch in main, not a renderer CORS check. + const url = String(details.url || '') + if ( + !details.webContentsId && + (/\/api\/translate(?:\/|\?|$)/u.test(url) || /\/api\/languagetool(?:\/|\?|$)/u.test(url)) + ) { + callback({ cancel: false, responseHeaders: details.responseHeaders }) + return + } const raw = details.responseHeaders if (!raw) { callback({ cancel: false }) @@ -267,51 +276,46 @@ const STRIP_OUTBOUND_REQUEST_HEADERS = new Set([ 'keep-alive' ]) -function requestImwaldBackend(urlString, { method, headers, body }) { - return new Promise((resolve, reject) => { - const u = new URL(urlString) - const useTls = u.protocol === 'https:' - const lib = useTls ? https : http - const port = u.port || (useTls ? 443 : 80) - const safeHeaders = {} - if (headers && typeof headers === 'object') { - for (const [k, v] of Object.entries(headers)) { - if (STRIP_OUTBOUND_REQUEST_HEADERS.has(k.toLowerCase())) continue - if (typeof v === 'string') safeHeaders[k] = v - } - } - const opts = { - hostname: u.hostname, - port, - path: `${u.pathname}${u.search}`, - method: (method || 'GET').toUpperCase(), - headers: safeHeaders +/** + * Use Chromium’s network stack (same TLS / HTTP2 behavior as the browser). Node’s `https` can fail + * where `net.fetch` succeeds (e.g. cert chains, SNI, corporate proxies already trusted by Chromium). + */ +async function requestImwaldBackend(urlString, { method, headers, body }) { + const safeHeaders = {} + if (headers && typeof headers === 'object') { + for (const [k, v] of Object.entries(headers)) { + if (STRIP_OUTBOUND_REQUEST_HEADERS.has(k.toLowerCase())) continue + if (typeof v === 'string') safeHeaders[k] = v } - const req = lib.request(opts, (res) => { - const chunks = [] - res.on('data', (chunk) => chunks.push(chunk)) - res.on('end', () => { - const buf = Buffer.concat(chunks) - const outHeaders = {} - for (const [k, v] of Object.entries(res.headers)) { - if (v === undefined) continue - outHeaders[k] = Array.isArray(v) ? v.join(', ') : String(v) - } - resolve({ - status: res.statusCode || 0, - statusText: res.statusMessage || '', - headers: outHeaders, - body: buf.toString('utf8') - }) - }) - }) - req.on('error', reject) - if (body) req.write(body, 'utf8') - req.end() + } + const m = (method || 'GET').toUpperCase() + const init = { + method: m, + headers: safeHeaders + } + if (body != null && m !== 'GET' && m !== 'HEAD') { + init.body = body + } + const res = await net.fetch(urlString, init) + const text = await res.text() + const outHeaders = {} + res.headers.forEach((value, key) => { + outHeaders[key] = value }) + return { + status: res.status, + statusText: res.statusText || '', + headers: outHeaders, + body: text + } } function registerImwaldBackendRequestIpc() { + try { + ipcMain.removeHandler('imwald:backend-request') + } catch { + /* ignore if channel was never registered */ + } ipcMain.handle('imwald:backend-request', async (_event, payload) => { const url = payload && typeof payload.url === 'string' ? payload.url : '' if (!isAllowedImwaldBackendUrl(url)) { diff --git a/electron/preload.cjs b/electron/preload.cjs index f3ff2286..aa507dac 100644 --- a/electron/preload.cjs +++ b/electron/preload.cjs @@ -2,12 +2,16 @@ const { contextBridge, ipcRenderer } = require('electron') -contextBridge.exposeInMainWorld('imwaldElectron', { - isElectron: true, - reloadApp: () => ipcRenderer.invoke('imwald:reload-app'), - /** - * Same-origin translate / LanguageTool from the renderer hits CORS when the shell is loopback. - * Main process performs the HTTP(S) request (allowlisted host + path only). - */ - backendRequest: (payload) => ipcRenderer.invoke('imwald:backend-request', payload) -}) +try { + contextBridge.exposeInMainWorld('imwaldElectron', { + isElectron: true, + reloadApp: () => ipcRenderer.invoke('imwald:reload-app'), + /** + * Same-origin translate / LanguageTool from the renderer hits CORS when the shell is loopback. + * Main process performs the HTTP(S) request (allowlisted host + path only). + */ + backendRequest: (payload) => ipcRenderer.invoke('imwald:backend-request', payload) + }) +} catch (err) { + console.error('[imwald] preload: failed to expose imwaldElectron', err) +} diff --git a/package-lock.json b/package-lock.json index 2a2bb241..744f48e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "imwald", - "version": "23.0.9", + "version": "23.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "imwald", - "version": "23.0.9", + "version": "23.0.10", "license": "MIT", "dependencies": { "@asciidoctor/core": "^3.0.4", diff --git a/package.json b/package.json index 026c218d..6f8c69e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "imwald", - "version": "23.0.9", + "version": "23.0.10", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery", "private": true, "type": "module", @@ -171,6 +171,9 @@ "electron/**/*", "package.json" ], + "asarUnpack": [ + "electron/**/*" + ], "linux": { "target": [ "AppImage", diff --git a/src/lib/electron-aware-fetch.ts b/src/lib/electron-aware-fetch.ts index 08947cbd..6d7a99bb 100644 --- a/src/lib/electron-aware-fetch.ts +++ b/src/lib/electron-aware-fetch.ts @@ -1,5 +1,3 @@ -import { isImwaldElectron } from '@/lib/client-platform' - export type ImwaldBackendResponse = { status: number statusText: string @@ -16,7 +14,8 @@ export async function electronAwareFetch(input: string, init?: RequestInit): Pro throw new DOMException('Aborted', 'AbortError') } const bridge = typeof window !== 'undefined' ? window.imwaldElectron : undefined - if (!isImwaldElectron() || typeof bridge?.backendRequest !== 'function') { + /** Prefer main-process fetch whenever the preload bridge exists (do not rely on `isElectron` alone). */ + if (typeof bridge?.backendRequest !== 'function') { return fetch(input, init) }