diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c85a7c84f..ad5592ec4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -67,3 +67,26 @@ jobs: - name: Lint run: npm run lint + + # The build the release tarball and a manual install use, with its guard at + # the end. Docker and the Lite images build another way (webpack, export), so + # nothing else would notice this one breaking before a release does. + standalone-build: + name: Standalone build and guard + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up Node + uses: actions/setup-node@v7 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build diff --git a/lib/desktop/__tests__/trust-flag.test.ts b/lib/desktop/__tests__/trust-flag.test.ts index c40bd5846..0edd41532 100644 --- a/lib/desktop/__tests__/trust-flag.test.ts +++ b/lib/desktop/__tests__/trust-flag.test.ts @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { readFileSync } from 'node:fs'; import path from 'node:path'; import { build } from 'esbuild'; -import { desktopBuildFlag } from '../../../scripts/desktop-build-flag.mjs'; +import { DESKTOP_TRUST_MARKER, desktopBuildFlag } from '../../../scripts/desktop-build-flag.mjs'; const ROOT = path.resolve(__dirname, '../../..'); const TRUST_KEY = Symbol.for('bulwark.desktop.trustedJmapOrigins'); @@ -112,6 +112,28 @@ describe('the flag is fixed at build time', () => { } }); + /** Compiles the trust check alone, kept as an export the way Turbopack keeps it. */ + async function trustCheckOfBuild(flag: string): Promise { + const result = await build({ + entryPoints: [path.join(ROOT, 'lib/desktop/trusted-servers.ts')], + bundle: true, + write: false, + minify: true, + format: 'esm', + platform: 'node', + define: { 'process.env.NEXT_PUBLIC_BULWARK_DESKTOP': JSON.stringify(flag) }, + }); + return result.outputFiles[0].text; + } + + it('a server build holds no trust path, even when a bundler keeps the unused export', async () => { + expect(await trustCheckOfBuild('')).not.toContain(DESKTOP_TRUST_MARKER); + }); + + it('a desktop build still holds it', async () => { + expect(await trustCheckOfBuild('1')).toContain(DESKTOP_TRUST_MARKER); + }); + it('no image build sets it', () => { for (const file of ['Dockerfile', 'Dockerfile.lite']) { expect(readFileSync(path.join(ROOT, file), 'utf8')).not.toContain('NEXT_PUBLIC_BULWARK_DESKTOP'); diff --git a/lib/desktop/trusted-servers.ts b/lib/desktop/trusted-servers.ts index e2cf034b5..8fa60c651 100644 --- a/lib/desktop/trusted-servers.ts +++ b/lib/desktop/trusted-servers.ts @@ -7,8 +7,9 @@ * or the environment can write it, so script in a page can't aim the login * routes at the LAN. * - * Callers guard this with IS_DESKTOP_BUILD, so a server build compiles the - * whole thing away. scripts/standalone-guard.mjs checks that it did. + * Callers check the flag too, but Turbopack keeps an imported export after the + * call is gone. So the check also starts here, spelled out: every bundler then + * drops the body from a server build. scripts/standalone-guard.mjs checks it did. */ type TrustedOrigins = ReadonlySet; @@ -25,6 +26,7 @@ function originOf(url: string): string | null { } export function isDesktopTrustedServer(url: string | null | undefined): boolean { + if (process.env.NEXT_PUBLIC_BULWARK_DESKTOP !== '1') return false; if (!url) return false; const origin = originOf(url); if (!origin) return false;