mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 08:15:41 +00:00
fix: compile the desktop trust path out of every other build, and fail a build that keeps it
This commit is contained in:
@@ -993,30 +993,34 @@ function LoginPageContent() {
|
||||
};
|
||||
|
||||
/** The desktop build: the server's own sign-in page in the system browser. */
|
||||
// Spelled out, not IS_DESKTOP_BUILD: only this form keeps the route out of other
|
||||
// builds' bundles, which Lite's check on API strings relies on.
|
||||
const handleDesktopBrowserSignIn = async () => {
|
||||
const target = jmapEndpoint.trim();
|
||||
if (!target) {
|
||||
setDesktopNotice(tDesktop("server_needed"));
|
||||
return;
|
||||
if (process.env.NEXT_PUBLIC_BULWARK_DESKTOP === '1') {
|
||||
const target = jmapEndpoint.trim();
|
||||
if (!target) {
|
||||
setDesktopNotice(tDesktop("server_needed"));
|
||||
return;
|
||||
}
|
||||
setDesktopNotice(null);
|
||||
setDesktopWaiting(true);
|
||||
const prefix = getPathPrefix(params.locale as string);
|
||||
const outcome = await runDesktopBrowserSignIn({
|
||||
serverUrl: target,
|
||||
callbackPath: `${prefix}/${params.locale}/auth/callback`,
|
||||
slot: useAccountStore.getState().getNextCookieSlot(),
|
||||
remember: persistProviderSession,
|
||||
addAccount: isAddAccountMode,
|
||||
});
|
||||
if (outcome.kind === "callback") {
|
||||
window.location.href = outcome.callbackUrl;
|
||||
return;
|
||||
}
|
||||
setDesktopWaiting(false);
|
||||
if (outcome.kind === "unavailable") setDesktopNotice(tDesktop("browser_unavailable"));
|
||||
else if (outcome.kind === "declined") setDesktopNotice(tDesktop("server_declined"));
|
||||
else if (outcome.kind === "failed") setDesktopNotice(tDesktop("browser_failed"));
|
||||
}
|
||||
setDesktopNotice(null);
|
||||
setDesktopWaiting(true);
|
||||
const prefix = getPathPrefix(params.locale as string);
|
||||
const outcome = await runDesktopBrowserSignIn({
|
||||
serverUrl: target,
|
||||
callbackPath: `${prefix}/${params.locale}/auth/callback`,
|
||||
slot: useAccountStore.getState().getNextCookieSlot(),
|
||||
remember: persistProviderSession,
|
||||
addAccount: isAddAccountMode,
|
||||
});
|
||||
if (outcome.kind === "callback") {
|
||||
window.location.href = outcome.callbackUrl;
|
||||
return;
|
||||
}
|
||||
setDesktopWaiting(false);
|
||||
if (outcome.kind === "unavailable") setDesktopNotice(tDesktop("browser_unavailable"));
|
||||
else if (outcome.kind === "declined") setDesktopNotice(tDesktop("server_declined"));
|
||||
else if (outcome.kind === "failed") setDesktopNotice(tDesktop("browser_failed"));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
@@ -1858,7 +1862,7 @@ function LoginPageContent() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{IS_DESKTOP_BUILD && !hasServerList && browserSignInAvailable() && (
|
||||
{process.env.NEXT_PUBLIC_BULWARK_DESKTOP === "1" && !hasServerList && browserSignInAvailable() && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
||||
@@ -173,7 +173,8 @@ export async function POST(request: NextRequest) {
|
||||
resolvedServerId = matchedEntry.id;
|
||||
} else if (configuredServerUrl && !customRequested) {
|
||||
upstreamUrl = configuredServerUrl;
|
||||
} else if (IS_DESKTOP_BUILD && isDesktopTrustedServer(serverUrl)) {
|
||||
// Spelled out as in lib/admin/jmap-servers.ts, so other builds don't hold it.
|
||||
} else if (process.env.NEXT_PUBLIC_BULWARK_DESKTOP === '1' && isDesktopTrustedServer(serverUrl)) {
|
||||
upstreamUrl = serverUrl;
|
||||
} else if (allowCustomEndpoint) {
|
||||
if (!(await isPublicHttpUrl(serverUrl))) {
|
||||
|
||||
@@ -39,6 +39,13 @@ describe('the desktop trust path', () => {
|
||||
expect(guardStandalone(dir, { desktopBuild: false })).toEqual(['.next/server/chunks/123.js (desktop trust path in a server build)']);
|
||||
});
|
||||
|
||||
it('fails a server build that still reads the flag at runtime', () => {
|
||||
const dir = tempDir();
|
||||
put(dir, 'server.js');
|
||||
put(dir, '.next/server/chunks/9.js', 'let a="1"===process.env.NEXT_PUBLIC_BULWARK_DESKTOP');
|
||||
expect(guardStandalone(dir, { desktopBuild: false })).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('passes the desktop build itself', () => {
|
||||
const dir = tempDir();
|
||||
put(dir, 'server.js');
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* Multi-server JMAP support: schema, parsing, lookup, and redaction helpers.
|
||||
*/
|
||||
import { IS_DESKTOP_BUILD } from '@/lib/desktop/flag';
|
||||
import { isDesktopTrustedServer } from '@/lib/desktop/trusted-servers';
|
||||
|
||||
|
||||
@@ -226,9 +225,10 @@ export function resolveTrustedJmapUrl(
|
||||
}
|
||||
const matched = servers.find((s) => normalizeUrl(s.url) === target);
|
||||
if (matched) return matched.url;
|
||||
// A server the user approved in the desktop app. Compiled out of every other
|
||||
// build, so nothing at runtime can open this path on a server.
|
||||
if (IS_DESKTOP_BUILD && isDesktopTrustedServer(requestedUrl)) return trimUrl(requestedUrl);
|
||||
// A server the user approved in the desktop app. Spelled out, not IS_DESKTOP_BUILD:
|
||||
// only this form lets the bundler drop the branch and the module behind it, so
|
||||
// that no other build holds this path at all (scripts/standalone-guard.mjs checks).
|
||||
if (process.env.NEXT_PUBLIC_BULWARK_DESKTOP === '1' && isDesktopTrustedServer(requestedUrl)) return trimUrl(requestedUrl);
|
||||
// No match - caller decides whether to honor the request anyway (e.g. when
|
||||
// allowCustomJmapEndpoint is enabled).
|
||||
return null;
|
||||
|
||||
@@ -104,6 +104,14 @@ describe('the flag is fixed at build time', () => {
|
||||
expect(source).toContain("process.env.NEXT_PUBLIC_BULWARK_DESKTOP === '1'");
|
||||
});
|
||||
|
||||
it('the trust check uses the form the bundler can drop', () => {
|
||||
for (const file of ['lib/admin/jmap-servers.ts', 'app/api/auth/totp-token-exchange/route.ts']) {
|
||||
const source = readFileSync(path.join(ROOT, file), 'utf8');
|
||||
expect(source, file).toMatch(/process\.env\.NEXT_PUBLIC_BULWARK_DESKTOP === '1' && isDesktopTrustedServer/);
|
||||
expect(source, file).not.toMatch(/IS_DESKTOP_BUILD && isDesktopTrustedServer/);
|
||||
}
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
@@ -92,9 +92,10 @@ export type DesktopPluginInstallResult =
|
||||
export interface DesktopPluginsBridge {
|
||||
/**
|
||||
* Installs from the directory through the local admin API, with its signature
|
||||
* and hash checks. The renderer never holds an admin session. Standalone only.
|
||||
* and hash checks, after a native prompt. No version means the latest. The
|
||||
* renderer never holds an admin session. Standalone only.
|
||||
*/
|
||||
install: (slug: string, version: string) => Promise<DesktopPluginInstallResult>;
|
||||
install: (slug: string, version?: string) => Promise<DesktopPluginInstallResult>;
|
||||
}
|
||||
|
||||
export interface BulwarkDesktopBridge {
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
/** 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. */
|
||||
export const DESKTOP_FLAG_READ = 'NEXT_PUBLIC_BULWARK_DESKTOP';
|
||||
|
||||
export function desktopBuildFlag(env = process.env) {
|
||||
return env.NEXT_PUBLIC_BULWARK_DESKTOP === '1' ? '1' : '';
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
import { existsSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { DESKTOP_TRUST_MARKER, desktopBuildFlag } from './desktop-build-flag.mjs';
|
||||
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']);
|
||||
@@ -154,8 +154,11 @@ export function findDesktopTrustPath(standaloneDir) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name !== 'node_modules') visit(full);
|
||||
} else if (entry.name.endsWith('.js') && readFileSync(full, 'utf8').includes(DESKTOP_TRUST_MARKER)) {
|
||||
found.push(`${toPosix(path.relative(standaloneDir, full))} (desktop trust path in a server build)`);
|
||||
} else if (entry.name.endsWith('.js')) {
|
||||
const text = readFileSync(full, 'utf8');
|
||||
if (text.includes(DESKTOP_TRUST_MARKER) || text.includes(DESKTOP_FLAG_READ)) {
|
||||
found.push(`${toPosix(path.relative(standaloneDir, full))} (desktop trust path in a server build)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user