DDL Change Control
Formal DDL approval / change-control workflow. SOX 404 ICFR and PCI DSS Req 6.5 / 10.7 require a documented change-control process for any schema change touching financial reporting or cardholder- data systems. The framework's existing audit emission on DDL only logs that a change happened; this primitive enforces a multi- approver, time-windowed, hash-anchored flow BEFORE the change applies.
Lifecycle: propose(sql, opts) captures the SQL under a SHA3-512 hash and optional signed payload; approve(changeId, approver) adds an approver signature (rejecting self-approval under SOX/PCI postures); reject(changeId, reviewer, reason) terminates; applyApproved(changeId, runner) executes the SQL via the operator-supplied runner ONLY when the change has the minimum approver count, the window is open, and the stored SQL still hashes to its captured digest (defense against in-memory tampering between propose and apply).
Window grammar accepts "always" (24/7), "Mon-Fri 09:00-17:00 UTC", or "Mon,Wed,Fri 14:00-18:00 UTC". Postures sox-404 / sox / pci-dss enforce minimum 2 approvers and disable self- approval. Audit emissions live in the ddl.* namespace: ddl.change.proposed / .approved / .rejected / .applied / .apply_refused (the last carrying the refusal reason — insufficient-approvals / window-closed / sql-tampered / self- approval-denied). State is in-process by default; operators pass a durable opts.store ({ get, put, list }) for cluster-wide visibility.
b.ddlChangeControl.create(opts) #
{
audit: Object, // b.audit instance (safeEmit-shaped)
approvers: number, // minimum approvals before applyApproved (default 2; ≥2 under SOX/PCI)
windowSpec: string, // "always" | "Mon-Fri 09:00-17:00 UTC" | "Mon,Wed 14:00-18:00 UTC"
posture: string, // "sox-404" | "sox" | "pci-dss" (forces approvers≥2 + no self-approval)
signWith: Function, // (bytes) → signature; signs propose+approve payloads
verifyWith: Function, // (bytes, sig) → boolean; reserved for store-backed restoration
store: Object, // { get(id), put(id, change), list() }; default in-memory Map
now: Function, // () → ms; testing override
selfApproval: boolean, // allow proposer to approve own change (forced false under listed postures)
}
Build a DDL change-control workflow. Returns { propose, approve, reject, applyApproved, list, get, posture, approvers, windowSpec }. propose returns { changeId, sqlHash }; approve returns { changeId, signaturesCount, thresholdMet }; applyApproved runs the SQL through the operator-supplied runner and returns { changeId, result, durationMs }.
var ddl = b.ddlChangeControl.create({
audit: auditInstance,
approvers: 2,
windowSpec: "Mon-Fri 09:00-17:00 UTC",
posture: "sox-404",
});
var p = await ddl.propose("ALTER TABLE accounts ADD COLUMN region TEXT", {
proposer: "alice",
reason: "data-residency expansion",
ticket: "JIRA-123",
});
p.changeId; // → "<32-hex token>"
p.sqlHash; // → ""
await ddl.approve(p.changeId, "bob");
var a2 = await ddl.approve(p.changeId, "carol");
a2.thresholdMet; // → true
var applied = await ddl.applyApproved(p.changeId, async function (sql) {
return { rowsAffected: 0 };
});
applied.result.rowsAffected; // → 0
Last updated 2026-08-08T16:39:49.652Z by seeder.