// 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() };