feat: move acme certificate automation into a plugin (C7)

This commit is contained in:
LukeGus committed 2026-09-24 21:19:27 -05:00
1 parent 4dbce08379
commit 8c1e02af89
123 files changed
+4808 -1912

No files matched your search

+129
View File
@@ -0,0 +1,129 @@
/**
* ctx.system: the server certificate. Core serves it; a plugin under
* system:tls may replace it, reload it, answer ACME http-01 challenges and
* say that it renews it. Every call is audited.
*/
import type { PluginSystem } from "@termix/plugin-sdk/backend";
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
import { assertCapability } from "./permissions.js";
import type { DisposableBag } from "./disposables.js";
type AuditFn = (
action: string,
details: string,
outcome: { success: boolean; errorMessage?: string },
) => Promise<void>;
interface Deps {
manifest: PluginManifest;
bag: DisposableBag;
audit: AuditFn;
}
const CAPABILITY = "system:tls";
async function audited<T>(
deps: Deps,
action: string,
details: string,
fn: () => Promise<T>,
): Promise<T> {
try {
await assertCapability(
deps.manifest.id,
CAPABILITY,
deps.manifest.capabilities,
);
const result = await fn();
await deps.audit(action, details, { success: true });
return result;
} catch (error) {
await deps.audit(action, details, {
success: false,
errorMessage: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/**
* Puts an undo in the plugin's bag and hands back one that also takes it out
* again, so it runs once whether the plugin or deactivate gets there first.
*/
function tracked(deps: Deps, undo: () => void, label: string): () => void {
let done = false;
const run = () => {
if (done) return;
done = true;
undo();
};
const untrack = deps.bag.add(run, label);
return () => {
untrack();
run();
};
}
export function createPluginSystem(deps: Deps): PluginSystem {
return {
tlsStatus: () =>
audited(deps, "tls_status", "read certificate status", async () => {
const { getTlsStatus } = await import("../tls/tls-service.js");
return getTlsStatus();
}),
writeTlsCertificate: (certificatePem, privateKeyPem) =>
audited(
deps,
"tls_write",
"replaced the server certificate",
async () => {
const { writeTlsCertificate } = await import("../tls/tls-service.js");
return writeTlsCertificate(certificatePem, privateKeyPem);
},
),
reloadTls: () =>
audited(
deps,
"tls_reload",
"reloaded the server certificate",
async () => {
const { reloadTls } = await import("../tls/tls-service.js");
return reloadTls();
},
),
publishHttpChallenge: (token, content) =>
audited(
deps,
"tls_challenge",
"published an ACME challenge",
async () => {
const { publishChallenge } =
await import("../tls/acme-challenges.js");
return tracked(
deps,
publishChallenge(token, content),
"acme challenge",
);
},
),
registerTlsRenewer: () =>
audited(
deps,
"tls_renewer",
"renews the server certificate",
async () => {
const { registerTlsRenewer } = await import("../tls/tls-service.js");
const undo = registerTlsRenewer({
pluginId: deps.manifest.id,
pluginName: deps.manifest.name,
});
return tracked(deps, undo, "tls renewer");
},
),
};
}
+2
View File
@@ -57,6 +57,7 @@ import { createPluginHosts } from "./ctx-hosts.js";
import { createPluginSchedule } from "./schedule.js";
import { createPluginFetch, createPluginNotify } from "./ctx-notify.js";
import { createPluginProcess } from "./ctx-process.js";
import { createPluginSystem } from "./ctx-system.js";
import { createPluginCredentials } from "./ctx-credentials.js";
import { isElectronIpcAvailable } from "../utils/electron-ipc-bridge.js";
@@ -828,6 +829,7 @@ export function createPluginContext(
audit: auditCall,
}),
auth: createPluginAuth({ manifest, bag: handle.bag, audit: auditCall }),
system: createPluginSystem({ manifest, bag: handle.bag, audit: auditCall }),
desktop: {
openIsolatedWindow: (request) => desktopOpenIsolatedWindow(request),