@blamejs/core 0.4.1

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 (160) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/LICENSE +201 -0
  3. package/LTS-CALENDAR.md +29 -0
  4. package/MIGRATING.md +7 -0
  5. package/NOTICE +59 -0
  6. package/README.md +100 -0
  7. package/bin/blamejs.js +13 -0
  8. package/index.js +253 -0
  9. package/lib/api-key.js +705 -0
  10. package/lib/api-snapshot.js +335 -0
  11. package/lib/app-shutdown.js +381 -0
  12. package/lib/app.js +364 -0
  13. package/lib/atomic-file.js +525 -0
  14. package/lib/audit-chain.js +168 -0
  15. package/lib/audit-sign.js +319 -0
  16. package/lib/audit-tools.js +682 -0
  17. package/lib/audit.js +753 -0
  18. package/lib/auth/jwt.js +280 -0
  19. package/lib/auth/oauth.js +691 -0
  20. package/lib/auth/passkey.js +185 -0
  21. package/lib/auth/password.js +139 -0
  22. package/lib/auth/totp.js +17 -0
  23. package/lib/auth-header.js +81 -0
  24. package/lib/backup/bundle.js +219 -0
  25. package/lib/backup/crypto.js +174 -0
  26. package/lib/backup/index.js +490 -0
  27. package/lib/backup/manifest.js +275 -0
  28. package/lib/bundler.js +295 -0
  29. package/lib/cache.js +819 -0
  30. package/lib/chain-writer.js +234 -0
  31. package/lib/cli-helpers.js +201 -0
  32. package/lib/cli.js +1377 -0
  33. package/lib/cluster-provider-db.js +245 -0
  34. package/lib/cluster-storage.js +166 -0
  35. package/lib/cluster.js +691 -0
  36. package/lib/consent.js +222 -0
  37. package/lib/constants.js +186 -0
  38. package/lib/cookies.js +293 -0
  39. package/lib/credential-hash.js +303 -0
  40. package/lib/crypto-field.js +159 -0
  41. package/lib/crypto.js +250 -0
  42. package/lib/db-query.js +297 -0
  43. package/lib/db-schema.js +250 -0
  44. package/lib/db.js +1054 -0
  45. package/lib/deprecate.js +226 -0
  46. package/lib/dev.js +324 -0
  47. package/lib/error-page.js +424 -0
  48. package/lib/events.js +135 -0
  49. package/lib/external-db.js +422 -0
  50. package/lib/forms.js +378 -0
  51. package/lib/framework-error.js +189 -0
  52. package/lib/framework-schema.js +604 -0
  53. package/lib/handlers.js +350 -0
  54. package/lib/html-balance.js +227 -0
  55. package/lib/http-client.js +615 -0
  56. package/lib/i18n.js +780 -0
  57. package/lib/jobs.js +181 -0
  58. package/lib/lazy-require.js +48 -0
  59. package/lib/log-stream-local.js +137 -0
  60. package/lib/log-stream-webhook.js +170 -0
  61. package/lib/log-stream.js +211 -0
  62. package/lib/log.js +355 -0
  63. package/lib/mail-bounce.js +507 -0
  64. package/lib/mail.js +701 -0
  65. package/lib/metrics.js +647 -0
  66. package/lib/middleware/api-encrypt.js +553 -0
  67. package/lib/middleware/attach-user.js +156 -0
  68. package/lib/middleware/body-parser.js +883 -0
  69. package/lib/middleware/bot-guard.js +148 -0
  70. package/lib/middleware/compression.js +436 -0
  71. package/lib/middleware/cors.js +236 -0
  72. package/lib/middleware/csp-nonce.js +332 -0
  73. package/lib/middleware/csrf-protect.js +275 -0
  74. package/lib/middleware/error-handler.js +46 -0
  75. package/lib/middleware/health.js +358 -0
  76. package/lib/middleware/index.js +52 -0
  77. package/lib/middleware/rate-limit.js +319 -0
  78. package/lib/middleware/request-id.js +53 -0
  79. package/lib/middleware/require-auth.js +95 -0
  80. package/lib/middleware/security-headers.js +91 -0
  81. package/lib/migrations.js +353 -0
  82. package/lib/mtls-ca.js +333 -0
  83. package/lib/mtls-engine-default.js +285 -0
  84. package/lib/nonce-store.js +177 -0
  85. package/lib/notify.js +643 -0
  86. package/lib/ntp-check.js +178 -0
  87. package/lib/object-store/azure-blob.js +467 -0
  88. package/lib/object-store/gcs.js +469 -0
  89. package/lib/object-store/http-put.js +153 -0
  90. package/lib/object-store/index.js +140 -0
  91. package/lib/object-store/local.js +163 -0
  92. package/lib/object-store/retry.js +15 -0
  93. package/lib/object-store/sigv4.js +535 -0
  94. package/lib/observability.js +114 -0
  95. package/lib/pagination.js +371 -0
  96. package/lib/parsers/index.js +64 -0
  97. package/lib/parsers/safe-csv.js +224 -0
  98. package/lib/parsers/safe-env.js +614 -0
  99. package/lib/parsers/safe-toml.js +745 -0
  100. package/lib/parsers/safe-xml.js +379 -0
  101. package/lib/parsers/safe-yaml.js +977 -0
  102. package/lib/permissions.js +430 -0
  103. package/lib/pqc-agent.js +85 -0
  104. package/lib/pqc-gate.js +266 -0
  105. package/lib/protocol-dispatcher.js +144 -0
  106. package/lib/queue-local.js +327 -0
  107. package/lib/queue.js +430 -0
  108. package/lib/redact.js +192 -0
  109. package/lib/render.js +193 -0
  110. package/lib/request-helpers.js +178 -0
  111. package/lib/restore-bundle.js +239 -0
  112. package/lib/restore-rollback.js +254 -0
  113. package/lib/restore.js +301 -0
  114. package/lib/retry.js +329 -0
  115. package/lib/router.js +437 -0
  116. package/lib/safe-async.js +520 -0
  117. package/lib/safe-buffer.js +162 -0
  118. package/lib/safe-json.js +532 -0
  119. package/lib/safe-schema.js +1176 -0
  120. package/lib/safe-sql.js +157 -0
  121. package/lib/safe-url.js +109 -0
  122. package/lib/scheduler.js +680 -0
  123. package/lib/seeders.js +622 -0
  124. package/lib/session.js +304 -0
  125. package/lib/slug.js +243 -0
  126. package/lib/static.js +268 -0
  127. package/lib/storage.js +470 -0
  128. package/lib/subject.js +281 -0
  129. package/lib/template.js +781 -0
  130. package/lib/testing.js +621 -0
  131. package/lib/totp.js +285 -0
  132. package/lib/tracing.js +484 -0
  133. package/lib/validate-opts.js +56 -0
  134. package/lib/vault/index.js +299 -0
  135. package/lib/vault/passphrase-ops.js +311 -0
  136. package/lib/vault/passphrase-source.js +198 -0
  137. package/lib/vault/rotate.js +761 -0
  138. package/lib/vault/wrap.js +289 -0
  139. package/lib/vendor/MANIFEST.json +84 -0
  140. package/lib/vendor/argon2/argon2.cjs +466 -0
  141. package/lib/vendor/argon2/argon2.d.cts +62 -0
  142. package/lib/vendor/argon2/package.json +1 -0
  143. package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
  144. package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
  145. package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
  146. package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
  147. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
  148. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
  149. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
  150. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
  151. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
  152. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
  153. package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
  154. package/lib/vendor/noble-ciphers.cjs +9 -0
  155. package/lib/vendor/pki.cjs +181 -0
  156. package/lib/vendor/simplewebauthn-server.cjs +328 -0
  157. package/lib/webhook.js +632 -0
  158. package/lib/websocket-channels.js +413 -0
  159. package/lib/websocket.js +833 -0
  160. package/package.json +39 -0
