diff --git a/blocksuite/affine/all/src/__tests__/adapters/html.unit.spec.ts b/blocksuite/affine/all/src/__tests__/adapters/html.unit.spec.ts index 1fb0338f9..382ff1957 100644 --- a/blocksuite/affine/all/src/__tests__/adapters/html.unit.spec.ts +++ b/blocksuite/affine/all/src/__tests__/adapters/html.unit.spec.ts @@ -2214,7 +2214,7 @@ describe('html to snapshot', () => { test('iframe', async () => { const html = template( - `` + `` ); const blockSnapshot: BlockSnapshot = { diff --git a/blocksuite/affine/blocks/embed/src/embed-figma-block/embed-figma-block.ts b/blocksuite/affine/blocks/embed/src/embed-figma-block/embed-figma-block.ts index 75df5e5ac..590733cbf 100644 --- a/blocksuite/affine/blocks/embed/src/embed-figma-block/embed-figma-block.ts +++ b/blocksuite/affine/blocks/embed/src/embed-figma-block/embed-figma-block.ts @@ -82,7 +82,8 @@ export class EmbedFigmaBlockComponent extends EmbedBlockComponent diff --git a/blocksuite/affine/blocks/embed/src/embed-iframe-block/configs/providers/bilibili.ts b/blocksuite/affine/blocks/embed/src/embed-iframe-block/configs/providers/bilibili.ts new file mode 100644 index 000000000..aeee9e80c --- /dev/null +++ b/blocksuite/affine/blocks/embed/src/embed-iframe-block/configs/providers/bilibili.ts @@ -0,0 +1,79 @@ +import { EmbedIframeConfigExtension } from '@blocksuite/affine-shared/services'; + +import { + type EmbedIframeUrlValidationOptions, + validateEmbedIframeUrl, +} from '../../utils'; + +const BILIBILI_DEFAULT_WIDTH_IN_SURFACE = 800; +const BILIBILI_DEFAULT_HEIGHT_IN_SURFACE = 450; +const BILIBILI_DEFAULT_HEIGHT_IN_NOTE = 450; +const BILIBILI_DEFAULT_WIDTH_PERCENT = 100; + +const bilibiliValidationOptions: EmbedIframeUrlValidationOptions = { + protocols: ['https:'], + hostnames: ['player.bilibili.com', 'www.bilibili.com', 'bilibili.com'], +}; + +const biliPlayerValidationOptions: EmbedIframeUrlValidationOptions = { + protocols: ['https:'], + hostnames: ['player.bilibili.com'], +}; + +const AV_REGEX = /av([0-9]+)/i; +const BV_REGEX = /(BV[0-9A-Za-z]{10})/; + +const extractAvid = (url: string) => { + const match = url.match(AV_REGEX); + return match ? match[1] : undefined; +}; + +const extractBvid = (url: string) => { + const match = url.match(BV_REGEX); + return match ? match[1] : undefined; +}; + +const buildBiliPlayerEmbedUrl = (url: string) => { + // If the user pasted the embed URL directly, keep it + if (validateEmbedIframeUrl(url, biliPlayerValidationOptions)) { + return url; + } + const avid = extractAvid(url); + if (avid) { + const params = new URLSearchParams({ + aid: avid, + autoplay: '0', + }); + return `https://player.bilibili.com/player.html?${params.toString()}`; + } + const bvid = extractBvid(url); + if (bvid) { + const params = new URLSearchParams({ + bvid, + autoplay: '0', + }); + return `https://player.bilibili.com/player.html?${params.toString()}`; + } + return undefined; +}; + +const bilibiliConfig = { + name: 'bilibili', + match: (url: string) => + validateEmbedIframeUrl(url, bilibiliValidationOptions) && + (!!extractAvid(url) || !!extractBvid(url)), + buildOEmbedUrl: buildBiliPlayerEmbedUrl, + useOEmbedUrlDirectly: true, + options: { + widthInSurface: BILIBILI_DEFAULT_WIDTH_IN_SURFACE, + heightInSurface: BILIBILI_DEFAULT_HEIGHT_IN_SURFACE, + heightInNote: BILIBILI_DEFAULT_HEIGHT_IN_NOTE, + widthPercent: BILIBILI_DEFAULT_WIDTH_PERCENT, + allow: 'clipboard-write; encrypted-media; picture-in-picture', + sandbox: 'allow-same-origin allow-scripts', + style: 'border: none; border-radius: 8px;', + allowFullscreen: true, + }, +}; + +export const BilibiliEmbedConfig = EmbedIframeConfigExtension(bilibiliConfig); diff --git a/blocksuite/affine/blocks/embed/src/embed-iframe-block/configs/providers/generic.ts b/blocksuite/affine/blocks/embed/src/embed-iframe-block/configs/providers/generic.ts index 6d96eff46..ae1fe021a 100644 --- a/blocksuite/affine/blocks/embed/src/embed-iframe-block/configs/providers/generic.ts +++ b/blocksuite/affine/blocks/embed/src/embed-iframe-block/configs/providers/generic.ts @@ -67,8 +67,9 @@ const genericConfig = { heightInNote: GENERIC_DEFAULT_HEIGHT_IN_NOTE, allowFullscreen: true, style: 'border: none; border-radius: 8px;', - allow: 'clipboard-read; clipboard-write; picture-in-picture;', + allow: '', referrerpolicy: 'no-referrer-when-downgrade', + sandbox: 'allow-scripts', }, }; diff --git a/blocksuite/affine/blocks/embed/src/embed-iframe-block/configs/providers/index.ts b/blocksuite/affine/blocks/embed/src/embed-iframe-block/configs/providers/index.ts index 343e90c32..94afa63ce 100644 --- a/blocksuite/affine/blocks/embed/src/embed-iframe-block/configs/providers/index.ts +++ b/blocksuite/affine/blocks/embed/src/embed-iframe-block/configs/providers/index.ts @@ -1,3 +1,4 @@ +import { BilibiliEmbedConfig } from './bilibili'; import { ExcalidrawEmbedConfig } from './excalidraw'; import { GenericEmbedConfig } from './generic'; import { GoogleDocsEmbedConfig } from './google-docs'; @@ -11,5 +12,6 @@ export const EmbedIframeConfigExtensions = [ MiroEmbedConfig, ExcalidrawEmbedConfig, GoogleDocsEmbedConfig, + BilibiliEmbedConfig, GenericEmbedConfig, ]; diff --git a/blocksuite/affine/blocks/embed/src/embed-iframe-block/embed-iframe-block.ts b/blocksuite/affine/blocks/embed/src/embed-iframe-block/embed-iframe-block.ts index 326e5f811..826733ca9 100644 --- a/blocksuite/affine/blocks/embed/src/embed-iframe-block/embed-iframe-block.ts +++ b/blocksuite/affine/blocks/embed/src/embed-iframe-block/embed-iframe-block.ts @@ -23,7 +23,7 @@ import { type ReadonlySignal, signal, } from '@preact/signals-core'; -import { html } from 'lit'; +import { html, nothing } from 'lit'; import { query } from 'lit/decorators.js'; import { type ClassInfo, classMap } from 'lit/directives/class-map.js'; import { ifDefined } from 'lit/directives/if-defined.js'; @@ -45,6 +45,10 @@ import { safeGetIframeSrc } from './utils.js'; export type EmbedIframeStatus = 'idle' | 'loading' | 'success' | 'error'; +const TRUSTED_SANDBOX = + 'allow-same-origin allow-scripts allow-forms allow-presentation'; +const UNTRUSTED_SANDBOX = 'allow-scripts'; + export class EmbedIframeBlockComponent extends CaptionedBlockComponent { selectedStyle$: ReadonlySignal | null = computed( () => ({ @@ -89,6 +93,7 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent - `; + ${sourceHost + ? html`
${sourceHost}
` + : nothing}`; + }; + + private readonly _getSourceHost = () => { + const url = this.model.props.url ?? this.model.props.iframeUrl; + if (!url) return null; + try { + const parsed = new URL(url); + return parsed.hostname; + } catch { + return null; + } }; private readonly _renderContent = () => { diff --git a/blocksuite/affine/blocks/embed/src/embed-iframe-block/style.ts b/blocksuite/affine/blocks/embed/src/embed-iframe-block/style.ts index 0d2236f39..8065d9fad 100644 --- a/blocksuite/affine/blocks/embed/src/embed-iframe-block/style.ts +++ b/blocksuite/affine/blocks/embed/src/embed-iframe-block/style.ts @@ -23,6 +23,19 @@ export const embedIframeBlockStyles = css` height: 100%; display: none; } + + .affine-embed-iframe-source { + position: absolute; + left: 8px; + bottom: 8px; + padding: 2px 6px; + background: rgba(0, 0, 0, 0.7); + color: #fff; + border-radius: 4px; + font-size: 12px; + line-height: 16px; + pointer-events: none; + } .affine-embed-iframe-block-overlay.show { display: block; } diff --git a/blocksuite/affine/blocks/embed/src/embed-loom-block/embed-loom-block.ts b/blocksuite/affine/blocks/embed/src/embed-loom-block/embed-loom-block.ts index 3fe0b454d..5cd1cf85f 100644 --- a/blocksuite/affine/blocks/embed/src/embed-loom-block/embed-loom-block.ts +++ b/blocksuite/affine/blocks/embed/src/embed-loom-block/embed-loom-block.ts @@ -124,7 +124,8 @@ export class EmbedLoomBlockComponent extends EmbedBlockComponent< diff --git a/blocksuite/affine/blocks/embed/src/embed-youtube-block/embed-youtube-block.ts b/blocksuite/affine/blocks/embed/src/embed-youtube-block/embed-youtube-block.ts index 2a3d70eb1..f3bf9bfec 100644 --- a/blocksuite/affine/blocks/embed/src/embed-youtube-block/embed-youtube-block.ts +++ b/blocksuite/affine/blocks/embed/src/embed-youtube-block/embed-youtube-block.ts @@ -148,8 +148,8 @@ export class EmbedYoutubeBlockComponent extends EmbedBlockComponent< type="text/html" src=${`https://www.youtube.com/embed/${videoId}`} frameborder="0" - allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" - allowfullscreen + allow="fullscreen; autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share" + sandbox="allow-scripts allow-same-origin allow-presentation" loading="lazy" credentialless > diff --git a/blocksuite/affine/shared/src/services/embed-iframe/embed-iframe-config.ts b/blocksuite/affine/shared/src/services/embed-iframe/embed-iframe-config.ts index 00b599c84..626b6c6bd 100644 --- a/blocksuite/affine/shared/src/services/embed-iframe/embed-iframe-config.ts +++ b/blocksuite/affine/shared/src/services/embed-iframe/embed-iframe-config.ts @@ -27,6 +27,7 @@ export type IframeOptions = { allow?: string; allowFullscreen?: boolean; containerBorderRadius?: number; + sandbox?: string; }; /** diff --git a/blocksuite/affine/shared/src/services/embed-iframe/embed-iframe-service.ts b/blocksuite/affine/shared/src/services/embed-iframe/embed-iframe-service.ts index edcca85bc..0b3899974 100644 --- a/blocksuite/affine/shared/src/services/embed-iframe/embed-iframe-service.ts +++ b/blocksuite/affine/shared/src/services/embed-iframe/embed-iframe-service.ts @@ -106,6 +106,7 @@ export class EmbedIframeService } const oEmbedUrl = config.buildOEmbedUrl(url); + if (!oEmbedUrl) { return null; } diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index 93c1c076b..1cf17a61a 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -58,7 +58,7 @@ "@opentelemetry/core": "^2.2.0", "@opentelemetry/exporter-prometheus": "^0.208.0", "@opentelemetry/exporter-zipkin": "^2.2.0", - "@opentelemetry/host-metrics": "^0.37.0", + "@opentelemetry/host-metrics": "^0.38.0", "@opentelemetry/instrumentation": "^0.208.0", "@opentelemetry/instrumentation-graphql": "^0.56.0", "@opentelemetry/instrumentation-http": "^0.208.0", diff --git a/packages/common/debug/src/index.ts b/packages/common/debug/src/index.ts index ad564f9a7..eabe33b12 100644 --- a/packages/common/debug/src/index.ts +++ b/packages/common/debug/src/index.ts @@ -4,15 +4,31 @@ type LogLevel = 'debug' | 'info' | 'warn' | 'error'; const SESSION_KEY = 'affine:debug'; if (typeof window !== 'undefined') { + const getSessionValue = (key: string) => { + try { + return sessionStorage.getItem(key); + } catch { + return null; + } + }; + + const setSessionValue = (key: string, value: string) => { + try { + sessionStorage.setItem(key, value); + } catch { + // ignore if storage is not accessible (e.g., sandboxed renderer) + } + }; + // enable debug logs if the URL search string contains `debug` // e.g. http://localhost:3000/?debug if (window.location.search.includes('debug')) { // enable debug logs for the current session // since the query string may be removed by the browser after navigations, // we need to store the debug flag in sessionStorage - sessionStorage.setItem(SESSION_KEY, 'true'); + setSessionValue(SESSION_KEY, 'true'); } - if (sessionStorage.getItem(SESSION_KEY) === 'true') { + if (getSessionValue(SESSION_KEY) === 'true') { // enable all debug logs by default debug.enable('*'); console.warn('Debug logs enabled'); diff --git a/packages/common/error/src/index.ts b/packages/common/error/src/index.ts index 7a2366a16..d24af347f 100644 --- a/packages/common/error/src/index.ts +++ b/packages/common/error/src/index.ts @@ -4,7 +4,8 @@ import { GraphQLError as BaseGraphQLError } from 'graphql'; export type ErrorName = | keyof typeof ErrorNames | 'NETWORK_ERROR' - | 'CONTENT_TOO_LARGE'; + | 'CONTENT_TOO_LARGE' + | 'REQUEST_ABORTED'; export interface UserFriendlyErrorResponse { status: number; diff --git a/packages/frontend/apps/electron/src/main/constants.ts b/packages/frontend/apps/electron/src/main/constants.ts index 6d228df99..e4ed7c007 100644 --- a/packages/frontend/apps/electron/src/main/constants.ts +++ b/packages/frontend/apps/electron/src/main/constants.ts @@ -7,7 +7,7 @@ export const anotherOrigin = `assets://${anotherHost}`; export const onboardingViewUrl = `${mainWindowOrigin}/onboarding`; export const shellViewUrl = `${mainWindowOrigin}/shell.html`; export const backgroundWorkerViewUrl = `${mainWindowOrigin}/background-worker.html`; -export const customThemeViewUrl = `${mainWindowOrigin}/theme-editor.html`; +export const customThemeViewUrl = `${mainWindowOrigin}/theme-editor`; // mitigate the issue that popup window share the same zoom level of the main window // Notes from electron official docs: diff --git a/packages/frontend/apps/electron/src/main/events.ts b/packages/frontend/apps/electron/src/main/events.ts index f2f35a705..f75fcbe3a 100644 --- a/packages/frontend/apps/electron/src/main/events.ts +++ b/packages/frontend/apps/electron/src/main/events.ts @@ -1,6 +1,9 @@ -import { BrowserWindow, WebContentsView } from 'electron'; +import { ipcMain, webContents } from 'electron'; -import { AFFINE_EVENT_CHANNEL_NAME } from '../shared/type'; +import { + AFFINE_EVENT_CHANNEL_NAME, + AFFINE_EVENT_SUBSCRIBE_CHANNEL_NAME, +} from '../shared/type'; import { applicationMenuEvents } from './application-menu'; import { beforeAppQuit } from './cleanup'; import { logger } from './logger'; @@ -19,12 +22,64 @@ export const allEvents = { popup: popupEvents, }; -function getActiveWindows() { - return BrowserWindow.getAllWindows().filter(win => !win.isDestroyed()); +const subscriptions = new Map>(); + +function getTargetContents(channel: string) { + const targets: Electron.WebContents[] = []; + subscriptions.forEach((channels, id) => { + if (!channels.has(channel)) return; + const wc = webContents.fromId(id); + if (wc && !wc.isDestroyed()) { + targets.push(wc); + } + }); + return targets; +} + +function addSubscription(sender: Electron.WebContents, channel: string) { + const id = sender.id; + const set = subscriptions.get(id) ?? new Set(); + set.add(channel); + if (!subscriptions.has(id)) { + sender.once('destroyed', () => { + subscriptions.delete(id); + }); + } + subscriptions.set(id, set); +} + +function removeSubscription(sender: Electron.WebContents, channel: string) { + const id = sender.id; + const set = subscriptions.get(id); + if (!set) return; + set.delete(channel); + if (set.size === 0) { + subscriptions.delete(id); + } else { + subscriptions.set(id, set); + } } export function registerEvents() { const unsubs: (() => void)[] = []; + + const onSubscribe = ( + event: Electron.IpcMainEvent, + action: 'subscribe' | 'unsubscribe', + channel: string + ) => { + if (typeof channel !== 'string') return; + if (action === 'subscribe') { + addSubscription(event.sender, channel); + } else { + removeSubscription(event.sender, channel); + } + }; + + ipcMain.on(AFFINE_EVENT_SUBSCRIBE_CHANNEL_NAME, onSubscribe); + unsubs.push(() => + ipcMain.removeListener(AFFINE_EVENT_SUBSCRIBE_CHANNEL_NAME, onSubscribe) + ); // register events for (const [namespace, namespaceEvents] of Object.entries(allEvents)) { for (const [key, eventRegister] of Object.entries(namespaceEvents)) { @@ -40,22 +95,10 @@ export function registerEvents() { typeof a !== 'object' ) ); - // is this efficient? - getActiveWindows().forEach(win => { - if (win.isDestroyed()) { - return; + getTargetContents(chan).forEach(wc => { + if (!wc.isDestroyed()) { + wc.send(AFFINE_EVENT_CHANNEL_NAME, chan, ...args); } - // .webContents could be undefined if the window is destroyed - win.webContents?.send(AFFINE_EVENT_CHANNEL_NAME, chan, ...args); - win.contentView.children.forEach(child => { - if ( - child instanceof WebContentsView && - child.webContents && - !child.webContents.isDestroyed() - ) { - child.webContents?.send(AFFINE_EVENT_CHANNEL_NAME, chan, ...args); - } - }); }); }); unsubs.push(unsubscribe); diff --git a/packages/frontend/apps/electron/src/main/helper-process.ts b/packages/frontend/apps/electron/src/main/helper-process.ts index d395d05fa..c6d791929 100644 --- a/packages/frontend/apps/electron/src/main/helper-process.ts +++ b/packages/frontend/apps/electron/src/main/helper-process.ts @@ -76,6 +76,16 @@ class HelperProcessManager { beforeAppQuit(() => { this.#process.kill(); }); + + this.#process.on('exit', code => { + logger.error('[helper] process exited', { code }); + HelperProcessManager._instance = null; + }); + + this.#process.on('error', err => { + logger.error('[helper] process error', err); + HelperProcessManager._instance = null; + }); } // bridge renderer <-> helper process diff --git a/packages/frontend/apps/electron/src/main/protocol.ts b/packages/frontend/apps/electron/src/main/protocol.ts index 305eeb28f..ba986160e 100644 --- a/packages/frontend/apps/electron/src/main/protocol.ts +++ b/packages/frontend/apps/electron/src/main/protocol.ts @@ -5,6 +5,7 @@ import { app, net, protocol, session } from 'electron'; import cookieParser from 'set-cookie-parser'; import { isWindows, resourcesPath } from '../shared/utils'; +import { isDev } from './config'; import { anotherHost, mainHost } from './constants'; import { logger } from './logger'; @@ -13,11 +14,9 @@ protocol.registerSchemesAsPrivileged([ scheme: 'assets', privileges: { secure: true, - allowServiceWorkers: true, corsEnabled: true, supportFetchAPI: true, standard: true, - bypassCSP: true, stream: true, }, }, @@ -28,7 +27,6 @@ protocol.registerSchemesAsPrivileged([ corsEnabled: true, supportFetchAPI: true, standard: true, - bypassCSP: true, stream: true, }, }, @@ -59,8 +57,13 @@ async function handleFileRequest(request: Request) { 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 && !isFontRequest) { + // Redirect to webpack dev server if available + if ( + isDev && + process.env.DEV_SERVER_URL && + !isAbsolutePath && + !isFontRequest + ) { const devServerUrl = new URL( `${urlObject.pathname}${urlObject.search}`, process.env.DEV_SERVER_URL @@ -103,19 +106,6 @@ async function handleFileRequest(request: Request) { return net.fetch(pathToFileURL(filepath).toString(), clonedRequest); } -// whitelist for cors -// url patterns that are allowed to have cors headers -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$/, @@ -123,6 +113,44 @@ const needRefererDomains = [ ]; const defaultReferer = 'https://client.affine.local/'; +function setHeader( + headers: Record, + name: string, + value: string +) { + Object.keys(headers).forEach(key => { + if (key.toLowerCase() === name.toLowerCase()) { + delete headers[key]; + } + }); + headers[name] = [value]; +} + +function ensureFrameAncestors( + headers: Record, + directive: string +) { + const cspHeaderKey = Object.keys(headers).find( + key => key.toLowerCase() === 'content-security-policy' + ); + if (!cspHeaderKey) { + headers['Content-Security-Policy'] = [`frame-ancestors ${directive}`]; + return; + } + + const values = headers[cspHeaderKey]; + headers[cspHeaderKey] = values.map(val => { + if (typeof val !== 'string') return val as any; + const directives = val + .split(';') + .map(v => v.trim()) + .filter(Boolean) + .filter(d => !d.toLowerCase().startsWith('frame-ancestors')); + directives.push(`frame-ancestors ${directive}`); + return directives.join('; '); + }); +} + export function registerProtocol() { protocol.handle('file', request => { return handleFileRequest(request); @@ -172,79 +200,19 @@ export function registerProtocol() { } } - const hostname = new URL(url).hostname; - if (!corsWhitelist.some(domainRegex => domainRegex.test(hostname))) { + const { protocol } = new URL(url); + + // Only adjust CORS for assets/file responses; leave remote http(s) headers intact + if (protocol === 'assets:' || protocol === 'file:') { delete responseHeaders['access-control-allow-origin']; 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", - // if response header contains "content-security-policy", remove "frame-ancestors/frame-src" - delete responseHeaders['x-frame-options']; - delete responseHeaders['X-Frame-Options']; - - // Handle Content Security Policy headers - const cspHeaders = [ - 'content-security-policy', - 'Content-Security-Policy', - ]; - for (const cspHeader of cspHeaders) { - const cspValues = responseHeaders[cspHeader]; - if (cspValues) { - // Remove frame-ancestors and frame-src directives from CSP - const modifiedCspValues = cspValues - .map(cspValue => { - if (typeof cspValue === 'string') { - return cspValue - .split(';') - .filter(directive => { - const trimmed = directive.trim().toLowerCase(); - return ( - !trimmed.startsWith('frame-ancestors') && - !trimmed.startsWith('frame-src') - ); - }) - .join(';'); - } - return cspValue; - }) - .filter( - value => value && typeof value === 'string' && value.trim() - ); - - if (modifiedCspValues.length > 0) { - responseHeaders[cspHeader] = modifiedCspValues; - } else { - delete responseHeaders[cspHeader]; - } - } + if (protocol === 'assets:' || protocol === 'file:') { + setHeader(responseHeaders, 'X-Frame-Options', 'SAMEORIGIN'); + ensureFrameAncestors(responseHeaders, "'self'"); } } })() diff --git a/packages/frontend/apps/electron/src/main/web-preferences.ts b/packages/frontend/apps/electron/src/main/web-preferences.ts new file mode 100644 index 000000000..497edd965 --- /dev/null +++ b/packages/frontend/apps/electron/src/main/web-preferences.ts @@ -0,0 +1,19 @@ +import type { WebPreferences } from 'electron'; + +const DEFAULT_WEB_PREFERENCES: Pick< + WebPreferences, + 'contextIsolation' | 'nodeIntegration' | 'sandbox' +> = { + sandbox: true, + contextIsolation: true, + nodeIntegration: false, +}; + +export function buildWebPreferences( + overrides: Partial = {} +): WebPreferences { + return { + ...DEFAULT_WEB_PREFERENCES, + ...overrides, + }; +} diff --git a/packages/frontend/apps/electron/src/main/windows-manager/custom-theme-window.ts b/packages/frontend/apps/electron/src/main/windows-manager/custom-theme-window.ts index ebd957deb..01bb5e29c 100644 --- a/packages/frontend/apps/electron/src/main/windows-manager/custom-theme-window.ts +++ b/packages/frontend/apps/electron/src/main/windows-manager/custom-theme-window.ts @@ -5,6 +5,7 @@ import { BrowserWindow, type Display, screen } from 'electron'; import { isMacOS } from '../../shared/utils'; import { customThemeViewUrl } from '../constants'; import { logger } from '../logger'; +import { buildWebPreferences } from '../web-preferences'; let customThemeWindow: Promise | undefined; @@ -26,11 +27,11 @@ async function createCustomThemeWindow(additionalArguments: string[]) { resizable: true, maximizable: false, fullscreenable: false, - webPreferences: { + webPreferences: buildWebPreferences({ webgl: true, preload: join(__dirname, './preload.js'), additionalArguments: additionalArguments, - }, + }), }); await browserWindow.loadURL(customThemeViewUrl); diff --git a/packages/frontend/apps/electron/src/main/windows-manager/main-window.ts b/packages/frontend/apps/electron/src/main/windows-manager/main-window.ts index 8fc5dfa72..066010e73 100644 --- a/packages/frontend/apps/electron/src/main/windows-manager/main-window.ts +++ b/packages/frontend/apps/electron/src/main/windows-manager/main-window.ts @@ -13,6 +13,7 @@ import { logger } from '../logger'; import { MenubarStateKey, MenubarStateSchema } from '../shared-state-schema'; import { globalStateStorage } from '../shared-storage/storage'; import { uiSubjects } from '../ui/subject'; +import { buildWebPreferences } from '../web-preferences'; const IS_DEV: boolean = process.env.NODE_ENV === 'development' && !process.env.CI; @@ -56,6 +57,7 @@ export class MainWindowManager { show: false, width: 100, height: 100, + webPreferences: buildWebPreferences(), }); this.hiddenMacWindow.on('close', () => { this.cleanupWindows(); @@ -95,11 +97,9 @@ export class MainWindowManager { // backgroundMaterial: 'mica', height: mainWindowState.height, show: false, // Use 'ready-to-show' event to show window - webPreferences: { + webPreferences: buildWebPreferences({ webgl: true, - contextIsolation: true, - sandbox: false, - }, + }), }); const helper = await ensureHelperProcess(); helper.connectMain(browserWindow); @@ -283,10 +283,10 @@ export async function openUrlInHiddenWindow(urlObj: URL) { const win = new BrowserWindow({ width: 1200, height: 600, - webPreferences: { + webPreferences: buildWebPreferences({ preload: join(__dirname, './preload.js'), additionalArguments: await getWindowAdditionalArguments(), - }, + }), show: BUILD_CONFIG.debug, }); diff --git a/packages/frontend/apps/electron/src/main/windows-manager/onboarding.ts b/packages/frontend/apps/electron/src/main/windows-manager/onboarding.ts index ff79c2f3c..1c02e58de 100644 --- a/packages/frontend/apps/electron/src/main/windows-manager/onboarding.ts +++ b/packages/frontend/apps/electron/src/main/windows-manager/onboarding.ts @@ -6,6 +6,7 @@ import { isDev } from '../config'; import { onboardingViewUrl } from '../constants'; // import { getExposedMeta } from './exposed'; import { logger } from '../logger'; +import { buildWebPreferences } from '../web-preferences'; import { fullscreenAndCenter, getScreenSize } from './utils'; // todo: not all window need all of the exposed meta @@ -40,11 +41,11 @@ async function createOnboardingWindow(additionalArguments: string[]) { transparent: true, hasShadow: false, roundedCorners: false, - webPreferences: { + webPreferences: buildWebPreferences({ webgl: true, preload: join(__dirname, './preload.js'), additionalArguments: additionalArguments, - }, + }), }); // workaround for the phantom title bar on windows when losing focus diff --git a/packages/frontend/apps/electron/src/main/windows-manager/popup.ts b/packages/frontend/apps/electron/src/main/windows-manager/popup.ts index 7e79f94ae..d856aa608 100644 --- a/packages/frontend/apps/electron/src/main/windows-manager/popup.ts +++ b/packages/frontend/apps/electron/src/main/windows-manager/popup.ts @@ -11,6 +11,7 @@ import { BehaviorSubject } from 'rxjs'; import { popupViewUrl } from '../constants'; import { logger } from '../logger'; import type { MainEventRegister, NamespaceHandlers } from '../type'; +import { buildWebPreferences } from '../web-preferences'; import { getCurrentDisplay } from './utils'; type PopupWindowType = 'notification' | 'recording'; @@ -85,17 +86,15 @@ abstract class PopupWindow { visualEffectState: 'active', vibrancy: 'under-window', ...this.windowOptions, - webPreferences: { - ...this.windowOptions.webPreferences, + webPreferences: buildWebPreferences({ webgl: true, - contextIsolation: true, - sandbox: false, transparent: true, spellcheck: false, preload: join(__dirname, './preload.js'), // this points to the bundled preload module + ...this.windowOptions.webPreferences, // serialize exposed meta that to be used in preload additionalArguments: await getAdditionalArguments(this.name), - }, + }), }); // it seems that the dock will disappear when popup windows are shown diff --git a/packages/frontend/apps/electron/src/main/windows-manager/tab-views.ts b/packages/frontend/apps/electron/src/main/windows-manager/tab-views.ts index e9ac9a36c..cba954ec8 100644 --- a/packages/frontend/apps/electron/src/main/windows-manager/tab-views.ts +++ b/packages/frontend/apps/electron/src/main/windows-manager/tab-views.ts @@ -39,6 +39,7 @@ import { type WorkbenchViewMeta, } from '../shared-state-schema'; import { globalStateStorage } from '../shared-storage/storage'; +import { buildWebPreferences } from '../web-preferences'; import { getMainWindow, MainWindowManager } from './main-window'; async function getAdditionalArguments() { @@ -511,6 +512,7 @@ export class WebContentViewsManager { if (view) { this.resizeView(view); } + this.updateBackgroundThrottling(); return view; }; @@ -759,6 +761,10 @@ export class WebContentViewsManager { this.mainWindow?.on('focus', () => { focusActiveView(); + this.updateBackgroundThrottling(); + }); + this.mainWindow?.on('blur', () => { + this.updateBackgroundThrottling(); }); combineLatest([ @@ -768,6 +774,7 @@ export class WebContentViewsManager { // makes sure the active view is always focused if (window?.isFocused()) { focusActiveView(); + this.updateBackgroundThrottling(); } }); }; @@ -810,17 +817,15 @@ export class WebContentViewsManager { additionalArguments.push(`--view-id=${viewId}`); const view = new WebContentsView({ - webPreferences: { + webPreferences: buildWebPreferences({ webgl: true, transparent: true, - contextIsolation: true, - sandbox: false, spellcheck: spellCheckSettings.enabled, preload: join(__dirname, './preload.js'), // this points to the bundled preload module // serialize exposed meta that to be used in preload additionalArguments: additionalArguments, - backgroundThrottling: false, - }, + backgroundThrottling: true, + }), }); view.webContents.on('context-menu', (_event, params) => { @@ -872,13 +877,16 @@ export class WebContentViewsManager { }); this.webViewsMap$.next(this.tabViewsMap.set(viewId, view)); - let unsub = () => {}; + let disconnectHelperProcess: (() => void) | null = null; // shell process do not need to connect to helper process if (type !== 'shell') { view.webContents.on('did-finish-load', () => { - unsub(); - unsub = helperProcessManager.connectRenderer(view.webContents); + disconnectHelperProcess?.(); + disconnectHelperProcess = helperProcessManager.connectRenderer( + view.webContents + ); + this.updateBackgroundThrottling(); }); } else { view.webContents.on('focus', () => { @@ -892,6 +900,8 @@ export class WebContentViewsManager { } view.webContents.on('destroyed', () => { + disconnectHelperProcess?.(); + disconnectHelperProcess = null; this.webViewsMap$.next( new Map( [...this.tabViewsMap.entries()].filter(([key]) => key !== viewId) @@ -902,6 +912,7 @@ export class WebContentViewsManager { if (this.tabViewsMap.size === 0) { app.quit(); } + this.updateBackgroundThrottling(); }); this.resizeView(view); @@ -920,6 +931,30 @@ export class WebContentViewsManager { return view; }; + private readonly updateBackgroundThrottling = () => { + const mainFocused = this.mainWindow?.isFocused() ?? false; + const activeId = this.activeWorkbenchId; + this.webViewsMap$.value.forEach((view, id) => { + if (id === 'shell') { + return; + } + const shouldThrottle = !mainFocused || id !== activeId; + try { + view.webContents.setBackgroundThrottling(shouldThrottle); + } catch (err) { + logger.warn('failed to set backgroundThrottling', err); + } + }); + if (this.shellView) { + const shellThrottle = !mainFocused; + try { + this.shellView.webContents.setBackgroundThrottling(shellThrottle); + } catch (err) { + logger.warn('failed to set shell backgroundThrottling', err); + } + } + }; + private async skipOnboarding(view: WebContentsView) { await view.webContents.executeJavaScript(` window.localStorage.setItem('app_config', '{"onBoarding":false}'); diff --git a/packages/frontend/apps/electron/src/main/worker/pool.ts b/packages/frontend/apps/electron/src/main/worker/pool.ts index 1b8f07637..4a8ec3c7c 100644 --- a/packages/frontend/apps/electron/src/main/worker/pool.ts +++ b/packages/frontend/apps/electron/src/main/worker/pool.ts @@ -5,6 +5,7 @@ import { BrowserWindow, MessageChannelMain, type WebContents } from 'electron'; import { backgroundWorkerViewUrl } from '../constants'; import { ensureHelperProcess } from '../helper-process'; import { logger } from '../logger'; +import { buildWebPreferences } from '../web-preferences'; async function getAdditionalArguments() { const { getExposedMeta } = await import('../exposed'); @@ -41,10 +42,10 @@ export class WorkerManager { const worker = new BrowserWindow({ width: 1200, height: 600, - webPreferences: { + webPreferences: buildWebPreferences({ preload: join(__dirname, './preload.js'), additionalArguments: additionalArguments, - }, + }), show: false, }); @@ -56,9 +57,32 @@ export class WorkerManager { }; let disconnectHelperProcess: (() => void) | null = null; - worker.on('closed', () => { + let cleanedUp = false; + const cleanup = () => { + if (cleanedUp) { + return; + } + cleanedUp = true; this.workers.delete(key); disconnectHelperProcess?.(); + disconnectHelperProcess = null; + }; + const handleWorkerFailure = (reason: string) => { + logger.error('[worker] renderer process gone', { key, reason }); + record.loaded.reject(new Error(`worker ${key} failed: ${reason}`)); + cleanup(); + try { + worker.destroy(); + } catch (err) { + logger.warn('failed to destroy worker window', err); + } + }; + worker.on('closed', cleanup); + worker.webContents.on('render-process-gone', (_event, details) => { + handleWorkerFailure(details.reason ?? 'unknown'); + }); + worker.webContents.on('unresponsive', () => { + handleWorkerFailure('unresponsive'); }); worker.loadURL(backgroundWorkerViewUrl).catch(e => { logger.error('failed to load url', e); diff --git a/packages/frontend/apps/electron/src/preload/electron-api.ts b/packages/frontend/apps/electron/src/preload/electron-api.ts index 420d025e2..4429699b4 100644 --- a/packages/frontend/apps/electron/src/preload/electron-api.ts +++ b/packages/frontend/apps/electron/src/preload/electron-api.ts @@ -10,6 +10,7 @@ import { z } from 'zod'; import { AFFINE_API_CHANNEL_NAME, AFFINE_EVENT_CHANNEL_NAME, + AFFINE_EVENT_SUBSCRIBE_CHANNEL_NAME, type ExposedMeta, type HelperToRenderer, type RendererToHelper, @@ -83,6 +84,33 @@ function getMainAPIs() { // channel -> callback[] const listenersMap = new Map void)[]>(); + const subscribeCounts = new Map(); + + const subscribe = (channel: string) => { + const count = (subscribeCounts.get(channel) ?? 0) + 1; + subscribeCounts.set(channel, count); + if (count === 1) { + ipcRenderer.send( + AFFINE_EVENT_SUBSCRIBE_CHANNEL_NAME, + 'subscribe', + channel + ); + } + }; + + const unsubscribe = (channel: string) => { + const count = (subscribeCounts.get(channel) ?? 0) - 1; + if (count <= 0) { + subscribeCounts.delete(channel); + ipcRenderer.send( + AFFINE_EVENT_SUBSCRIBE_CHANNEL_NAME, + 'unsubscribe', + channel + ); + } else { + subscribeCounts.set(channel, count); + } + }; ipcRenderer.on(AFFINE_EVENT_CHANNEL_NAME, (_event, channel, ...args) => { if (typeof channel !== 'string') { @@ -108,12 +136,20 @@ function getMainAPIs() { ...(listenersMap.get(channel) ?? []), callback, ]); + subscribe(channel); return () => { const listeners = listenersMap.get(channel) ?? []; const index = listeners.indexOf(callback); - if (index !== -1) { - listeners.splice(index, 1); + if (index === -1) { + return; + } + listeners.splice(index, 1); + unsubscribe(channel); + if (listeners.length === 0) { + listenersMap.delete(channel); + } else { + listenersMap.set(channel, listeners); } }; }, diff --git a/packages/frontend/apps/electron/src/preload/shared-storage.ts b/packages/frontend/apps/electron/src/preload/shared-storage.ts index 0e96daa05..7a0236605 100644 --- a/packages/frontend/apps/electron/src/preload/shared-storage.ts +++ b/packages/frontend/apps/electron/src/preload/shared-storage.ts @@ -6,16 +6,6 @@ import { AFFINE_EVENT_CHANNEL_NAME, } from '../shared/type'; -// Load persisted data from main process synchronously at preload time -const initialGlobalState = ipcRenderer.sendSync( - AFFINE_API_CHANNEL_NAME, - 'sharedStorage:getAllGlobalState' -); -const initialGlobalCache = ipcRenderer.sendSync( - AFFINE_API_CHANNEL_NAME, - 'sharedStorage:getAllGlobalCache' -); - // Unique id for this renderer instance, used to ignore self-originated broadcasts const CLIENT_ID: string = Math.random().toString(36).slice(2); @@ -35,42 +25,97 @@ function createSharedStorageApi( } ) { const memory = new MemoryMemento(); - memory.setAll(init); - ipcRenderer.on(AFFINE_EVENT_CHANNEL_NAME, (_event, channel, updates) => { - if (channel === `sharedStorage:${event}`) { - for (const [key, raw] of Object.entries(updates)) { - // support both legacy plain value and new { v, r, s } structure - let value: any; - let source: string | undefined; + const revisions = new Map(); + const updateQueue: Record[] = []; + let loaded = false; - if (raw && typeof raw === 'object' && 'v' in raw) { - value = (raw as any).v; - source = (raw as any).s; - } else { - value = raw; - } + const applyUpdates = (updates: Record) => { + for (const [key, raw] of Object.entries(updates)) { + // '*' means "reset everything" coming from a clear operation + if (key === '*') { + memory.clear(); + revisions.clear(); + continue; + } - // Ignore our own broadcasts - if (source && source === CLIENT_ID) { + // support both legacy plain value and new { v, r, s } structure + let value: any; + let source: string | undefined; + let rev: number | undefined; + + if (raw && typeof raw === 'object' && 'v' in raw) { + value = raw.v; + source = raw.s; + rev = typeof raw.r === 'number' ? raw.r : undefined; + } else { + value = raw; + } + + // Ignore our own broadcasts + if (source && source === CLIENT_ID) { + continue; + } + + if (rev !== undefined) { + const current = revisions.get(key) ?? -1; + if (rev <= current) { continue; } + revisions.set(key, rev); + } - if (value === undefined) { - memory.del(key); - } else { - memory.set(key, value); - } + if (value === undefined) { + memory.del(key); + } else { + memory.set(key, value); + } + } + }; + + ipcRenderer.on(AFFINE_EVENT_CHANNEL_NAME, (_event, channel, updates) => { + if (channel === `sharedStorage:${event}`) { + if (loaded) { + applyUpdates(updates); + } else { + updateQueue.push(updates); } } }); + const initPromise = (async () => { + try { + memory.setAll(init); + const latest = await ipcRenderer.invoke( + AFFINE_API_CHANNEL_NAME, + event === 'onGlobalStateChanged' + ? 'sharedStorage:getAllGlobalState' + : 'sharedStorage:getAllGlobalCache' + ); + if (latest && typeof latest === 'object') { + memory.setAll(latest); + } + } catch (err) { + console.error('Failed to load initial shared storage', err); + } finally { + loaded = true; + while (updateQueue.length) { + const updates = updateQueue.shift(); + if (updates) { + applyUpdates(updates); + } + } + } + })(); + return { + ready: initPromise, del(key: string) { memory.del(key); invokeWithCatch(`sharedStorage:${api.del}`, key, CLIENT_ID); }, clear() { memory.clear(); + revisions.clear(); invokeWithCatch(`sharedStorage:${api.clear}`, CLIENT_ID); }, get(key: string): T | undefined { @@ -90,25 +135,17 @@ function createSharedStorageApi( }; } -export const globalState = createSharedStorageApi( - initialGlobalState, - 'onGlobalStateChanged', - { - clear: 'clearGlobalState', - del: 'delGlobalState', - set: 'setGlobalState', - } -); +export const globalState = createSharedStorageApi({}, 'onGlobalStateChanged', { + clear: 'clearGlobalState', + del: 'delGlobalState', + set: 'setGlobalState', +}); -export const globalCache = createSharedStorageApi( - initialGlobalCache, - 'onGlobalCacheChanged', - { - clear: 'clearGlobalCache', - del: 'delGlobalCache', - set: 'setGlobalCache', - } -); +export const globalCache = createSharedStorageApi({}, 'onGlobalCacheChanged', { + clear: 'clearGlobalCache', + del: 'delGlobalCache', + set: 'setGlobalCache', +}); export const sharedStorage = { globalState, diff --git a/packages/frontend/apps/electron/src/shared/type.ts b/packages/frontend/apps/electron/src/shared/type.ts index 231ee3baf..088b96a43 100644 --- a/packages/frontend/apps/electron/src/shared/type.ts +++ b/packages/frontend/apps/electron/src/shared/type.ts @@ -30,3 +30,4 @@ export type MainToHelper = Pick< export const AFFINE_API_CHANNEL_NAME = 'affine-ipc-api'; export const AFFINE_EVENT_CHANNEL_NAME = 'affine-ipc-event'; +export const AFFINE_EVENT_SUBSCRIBE_CHANNEL_NAME = 'affine-ipc-event-subscribe'; diff --git a/packages/frontend/core/src/blocksuite/ai/messages/wrapper.ts b/packages/frontend/core/src/blocksuite/ai/messages/wrapper.ts index c06374877..6f06b193e 100644 --- a/packages/frontend/core/src/blocksuite/ai/messages/wrapper.ts +++ b/packages/frontend/core/src/blocksuite/ai/messages/wrapper.ts @@ -75,7 +75,6 @@ export const createIframeRenderer: ( class="ai-answer-iframe" sandbox="allow-scripts" scrolling="no" - allowfullscreen .srcdoc=${preprocessHtml(answer)} > `; diff --git a/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts b/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts index 1fdff183a..63ac1bd49 100644 --- a/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts +++ b/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts @@ -1,6 +1,6 @@ import { showAILoginRequiredAtom } from '@affine/core/components/affine/auth/ai-login-required'; import type { AIToolsConfig } from '@affine/core/modules/ai-button'; -import type { UserFriendlyError } from '@affine/error'; +import { UserFriendlyError } from '@affine/error'; import { addContextBlobMutation, addContextCategoryMutation, @@ -50,6 +50,20 @@ export enum Endpoint { type OptionsField = RequestOptions['variables'] extends { options: infer U } ? U : never; +function toUserFriendlyError(err: any): UserFriendlyError { + return err instanceof UserFriendlyError + ? err + : UserFriendlyError.fromAny(err); +} + +function isAbortError(error: UserFriendlyError) { + return ( + error.name === 'REQUEST_ABORTED' || + error.code === 'REQUEST_ABORTED' || + error.message?.toLowerCase().includes('aborted') === true + ); +} + function codeToError(error: UserFriendlyError) { switch (error.status) { case 401: @@ -66,7 +80,7 @@ function codeToError(error: UserFriendlyError) { } export function resolveError(err: any) { - return codeToError(err); + return codeToError(toUserFriendlyError(err)); } export function handleError(src: any) { @@ -185,7 +199,11 @@ export class CopilotClient { }); return res.currentUser?.copilot?.chats.edges.map(e => e.node); } catch (err) { - throw resolveError(err); + const parsed = toUserFriendlyError(err); + if (isAbortError(parsed)) { + return []; + } + throw resolveError(parsed); } } @@ -205,7 +223,11 @@ export class CopilotClient { }); return res.currentUser?.copilot?.chats.edges.map(e => e.node); } catch (err) { - throw resolveError(err); + const parsed = toUserFriendlyError(err); + if (isAbortError(parsed)) { + return []; + } + throw resolveError(parsed); } } @@ -230,7 +252,11 @@ export class CopilotClient { return res.currentUser?.copilot?.chats.edges.map(e => e.node); } catch (err) { - throw resolveError(err); + const parsed = toUserFriendlyError(err); + if (isAbortError(parsed)) { + return []; + } + throw resolveError(parsed); } } @@ -255,7 +281,11 @@ export class CopilotClient { return res.currentUser?.copilot?.chats.edges.map(e => e.node); } catch (err) { - throw resolveError(err); + const parsed = toUserFriendlyError(err); + if (isAbortError(parsed)) { + return []; + } + throw resolveError(parsed); } } diff --git a/packages/frontend/core/src/modules/cloud/services/fetch.ts b/packages/frontend/core/src/modules/cloud/services/fetch.ts index d5c7cf8a9..18319dbc0 100644 --- a/packages/frontend/core/src/modules/cloud/services/fetch.ts +++ b/packages/frontend/core/src/modules/cloud/services/fetch.ts @@ -63,13 +63,22 @@ export class FetchService extends Service { } ); } catch (err: any) { + const isAbort = + err?.name === 'AbortError' || + err?.code === 'ABORT_ERR' || + err?.type === 'aborted' || + abortController.signal.aborted; + + const message = + err?.message || (isAbort ? 'Request aborted' : 'Unknown network error'); + throw new UserFriendlyError({ - status: 504, - code: 'NETWORK_ERROR', - type: 'NETWORK_ERROR', - name: 'NETWORK_ERROR', - message: `Network error: ${err.message}`, - stacktrace: err.stack, + status: isAbort ? 499 : 504, + code: isAbort ? 'REQUEST_ABORTED' : 'NETWORK_ERROR', + type: isAbort ? 'REQUEST_ABORTED' : 'NETWORK_ERROR', + name: isAbort ? 'REQUEST_ABORTED' : 'NETWORK_ERROR', + message: `Network error: ${message}`, + stacktrace: err?.stack, }); } finally { clearTimeout(timeoutId); diff --git a/yarn.lock b/yarn.lock index 621539ec1..ad328b9dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -948,7 +948,7 @@ __metadata: "@opentelemetry/core": "npm:^2.2.0" "@opentelemetry/exporter-prometheus": "npm:^0.208.0" "@opentelemetry/exporter-zipkin": "npm:^2.2.0" - "@opentelemetry/host-metrics": "npm:^0.37.0" + "@opentelemetry/host-metrics": "npm:^0.38.0" "@opentelemetry/instrumentation": "npm:^0.208.0" "@opentelemetry/instrumentation-graphql": "npm:^0.56.0" "@opentelemetry/instrumentation-http": "npm:^0.208.0" @@ -11440,14 +11440,14 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/host-metrics@npm:^0.37.0": - version: 0.37.0 - resolution: "@opentelemetry/host-metrics@npm:0.37.0" +"@opentelemetry/host-metrics@npm:^0.38.0": + version: 0.38.0 + resolution: "@opentelemetry/host-metrics@npm:0.38.0" dependencies: systeminformation: "npm:5.23.8" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/c2929a28f046ff99633ae4dafb758967be5623fa9c456f01a82d7f58823bf328a388f928e36a0591d963adff532fe9461abbebc532a7604ec48f3a194cb7f129 + checksum: 10/22f088b4c1541e86b4447d6386b771bed9c332bf0449b5c6fdc1ebbc1ccd5dc5f37223ad10e4591040cd99123596a92f05aa77587cdd2702c53c6a1b77169b79 languageName: node linkType: hard