feat: merge the webmail side of the desktop app

# Conflicts:
#	lib/desktop/types.ts
#	locales/ar/common.json
#	locales/ca/common.json
#	locales/cs/common.json
#	locales/da/common.json
#	locales/de/common.json
#	locales/en/common.json
#	locales/es/common.json
#	locales/et/common.json
#	locales/fa/common.json
#	locales/fr/common.json
#	locales/he/common.json
#	locales/hu/common.json
#	locales/it/common.json
#	locales/ja/common.json
#	locales/ko/common.json
#	locales/lv/common.json
#	locales/mn/common.json
#	locales/nb/common.json
#	locales/nl/common.json
#	locales/pl/common.json
#	locales/pt/common.json
#	locales/ro/common.json
#	locales/ru/common.json
#	locales/sk/common.json
#	locales/th/common.json
#	locales/tr/common.json
#	locales/uk/common.json
#	locales/vi/common.json
#	locales/zh-TW/common.json
#	locales/zh/common.json
#	stores/account-store.ts
This commit is contained in:
Linus Rath
2026-11-03 08:48:10 +01:00
72 changed files with 2099 additions and 42 deletions
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env node
// The webmail build the desktop app embeds: `npm run build:desktop`. It's the
// regular standalone build with NEXT_PUBLIC_BULWARK_DESKTOP compiled in, using
// webpack (a third of Turbopack's server size, and its hashed external links
// don't relocate). No other build sets the flag: repos/desktop's
// scripts/fetch-webmail.mjs refuses a tree without it.
import { spawnSync } from 'node:child_process';
const env = { ...process.env, NEXT_PUBLIC_BULWARK_DESKTOP: '1', NEXT_TELEMETRY_DISABLED: '1' };
const isWindows = process.platform === 'win32';
for (const [command, ...args] of [
['node', 'scripts/build-pst-worker.mjs'],
['node', 'scripts/build-store-worker.mjs'],
['node', 'scripts/build-plugin-sandbox.mjs'],
['npx', 'next', 'build', '--webpack'],
['node', 'scripts/standalone-guard.mjs'],
]) {
const run = spawnSync(command, args, { env, stdio: 'inherit', shell: isWindows });
if (run.status !== 0) process.exit(run.status ?? 1);
}
+19
View File
@@ -0,0 +1,19 @@
// The value next.config.ts inlines as NEXT_PUBLIC_BULWARK_DESKTOP.
//
// Always defined, so every build compiles `process.env.NEXT_PUBLIC_BULWARK_DESKTOP`
// to a literal. Left undefined, Next would leave the read in the server bundle,
// and setting the variable when starting a server build would turn the flag on.
/** The marker scripts/standalone-guard.mjs looks for in a non-desktop build. */
export const DESKTOP_TRUST_MARKER = 'bulwark.desktop.trustedJmapOrigins';
/**
* A read of the flag that survived compiling: then setting it at runtime would
* work. Only the read: Next also writes the bare name into server.js with the
* config's `env` block, as a JSON key with its compiled value.
*/
export const DESKTOP_FLAG_READ = /process\.env(\.NEXT_PUBLIC_BULWARK_DESKTOP\b|\[\s*["'`]NEXT_PUBLIC_BULWARK_DESKTOP["'`]\s*\])/;
export function desktopBuildFlag(env = process.env) {
return env.NEXT_PUBLIC_BULWARK_DESKTOP === '1' ? '1' : '';
}
+28 -2
View File
@@ -18,6 +18,7 @@
import { existsSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { DESKTOP_FLAG_READ, DESKTOP_TRUST_MARKER, desktopBuildFlag } from './desktop-build-flag.mjs';
/** Top-level project entries the running server needs. Traces drop the rest. */
const TRACE_ALLOWED = new Set(['node_modules', '.next', 'public', 'locales', 'package.json', 'VERSION']);
@@ -141,10 +142,35 @@ export function removeCopiedEnvFiles(standaloneDir) {
return removed;
}
/**
* Server bundles that still hold the desktop app's trust path. A server build
* compiles it away (lib/desktop/flag.ts); finding it means the flag leaked into
* this build or stopped being inlined, and a visitor could reach private hosts.
*/
export function findDesktopTrustPath(standaloneDir) {
const found = [];
const visit = (dir) => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name !== 'node_modules') visit(full);
} else if (entry.name.endsWith('.js')) {
const text = readFileSync(full, 'utf8');
if (text.includes(DESKTOP_TRUST_MARKER) || DESKTOP_FLAG_READ.test(text)) {
found.push(`${toPosix(path.relative(standaloneDir, full))} (desktop trust path in a server build)`);
}
}
}
};
visit(standaloneDir);
return found.sort();
}
/** Cleans and checks a standalone tree. Returns what is still wrong with it. */
export function guardStandalone(standaloneDir) {
export function guardStandalone(standaloneDir, { desktopBuild = desktopBuildFlag() === '1' } = {}) {
removeCopiedEnvFiles(standaloneDir);
return findForbidden(standaloneDir);
const forbidden = findForbidden(standaloneDir);
return desktopBuild ? forbidden : [...forbidden, ...findDesktopTrustPath(standaloneDir)];
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {