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

120 lines
5.1 KiB
JavaScript

// Compile-time constants for the site-sync plugin.
//
// site-sync is a one-way DESIRED-STATE deployer: it snapshots a source site's
// current design state, diffs it against a target, and applies the difference so
// the target converges to the source. Unlike dev-deploy (an ops journal) there is
// no edit log -- identity is derived from the current state itself (entities keyed
// by NAME, the same key Saltcorn's *_pack builders make canonical; roles by id).
// SYNCABLE_CONFIG_KEYS, ROW_SYNC_LOCKED_TABLES, DATA_MODES (stored per table in
// _ss_table_modes) and the isInfraName / isLockedTable / isSyncedMode predicates
// are shared with dev-deploy: one source of truth in lib/common/syncConstants.js,
// re-exported here. capture.js intersects the produced config to
// SYNCABLE_CONFIG_KEYS and apply refuses any other key -- install_pack's
// setConfig clobbers unconditionally, so a compromised source must not reach
// security config. (install_pack also writes function_code_pages from
// pack.code_pages; site-sync handles code pages as a first-class previewed +
// prunable kind in diff.js, never as raw config.)
const syncConstants = require("./common/syncConstants");
const { ADMIN_ROLE_ID } = require("./common/adminGate");
const PLUGIN_NAME = "site-sync";
const PLUGIN_VERSION = require("../package.json").version;
// Saltcorn role ids used by the file layer: ADMIN (lib/common/adminGate.js) is the
// most privileged role, PUBLIC the least (min_role_read = PUBLIC means anyone may
// read). Synced files are owned by the admin user (file ownership is not portable
// between user tables).
const PUBLIC_ROLE_ID = 100;
const ADMIN_USER_ID = 1;
// How a sync treats entities present in the TARGET but absent from the SOURCE.
// mirror (default) -- prune them so the target exactly matches the source.
// additive -- never remove; the target stays a superset of the source.
const SYNC_MODES = {
MIRROR: "mirror",
ADDITIVE: "additive"
};
// Policy for DATA-BEARING drops (dropping a table that holds rows, or a field that
// holds column data). Metadata-only deletes (views, pages, triggers, empty fields)
// prune freely under mirror mode; a data-bearing drop is gated by this policy.
// confirm (default) -- allowed only when the table name was TYPED into the
// prunable allow-list (never pre-filled) AND the data-loss
// acknowledgement was given in the apply request.
// refuse -- never drop a data-bearing table/field (leave it, warn).
// auto -- drop without the acknowledgement (still requires the
// typed allow-list).
const DELETE_POLICY = {
AUTO: "auto",
CONFIRM: "confirm",
REFUSE: "refuse"
};
// Whether a paired peer may trigger an apply remotely.
// confirm (default) -- only an admin applies a previewed plan by plan_id.
// auto -- a paired peer may push-apply remotely
// (POST /site-sync/api/apply); the pull path always previews.
const APPLY_POLICY = {
AUTO: "auto",
CONFIRM: "confirm"
};
// Lifecycle of a persisted plan (_ss_plans.status). Transitions are claimed
// atomically (UPDATE ... WHERE status IN (...)) so a double-submit cannot run two
// applies or two rollbacks of the same plan concurrently.
const PLAN_STATUS = {
PENDING: "pending",
APPLYING: "applying",
APPLIED: "applied",
ROLLING_BACK: "rolling_back",
ROLLED_BACK: "rolled_back"
};
// Custom request Content-Type for signed peer bodies. Chosen so Saltcorn's
// express.json() middleware leaves the stream untouched and peerAuth can read the
// exact raw bytes for the HMAC. Must match on both the sign and verify sides.
const PEER_CONTENT_TYPE = "application/vnd.site-sync+json";
// Signed-request header names. The HMAC canonical contains NO header names, so
// these are free to rename -- but the sign side (transport.js) and verify side
// (peerAuth.js) must agree byte-for-byte.
const HEADERS = {
ENV_ID: "X-SS-Env-Id",
TIMESTAMP: "X-SS-Timestamp",
NONCE: "X-SS-Nonce",
SIGNATURE: "X-SS-Signature",
RESPONSE_SIGNATURE: "X-SS-Response-Signature"
};
// HKDF salt AND info of the KEK sealing peer secrets at rest (lib/peers.js).
// site-sync-specific, so site-sync and dev-deploy on one instance derive
// INDEPENDENT KEKs from the same session secret. Must stay byte-identical
// forever: changing it orphans every existing pairing.
const PEER_KEK_INFO = "site-sync:peer-secrets:aes-gcm-key:v1";
// Registered peers table and the raw nonce store of the inbound replay guard
// (both created in lib/schema.js).
const PEERS_TABLE = "_ss_peers";
const SEEN_NONCES_TABLE = "_ss_seen_nonces";
// The raw site-wide apply lock (lib/siteLock.js; created in lib/schema.js).
const APPLY_LOCK_TABLE = "_ss_apply_lock";
module.exports = {
...syncConstants,
PLUGIN_NAME,
PLUGIN_VERSION,
ADMIN_ROLE_ID,
PUBLIC_ROLE_ID,
ADMIN_USER_ID,
SYNC_MODES,
DELETE_POLICY,
APPLY_POLICY,
PLAN_STATUS,
PEER_CONTENT_TYPE,
HEADERS,
PEER_KEK_INFO,
PEERS_TABLE,
SEEN_NONCES_TABLE,
APPLY_LOCK_TABLE
};