Files
orca/config/scripts/electron-vite-output-contract.test.ts
T
Neil 56fcb544e0 fix(browser): move cookie scoping off psl's stale suffix list (#20421)
* fix(browser): move cookie scoping off psl's stale suffix list

psl@1.15.0 is its latest release and ships a Dec-2024 snapshot of the
public suffix list. Measured against the current upstream list, it fails
to recognise 600 of 10,030 suffixes; tldts misses 2.

That gap is a cookie-isolation bug. psl does not know `api.br` is a
suffix, so it falls back to the `br` rule and maps foo.api.br, bar.api.br
and example.api.br all onto the single family `api.br`. Unrelated
registrants then share a removal scope, and a replace-mode import for one
clears the others' cookies. The same holds for seg.ar, co.az, gov.cz and
~597 more.

tldts is called with allowPrivateDomains, without which the PSL's PRIVATE
section is ignored and every *.github.io / *.s3.amazonaws.com / *.vercel.app
tenant collapses into one family — 21 of 49 probed hosts changed family
under the default. The new test pins that boundary.

One deliberate behaviour change: hosts under `.local` (not in the PSL)
were their own family under psl, which returned an all-null parse for
them; they now resolve to the two-label boundary (app.orca.local ->
orca.local), matching what Chromium treats as the registrable domain.

* fix(build): bundle tldts into the main process like psl was

psl sat in BUNDLED_MAIN_DEPENDENCIES, so it was inlined into the main
bundle rather than externalized and copied into resources/node_modules.
Swapping the dependency without moving that entry left a bare tldts
import that afterPack's runtime-closure check rejects.

* fix(build): point the output contract at tldts and drop the psl shim

The contract test still asserted psl was in BUNDLED_MAIN_DEPENDENCIES, so
it failed once the entry became tldts. src/types/psl.ts declared a module
that no longer resolves; tldts ships its own types.

* test(browser): pin the suffix boundaries the tldts swap moved

Three semantic changes shipped untested:

- `.local` is unlisted, and the libraries disagreed on what that means. psl
  returned an all-null parse so every `*.orca.local` host was its own family;
  tldts stops at `orca.local`. The consequence is wider than the family name —
  importDomainAncestors now yields the shared parent, so a replace-mode import
  of one host clears non-host-only cookies every sibling shares.
- psl's snapshot had `compute.amazonaws.com` as a literal PRIVATE suffix; the
  current list only carries the wildcard, so the bare host is ICANN now.
- The renderer's `psl.isValid` gate had no direct test at all — nothing imported
  the module from a test.

Also drops comments that explained a boundary in terms of psl's internals. One
was wrong under tldts: bracketed IPv6 does not reach an error branch, it parses
with the brackets stripped and falls through the unlisted path.
2026-09-12 16:00:54 -07:00

243 lines
9.1 KiB
TypeScript

