feat: resolve deep links to the local instance

This commit is contained in:
Linus Rath
2026-09-20 19:59:37 +02:00
parent c6bb4aace9
commit 77ef60caae
13 changed files with 797 additions and 5 deletions
+38
View File
@@ -155,6 +155,41 @@ export function buildLiteConfig(env = {}, { target = "static" } = {}) {
}
/** policy.json: only the gates Lite must pin; the app merges the rest over its defaults. */
/**
* What `/api/connector/capabilities` answers on a server build, as a file.
*
* Lite is a static export with no route handlers, so a connector probing this
* instance gets this instead - same document, same meaning. The connector
* tries the API path first and falls back to `/connector.json`.
*
* The target names are read out of lib/connector/registry.ts rather than
* duplicated here: a list that drifts would have this build telling connectors
* it can resolve links it cannot.
*/
export function buildConnectorCapabilities({ appName, version, basePath = "" }) {
const source = readFileSync(
join(resolveRepoRoot(import.meta.url), "lib", "connector", "registry.ts"),
"utf8",
);
const targets = [...source.matchAll(/^\s{4}name: '([a-z0-9_]+)',$/gm)].map((m) => m[1]);
if (targets.length === 0) {
throw new Error("no connector targets found in lib/connector/registry.ts");
}
return (
JSON.stringify(
{
product: "bulwark-webmail",
appName: appName || "Bulwark",
version: version || null,
connectorPath: `${basePath || ""}/connector`,
targets,
},
null,
2,
) + "\n"
);
}
export function buildLitePolicy() {
return {
_comment: "Optional admin policy for Bulwark Lite (same shape as the admin dashboard's policy). Plugins and sidebar apps stay off in Lite.",
@@ -331,6 +366,9 @@ export function buildHeaders({ basePath = "", connectSrc = "*" }) {
Content-Security-Policy: ${csp}
${basePath || ""}/_next/static/*
Cache-Control: public, max-age=31536000, immutable
${basePath || ""}/connector.json
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=300
`;
}
+4 -1
View File
@@ -11,7 +11,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node
import { join } from "node:path";
import {
STALWART_EXCLUDED_DIRS, STALWART_EXCLUDED_FILES, STALWART_ZIP_NAME,
buildCaddyExample, buildHeaders, buildLiteConfig, buildLitePolicy, buildManifest, buildNginxExample, buildNotFoundShim, buildReadme, buildRedirects, buildRootRedirect,
buildCaddyExample, buildConnectorCapabilities, buildHeaders, buildLiteConfig, buildLitePolicy, buildManifest, buildNginxExample, buildNotFoundShim, buildReadme, buildRedirects, buildRootRedirect,
buildStalwartEntry, buildStalwartManifest, buildStalwartReadme, buildZip, collectZipEntries, discoverBuiltLocales, discoverShellDirs, findSegmentPrefetchDirs, isMainModule, makeBuildId, normalizeBasePath, parseLiteTarget, resolveRepoRoot,
} from "./lib.mjs";
@@ -64,6 +64,9 @@ function staticFiles({ config, basePath, locales, defaultLocale, version, commit
return {
"config.json": JSON.stringify(config, null, 2) + "\n",
"policy.json": JSON.stringify(buildLitePolicy(), null, 2) + "\n",
// The file form of /api/connector/capabilities - a static export has no
// route handlers, and a connector probing this instance needs an answer.
"connector.json": buildConnectorCapabilities({ appName: config.appName, version, basePath }),
"manifest.webmanifest": JSON.stringify(buildManifest({ appName: config.appName, basePath }), null, 2) + "\n",
"index.html": buildRootRedirect({ basePath, locales, defaultLocale }),
// Replaces Next's default not-found page: only this shim replays deep links
+4 -1
View File
@@ -45,7 +45,10 @@ export function verifyExport({ root = repoRoot, target, safeBundleBytes = STALWA
const required = resolvedTarget === "stalwart"
? ["index.html", "config.json", "policy.json", "manifest.json", "LITE-README.md", "lite-build.json"]
: ["index.html", "404.html", "config.json", "policy.json", "manifest.webmanifest", "_redirects", "_headers", "LITE-README.md", "lite-build.json"];
// connector.json is the static target only: the Stalwart bundle is served
// without custom headers, so a connector's cross-origin probe could not
// read it anyway (it falls back to adding the instance unverified).
: ["index.html", "404.html", "config.json", "policy.json", "connector.json", "manifest.webmanifest", "_redirects", "_headers", "LITE-README.md", "lite-build.json"];
for (const file of required) {
if (!existsSync(join(outDir, file))) problems.push(`missing out/${file}`);
}