mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
* 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.
243 lines
9.1 KiB
TypeScript
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)')
|
|
})
|
|
})
|