sc-site-sync/index.js
2026-10-19 01:25:16 -05:00

69 lines
2.8 KiB
JavaScript

// site-sync: Saltcorn plugin for one-way DESIRED-STATE synchronization.
//
// Snapshot a source site's current design state (via Saltcorn's own
// backup.create_pack_json), diff it against a target, preview a plan, and apply
// the difference so the target converges to the source. No edit log. User data is
// protected by per-table data modes; the users table is hard-locked.
const { PLUGIN_NAME, PLUGIN_VERSION } = require("./lib/constants");
const { createAllTables } = require("./lib/schema");
const { initEnvIfMissing } = require("./lib/env");
const { createDashboardWorkflow } = require("./lib/common/dashboardWorkflow");
const { ADMIN_URLS } = require("./lib/render");
const { routes } = require("./lib/routes");
const { API_PREFIX } = require("./lib/transport");
const mcpTools = require("./lib/mcpTools");
const log = (msg) => {
// eslint-disable-next-line no-console
console.log(`[${PLUGIN_NAME}] ${msg}`);
};
// Register the machine-API prefix in Saltcorn's disable_csrf_routes so peers can
// POST to it with the custom Content-Type. Belt-and-suspenders: the api routes
// ALSO carry noCsrf:true on the route object.
const ensureCsrfBypass = async () => {
try {
const { getState } = require("@saltcorn/data/db/state");
const current = getState().getConfig("disable_csrf_routes", "");
const entries = current.split(",").map((s) => s.trim()).filter(Boolean);
if (!entries.includes(API_PREFIX)) {
entries.push(API_PREFIX);
await getState().setConfig("disable_csrf_routes", entries.join(","));
}
} catch (e) {
// eslint-disable-next-line no-console
console.error(`[${PLUGIN_NAME}] failed to register csrf bypass:`, e);
}
};
const onLoad = async (cfg) => {
try {
await createAllTables();
const env = await initEnvIfMissing();
await ensureCsrfBypass();
log(`v${PLUGIN_VERSION} loaded env_id=${env.env_id}`);
} catch (err) {
// eslint-disable-next-line no-console
console.error(`[${PLUGIN_NAME}] onLoad failed:`, err);
throw err;
}
};
module.exports = {
sc_plugin_api_version: 1,
plugin_name: PLUGIN_NAME,
// The plugin gear (Settings -> Plugins) bounces straight to the dashboard.
configuration_workflow: () => createDashboardWorkflow({ name: PLUGIN_NAME, url: ADMIN_URLS.DASHBOARD }),
onLoad: onLoad,
// With configuration_workflow present, Saltcorn invokes capability keys as
// (cfg)=>value (state.ts withCfg), so routes must be a function, not the array.
routes: () => routes,
// Tools for the saltcorn-mcp server (its docs/pluginTools.md): an MCP client
// drives site-sync as an admin drives the UI (lib/mcpTools.js).
mcp_tools: () => mcpTools.tools()
};