@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.
- package/CHANGELOG.md +230 -0
- package/LICENSE +201 -0
- package/LTS-CALENDAR.md +29 -0
- package/MIGRATING.md +7 -0
- package/NOTICE +59 -0
- package/README.md +100 -0
- package/bin/blamejs.js +13 -0
- package/index.js +253 -0
- package/lib/api-key.js +705 -0
- package/lib/api-snapshot.js +335 -0
- package/lib/app-shutdown.js +381 -0
- package/lib/app.js +364 -0
- package/lib/atomic-file.js +525 -0
- package/lib/audit-chain.js +168 -0
- package/lib/audit-sign.js +319 -0
- package/lib/audit-tools.js +682 -0
- package/lib/audit.js +753 -0
- package/lib/auth/jwt.js +280 -0
- package/lib/auth/oauth.js +691 -0
- package/lib/auth/passkey.js +185 -0
- package/lib/auth/password.js +139 -0
- package/lib/auth/totp.js +17 -0
- package/lib/auth-header.js +81 -0
- package/lib/backup/bundle.js +219 -0
- package/lib/backup/crypto.js +174 -0
- package/lib/backup/index.js +490 -0
- package/lib/backup/manifest.js +275 -0
- package/lib/bundler.js +295 -0
- package/lib/cache.js +819 -0
- package/lib/chain-writer.js +234 -0
- package/lib/cli-helpers.js +201 -0
- package/lib/cli.js +1377 -0
- package/lib/cluster-provider-db.js +245 -0
- package/lib/cluster-storage.js +166 -0
- package/lib/cluster.js +691 -0
- package/lib/consent.js +222 -0
- package/lib/constants.js +186 -0
- package/lib/cookies.js +293 -0
- package/lib/credential-hash.js +303 -0
- package/lib/crypto-field.js +159 -0
- package/lib/crypto.js +250 -0
- package/lib/db-query.js +297 -0
- package/lib/db-schema.js +250 -0
- package/lib/db.js +1054 -0
- package/lib/deprecate.js +226 -0
- package/lib/dev.js +324 -0
- package/lib/error-page.js +424 -0
- package/lib/events.js +135 -0
- package/lib/external-db.js +422 -0
- package/lib/forms.js +378 -0
- package/lib/framework-error.js +189 -0
- package/lib/framework-schema.js +604 -0
- package/lib/handlers.js +350 -0
- package/lib/html-balance.js +227 -0
- package/lib/http-client.js +615 -0
- package/lib/i18n.js +780 -0
- package/lib/jobs.js +181 -0
- package/lib/lazy-require.js +48 -0
- package/lib/log-stream-local.js +137 -0
- package/lib/log-stream-webhook.js +170 -0
- package/lib/log-stream.js +211 -0
- package/lib/log.js +355 -0
- package/lib/mail-bounce.js +507 -0
- package/lib/mail.js +701 -0
- package/lib/metrics.js +647 -0
- package/lib/middleware/api-encrypt.js +553 -0
- package/lib/middleware/attach-user.js +156 -0
- package/lib/middleware/body-parser.js +883 -0
- package/lib/middleware/bot-guard.js +148 -0
- package/lib/middleware/compression.js +436 -0
- package/lib/middleware/cors.js +236 -0
- package/lib/middleware/csp-nonce.js +332 -0
- package/lib/middleware/csrf-protect.js +275 -0
- package/lib/middleware/error-handler.js +46 -0
- package/lib/middleware/health.js +358 -0
- package/lib/middleware/index.js +52 -0
- package/lib/middleware/rate-limit.js +319 -0
- package/lib/middleware/request-id.js +53 -0
- package/lib/middleware/require-auth.js +95 -0
- package/lib/middleware/security-headers.js +91 -0
- package/lib/migrations.js +353 -0
- package/lib/mtls-ca.js +333 -0
- package/lib/mtls-engine-default.js +285 -0
- package/lib/nonce-store.js +177 -0
- package/lib/notify.js +643 -0
- package/lib/ntp-check.js +178 -0
- package/lib/object-store/azure-blob.js +467 -0
- package/lib/object-store/gcs.js +469 -0
- package/lib/object-store/http-put.js +153 -0
- package/lib/object-store/index.js +140 -0
- package/lib/object-store/local.js +163 -0
- package/lib/object-store/retry.js +15 -0
- package/lib/object-store/sigv4.js +535 -0
- package/lib/observability.js +114 -0
- package/lib/pagination.js +371 -0
- package/lib/parsers/index.js +64 -0
- package/lib/parsers/safe-csv.js +224 -0
- package/lib/parsers/safe-env.js +614 -0
- package/lib/parsers/safe-toml.js +745 -0
- package/lib/parsers/safe-xml.js +379 -0
- package/lib/parsers/safe-yaml.js +977 -0
- package/lib/permissions.js +430 -0
- package/lib/pqc-agent.js +85 -0
- package/lib/pqc-gate.js +266 -0
- package/lib/protocol-dispatcher.js +144 -0
- package/lib/queue-local.js +327 -0
- package/lib/queue.js +430 -0
- package/lib/redact.js +192 -0
- package/lib/render.js +193 -0
- package/lib/request-helpers.js +178 -0
- package/lib/restore-bundle.js +239 -0
- package/lib/restore-rollback.js +254 -0
- package/lib/restore.js +301 -0
- package/lib/retry.js +329 -0
- package/lib/router.js +437 -0
- package/lib/safe-async.js +520 -0
- package/lib/safe-buffer.js +162 -0
- package/lib/safe-json.js +532 -0
- package/lib/safe-schema.js +1176 -0
- package/lib/safe-sql.js +157 -0
- package/lib/safe-url.js +109 -0
- package/lib/scheduler.js +680 -0
- package/lib/seeders.js +622 -0
- package/lib/session.js +304 -0
- package/lib/slug.js +243 -0
- package/lib/static.js +268 -0
- package/lib/storage.js +470 -0
- package/lib/subject.js +281 -0
- package/lib/template.js +781 -0
- package/lib/testing.js +621 -0
- package/lib/totp.js +285 -0
- package/lib/tracing.js +484 -0
- package/lib/validate-opts.js +56 -0
- package/lib/vault/index.js +299 -0
- package/lib/vault/passphrase-ops.js +311 -0
- package/lib/vault/passphrase-source.js +198 -0
- package/lib/vault/rotate.js +761 -0
- package/lib/vault/wrap.js +289 -0
- package/lib/vendor/MANIFEST.json +84 -0
- package/lib/vendor/argon2/argon2.cjs +466 -0
- package/lib/vendor/argon2/argon2.d.cts +62 -0
- package/lib/vendor/argon2/package.json +1 -0
- package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
- package/lib/vendor/noble-ciphers.cjs +9 -0
- package/lib/vendor/pki.cjs +181 -0
- package/lib/vendor/simplewebauthn-server.cjs +328 -0
- package/lib/webhook.js +632 -0
- package/lib/websocket-channels.js +413 -0
- package/lib/websocket.js +833 -0
- 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
|
+
};
|