@blamejs/core 0.8.42 → 0.8.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. package/sbom.cyclonedx.json +7 -7
@@ -0,0 +1,523 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.ddlChangeControl
4
+ * @nav Compliance
5
+ * @title DDL Change Control
6
+ *
7
+ * @intro
8
+ * Formal DDL approval / change-control workflow. SOX 404 ICFR and
9
+ * PCI DSS Req 6.5 / 10.7 require a documented change-control process
10
+ * for any schema change touching financial reporting or cardholder-
11
+ * data systems. The framework's existing audit emission on DDL only
12
+ * logs that a change happened; this primitive enforces a multi-
13
+ * approver, time-windowed, hash-anchored flow BEFORE the change
14
+ * applies.
15
+ *
16
+ * Lifecycle: `propose(sql, opts)` captures the SQL under a SHA3-512
17
+ * hash and optional signed payload; `approve(changeId, approver)`
18
+ * adds an approver signature (rejecting self-approval under SOX/PCI
19
+ * postures); `reject(changeId, reviewer, reason)` terminates;
20
+ * `applyApproved(changeId, runner)` executes the SQL via the
21
+ * operator-supplied runner ONLY when the change has the minimum
22
+ * approver count, the window is open, and the stored SQL still
23
+ * hashes to its captured digest (defense against in-memory
24
+ * tampering between propose and apply).
25
+ *
26
+ * Window grammar accepts `"always"` (24/7), `"Mon-Fri 09:00-17:00
27
+ * UTC"`, or `"Mon,Wed,Fri 14:00-18:00 UTC"`. Postures `sox-404` /
28
+ * `sox` / `pci-dss` enforce minimum 2 approvers and disable self-
29
+ * approval. Audit emissions live in the `ddl.*` namespace:
30
+ * `ddl.change.proposed` / `.approved` / `.rejected` / `.applied` /
31
+ * `.apply_refused` (the last carrying the refusal reason —
32
+ * insufficient-approvals / window-closed / sql-tampered / self-
33
+ * approval-denied). State is in-process by default; operators pass
34
+ * a durable `opts.store` ({ get, put, list }) for cluster-wide
35
+ * visibility.
36
+ *
37
+ * @card
38
+ * Formal DDL approval / change-control workflow.
39
+ */
40
+
41
+ var validateOpts = require("./validate-opts");
42
+ var { sha3Hash, generateToken } = require("./crypto");
43
+ var C = require("./constants");
44
+ var { DdlChangeControlError } = require("./framework-error");
45
+
46
+ var STATE_PROPOSED = "proposed";
47
+ var STATE_APPROVED = "approved";
48
+ var STATE_REJECTED = "rejected";
49
+ var STATE_APPLIED = "applied";
50
+ var STATE_FAILED = "failed";
51
+
52
+ var POSTURES_REQUIRING_CHANGE_CONTROL = ["sox-404", "sox", "pci-dss"];
53
+
54
+ // Window spec grammar - operator-friendly subset:
55
+ // "Mon-Fri 09:00-17:00 UTC"
56
+ // "Mon,Wed,Fri 14:00-18:00 UTC"
57
+ // "always" (24/7)
58
+ var DAY_NAMES = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
59
+
60
+ function _parseWindowSpec(spec) {
61
+ if (typeof spec !== "string" || spec.length === 0) {
62
+ throw new DdlChangeControlError("ddlChangeControl/bad-window",
63
+ "windowSpec must be a non-empty string");
64
+ }
65
+ var trimmed = spec.trim();
66
+ if (trimmed.toLowerCase() === "always") {
67
+ return { always: true };
68
+ }
69
+ var parts = trimmed.split(/\s+/);
70
+ if (parts.length !== 3) {
71
+ throw new DdlChangeControlError("ddlChangeControl/bad-window",
72
+ "windowSpec must be 'always' or '<days> <HH:MM-HH:MM> UTC' - got " + JSON.stringify(spec));
73
+ }
74
+ if (parts[2].toUpperCase() !== "UTC") {
75
+ throw new DdlChangeControlError("ddlChangeControl/bad-window",
76
+ "windowSpec timezone must be UTC - got " + parts[2]);
77
+ }
78
+ var days = new Set();
79
+ var dayParts = parts[0].split(",");
80
+ for (var i = 0; i < dayParts.length; i++) {
81
+ var dp = dayParts[i].trim().toLowerCase();
82
+ if (dp.indexOf("-") !== -1) {
83
+ var range = dp.split("-");
84
+ if (range.length !== 2) {
85
+ throw new DdlChangeControlError("ddlChangeControl/bad-window",
86
+ "windowSpec day-range must be 'A-B' - got " + dp);
87
+ }
88
+ var lo = DAY_NAMES.indexOf(range[0]);
89
+ var hi = DAY_NAMES.indexOf(range[1]);
90
+ if (lo === -1 || hi === -1) {
91
+ throw new DdlChangeControlError("ddlChangeControl/bad-window",
92
+ "windowSpec unknown day in range " + dp);
93
+ }
94
+ if (lo <= hi) {
95
+ for (var d = lo; d <= hi; d++) days.add(d);
96
+ } else {
97
+ for (var d2 = lo; d2 < DAY_NAMES.length; d2++) days.add(d2);
98
+ for (var d3 = 0; d3 <= hi; d3++) days.add(d3);
99
+ }
100
+ } else {
101
+ var idx = DAY_NAMES.indexOf(dp);
102
+ if (idx === -1) {
103
+ throw new DdlChangeControlError("ddlChangeControl/bad-window",
104
+ "windowSpec unknown day '" + dp + "'");
105
+ }
106
+ days.add(idx);
107
+ }
108
+ }
109
+ var hourParts = parts[1].split("-");
110
+ if (hourParts.length !== 2) {
111
+ throw new DdlChangeControlError("ddlChangeControl/bad-window",
112
+ "windowSpec hour-range must be 'HH:MM-HH:MM' - got " + parts[1]);
113
+ }
114
+ var startMin = _parseHHMM(hourParts[0]);
115
+ var endMin = _parseHHMM(hourParts[1]);
116
+ if (startMin >= endMin) {
117
+ throw new DdlChangeControlError("ddlChangeControl/bad-window",
118
+ "windowSpec start must be < end - got " + parts[1]);
119
+ }
120
+ return { always: false, days: days, startMin: startMin, endMin: endMin };
121
+ }
122
+
123
+ function _parseHHMM(s) {
124
+ var m = /^(\d{2}):(\d{2})$/.exec(s);
125
+ if (!m) {
126
+ throw new DdlChangeControlError("ddlChangeControl/bad-window",
127
+ "windowSpec time must be HH:MM - got " + s);
128
+ }
129
+ var hh = parseInt(m[1], 10);
130
+ var mm = parseInt(m[2], 10);
131
+ if (hh < 0 || hh > 23 || mm < 0 || mm > 59) {
132
+ throw new DdlChangeControlError("ddlChangeControl/bad-window",
133
+ "windowSpec time out of range - got " + s);
134
+ }
135
+ return hh * 60 + mm; // allow:raw-time-literal — converting HH:MM to minute-of-day, not "60 seconds"
136
+ }
137
+
138
+ function _isInWindow(window, nowMs) {
139
+ if (!window) return true;
140
+ if (window.always) return true;
141
+ var d = new Date(nowMs);
142
+ var dayIdx = d.getUTCDay();
143
+ if (!window.days.has(dayIdx)) return false;
144
+ var min = d.getUTCHours() * 60 + d.getUTCMinutes(); // allow:raw-time-literal — converting HH:MM to minute-of-day, not "60 seconds"
145
+ return min >= window.startMin && min < window.endMin;
146
+ }
147
+
148
+ function _memoryStore() {
149
+ var byId = new Map();
150
+ return {
151
+ get: function (id) { return byId.get(id) || null; },
152
+ put: function (id, change) { byId.set(id, change); },
153
+ list: function () { return Array.from(byId.values()); },
154
+ };
155
+ }
156
+
157
+ /**
158
+ * @primitive b.ddlChangeControl.create
159
+ * @signature b.ddlChangeControl.create(opts)
160
+ * @since 0.8.48
161
+ * @status stable
162
+ * @compliance sox-404, pci-dss
163
+ * @related b.audit, b.compliance, b.dualControl
164
+ *
165
+ * Build a DDL change-control workflow. Returns
166
+ * `{ propose, approve, reject, applyApproved, list, get, posture,
167
+ * approvers, windowSpec }`. `propose` returns `{ changeId, sqlHash }`;
168
+ * `approve` returns `{ changeId, signaturesCount, thresholdMet }`;
169
+ * `applyApproved` runs the SQL through the operator-supplied runner
170
+ * and returns `{ changeId, result, durationMs }`.
171
+ *
172
+ * @opts
173
+ * audit: Object, // b.audit instance (safeEmit-shaped)
174
+ * approvers: number, // minimum approvals before applyApproved (default 2; ≥2 under SOX/PCI)
175
+ * windowSpec: string, // "always" | "Mon-Fri 09:00-17:00 UTC" | "Mon,Wed 14:00-18:00 UTC"
176
+ * posture: string, // "sox-404" | "sox" | "pci-dss" (forces approvers≥2 + no self-approval)
177
+ * signWith: Function, // (bytes) → signature; signs propose+approve payloads
178
+ * verifyWith: Function, // (bytes, sig) → boolean; reserved for store-backed restoration
179
+ * store: Object, // { get(id), put(id, change), list() }; default in-memory Map
180
+ * now: Function, // () → ms; testing override
181
+ * selfApproval: boolean, // allow proposer to approve own change (forced false under listed postures)
182
+ *
183
+ * @example
184
+ * var ddl = b.ddlChangeControl.create({
185
+ * audit: auditInstance,
186
+ * approvers: 2,
187
+ * windowSpec: "Mon-Fri 09:00-17:00 UTC",
188
+ * posture: "sox-404",
189
+ * });
190
+ *
191
+ * var p = await ddl.propose("ALTER TABLE accounts ADD COLUMN region TEXT", {
192
+ * proposer: "alice",
193
+ * reason: "data-residency expansion",
194
+ * ticket: "JIRA-123",
195
+ * });
196
+ * p.changeId; // → "<32-hex token>"
197
+ * p.sqlHash; // → "<sha3-512 hex>"
198
+ *
199
+ * await ddl.approve(p.changeId, "bob");
200
+ * var a2 = await ddl.approve(p.changeId, "carol");
201
+ * a2.thresholdMet; // → true
202
+ *
203
+ * var applied = await ddl.applyApproved(p.changeId, async function (sql) {
204
+ * return { rowsAffected: 0 };
205
+ * });
206
+ * applied.result.rowsAffected; // → 0
207
+ */
208
+ function create(opts) {
209
+ opts = opts || {};
210
+ validateOpts(opts, [
211
+ "audit", "approvers", "windowSpec", "posture", "signWith",
212
+ "verifyWith", "store", "now", "selfApproval",
213
+ ], "ddlChangeControl.create");
214
+ validateOpts.auditShape(opts.audit, "ddlChangeControl",
215
+ DdlChangeControlError, "ddlChangeControl/bad-audit");
216
+ validateOpts.optionalFunction(opts.signWith,
217
+ "ddlChangeControl: signWith", DdlChangeControlError, "ddlChangeControl/bad-signer");
218
+ validateOpts.optionalFunction(opts.verifyWith,
219
+ "ddlChangeControl: verifyWith", DdlChangeControlError, "ddlChangeControl/bad-verifier");
220
+ validateOpts.optionalFunction(opts.now,
221
+ "ddlChangeControl: now", DdlChangeControlError, "ddlChangeControl/bad-now");
222
+ validateOpts.optionalNonEmptyString(opts.posture,
223
+ "ddlChangeControl: posture", DdlChangeControlError, "ddlChangeControl/bad-posture");
224
+
225
+ var approvers = 2;
226
+ if (opts.approvers !== undefined) {
227
+ if (typeof opts.approvers !== "number" || !isFinite(opts.approvers) ||
228
+ opts.approvers < 1) {
229
+ throw new DdlChangeControlError("ddlChangeControl/bad-approvers",
230
+ "approvers must be a positive integer");
231
+ }
232
+ approvers = Math.floor(opts.approvers);
233
+ }
234
+ var posture = opts.posture || null;
235
+ if (posture && POSTURES_REQUIRING_CHANGE_CONTROL.indexOf(posture) !== -1 && approvers < 2) {
236
+ throw new DdlChangeControlError("ddlChangeControl/insufficient-approvers",
237
+ "posture '" + posture + "' requires approvers >= 2 (SOX 404 / PCI-DSS 6.5)");
238
+ }
239
+
240
+ var window = opts.windowSpec ? _parseWindowSpec(opts.windowSpec) : null;
241
+ var auditMod = opts.audit && typeof opts.audit.safeEmit === "function" ? opts.audit : null;
242
+ var signWith = typeof opts.signWith === "function" ? opts.signWith : null;
243
+ var now = typeof opts.now === "function" ? opts.now : Date.now;
244
+ var store = opts.store && typeof opts.store === "object" &&
245
+ typeof opts.store.get === "function" && typeof opts.store.put === "function"
246
+ ? opts.store : _memoryStore();
247
+ var selfApprovalAllowed = opts.selfApproval === true;
248
+ if (posture && POSTURES_REQUIRING_CHANGE_CONTROL.indexOf(posture) !== -1) {
249
+ selfApprovalAllowed = false;
250
+ }
251
+
252
+ function _emit(action, metadata, outcome) {
253
+ if (!auditMod) return;
254
+ try {
255
+ auditMod.safeEmit({
256
+ action: action,
257
+ outcome: outcome || "success",
258
+ metadata: metadata || {},
259
+ });
260
+ } catch (_e) { /* audit best-effort */ }
261
+ }
262
+
263
+ function _hashSql(sql) {
264
+ return sha3Hash(Buffer.from(sql, "utf8"));
265
+ }
266
+
267
+ async function propose(sql, options) {
268
+ options = options || {};
269
+ if (typeof sql !== "string" || sql.length === 0) {
270
+ throw new DdlChangeControlError("ddlChangeControl/bad-sql",
271
+ "propose: sql must be a non-empty string");
272
+ }
273
+ if (typeof options.proposer !== "string" || options.proposer.length === 0) {
274
+ throw new DdlChangeControlError("ddlChangeControl/missing-proposer",
275
+ "propose: opts.proposer is required (non-empty string)");
276
+ }
277
+ var changeId = generateToken(C.BYTES.bytes(16));
278
+ var sqlHash = _hashSql(sql);
279
+ var proposedAt = now();
280
+ var proposalSig = signWith ? signWith(Buffer.from(
281
+ JSON.stringify({ changeId: changeId, sqlHash: sqlHash, proposer: options.proposer, proposedAt: proposedAt }),
282
+ "utf8"
283
+ )) : null;
284
+ var change = {
285
+ changeId: changeId,
286
+ sqlHash: sqlHash,
287
+ sql: sql,
288
+ proposer: options.proposer,
289
+ reason: options.reason || null,
290
+ ticket: options.ticket || null,
291
+ proposedAt: proposedAt,
292
+ proposalSignature: proposalSig
293
+ ? (Buffer.isBuffer(proposalSig) ? proposalSig.toString("base64") : String(proposalSig))
294
+ : null,
295
+ state: STATE_PROPOSED,
296
+ approvals: [],
297
+ rejection: null,
298
+ appliedAt: null,
299
+ applier: null,
300
+ applyDurationMs: null,
301
+ applyError: null,
302
+ };
303
+ store.put(changeId, change);
304
+ _emit("ddl.change.proposed", {
305
+ changeId: changeId, sqlHash: sqlHash, proposer: options.proposer,
306
+ reason: options.reason || null, ticket: options.ticket || null,
307
+ });
308
+ return { changeId: changeId, sqlHash: sqlHash };
309
+ }
310
+
311
+ async function approve(changeId, approver, options) {
312
+ options = options || {};
313
+ if (typeof changeId !== "string" || changeId.length === 0) {
314
+ throw new DdlChangeControlError("ddlChangeControl/bad-id",
315
+ "approve: changeId must be a non-empty string");
316
+ }
317
+ if (typeof approver !== "string" || approver.length === 0) {
318
+ throw new DdlChangeControlError("ddlChangeControl/missing-approver",
319
+ "approve: approver must be a non-empty string");
320
+ }
321
+ var change = store.get(changeId);
322
+ if (!change) {
323
+ throw new DdlChangeControlError("ddlChangeControl/unknown-change",
324
+ "approve: unknown changeId '" + changeId + "'");
325
+ }
326
+ if (change.state === STATE_REJECTED) {
327
+ throw new DdlChangeControlError("ddlChangeControl/already-rejected",
328
+ "approve: change '" + changeId + "' is already rejected");
329
+ }
330
+ if (change.state === STATE_APPLIED) {
331
+ throw new DdlChangeControlError("ddlChangeControl/already-applied",
332
+ "approve: change '" + changeId + "' is already applied");
333
+ }
334
+ if (!selfApprovalAllowed && approver === change.proposer) {
335
+ _emit("ddl.change.apply_refused", {
336
+ changeId: changeId, reason: "self-approval-denied", actor: approver,
337
+ }, "denied");
338
+ throw new DdlChangeControlError("ddlChangeControl/self-approval-denied",
339
+ "approve: proposer '" + approver + "' cannot approve their own change under posture '" +
340
+ (posture || "default") + "'");
341
+ }
342
+ for (var i = 0; i < change.approvals.length; i++) {
343
+ if (change.approvals[i].approver === approver) {
344
+ throw new DdlChangeControlError("ddlChangeControl/duplicate-approval",
345
+ "approve: '" + approver + "' has already approved this change");
346
+ }
347
+ }
348
+ var approvedAt = now();
349
+ var approvalSig = signWith ? signWith(Buffer.from(
350
+ JSON.stringify({ changeId: changeId, approver: approver, approvedAt: approvedAt, sqlHash: change.sqlHash }),
351
+ "utf8"
352
+ )) : null;
353
+ change.approvals.push({
354
+ approver: approver,
355
+ approvedAt: approvedAt,
356
+ signature: approvalSig
357
+ ? (Buffer.isBuffer(approvalSig) ? approvalSig.toString("base64") : String(approvalSig))
358
+ : null,
359
+ reason: options.reason || null,
360
+ });
361
+ if (change.approvals.length >= approvers) change.state = STATE_APPROVED;
362
+ store.put(changeId, change);
363
+ _emit("ddl.change.approved", {
364
+ changeId: changeId, approver: approver, signaturesCount: change.approvals.length,
365
+ threshold: approvers,
366
+ });
367
+ return {
368
+ changeId: changeId,
369
+ signaturesCount: change.approvals.length,
370
+ thresholdMet: change.state === STATE_APPROVED,
371
+ };
372
+ }
373
+
374
+ async function reject(changeId, reviewer, reason) {
375
+ if (typeof changeId !== "string" || changeId.length === 0) {
376
+ throw new DdlChangeControlError("ddlChangeControl/bad-id",
377
+ "reject: changeId must be a non-empty string");
378
+ }
379
+ if (typeof reviewer !== "string" || reviewer.length === 0) {
380
+ throw new DdlChangeControlError("ddlChangeControl/missing-reviewer",
381
+ "reject: reviewer must be a non-empty string");
382
+ }
383
+ var change = store.get(changeId);
384
+ if (!change) {
385
+ throw new DdlChangeControlError("ddlChangeControl/unknown-change",
386
+ "reject: unknown changeId '" + changeId + "'");
387
+ }
388
+ if (change.state === STATE_APPLIED) {
389
+ throw new DdlChangeControlError("ddlChangeControl/already-applied",
390
+ "reject: change '" + changeId + "' is already applied");
391
+ }
392
+ change.state = STATE_REJECTED;
393
+ change.rejection = { reviewer: reviewer, reason: reason || null, rejectedAt: now() };
394
+ store.put(changeId, change);
395
+ _emit("ddl.change.rejected", {
396
+ changeId: changeId, reviewer: reviewer, reason: reason || null,
397
+ });
398
+ }
399
+
400
+ function list() {
401
+ return store.list().map(function (c) {
402
+ return {
403
+ changeId: c.changeId,
404
+ sqlHash: c.sqlHash,
405
+ proposer: c.proposer,
406
+ proposedAt: c.proposedAt,
407
+ state: c.state,
408
+ approvals: c.approvals.map(function (a) {
409
+ return { approver: a.approver, approvedAt: a.approvedAt };
410
+ }),
411
+ appliedAt: c.appliedAt,
412
+ applier: c.applier,
413
+ };
414
+ });
415
+ }
416
+
417
+ function get(changeId) {
418
+ var c = store.get(changeId);
419
+ if (!c) return null;
420
+ return structuredClone(c);
421
+ }
422
+
423
+ async function applyApproved(changeId, runner) {
424
+ if (typeof runner !== "function") {
425
+ throw new DdlChangeControlError("ddlChangeControl/bad-runner",
426
+ "applyApproved: runner must be an async function (sql) => result");
427
+ }
428
+ var change = store.get(changeId);
429
+ if (!change) {
430
+ throw new DdlChangeControlError("ddlChangeControl/unknown-change",
431
+ "applyApproved: unknown changeId '" + changeId + "'");
432
+ }
433
+ if (change.state === STATE_APPLIED) {
434
+ throw new DdlChangeControlError("ddlChangeControl/already-applied",
435
+ "applyApproved: change '" + changeId + "' is already applied");
436
+ }
437
+ if (change.state === STATE_REJECTED) {
438
+ throw new DdlChangeControlError("ddlChangeControl/already-rejected",
439
+ "applyApproved: change '" + changeId + "' is rejected");
440
+ }
441
+ if (change.approvals.length < approvers) {
442
+ _emit("ddl.change.apply_refused", {
443
+ changeId: changeId,
444
+ reason: "insufficient-approvals: " + change.approvals.length + "/" + approvers,
445
+ }, "denied");
446
+ throw new DdlChangeControlError("ddlChangeControl/insufficient-approvals",
447
+ "applyApproved: change '" + changeId + "' has " + change.approvals.length +
448
+ " approvals; threshold is " + approvers);
449
+ }
450
+ if (!_isInWindow(window, now())) {
451
+ _emit("ddl.change.apply_refused", {
452
+ changeId: changeId, reason: "window-closed",
453
+ }, "denied");
454
+ throw new DdlChangeControlError("ddlChangeControl/window-closed",
455
+ "applyApproved: change '" + changeId + "' refused - outside allowed window");
456
+ }
457
+ var currentHash = _hashSql(change.sql);
458
+ if (currentHash !== change.sqlHash) {
459
+ _emit("ddl.change.apply_refused", {
460
+ changeId: changeId, reason: "sql-tampered",
461
+ }, "denied");
462
+ throw new DdlChangeControlError("ddlChangeControl/sql-tampered",
463
+ "applyApproved: stored SQL no longer matches its hash - refusing to apply");
464
+ }
465
+ var startedAt = now();
466
+ var result;
467
+ try {
468
+ result = await runner(change.sql, {
469
+ changeId: changeId, sqlHash: change.sqlHash,
470
+ approvals: change.approvals.slice(),
471
+ });
472
+ } catch (e) {
473
+ change.state = STATE_FAILED;
474
+ change.applyError = (e && e.message) || String(e);
475
+ store.put(changeId, change);
476
+ _emit("ddl.change.applied", {
477
+ changeId: changeId, sqlHash: change.sqlHash,
478
+ applier: change.applier, durationMs: now() - startedAt,
479
+ reason: change.applyError,
480
+ }, "failure");
481
+ throw e;
482
+ }
483
+ change.state = STATE_APPLIED;
484
+ change.appliedAt = now();
485
+ change.applyDurationMs = change.appliedAt - startedAt;
486
+ change.applier = "runner";
487
+ store.put(changeId, change);
488
+ _emit("ddl.change.applied", {
489
+ changeId: changeId, sqlHash: change.sqlHash,
490
+ durationMs: change.applyDurationMs,
491
+ });
492
+ return {
493
+ changeId: changeId,
494
+ result: result,
495
+ durationMs: change.applyDurationMs,
496
+ };
497
+ }
498
+
499
+ return {
500
+ propose: propose,
501
+ approve: approve,
502
+ reject: reject,
503
+ applyApproved: applyApproved,
504
+ list: list,
505
+ get: get,
506
+ posture: posture,
507
+ approvers: approvers,
508
+ windowSpec: opts.windowSpec || null,
509
+ };
510
+ }
511
+
512
+ module.exports = {
513
+ create: create,
514
+ STATES: {
515
+ PROPOSED: STATE_PROPOSED,
516
+ APPROVED: STATE_APPROVED,
517
+ REJECTED: STATE_REJECTED,
518
+ APPLIED: STATE_APPLIED,
519
+ FAILED: STATE_FAILED,
520
+ },
521
+ POSTURES_REQUIRING_CHANGE_CONTROL: POSTURES_REQUIRING_CHANGE_CONTROL,
522
+ DdlChangeControlError: DdlChangeControlError,
523
+ };