@@ -0,0 +1,422 @@
1
+ "use strict";
2
+ /**
3
+ * External database service — pluggable wrapper for app-data DB connections.
4
+ *
5
+ * Framework state (audit_log, consent_log, _blamejs_*) stays in the local
6
+ * SQLite via b.db. This module is for APP DATA — when an operator wants to
7
+ * keep their app's domain tables in Postgres / MySQL / MongoDB / libsql /
8
+ * etc., they configure a backend here and use b.externalDb.query() instead
9
+ * of b.db.from() for those tables.
10
+ *
11
+ * Bring-your-own-client design (per "zero npm runtime deps" rule):
12
+ * The operator supplies the actual DB driver via the backend's connect/
13
+ * query/close functions. The framework adds:
14
+ * - Connection pooling (lazy-create, reuse across queries)
15
+ * - Retry on transient errors (5xx-equivalent + network)
16
+ * - Circuit breaker per-backend
17
+ * - Classification routing (which backend serves which data class)
18
+ * - Residency enforcement (boot-time validation against
19
+ * db.getDataResidency().region)
20
+ * - Audit hooks (system.externaldb.{query,transaction,connect.failure})
21
+ *
22
+ * Built-in protocol adapters (native pg-wire, libsql-HTTP, MongoDB wire)
23
+ * are not currently bundled — operators supply `connect`/`query`/`close`
24
+ * directly using their wire client of choice. When framework-bundled
25
+ * adapters land they will be available as `b.externalDb.adapters.pg`,
26
+ * `.libsqlHttp`, etc., but the bring-your-own-client API is the
27
+ * permanent surface.
28
+ *
29
+ * Public API:
30
+ * externalDb.init({ backends: { name: { connect, query, close?, ... } },
31
+ * defaultBackend? })
32
+ * externalDb.query(sql, params?, opts?) → { rows, rowCount }
33
+ * externalDb.transaction(fn, opts?) → fn's return value
34
+ * externalDb.healthCheck(backendName?) → backend status
35
+ * externalDb.listBackends()
36
+ * externalDb.shutdown()
37
+ *
38
+ * Backend config:
39
+ * {
40
+ * connect(): async () → client (returns operator's DB client)
41
+ * query(client, sql, params): async → { rows, rowCount }
42
+ * close(client): async → void
43
+ * ping(client): async → bool (optional health check)
44
+ * beginTx(client): async → void (optional; default 'BEGIN')
45
+ * commit(client): async → void (optional; default 'COMMIT')
46
+ * rollback(client): async → void (optional; default 'ROLLBACK')
47
+ * pool: { min: 1, max: 10, idleTimeoutMs: C.TIME.minutes(1) }
48
+ * classifications: ['personal' | 'operational' | 'public' | <custom>]
49
+ * residencyTag: 'EU' | 'US' | ...
50
+ * retry, breaker
51
+ * }
52
+ */
53
+ var retryHelper = require("./retry");
54
+ var C = require("./constants");
55
+ var lazyRequire = require("./lazy-require");
56
+ var { ExternalDbError } = require("./framework-error");
57
+
58
+ var audit = lazyRequire(function () { return require("./audit"); });
59
+ var db = lazyRequire(function () { return require("./db"); });
60
+
61
+ var _err = ExternalDbError.factory;
62
+
63
+ var initialized = false;
64
+ var backends = {};
65
+ var defaultBackend = null;
66
+
67
+ // ---- Pool ----
68
+ //
69
+ // Per-backend pool with lazy creation + LRU-ish reuse. Connections returned
70
+ // to the pool when query/transaction completes; idle connections expire.
71
+
72
+ class Pool {
73
+ constructor(name, config) {
74
+ this.name = name;
75
+ this.config = Object.assign({ min: 1, max: 10, idleTimeoutMs: C.TIME.minutes(1) }, config.pool || {});
76
+ this.connect = config.connect;
77
+ this.close = config.close || function () { return Promise.resolve(); };
78
+ this.idle = []; // [{ client, lastUsedAt }]
79
+ this.active = 0; // count of in-use clients
80
+ this.waiters = []; // queued acquisitions when at max
81
+ this._reaper = setInterval(this._reapIdle.bind(this), C.TIME.seconds(10));
82
+ this._reaper.unref();
83
+ }
84
+
85
+ async acquire() {
86
+ if (this.idle.length > 0) {
87
+ var entry = this.idle.pop();
88
+ this.active += 1;
89
+ return entry.client;
90
+ }
91
+ if (this.active < this.config.max) {
92
+ this.active += 1;
93
+ try {
94
+ return await this.connect();
95
+ } catch (e) {
96
+ this.active -= 1;
97
+ throw e;
98
+ }
99
+ }
100
+ // At max — wait for a release
101
+ return new Promise((function (self) {
102
+ return function (resolve, reject) {
103
+ var entry = { resolve: resolve, reject: reject };
104
+ self.waiters.push(entry);
105
+ };
106
+ })(this));
107
+ }
108
+
109
+ release(client) {
110
+ this.active -= 1;
111
+ if (this.waiters.length > 0) {
112
+ var w = this.waiters.shift();
113
+ this.active += 1;
114
+ w.resolve(client);
115
+ return;
116
+ }
117
+ this.idle.push({ client: client, lastUsedAt: Date.now() });
118
+ }
119
+
120
+ async destroy(client) {
121
+ this.active -= 1;
122
+ try { await this.close(client); } catch (_e) { /* best effort */ }
123
+ if (this.waiters.length > 0) {
124
+ var w = this.waiters.shift();
125
+ this.acquire().then(w.resolve, w.reject);
126
+ }
127
+ }
128
+
129
+ _reapIdle() {
130
+ var now = Date.now();
131
+ var keep = [];
132
+ var self = this;
133
+ this.idle.forEach(function (entry) {
134
+ if ((now - entry.lastUsedAt) >= self.config.idleTimeoutMs) {
135
+ Promise.resolve().then(function () { return self.close(entry.client); }).catch(function () {});
136
+ } else {
137
+ keep.push(entry);
138
+ }
139
+ });
140
+ this.idle = keep;
141
+ }
142
+
143
+ async drain() {
144
+ if (this._reaper) { clearInterval(this._reaper); this._reaper = null; }
145
+ var idleClients = this.idle.map(function (e) { return e.client; });
146
+ this.idle = [];
147
+ var self = this;
148
+ await Promise.all(idleClients.map(function (c) {
149
+ return Promise.resolve().then(function () { return self.close(c); }).catch(function () {});
150
+ }));
151
+ this.waiters.forEach(function (w) { w.reject(_err("POOL_DRAINED", "pool is shutting down", true)); });
152
+ this.waiters = [];
153
+ }
154
+
155
+ stats() {
156
+ return { active: this.active, idle: this.idle.length, waiters: this.waiters.length };
157
+ }
158
+ }
159
+
160
+ // ---- Init ----
161
+
162
+ function init(opts) {
163
+ if (initialized) return;
164
+ if (!opts || !opts.backends) throw new Error("externalDb.init({ backends }) is required");
165
+
166
+ backends = {};
167
+ for (var name in opts.backends) {
168
+ var cfg = opts.backends[name];
169
+ if (typeof cfg.connect !== "function") {
170
+ throw _err("INVALID_CONFIG", "backend '" + name + "' missing connect() function", true);
171
+ }
172
+ if (typeof cfg.query !== "function") {
173
+ throw _err("INVALID_CONFIG", "backend '" + name + "' missing query() function", true);
174
+ }
175
+ backends[name] = {
176
+ name: name,
177
+ pool: new Pool(name, cfg),
178
+ query: cfg.query,
179
+ ping: cfg.ping || null,
180
+ beginTx: cfg.beginTx || function (client) { return cfg.query(client, "BEGIN", []); },
181
+ commit: cfg.commit || function (client) { return cfg.query(client, "COMMIT", []); },
182
+ rollback: cfg.rollback || function (client) { return cfg.query(client, "ROLLBACK", []); },
183
+ classifications: Array.isArray(cfg.classifications) && cfg.classifications.length > 0
184
+ ? cfg.classifications.slice()
185
+ : ["*"],
186
+ residencyTag: cfg.residencyTag || "unrestricted",
187
+ breaker: new retryHelper.CircuitBreaker("externalDb:" + name, cfg.breaker),
188
+ retryConfig: cfg.retry || null,
189
+ };
190
+ }
191
+
192
+ defaultBackend = opts.defaultBackend || Object.keys(backends)[0];
193
+ _validateResidency();
194
+ initialized = true;
195
+ }
196
+
197
+ function _validateResidency() {
198
+ var residency;
199
+ try { residency = db().getDataResidency(); } catch (_e) { residency = null; }
200
+ if (!residency || !residency.region) return;
201
+
202
+ var allowed = [residency.region].concat(residency.allowedStorageRegions || []);
203
+ for (var name in backends) {
204
+ var b = backends[name];
205
+ var serves = b.classifications.indexOf("*") !== -1 || b.classifications.indexOf("personal") !== -1;
206
+ if (!serves) continue;
207
+ if (allowed.indexOf(b.residencyTag) === -1) {
208
+ throw _err("RESIDENCY_VIOLATION",
209
+ "externalDb backend '" + name + "' serves 'personal' data with residencyTag '" +
210
+ b.residencyTag + "' but app's dataResidency.region is '" + residency.region + "'",
211
+ true);
212
+ }
213
+ }
214
+ }
215
+
216
+ // ---- Backend selection ----
217
+
218
+ function _pickBackend(opts) {
219
+ opts = opts || {};
220
+ if (opts.backend) {
221
+ var b = backends[opts.backend];
222
+ if (!b) throw _err("UNKNOWN_BACKEND", "no backend named '" + opts.backend + "'", true);
223
+ if (opts.classification && !_servesClassification(b, opts.classification)) {
224
+ throw _err("CLASSIFICATION_MISMATCH",
225
+ "backend '" + opts.backend + "' does not serve classification '" + opts.classification + "'", true);
226
+ }
227
+ return b;
228
+ }
229
+ var classification = opts.classification;
230
+ if (classification) {
231
+ for (var name in backends) {
232
+ if (_servesClassification(backends[name], classification)) return backends[name];
233
+ }
234
+ throw _err("NO_BACKEND_FOR_CLASSIFICATION",
235
+ "no backend serves classification '" + classification + "'", true);
236
+ }
237
+ return backends[defaultBackend] || null;
238
+ }
239
+
240
+ function _servesClassification(b, cls) {
241
+ return b.classifications.indexOf("*") !== -1 || b.classifications.indexOf(cls) !== -1;
242
+ }
243
+
244
+ // ---- Public API ----
245
+
246
+ async function query(sql, params, opts) {
247
+ _requireInit();
248
+ opts = opts || {};
249
+ var b = _pickBackend(opts);
250
+
251
+ var t0 = Date.now();
252
+ try {
253
+ var result = await retryHelper.withRetry(function () {
254
+ return b.breaker.wrap(async function () {
255
+ var client = await b.pool.acquire();
256
+ try {
257
+ var res = await b.query(client, sql, params || []);
258
+ b.pool.release(client);
259
+ return res;
260
+ } catch (e) {
261
+ // Connection-level errors → destroy the client; query errors →
262
+ // release back to the pool. Heuristic: any error with a code
263
+ // looking like a network/connection issue → destroy.
264
+ if (e && (e.code === "ECONNRESET" || e.code === "ECONNREFUSED" ||
265
+ e.code === "ETIMEDOUT" || e.code === "ENOTFOUND" ||
266
+ e.code === "EPIPE")) {
267
+ await b.pool.destroy(client);
268
+ } else {
269
+ b.pool.release(client);
270
+ }
271
+ throw e;
272
+ }
273
+ });
274
+ }, b.retryConfig);
275
+
276
+ _emit("system.externaldb.query", "success", {
277
+ backend: b.name,
278
+ durationMs: Date.now() - t0,
279
+ classification: opts.classification || null,
280
+ rowCount: result && result.rowCount,
281
+ // SQL is NOT logged by default — may contain sensitive literal values
282
+ // even in parameterized queries. Operators who want SQL in audit
283
+ // metadata pass opts.includeSqlInAudit: true (then sealed via
284
+ // field-crypto on the audit row).
285
+ sql: opts.includeSqlInAudit ? sql : null,
286
+ });
287
+ return result;
288
+ } catch (e) {
289
+ _emit("system.externaldb.query", "failure", {
290
+ backend: b.name,
291
+ durationMs: Date.now() - t0,
292
+ classification: opts.classification || null,
293
+ errorCode: e.code || null,
294
+ }, (e && e.message) || String(e));
295
+ throw e;
296
+ }
297
+ }
298
+
299
+ async function transaction(fn, opts) {
300
+ _requireInit();
301
+ if (typeof fn !== "function") throw _err("INVALID_FN", "transaction requires a function", true);
302
+ opts = opts || {};
303
+ var b = _pickBackend(opts);
304
+
305
+ var t0 = Date.now();
306
+ return await b.breaker.wrap(async function () {
307
+ var client = await b.pool.acquire();
308
+ var txClient = {
309
+ query: function (sql, params) { return b.query(client, sql, params || []); },
310
+ };
311
+ var committed = false;
312
+ try {
313
+ await b.beginTx(client);
314
+ var result = await fn(txClient);
315
+ await b.commit(client);
316
+ committed = true;
317
+ _emit("system.externaldb.transaction", "success", {
318
+ backend: b.name, durationMs: Date.now() - t0, classification: opts.classification || null,
319
+ });
320
+ return result;
321
+ } catch (e) {
322
+ try { if (!committed) await b.rollback(client); } catch (_e) { /* best effort */ }
323
+ _emit("system.externaldb.transaction", "failure", {
324
+ backend: b.name, durationMs: Date.now() - t0, classification: opts.classification || null,
325
+ errorCode: e.code || null,
326
+ }, (e && e.message) || String(e));
327
+ throw e;
328
+ } finally {
329
+ b.pool.release(client);
330
+ }
331
+ });
332
+ }
333
+
334
+ async function healthCheck(backendName) {
335
+ _requireInit();
336
+ if (backendName) {
337
+ return _pingBackend(backends[backendName]);
338
+ }
339
+ var out = {};
340
+ for (var name in backends) {
341
+ out[name] = await _pingBackend(backends[name]);
342
+ }
343
+ return out;
344
+ }
345
+
346
+ async function _pingBackend(b) {
347
+ if (!b) return { ok: false, error: "unknown backend" };
348
+ try {
349
+ var client = await b.pool.acquire();
350
+ try {
351
+ if (b.ping) await b.ping(client);
352
+ else await b.query(client, "SELECT 1", []);
353
+ b.pool.release(client);
354
+ return { ok: true, breakerState: b.breaker.getState(), pool: b.pool.stats() };
355
+ } catch (e) {
356
+ await b.pool.destroy(client);
357
+ return { ok: false, error: e.message, breakerState: b.breaker.getState() };
358
+ }
359
+ } catch (e) {
360
+ return { ok: false, error: e.message, breakerState: b.breaker.getState() };
361
+ }
362
+ }
363
+
364
+ function listBackends() {
365
+ if (!initialized) return [];
366
+ return Object.keys(backends).map(function (name) {
367
+ var b = backends[name];
368
+ return {
369
+ name: name,
370
+ classifications: b.classifications.slice(),
371
+ residencyTag: b.residencyTag,
372
+ breakerState: b.breaker.getState(),
373
+ pool: b.pool.stats(),
374
+ };
375
+ });
376
+ }
377
+
378
+ async function shutdown() {
379
+ if (!initialized) return;
380
+ for (var name in backends) {
381
+ try { await backends[name].pool.drain(); } catch (_e) { /* best effort */ }
382
+ }
383
+ backends = {};
384
+ defaultBackend = null;
385
+ initialized = false;
386
+ }
387
+
388
+ // Fire-and-forget audit emission. We CANNOT await this in cluster mode:
389
+ // audit storage routes back through external-db when cluster mode is
390
+ // active, so awaiting would create a recursive dependency (every audit
391
+ // row insert triggers an external-db query which would await another
392
+ // audit row insert). Tests that need audit-row durability before reading
393
+ // audit_log should flush microtasks explicitly.
394
+ function _emit(action, outcome, metadata, reason) {
395
+ audit().safeEmit({ action: action, outcome: outcome, reason: reason, metadata: metadata });
396
+ }
397
+
398
+ function _requireInit() {
399
+ if (!initialized) throw _err("NOT_INITIALIZED", "externalDb.init() must be called first", true);
400
+ }
401
+
402
+ function _resetForTest() {
403
+ Object.keys(backends).forEach(function (n) {
404
+ try { backends[n].pool.drain(); } catch (_e) {}
405
+ });
406
+ backends = {};
407
+ defaultBackend = null;
408
+ initialized = false;
409
+ audit.reset();
410
+ db.reset();
411
+ }
412
+
413
+ module.exports = {
414
+ init: init,
415
+ query: query,
416
+ transaction: transaction,
417
+ healthCheck: healthCheck,
418
+ listBackends: listBackends,
419
+ shutdown: shutdown,
420
+ Pool: Pool,
421
+ _resetForTest: _resetForTest,
422
+ };