Merge branch 'claude/activate-smime-plugin' into 'dev'
feat(smime): actually install the audited S/MIME plugin in real builds See merge request gitlab-instance-b9b5cf2f/vncmail-plus!5
This commit is contained in:
@@ -58,4 +58,27 @@ if (existsSync(sqlcipherSrc)) {
|
||||
);
|
||||
}
|
||||
|
||||
// The staged first-party plugin bundles (scripts/build-plugins.mjs). The
|
||||
// server installs these into its plugin registry at startup
|
||||
// (lib/admin/bundled-plugins.ts), reading them from
|
||||
// `<cwd>/vnc/plugins/build` - and Next's generated server.js chdir's to its
|
||||
// own directory, so "cwd" is this standalone dir in every packaged build.
|
||||
//
|
||||
// Output file tracing cannot find these: nothing imports them, they are read
|
||||
// by path at runtime. Without this copy the Electron/standalone build boots
|
||||
// with no S/MIME plugin at all while the policy toggle still says it is on -
|
||||
// the same silent-drop failure mode as the sqlcipher prebuilds above.
|
||||
const pluginsSrc = path.join(rootDir, "vnc", "plugins", "build");
|
||||
if (existsSync(pluginsSrc)) {
|
||||
const pluginsDest = path.join(standaloneDir, "vnc", "plugins", "build");
|
||||
rmSync(pluginsDest, { recursive: true, force: true });
|
||||
cpSync(pluginsSrc, pluginsDest, { recursive: true });
|
||||
console.log("Copied bundled first-party plugins into the standalone output");
|
||||
} else {
|
||||
console.warn(
|
||||
"No bundled plugins staged at vnc/plugins/build - " +
|
||||
'run "npm run build:plugins" first, or the packaged app ships without S/MIME',
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Assembled standalone server at", standaloneDir);
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env node
|
||||
// Builds the FIRST-PARTY plugins that ship with this fork (vnc/plugins/*) and
|
||||
// stages them where the running server can install them.
|
||||
//
|
||||
// Why this exists
|
||||
// ---------------
|
||||
// `vnc/plugins/smime` is audited SOURCE, not a prebuilt drop (upstream's own
|
||||
// smime.zip was deliberately distrusted). Nothing built it in any real build,
|
||||
// so the S/MIME policy toggle was on while no plugin existed. This script is
|
||||
// the missing build step; `lib/admin/bundled-plugins.ts` is the matching
|
||||
// install step that runs at server startup.
|
||||
//
|
||||
// Output layout (gitignored, produced not committed):
|
||||
//
|
||||
// vnc/plugins/build/<id>/manifest.json
|
||||
// vnc/plugins/build/<id>/<entrypoint> e.g. index.js
|
||||
//
|
||||
// That directory is copied into the container image (Dockerfile) and into
|
||||
// .next/standalone (scripts/assemble-standalone.mjs), so every distribution
|
||||
// path - container, Electron, local dev - boots with the same artifact.
|
||||
//
|
||||
// Each plugin keeps its OWN package.json + lockfile so its (crypto) deps stay
|
||||
// pinned by the audit, rather than being restated in the app's root manifest.
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import {
|
||||
cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync,
|
||||
} from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const rootDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
||||
const pluginsDir = path.join(rootDir, "vnc", "plugins");
|
||||
const outDir = path.join(pluginsDir, "build");
|
||||
|
||||
// Mirrors MAX_PLUGIN_SIZE in lib/plugin-types.ts - the cap the admin upload
|
||||
// route enforces. A first-party plugin must live inside the same budget.
|
||||
const MAX_BUNDLE_BYTES = 5 * 1024 * 1024;
|
||||
const ID_RE = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
||||
|
||||
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
|
||||
function run(cmd, args, cwd) {
|
||||
execFileSync(cmd, args, { cwd, stdio: "inherit" });
|
||||
}
|
||||
|
||||
function listPluginDirs() {
|
||||
if (!existsSync(pluginsDir)) return [];
|
||||
return readdirSync(pluginsDir)
|
||||
.filter((name) => name !== "build" && !name.startsWith("."))
|
||||
.map((name) => path.join(pluginsDir, name))
|
||||
.filter((dir) => statSync(dir).isDirectory())
|
||||
.filter((dir) => existsSync(path.join(dir, "manifest.json")));
|
||||
}
|
||||
|
||||
function buildOne(pluginDir) {
|
||||
const manifestPath = path.join(pluginDir, "manifest.json");
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
||||
const id = manifest.id;
|
||||
if (typeof id !== "string" || !ID_RE.test(id)) {
|
||||
throw new Error(`${manifestPath}: invalid plugin id ${JSON.stringify(id)}`);
|
||||
}
|
||||
const entrypoint =
|
||||
typeof manifest.entrypoint === "string" ? manifest.entrypoint : "index.js";
|
||||
if (entrypoint.includes("/") || entrypoint.includes("\\")) {
|
||||
throw new Error(`${manifestPath}: entrypoint must be a bare filename`);
|
||||
}
|
||||
|
||||
const pkgPath = path.join(pluginDir, "package.json");
|
||||
if (!existsSync(pkgPath)) {
|
||||
throw new Error(`${pluginDir}: no package.json, cannot build`);
|
||||
}
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
||||
if (!pkg.scripts?.build) {
|
||||
throw new Error(`${pkgPath}: no "build" script`);
|
||||
}
|
||||
|
||||
// Install the plugin's own deps only when they're missing. Keeps repeat
|
||||
// builds (and `npm run dev`) fast - the actual esbuild step is ~20ms.
|
||||
if (!existsSync(path.join(pluginDir, "node_modules"))) {
|
||||
const hasLock = existsSync(path.join(pluginDir, "package-lock.json"));
|
||||
console.log(`[build-plugins] installing deps for ${id}`);
|
||||
run(npm, hasLock ? ["ci"] : ["install", "--no-audit", "--no-fund"], pluginDir);
|
||||
}
|
||||
|
||||
console.log(`[build-plugins] building ${id}`);
|
||||
run(npm, ["run", "build"], pluginDir);
|
||||
|
||||
const built = path.join(pluginDir, "dist", entrypoint);
|
||||
if (!existsSync(built)) {
|
||||
throw new Error(`${id}: build produced no ${path.relative(rootDir, built)}`);
|
||||
}
|
||||
const size = statSync(built).size;
|
||||
if (size > MAX_BUNDLE_BYTES) {
|
||||
throw new Error(
|
||||
`${id}: bundle is ${(size / 1024 / 1024).toFixed(2)} MB, over the ` +
|
||||
`${MAX_BUNDLE_BYTES / 1024 / 1024} MB plugin limit`,
|
||||
);
|
||||
}
|
||||
|
||||
const stageDir = path.join(outDir, id);
|
||||
rmSync(stageDir, { recursive: true, force: true });
|
||||
mkdirSync(stageDir, { recursive: true });
|
||||
cpSync(manifestPath, path.join(stageDir, "manifest.json"));
|
||||
cpSync(built, path.join(stageDir, entrypoint));
|
||||
|
||||
console.log(
|
||||
`[build-plugins] staged ${id} v${manifest.version} ` +
|
||||
`(${(size / 1024).toFixed(0)} KB) -> ${path.relative(rootDir, stageDir)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const dirs = listPluginDirs();
|
||||
if (dirs.length === 0) {
|
||||
console.log("[build-plugins] no first-party plugins found under vnc/plugins");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Rebuild the staging root from scratch so a plugin removed from the tree does
|
||||
// not linger as a stale bundle that the server would happily keep installing.
|
||||
rmSync(outDir, { recursive: true, force: true });
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
|
||||
for (const dir of dirs) buildOne(dir);
|
||||
console.log(`[build-plugins] done (${dirs.length} plugin(s))`);
|
||||
Reference in New Issue
Block a user