Adds integration/tests/12-electron-mail-index.spec.ts (3 tests, all passing
against the real Stalwart fixture) and fixes what running it exposed. None of
these were visible from reading the code.
1. JMAP session fetch never followed a redirect. Stalwart 307-redirects
/.well-known/jmap to /jmap/session, and fetchJmapSession used
`redirect: 'manual'` and treated any non-2xx as failure - so every reindex
died with "JMAP session fetch failed (307)". Now follows up to 3 hops and
REFUSES to follow off-origin, because the user's credentials ride on every
hop; a blind `redirect: 'follow'` would hand the Authorization header to
whatever host a misconfigured session pointed at. Same bound and same
reasoning as lib/auth/verify-jmap-auth.ts.
2. The fd-3 key channel could only be adopted once per process, but its state
was module-scoped. Next re-evaluates route modules, so a second instance hit
`Could not open fd 3: Error: open EEXIST` from libuv. State moved to a
Symbol on globalThis - the one place in a Node process that survives module
re-evaluation.
3. Next's output file tracing does NOT carry @signalapp/sqlcipher's prebuilds/
into .next/standalone. It traced the package's JS and its node-gyp-build
dependency, but node-gyp-build resolves the .node binary by scanning a
directory at runtime, which no static tracer can follow - so `require()`
would have failed in every packaged build. scripts/assemble-standalone.mjs
now copies it, alongside the public/ and .next/static copies it already does
for the same "standalone output omits things" reason. All six platform/arch
prebuilds are copied, not just this host's, because electron-builder
cross-builds the x64 and arm64 macOS targets from one runner.
The three tests, and why it takes three - two constraints made a single
configuration impossible, and both were measured rather than assumed:
* The renderer cannot reach this fixture from a production build. Its CSP
pins connect-src to `'self' https: wss:` and the fixture's Stalwart is
plain HTTP. NODE_ENV=development at RUNTIME does not help: `next build`
INLINES process.env.NODE_ENV into the compiled middleware, so proxy.ts's
`isDev` is frozen at build time (observed: a standalone server started with
NODE_ENV=development still served the production CSP).
* The fd-3 channel cannot survive `next dev`, which forks its server with an
IPC channel that claims fd 3 (EEXIST); fd 4 there is not a pipe either
(ENOTTY).
So: PIPELINE drives the real standalone server over HTTP from Node with a
real fd-3 key channel (no browser, so no CSP) and asserts a real SMTP
delivery is findable by a word from its BODY, with a real snippet and
contextBlock, idempotent catch-up, working type filters, and - reading the
raw bytes of the .db AND its -wal - that nothing is recoverable in cleartext.
TRIGGER proves the event-driven wiring: a real delivery makes the renderer
POST /api/offline/reindex off its live push. WIRING launches the real shell
with no ELECTRON_LOAD_URL and asserts the routes are reachable (401, not 404
or 503) with real safeStorage behind them.
Each test now gets its own --user-data-dir. That is load-bearing, not hygiene:
Electron reuses one profile across launches, and a leftover jmap_stalwart_ctx
cookie from an earlier run made the WIRING test's 401 assertion pass as a 200.
Verified: typecheck clean; unit suite 2379 tests with the SAME 3 pre-existing
failures as the base commit b15098a6 (2 builtin-themes, 1 jmap-client-
resilience) and 48 net new passing; both `docker build`s succeed; the
hosted-deployment gate returns 404 with an empty body and materialises no file
in the production image; e2e/electron-smoke 4/4; 11-electron-notification
still passes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
62 lines
2.9 KiB
JavaScript
62 lines
2.9 KiB
JavaScript
#!/usr/bin/env node
|
|
// `next build --webpack` (see next.config.ts's `output: "standalone"`)
|
|
// emits .next/standalone/server.js but - deliberately, per Next's own docs -
|
|
// leaves out public/ and .next/static/. The Dockerfile copies both in by
|
|
// hand for the container image; this does the same thing for local Electron
|
|
// dev and packaging, so every path boots the exact same artifact.
|
|
import { cpSync, existsSync, rmSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const rootDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
const standaloneDir = path.join(rootDir, ".next", "standalone");
|
|
|
|
if (!existsSync(standaloneDir)) {
|
|
console.error(`Missing ${standaloneDir} - run "next build --webpack" first.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const publicSrc = path.join(rootDir, "public");
|
|
const publicDest = path.join(standaloneDir, "public");
|
|
rmSync(publicDest, { recursive: true, force: true });
|
|
cpSync(publicSrc, publicDest, { recursive: true });
|
|
|
|
const staticSrc = path.join(rootDir, ".next", "static");
|
|
const staticDest = path.join(standaloneDir, ".next", "static");
|
|
rmSync(staticDest, { recursive: true, force: true });
|
|
cpSync(staticSrc, staticDest, { recursive: true });
|
|
|
|
// The native SQLCipher prebuilds for the local search index (lib/mail-index/**).
|
|
//
|
|
// Next's output file tracing DOES pick up @signalapp/sqlcipher's JS
|
|
// (package.json + dist/index.cjs) and its node-gyp-build dependency, but NOT
|
|
// the prebuilds/ directory holding the actual .node binaries - node-gyp-build
|
|
// resolves those by scanning the directory at runtime, which no static tracer
|
|
// can follow. Verified by inspecting a real `build:standalone` output: the
|
|
// package was present, `prebuilds/` was absent, so `require()` would have
|
|
// failed at runtime in every packaged build.
|
|
//
|
|
// Copying the WHOLE prebuilds directory (all six platform/arch pairs, ~11 MB)
|
|
// rather than just this host's is deliberate: electron-builder cross-builds the
|
|
// x64 and arm64 macOS targets from one runner (electron-builder.config.js), so
|
|
// the artifact has to contain a prebuild for an arch this machine isn't.
|
|
//
|
|
// Skipped silently when absent - the package is an OPTIONAL dependency and is
|
|
// legitimately missing on musl/Alpine, where both Dockerfiles build.
|
|
const sqlcipherSrc = path.join(rootDir, "node_modules", "@signalapp", "sqlcipher", "prebuilds");
|
|
if (existsSync(sqlcipherSrc)) {
|
|
const sqlcipherDest = path.join(
|
|
standaloneDir, "node_modules", "@signalapp", "sqlcipher", "prebuilds",
|
|
);
|
|
rmSync(sqlcipherDest, { recursive: true, force: true });
|
|
cpSync(sqlcipherSrc, sqlcipherDest, { recursive: true });
|
|
console.log("Copied @signalapp/sqlcipher prebuilds into the standalone output");
|
|
} else {
|
|
console.log(
|
|
"@signalapp/sqlcipher not installed (optional dependency) - " +
|
|
"the encrypted local index will be disabled at runtime",
|
|
);
|
|
}
|
|
|
|
console.log("Assembled standalone server at", standaloneDir);
|