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