// HTTP routes for site-sync: the single route-registration hub. The admin // handlers are thin adapters over operations.js (shared with the MCP tools, so the // rules live there); the heavy lifting lives in plan.js / apply.js / rollback.js / // rows.js; this module holds the handlers, their rendering and the route table. // // Admin UI (session admin of THIS tenant; POSTs are CSRF-protected): // GET /admin/site-sync/ dashboard + settings // POST /admin/site-sync/settings // GET /admin/site-sync/peers peers, pairing, secret replacement // POST /admin/site-sync/peers/add // POST /admin/site-sync/peers/rotate // POST /admin/site-sync/peers/delete // GET /admin/site-sync/plan choose a source peer // POST /admin/site-sync/plan pull the peer's state + preview (persists a plan) // POST /admin/site-sync/apply // POST /admin/site-sync/rollback // GET /admin/site-sync/tables reference-data modes // POST /admin/site-sync/tables/set // // Machine API (HMAC peer auth, CSRF-exempt, every response signed): // GET /site-sync/api/health // GET /site-sync/api/state // GET /site-sync/api/file/:hash // POST /site-sync/api/plan // POST /site-sync/api/apply const fs = require("fs"); const db = require("@saltcorn/data/db"); const { toAbsolutePath } = require("./common/fileStore"); const { HTTP_STATUS } = require("./common/httpStatus"); const { localPluginList } = require("./common/pluginParity"); const { PLUGIN_NAME, PLUGIN_VERSION, APPLY_POLICY, DATA_MODES } = require("./constants"); const { requireEnv } = require("./env"); const peers = require("./peers"); const { requirePeerAuth, sendSigned, sendSignedJson } = require("./peerAuth"); const { API_PATHS, PEER_LIMITS } = require("./transport"); const { buildDesiredState } = require("./capture"); const planLib = require("./plan"); const rowsLib = require("./rows"); const diff = require("./diff"); const ops = require("./operations"); const { LOCK_STATES } = require("./siteLock"); const { ADMIN_URLS, requireAdmin, escapeHtml, csrfField, adminPage, flashRedirect, mkTable, post_btn, renderForm, link, Form, p, code, pre, span, strong } = require("./render"); // A content hash in a file URL: lowercase sha256 hex. const HASH_RE = /^[0-9a-f]{64}$/; // The only plan direction a remote peer may apply (pull/rollback plans are local). const PUSH_DIRECTION = "push"; // Comma-separated (or repeated) form field -> trimmed non-empty strings. const parseList = (raw) => { if (Array.isArray(raw)) { return raw.map((s) => String(s).trim()).filter(Boolean); } return String(raw || "").split(",").map((s) => s.trim()).filter(Boolean); }; // ---------------- Admin dashboard ---------------- // The site lock's state (operations.applyLockView) as a dashboard value. const lockBadge = (lock) => { if (lock.state === LOCK_STATES.RUNNING) { const h = lock.holder; return span({ class: "badge bg-primary" }, escapeHtml(`${h.operation} of plan ${h.plan_id} running since ${h.acquired_at}`)); } if (lock.state === LOCK_STATES.INTERRUPTED) { return span({ class: "badge bg-danger" }, "interrupted (see below)"); } return span({ class: "text-muted" }, "idle"); }; // A card for the plans an apply or rollback left in flight when the process running // it stopped (none: no card): roll each back from its restore point. const interruptedCards = (lock, csrf) => { if (lock.state !== LOCK_STATES.INTERRUPTED) { return []; } const items = lock.plans.map((pl) => { return p("Plan ", code(escapeHtml(pl.plan_id)), ` (${escapeHtml(pl.direction)}) was left ${escapeHtml(pl.status)}.`) + (pl.has_restore_point ? rollbackForm(pl.plan_id, csrf) : p({ class: "text-muted" }, "It has no restore point; the next apply or rollback releases it.")); }); return [{ type: "card", title: "Interrupted apply or rollback", contents: `
The process running an apply or rollback stopped part-way, so this site may be partly changed. ` + `Roll the plan back to its restore point, or apply it again; either recovers it.
` + items.join("") }]; }; const dashboard = async (req, res) => { if (!requireAdmin(req, res)) { return; } const st = await ops.status(); const env = st.env; const syncedCount = Object.values(st.syncedTables).reduce((a, n) => a + n, 0); const envRows = [ { k: "Env ID", v: code(escapeHtml(env.env_id)) }, { k: "Label", v: env.env_label ? escapeHtml(env.env_label) : span({ class: "text-muted" }, "(unset)") }, { k: "Sync mode", v: span({ class: "badge bg-info" }, escapeHtml(env.sync_mode)) }, { k: "Delete policy", v: span({ class: "badge bg-warning text-dark" }, escapeHtml(env.on_delete)) }, { k: "Apply policy", v: span({ class: "badge bg-secondary" }, escapeHtml(env.on_apply)) }, { k: "Require TLS", v: env.require_tls ? "yes" : "no" }, { k: "Apply / rollback", v: lockBadge(st.applyLock) }, { k: "Peers configured", v: escapeHtml(st.peerCount) }, { k: "Non-user tables", v: escapeHtml(syncedCount) }, { k: "Plugin version", v: escapeHtml(st.version) }, ]; const kv = mkTable([{ label: "Setting", key: "k" }, { label: "Value", key: (r) => r.v }], envRows); const settingsForm = new Form({ action: ADMIN_URLS.SETTINGS, submitLabel: "Save settings", fields: [ { name: "env_label", label: "Label", type: "String", default: env.env_label || "", attributes: { placeholder: "dev, test, prod" } }, { name: "sync_mode", label: "Sync mode", input_type: "select", default: env.sync_mode, options: ops.SETTING_OPTIONS.sync_mode }, { name: "on_delete", label: "Data-bearing drop policy", input_type: "select", default: env.on_delete, options: ops.SETTING_OPTIONS.on_delete }, { name: "on_apply", label: "Remote apply policy", input_type: "select", default: env.on_apply, options: ops.SETTING_OPTIONS.on_apply }, { name: "require_tls", label: "Require TLS for inbound peer traffic", type: "Bool", default: !!env.require_tls }, ], }); adminPage(req, res, "dashboard", ...interruptedCards(st.applyLock, csrfField(req)), { type: "card", title: "Environment", contents: kv }, { type: "card", title: "Settings", contents: renderForm(settingsForm, req.csrfToken()) } ); }; const saveSettings = async (req, res) => { if (!requireAdmin(req, res)) { return; } try { // The whole form: a blank label clears it, an unticked box turns TLS off. await ops.updateSettings({ env_label: req.body.env_label || "", sync_mode: req.body.sync_mode, on_delete: req.body.on_delete, on_apply: req.body.on_apply, require_tls: !!req.body.require_tls }); flashRedirect(req, res, ADMIN_URLS.DASHBOARD, "success", "settings saved"); } catch (err) { flashRedirect(req, res, ADMIN_URLS.DASHBOARD, "danger", err.message); } }; // ---------------- Peers ---------------- const peerOptions = (list) => list.map((peer) => ({ label: peer.label || peer.env_id, value: String(peer.peer_id) })); const peersView = async (req, res) => { if (!requireAdmin(req, res)) { return; } const env = await requireEnv(); const list = await peers.listPeers(); const actBtn = (url, label, cls, body, confirm) => post_btn(url, label, req.csrfToken(), { req, btnClass: cls, small: true, formClass: "d-inline me-1", body: body, confirm: confirm }); const peerTable = mkTable([ { label: "id", key: "peer_id" }, { label: "label", key: (peer) => escapeHtml(peer.label || "(unset)") }, { label: "env_id", key: (peer) => code(escapeHtml(peer.env_id)) }, { label: "base_url", key: (peer) => escapeHtml(peer.base_url) }, { label: "last seen", key: (peer) => escapeHtml(peer.last_seen_at || "never") }, { label: "actions", key: (peer) => actBtn(ADMIN_URLS.PLAN, "Plan", "btn-primary", { peer: peer.peer_id }, false) + actBtn(ADMIN_URLS.PEERS_ROTATE, "Rotate", "btn-warning", { peer_id: peer.peer_id }, true) + actBtn(ADMIN_URLS.PEERS_DELETE, "Delete", "btn-danger", { peer_id: peer.peer_id }, true), }, ], list); const addForm = new Form({ action: ADMIN_URLS.PEERS_ADD, submitLabel: "Pair", blurb: "A peer is a site-sync instance you can pull design state FROM (or push TO). Leave the shared secret blank to generate one (shown once after submit); paste the same secret in the peer's own pairing form.", fields: [ { name: "env_id", label: "Peer env_id", type: "String", required: true }, { name: "label", label: "Label", type: "String", attributes: { placeholder: "dev, prod, etc." } }, { name: "base_url", label: "Base URL", type: "String", required: true, attributes: { placeholder: "https://dev.example.com" } }, { name: "require_tls", label: "Require TLS", type: "Bool" }, { name: "existing_secret", label: "Existing secret (hex)", type: "String", attributes: { placeholder: "64 hex characters" } }, ], }); const replaceForm = new Form({ action: ADMIN_URLS.PEERS_ROTATE, submitLabel: "Replace secret", blurb: "After the other instance rotated this pairing's secret, paste the new secret it displayed here. Leave it blank to generate a new secret on this side instead.", fields: [ { name: "peer_id", label: "Peer", input_type: "select", required: true, options: peerOptions(list) }, { name: "secret", label: "New secret (hex)", type: "String", attributes: { placeholder: "64 hex characters" } }, ], }); adminPage(req, res, "peers", { type: "card", title: "Peers", contents: p("This instance's ", strong("env_id"), " is ", code(escapeHtml(env.env_id)), ". Paste this into the other instance's peer form.") + peerTable }, { type: "card", title: "Add peer", contents: renderForm(addForm, req.csrfToken()) }, { type: "card", title: "Replace a peer's secret", contents: list.length === 0 ? p({ class: "text-muted" }, "No peers configured.") : renderForm(replaceForm, req.csrfToken()) } ); }; // The one-time display of a shared secret (pairing / rotation). const secretCard = (title, secret, instruction) => ({ type: "card", title: title, contents: p("Shared secret (it will not be shown again):") + pre({ class: "user-select-all p-2 bg-light text-dark border rounded", style: "white-space:pre-wrap;word-break:break-all" }, escapeHtml(secret.toString("hex"))) + p(escapeHtml(instruction)) + p(link(ADMIN_URLS.PEERS, "Back to peers")) }); const peersAdd = async (req, res) => { if (!requireAdmin(req, res)) { return; } try { const { peer, secret } = await ops.addPeer({ envId: req.body.env_id, label: req.body.label, baseUrl: req.body.base_url, requireTls: req.body.require_tls, existingSecret: req.body.existing_secret }); adminPage(req, res, "peer paired", secretCard(`Peer ${escapeHtml(ops.peerName(peer))} paired`, secret, "Paste this secret into the peer's own pairing form for this instance (and give it this instance's env_id if it is a new pairing).")); } catch (err) { flashRedirect(req, res, ADMIN_URLS.PEERS, "danger", err.message); } }; // Rotate (generate + display) or replace (a secret pasted from the other side). const peersRotate = async (req, res) => { if (!requireAdmin(req, res)) { return; } try { const { peer, secret, generated } = await ops.rotatePeerSecret(req.body.peer_id, req.body.secret); if (!generated) { flashRedirect(req, res, ADMIN_URLS.PEERS, "success", `secret for peer ${ops.peerName(peer)} replaced`); return; } adminPage(req, res, "secret rotated", secretCard(`Peer ${escapeHtml(ops.peerName(peer))} secret rotated`, secret, "On the other instance, open Peers -> Replace a peer's secret, choose this instance, and paste this secret. Until then the pairing cannot authenticate.")); } catch (err) { flashRedirect(req, res, ADMIN_URLS.PEERS, "danger", err.message); } }; const peersDelete = async (req, res) => { if (!requireAdmin(req, res)) { return; } try { const { keptForRollback } = await ops.deletePeer(req.body.peer_id); flashRedirect(req, res, ADMIN_URLS.PEERS, "success", keptForRollback ? `peer deleted (${keptForRollback} applied plan(s) kept so they can still be rolled back)` : "peer deleted"); } catch (err) { flashRedirect(req, res, ADMIN_URLS.PEERS, "danger", err.message); } }; // ---------------- Plan (preview) ---------------- // GET: choose a source peer. Read-only -- the pull + plan persistence happen only // on the CSRF-protected POST below. const planSelect = async (req, res) => { if (!requireAdmin(req, res)) { return; } const peerList = await peers.listPeers(); const selForm = new Form({ action: ADMIN_URLS.PLAN, submitLabel: "Preview plan", fields: [ { name: "peer", label: "Source peer", input_type: "select", options: peerOptions(peerList) }, ], }); adminPage(req, res, "plan", { type: "card", title: "Preview a sync plan", contents: peerList.length === 0 ? p({ class: "text-muted" }, "No peers configured. Add one on the Peers tab.") : p("Pull a source peer's current design state and preview what would change here.") + renderForm(selForm, req.csrfToken()) } ); }; // POST: pull the chosen peer's state, persist a plan, and render the preview. const planPreview = async (req, res) => { if (!requireAdmin(req, res)) { return; } try { const { plan_id, plan, peer } = await ops.pullPlan(req.body.peer); const html = planLib.renderPlanHtml(plan_id, plan, ops.peerName(peer)); const applyForm = diff.isEmptyPlan(plan) ? "" : planLib.renderApplyForm(plan_id, plan, csrfField(req)); adminPage(req, res, "plan", { type: "card", title: "Plan preview", contents: html + applyForm } ); } catch (err) { flashRedirect(req, res, ADMIN_URLS.PLAN, "danger", err.message); } }; // ---------------- Apply ---------------- const warningList = (warnings) => { if (!warnings || warnings.length === 0) { return ""; } return `
Warnings (${warnings.length}):
`; }; // One-line outcome of an apply, from applyPlan's result. const applySummary = (result) => { const bits = [`${result.summary.total} planned change(s)`]; if (result.deletes.applied) { bits.push(`${result.deletes.deleted || 0} deleted`); bits.push(`${result.deletes.skipped.length} delete(s) skipped`); } if (result.rows.applied) { const rowOps = Object.values(result.rows.perTable).reduce((a, t) => a + t.inserted + t.updated + t.deleted, 0); bits.push(`${rowOps} row op(s)`); } return bits.join(", "); }; // Admin apply of a previewed plan. Data-bearing drops need the table names TYPED // into `prunable` (+ the confirmation tick under on_delete=confirm); table renames // need their `confirm_rename` box ticked. const applyView = async (req, res) => { if (!requireAdmin(req, res)) { return; } let result; try { result = await ops.applyPlan(req.body.plan_id, { prunable: parseList(req.body.prunable), confirmDataDrops: !!req.body.confirm_data, confirmRenames: parseList(req.body.confirm_rename) }); } catch (err) { if (!err.extra) { flashRedirect(req, res, ADMIN_URLS.PLAN, "danger", err.message); return; } // A failure part-way through can leave the target half-applied; the restore // point saved before any change is exactly what the admin needs now. const { planId, canRollback } = err.extra; adminPage(req, res, "apply failed", { type: "card", title: `Apply of plan ${escapeHtml(planId)} failed`, contents: `
${escapeHtml(err.message)}
` + (canRollback ? p("A restore point was saved before this apply changed anything. Roll back to it:") + rollbackForm(planId, csrfField(req)) : p("No restore point exists for this plan.")) + p(link(ADMIN_URLS.PLAN, "Back to plan")) } ); return; } const planId = result.plan_id; adminPage(req, res, "applied", { type: "card", title: `Applied plan ${escapeHtml(planId)}`, contents: p("Applied: ", strong(escapeHtml(applySummary(result)))) + warningList(result.warnings) + (result.canRollback ? p("Revert if this was a mistake:") + rollbackForm(planId, csrfField(req)) : "") + p(link(ADMIN_URLS.DASHBOARD, "Back to dashboard")) } ); }; const rollbackForm = (planId, csrf) => { return `
` + csrf + `` + `
Rollback reverts metadata, config, and managed rows to the pre-apply state. ` + `To also drop tables this apply created that have since gained rows, type their names and confirm:
` + `` + `
` + `
` + `
`; }; const rollbackView = async (req, res) => { if (!requireAdmin(req, res)) { return; } try { const result = await ops.rollbackPlan(req.body.plan_id, { prunable: parseList(req.body.prunable), confirmDataDrops: !!req.body.confirm_data }); for (const w of result.warnings) { req.flash("warning", escapeHtml(w)); } flashRedirect(req, res, ADMIN_URLS.DASHBOARD, "success", `rolled back plan ${result.plan_id} (${result.restoredTables} table(s) restored)`); } catch (err) { flashRedirect(req, res, ADMIN_URLS.DASHBOARD, "danger", err.message); } }; // ---------------- Tables (reference-data modes) ---------------- const tablesView = async (req, res) => { if (!requireAdmin(req, res)) { return; } const info = await ops.listTables(); const modeBtn = (r, mode, label, cls) => { if (r.mode === mode) { return `${escapeHtml(label)}`; } return post_btn(ADMIN_URLS.TABLES_SET, label, req.csrfToken(), { req, btnClass: "btn-outline-" + cls, small: true, formClass: "d-inline me-1", body: { table: r.name, mode: mode } }); }; const tbl = mkTable([ { label: "table", key: (r) => escapeHtml(r.name) }, { label: "rows", key: (r) => escapeHtml(r.rows) }, { label: "mode", key: (r) => r.locked ? `locked (user)` : escapeHtml(r.mode) }, { label: "set", key: (r) => r.locked ? span({ class: "text-muted" }, "system table") : modeBtn(r, DATA_MODES.USER, "User", "secondary") + modeBtn(r, DATA_MODES.STARTER, "Starter", "info") + modeBtn(r, DATA_MODES.MANAGED, "Managed", "primary") }, ], info); adminPage(req, res, "tables", { type: "card", title: "Reference-data modes", contents: p("Choose which tables site-sync propagates ", strong("row data"), " for. ", strong("User"), " (default): rows are never touched. ", strong("Starter"), ": rows are seeded once into a fresh table, then the target owns them. ", strong("Managed"), ": the source is canonical; rows are mirrored (added/updated/removed). Mark Starter/Managed on ", strong("reference/lookup"), " tables only -- never user-entered data.") + tbl } ); }; const tablesSet = async (req, res) => { if (!requireAdmin(req, res)) { return; } try { const { table, mode } = await ops.setTableMode(req.body.table, req.body.mode); flashRedirect(req, res, ADMIN_URLS.TABLES, "success", `${table} set to ${mode}`); } catch (err) { flashRedirect(req, res, ADMIN_URLS.TABLES, "danger", err.message); } }; // ---------------- Machine API ---------------- // Wrap a machine-API handler: HMAC-authenticate the request, then run it; any // error becomes a SIGNED 500 so the caller sees the real message instead of an // unsigned (and therefore rejected) error page. const peerRoute = (handler) => async (req, res) => { const peer = await requirePeerAuth(req, res); if (!peer) { return; } try { await handler(req, res, peer); } catch (err) { sendSignedJson(req, res, HTTP_STATUS.SERVER_ERROR, { error: err.message }); } }; const apiHealth = async (req, res) => { const env = await requireEnv(); sendSignedJson(req, res, HTTP_STATUS.OK, { plugin: PLUGIN_NAME, version: PLUGIN_VERSION, env_id: env.env_id, label: env.env_label, plugins: await localPluginList() }); }; // Serve THIS instance's sanitized desired-state pack to an authenticated peer. // Read-only: the source never mutates; the target pulls this and computes/applies // the plan on its own side, so every safety gate lives on the mutating side. const apiState = async (req, res) => { const env = await requireEnv(); const { pack, hash } = await buildDesiredState(); const { manifest } = await rowsLib.buildRowManifest(); sendSignedJson(req, res, HTTP_STATUS.OK, { plugin: PLUGIN_NAME, version: PLUGIN_VERSION, env_id: env.env_id, hash: hash, pack: pack, rows: manifest }); }; // PUSH endpoint: a source posts its desired-state pack; the TARGET still computes // and persists the plan on its own side (every gate lives on the mutating side). const apiPlan = async (req, res, peer) => { if (!req.body.pack) { sendSignedJson(req, res, HTTP_STATUS.BAD_REQUEST, { error: "pack required" }); return; } const { plan_id, plan } = await planLib.buildPlan({ peerRow: peer, env: await requireEnv(), desired: req.body.pack, desiredHash: req.body.hash, sourceEnvId: peer.env_id, direction: PUSH_DIRECTION, sourceRows: req.body.rows || {} }); sendSignedJson(req, res, HTTP_STATUS.OK, { plan_id: plan_id, summary: plan.summary, destructive: plan.destructive, precondition_hash: plan.precondition_hash }); }; // PUSH apply: a source triggers apply of a plan it pushed. Gated behind // on_apply=auto -- by default the target refuses remote-triggered mutation and the // admin must apply via the target's own UI. Even when enabled, all gates (drift // re-check, config allow-list, delete policy) run locally. const apiApply = async (req, res, peer) => { const env = await requireEnv(); if (env.on_apply !== APPLY_POLICY.AUTO) { sendSignedJson(req, res, HTTP_STATUS.FORBIDDEN, { error: "target requires admin confirmation (on_apply=confirm); apply via the target admin UI" }); return; } const planId = req.body.plan_id; if (!planId) { sendSignedJson(req, res, HTTP_STATUS.BAD_REQUEST, { error: "plan_id required" }); return; } // Bind the plan to the calling peer AND to the push direction: a peer may only // apply a plan it pushed itself -- never another peer's, nor a pull plan the // local admin previewed from it and chose not to apply. const owned = await planLib.loadPlan(planId); if (!owned || owned.peer_id !== peer.peer_id || owned.direction !== PUSH_DIRECTION) { sendSignedJson(req, res, HTTP_STATUS.NOT_FOUND, { error: "unknown plan" }); return; } let result; try { // The SENDER can never satisfy the human gates: an empty typed allow-list // means no data-bearing table/field is ever dropped remotely (whatever // on_delete says), and no confirmed renames means no table is renamed. result = await ops.applyPlan(planId, { prunable: [], confirmDataDrops: false, confirmRenames: [] }); } catch (err) { sendSignedJson(req, res, HTTP_STATUS.CONFLICT, { error: err.message }); return; } sendSignedJson(req, res, HTTP_STATUS.OK, { ok: true, result }); }; // Serve a file's bytes by CONTENT HASH to an authenticated peer. Only files // referenced by this instance's synced (managed/starter) rows or by its page // layouts / view configs / site logo are resolvable, the resolved path is // containment-checked against the tenant file store, and the bytes are read whole // (capped) so the response signature covers exactly what is sent. const apiFile = async (req, res) => { const hash = req.params.hash; if (!HASH_RE.test(hash || "")) { sendSignedJson(req, res, HTTP_STATUS.BAD_REQUEST, { error: "bad hash" }); return; } const fieldValue = await rowsLib.resolveFileByHash(hash); if (!fieldValue) { sendSignedJson(req, res, HTTP_STATUS.NOT_FOUND, { error: "file not found", hash }); return; } let absPath; try { absPath = toAbsolutePath(db, fieldValue); } catch (e) { sendSignedJson(req, res, HTTP_STATUS.BAD_REQUEST, { error: "refusing file path outside tenant store" }); return; } let stat; try { stat = await fs.promises.stat(absPath); } catch (e) { sendSignedJson(req, res, HTTP_STATUS.NOT_FOUND, { error: "file missing from the store", hash }); return; } if (stat.size > PEER_LIMITS.FILE_BYTES) { sendSignedJson(req, res, HTTP_STATUS.PAYLOAD_TOO_LARGE, { error: `file larger than ${PEER_LIMITS.FILE_BYTES} bytes`, hash }); return; } sendSigned(req, res, HTTP_STATUS.OK, await fs.promises.readFile(absPath), "application/octet-stream"); }; // ---------------- Route registration ---------------- const routes = [ { url: ADMIN_URLS.DASHBOARD, method: "get", callback: dashboard }, { url: ADMIN_URLS.SETTINGS, method: "post", callback: saveSettings }, { url: ADMIN_URLS.PEERS, method: "get", callback: peersView }, { url: ADMIN_URLS.PEERS_ADD, method: "post", callback: peersAdd }, { url: ADMIN_URLS.PEERS_ROTATE, method: "post", callback: peersRotate }, { url: ADMIN_URLS.PEERS_DELETE, method: "post", callback: peersDelete }, { url: ADMIN_URLS.PLAN, method: "get", callback: planSelect }, { url: ADMIN_URLS.PLAN, method: "post", callback: planPreview }, { url: ADMIN_URLS.APPLY, method: "post", callback: applyView }, { url: ADMIN_URLS.ROLLBACK, method: "post", callback: rollbackView }, { url: ADMIN_URLS.TABLES, method: "get", callback: tablesView }, { url: ADMIN_URLS.TABLES_SET, method: "post", callback: tablesSet }, { url: API_PATHS.HEALTH, method: "get", callback: peerRoute(apiHealth), noCsrf: true }, { url: API_PATHS.STATE, method: "get", callback: peerRoute(apiState), noCsrf: true }, { url: API_PATHS.FILE_ROUTE, method: "get", callback: peerRoute(apiFile), noCsrf: true }, { url: API_PATHS.PLAN, method: "post", callback: peerRoute(apiPlan), noCsrf: true }, { url: API_PATHS.APPLY, method: "post", callback: peerRoute(apiApply), noCsrf: true }, ]; module.exports = { routes };