feat: dispatch plugin HTTP routes and wire the runtime into startup

ctx.http.route now registers a real router on the /plugin-api/:pluginId
dispatcher, replacing its stub 404. A plugin never touches the Express
req/res: it gets a plain summary and returns a plain object, so sockets
and session cookies stay on this side of the boundary. Authorization and
cookie headers are not forwarded.

Plugins load last in the start-up sequence, after the server is fully
wired, and are terminated before the database on shutdown so none can
outlive it. A plugin that fails to load cannot stop the backend.

fix: correct the nginx plugin-api location regex

The pattern was ^/plugin-api/(/.*)?$, which needs a double slash and so
never matched /plugin-api/<id>/<route>. Every other block in the file
uses ^/prefix(/.*)?$. Left alone, plugin routes would work in dev and
404 only behind Docker.
This commit is contained in:
LukeGus committed 2026-09-21 07:19:02 -05:00
1 parent 4311a0e98a
commit 4e1dec8435
7 files changed
+695 -5

No files matched your search

+27
View File
@@ -343,6 +343,23 @@ async function provisionLocalDesktopUserIfNeeded(): Promise<void> {
await import("./automations/scheduler.js");
startAutomationScheduler();
// Last, so a plugin's activate() sees a fully wired server. A plugin that
// fails to load must not stop the backend, so this never rejects.
try {
const { initializePlugins } = await import("./plugins/index.js");
const loaded = await initializePlugins();
if (loaded.length > 0) {
systemLogger.info(`Loaded ${loaded.length} plugin(s)`, {
operation: "plugin_init",
});
}
} catch (error) {
systemLogger.warn("Plugin runtime failed to initialize", {
operation: "plugin_init",
error: getErrorMessage(error),
});
}
const { startAnalyticsHeartbeat } = await import("./utils/analytics.js");
startAnalyticsHeartbeat();
@@ -357,6 +374,16 @@ async function provisionLocalDesktopUserIfNeeded(): Promise<void> {
systemLogger.info(`Received ${signal}, initiating graceful shutdown...`, {
operation: "shutdown",
});
// Terminate plugin workers before the database goes away, so a plugin
// mid-write cannot outlive it.
try {
const { shutdownPlugins } = await import("./plugins/index.js");
await shutdownPlugins();
} catch {
// Nothing to stop.
}
// Only SQLite has anything to flush. On a client-server engine the writes
// committed as they happened, so there is no file to save and claiming
// otherwise in the log would be untrue.