import * as nodeFs from 'node:fs'
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import * as nodePath from 'node:path'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { EventEmitter } from 'node:events'
import { runInNewContext } from 'node:vm'
import { describe, expect, it } from 'vitest'
import {
DEV_BUNDLE_ID,
DEV_HELPER_BUNDLE_ID,
getDevHelperPlistPatches
} from './dev-electron-bundle-identity.mjs'
import {
BOOTSTRAP_FATAL_LOG_ENV_VAR,
BOOTSTRAP_FATAL_LOG_FILE_NAME,
createBootstrapFatalExitBanner
} from '../build-plugins/bootstrap-fatal-exit-banner'
import { createRequire } from 'node:module'
import { electronViteConfig } from '../../electron.vite.config'
import { BOOTSTRAP_FATAL_EXIT_GUARD_KEY } from '../../src/main/startup/bootstrap-fatal-exit-guard'
const targetConfig = readFileSync('config/electron-vite-target.config.cts', 'utf8')
const devRunner = readFileSync('config/scripts/run-electron-vite-dev.mjs', 'utf8')
type BootstrapProcessMock = EventEmitter & {
env: Record<string, string>
pid: number
exit: (code: number) => void
exitCode?: number
}
/** Runs the banner in a bare context and raises the bootstrap fault it guards against. */
function failBootstrapWithBanner(options: {
env: Record<string, string>
tmpdir?: string
stderrWrites?: string[]
}): BootstrapProcessMock {
const processMock = new EventEmitter() as BootstrapProcessMock
processMock.env = options.env
processMock.pid = 4242
processMock.exit = () => {}
const fsShim = {
...nodeFs,
writeSync: (descriptor: number, data: string) => {
if (descriptor === 2) {
options.stderrWrites?.push(data)
return data.length
}
return nodeFs.writeSync(descriptor, data)
}
}
const context = {
process: processMock,
setImmediate: () => {},
require: (specifier: string) => {
if (specifier === 'node:fs') {
return fsShim
}
if (specifier === 'node:path') {
return nodePath
}
if (specifier === 'node:os' && options.tmpdir !== undefined) {
return { tmpdir: () => options.tmpdir }
}
// Electron's own module is unreachable from a bootstrap fault this early.
throw new Error(`unexpected require: ${specifier}`)
}
}
runInNewContext(createBootstrapFatalExitBanner(), context)
processMock.emit('uncaughtException', new Error("Cannot find module 'ws'"))
return processMock
}
const electronBuilderConfig = createRequire(import.meta.url)('../electron-builder.config.cjs') as {
files: string[]
}
describe('Electron Vite output contract', () => {
it("minifies main and renderer with rolldown's in-process minifier", () => {
// Why: 'esbuild' routes every chunk through a second, undeclared transpiler.
expect(electronViteConfig.main?.build?.minify).toBe('oxc')
expect(electronViteConfig.renderer?.build?.minify).toBe('oxc')
expect(electronViteConfig.main?.esbuild).toBeUndefined()
expect(electronViteConfig.renderer?.esbuild).toBeUndefined()
})
it('emits hidden main source maps that packaging strips from app.asar', () => {
// Hidden maps decode minified crash traces without the bundle referencing
// files that the packaged app never ships.
expect(electronViteConfig.main?.build?.sourcemap).toBe('hidden')
expect(electronBuilderConfig.files).toContain('!out/**/*.map')
})
it('keeps main-process and plain-Node entries at stable CommonJS paths', () => {
const output = electronViteConfig.main?.build?.rollupOptions?.output
if (!output || Array.isArray(output)) {
throw new Error('Expected one main-process output')
}
expect(output.format).toBe('cjs')
expect(output.entryFileNames).toBe('[name].js')
expect(output.chunkFileNames).toBe('chunks/[name]-[hash].js')
})
it('externalizes packaged dependencies but bundles self-contained main dependencies', () => {
const external = electronViteConfig.main?.build?.rollupOptions?.external
if (typeof external !== 'function') {
throw new Error('Expected main-process external predicate')
}
expect(external('node-pty', undefined, false)).toBe(true)
expect(external('@parcel/watcher', undefined, false)).toBe(true)
expect(external('electron', undefined, false)).toBe(true)
expect(external('node:fs', undefined, false)).toBe(true)
expect(external('@xterm/headless', undefined, false)).toBe(false)
expect(external('@xterm/addon-serialize', undefined, false)).toBe(false)
expect(external('tldts', undefined, false)).toBe(false)
expect(external('zod', undefined, false)).toBe(false)
expect(electronViteConfig.main?.build?.externalizeDeps?.exclude).toContain('tldts')
expect(electronViteConfig.main?.build?.externalizeDeps?.exclude).toContain('zod')
})
it('bundles validation dependencies used by the sandboxed preload', () => {
expect(electronViteConfig.preload?.build?.externalizeDeps?.exclude).toContain('zod')
})
it('exits when a static import fails before source error guards load', () => {
const processMock = new EventEmitter() as EventEmitter & {
exit: (code: number) => void
exitCode?: number
stderr: { write: (chunk: string) => boolean }
}
let scheduledExit: (() => void) | null = null
let exitedWith: number | null = null
const stderrWrites: string[] = []
processMock.exit = (code) => {
exitedWith = code
}
processMock.stderr = {
write: (chunk) => {
stderrWrites.push(chunk)
return true
}
}
const context = {
process: processMock,
setImmediate: (callback: () => void) => {
scheduledExit = callback
}
}
runInNewContext(createBootstrapFatalExitBanner(), context)
processMock.emit('uncaughtException', new Error("Cannot find module 'zod'"))
expect(processMock.exitCode).toBe(1)
expect(scheduledExit).not.toBeNull()
scheduledExit?.()
expect(exitedWith).toBe(1)
expect(context).toHaveProperty(BOOTSTRAP_FATAL_EXIT_GUARD_KEY)
expect(stderrWrites.join('')).toContain("Cannot find module 'zod'")
})
it('records the bootstrap failure it exits on, since the guard hides Electron dialog', () => {
const logDirectory = mkdtempSync(join(tmpdir(), 'orca-bootstrap-fatal-'))
const logPath = join(logDirectory, 'fatal.log')
const stderrWrites: string[] = []
try {
const processMock = failBootstrapWithBanner({
env: { [BOOTSTRAP_FATAL_LOG_ENV_VAR]: logPath },
stderrWrites
})
expect(stderrWrites.join('')).toContain("Cannot find module 'ws'")
const recorded = readFileSync(logPath, 'utf8')
expect(recorded).toContain("Cannot find module 'ws'")
expect(recorded).toContain('pid=4242')
expect(processMock.exitCode).toBe(1)
} finally {
rmSync(logDirectory, { recursive: true, force: true })
}
})
it('creates the parent directory an overridden log path names but does not have', () => {
const logDirectory = mkdtempSync(join(tmpdir(), 'orca-bootstrap-fatal-'))
const logPath = join(logDirectory, 'nested', 'diagnostics', 'fatal.log')
try {
const processMock = failBootstrapWithBanner({
env: { [BOOTSTRAP_FATAL_LOG_ENV_VAR]: logPath }
})
expect(readFileSync(logPath, 'utf8')).toContain("Cannot find module 'ws'")
expect(processMock.exitCode).toBe(1)
} finally {
rmSync(logDirectory, { recursive: true, force: true })
}
})
it('falls back to the default location when the overridden log path is unwritable', () => {
const logDirectory = mkdtempSync(join(tmpdir(), 'orca-bootstrap-fatal-'))
const fallbackDirectory = join(logDirectory, 'fallback')
try {
const processMock = failBootstrapWithBanner({
// A directory can never be opened as the log file, so the override must yield.
env: { [BOOTSTRAP_FATAL_LOG_ENV_VAR]: logDirectory },
tmpdir: fallbackDirectory
})
const recorded = readFileSync(join(fallbackDirectory, BOOTSTRAP_FATAL_LOG_FILE_NAME), 'utf8')
expect(recorded).toContain("Cannot find module 'ws'")
expect(processMock.exitCode).toBe(1)
} finally {
rmSync(logDirectory, { recursive: true, force: true })
}
})
it('isolates renderer entry side effects behind strict facades', () => {
expect(electronViteConfig.renderer?.build?.rollupOptions?.preserveEntrySignatures).toBe(
'strict'
)
})
it('rejects prototype properties as build targets', () => {
// Own-property check only: an inherited key like `constructor` must not select a build target.
expect(targetConfig).toContain('Object.hasOwn(configByTarget, target)')
})
it('gives the dev terminal daemon helper the TCC identity watched by Orca', () => {
// Asserted on the values rather than the source text: the ids moved into
// dev-electron-bundle-identity.mjs so every dev bundle signs to one cdhash.
expect(DEV_HELPER_BUNDLE_ID).toBe(`${DEV_BUNDLE_ID}.helper`)
expect(getDevHelperPlistPatches()).toEqual([
{ key: 'CFBundleIdentifier', value: DEV_HELPER_BUNDLE_ID }
])
expect(devRunner).toContain("'Electron Helper.app',")
expect(devRunner).toContain('setPlistValue(helperPlistPath, key, value)')
})
})