@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,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Default cluster-coordination provider — DB-row-based leader election.
|
|
4
|
+
*
|
|
5
|
+
* Uses an externalDb backend (already configured via b.externalDb.init)
|
|
6
|
+
* as the coordination point. A single row in `_blamejs_leader` holds
|
|
7
|
+
* the current lease; acquireLease is `INSERT ... ON CONFLICT ... DO
|
|
8
|
+
* UPDATE WHERE expiresAt < now() RETURNING ...`, which is atomic in
|
|
9
|
+
* Postgres and SQLite.
|
|
10
|
+
*
|
|
11
|
+
* Fencing tokens: every successful acquire bumps a monotonic integer.
|
|
12
|
+
* Leader-only DB writes include the current token; the audit-tip row's
|
|
13
|
+
* CHECK constraint rejects any incoming token below the stored one,
|
|
14
|
+
* which fences out a partitioned old leader even if its application-
|
|
15
|
+
* layer `_requireLeader()` gate somehow allowed the call through.
|
|
16
|
+
*
|
|
17
|
+
* Dialects: Postgres and SQLite use identical surface SQL. MySQL's
|
|
18
|
+
* `ON DUPLICATE KEY UPDATE` doesn't support a WHERE clause; a MySQL
|
|
19
|
+
* fallback (SELECT ... FOR UPDATE inside an explicit transaction) is
|
|
20
|
+
* not yet implemented and operators on MySQL must supply their own
|
|
21
|
+
* provider until it lands.
|
|
22
|
+
*
|
|
23
|
+
* Public API:
|
|
24
|
+
* create({ externalDbBackend, dialect? }) → provider instance
|
|
25
|
+
*
|
|
26
|
+
* Provider instance:
|
|
27
|
+
* ensureSchema() async; idempotent CREATE
|
|
28
|
+
* TABLE + ALTER for
|
|
29
|
+
* endpoint migration
|
|
30
|
+
* acquireLease(nodeId, leaseTtlMs, opts?) async; → Lease | null.
|
|
31
|
+
* opts.endpoint persists
|
|
32
|
+
* in the row for discovery.
|
|
33
|
+
* renewLease(lease, opts?) async; → Lease (throws on
|
|
34
|
+
* takeover). opts.endpoint
|
|
35
|
+
* refreshes the row.
|
|
36
|
+
* releaseLease(lease) async; → void
|
|
37
|
+
* currentLeader() async; →
|
|
38
|
+
* { nodeId, leaseExpiresAt,
|
|
39
|
+
* fencingToken,
|
|
40
|
+
* endpoint } | null
|
|
41
|
+
*
|
|
42
|
+
* Lease object shape:
|
|
43
|
+
* { nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint }
|
|
44
|
+
*/
|
|
45
|
+
var { generateToken } = require("./crypto");
|
|
46
|
+
var externalDb = require("./external-db");
|
|
47
|
+
var { ClusterProviderError } = require("./framework-error");
|
|
48
|
+
|
|
49
|
+
var _err = ClusterProviderError.factory;
|
|
50
|
+
|
|
51
|
+
function create(config) {
|
|
52
|
+
if (!config || !config.externalDbBackend) {
|
|
53
|
+
throw _err("INVALID_CONFIG",
|
|
54
|
+
"cluster-provider-db requires { externalDbBackend: <name> }", true);
|
|
55
|
+
}
|
|
56
|
+
var backendName = config.externalDbBackend;
|
|
57
|
+
var dialect = (config.dialect || "postgres").toLowerCase();
|
|
58
|
+
if (dialect !== "postgres" && dialect !== "sqlite") {
|
|
59
|
+
throw _err("UNSUPPORTED_DIALECT",
|
|
60
|
+
"cluster-provider-db dialect must be 'postgres' or 'sqlite' (got: " + dialect + ")",
|
|
61
|
+
true);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Postgres uses $1/$2 placeholders; SQLite accepts the same so we use
|
|
65
|
+
// them uniformly. If a future dialect needs ?-placeholders, swap here.
|
|
66
|
+
function _placeholder(n) { return "$" + n; }
|
|
67
|
+
|
|
68
|
+
function _q(sql, params) {
|
|
69
|
+
return externalDb.query(sql, params || [], { backend: backendName });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function ensureSchema() {
|
|
73
|
+
// Postgres: BIGINT for ms-precision timestamps. SQLite: INTEGER works
|
|
74
|
+
// for either; pick INTEGER so the same DDL parses on both.
|
|
75
|
+
var intType = dialect === "postgres" ? "BIGINT" : "INTEGER";
|
|
76
|
+
|
|
77
|
+
await _q(
|
|
78
|
+
"CREATE TABLE IF NOT EXISTS _blamejs_leader (" +
|
|
79
|
+
" scope TEXT PRIMARY KEY," +
|
|
80
|
+
" nodeId TEXT NOT NULL," +
|
|
81
|
+
" leaseId TEXT NOT NULL," +
|
|
82
|
+
" acquiredAt " + intType + " NOT NULL," +
|
|
83
|
+
" expiresAt " + intType + " NOT NULL," +
|
|
84
|
+
" fencingToken " + intType + " NOT NULL," +
|
|
85
|
+
" endpoint TEXT," +
|
|
86
|
+
" CHECK (scope = 'leader')" +
|
|
87
|
+
")"
|
|
88
|
+
);
|
|
89
|
+
// Migration for installs that pre-date the endpoint column. Both
|
|
90
|
+
// Postgres (≥9.6) and SQLite (≥3.35, March 2021) support ADD COLUMN
|
|
91
|
+
// IF NOT EXISTS, but we go through try/catch to keep the path
|
|
92
|
+
// dialect-version-agnostic — the only "expected" failure here is
|
|
93
|
+
// "column already exists," which we swallow.
|
|
94
|
+
try {
|
|
95
|
+
await _q("ALTER TABLE _blamejs_leader ADD COLUMN endpoint TEXT");
|
|
96
|
+
} catch (_e) { /* column already exists — fine */ }
|
|
97
|
+
|
|
98
|
+
// _blamejs_cluster_state — single-row consistency check used to
|
|
99
|
+
// detect vault-key drift across cluster nodes. Same single-row
|
|
100
|
+
// invariant pattern as _blamejs_leader (PRIMARY KEY on a
|
|
101
|
+
// constant-valued scope column + CHECK).
|
|
102
|
+
await _q(
|
|
103
|
+
"CREATE TABLE IF NOT EXISTS _blamejs_cluster_state (" +
|
|
104
|
+
" scope TEXT PRIMARY KEY," +
|
|
105
|
+
" vaultKeyFp TEXT NOT NULL," +
|
|
106
|
+
" recordedAt " + intType + " NOT NULL," +
|
|
107
|
+
" recordedByNode TEXT NOT NULL," +
|
|
108
|
+
" CHECK (scope = 'state')" +
|
|
109
|
+
")"
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function acquireLease(nodeId, leaseTtlMs, opts) {
|
|
114
|
+
if (!nodeId) throw _err("INVALID_NODE_ID", "nodeId required", true);
|
|
115
|
+
if (typeof leaseTtlMs !== "number" || leaseTtlMs <= 0) {
|
|
116
|
+
throw _err("INVALID_TTL", "leaseTtlMs must be a positive number", true);
|
|
117
|
+
}
|
|
118
|
+
var endpoint = (opts && opts.endpoint) || null;
|
|
119
|
+
var leaseId = generateToken(16);
|
|
120
|
+
var nowMs = Date.now();
|
|
121
|
+
var expiresAt = nowMs + leaseTtlMs;
|
|
122
|
+
|
|
123
|
+
// Atomic acquire: insert if no row, OR steal if existing row's
|
|
124
|
+
// expiresAt has passed. Bump fencingToken on every successful
|
|
125
|
+
// mutation. RETURNING gives us the post-write state — which we
|
|
126
|
+
// compare to our nodeId to know whether we won.
|
|
127
|
+
var sql =
|
|
128
|
+
"INSERT INTO _blamejs_leader " +
|
|
129
|
+
" (scope, nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint) " +
|
|
130
|
+
"VALUES " +
|
|
131
|
+
" ('leader', " + _placeholder(1) + ", " + _placeholder(2) + ", " +
|
|
132
|
+
" " + _placeholder(3) + ", " + _placeholder(4) + ", 1, " + _placeholder(5) + ") " +
|
|
133
|
+
"ON CONFLICT (scope) DO UPDATE SET " +
|
|
134
|
+
" nodeId = EXCLUDED.nodeId," +
|
|
135
|
+
" leaseId = EXCLUDED.leaseId," +
|
|
136
|
+
" acquiredAt = EXCLUDED.acquiredAt," +
|
|
137
|
+
" expiresAt = EXCLUDED.expiresAt," +
|
|
138
|
+
" fencingToken = _blamejs_leader.fencingToken + 1," +
|
|
139
|
+
" endpoint = EXCLUDED.endpoint " +
|
|
140
|
+
"WHERE _blamejs_leader.expiresAt < " + _placeholder(6) + " " +
|
|
141
|
+
"RETURNING nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint";
|
|
142
|
+
|
|
143
|
+
var result = await _q(sql, [nodeId, leaseId, nowMs, expiresAt, endpoint, nowMs]);
|
|
144
|
+
|
|
145
|
+
if (!result.rows || result.rows.length === 0) {
|
|
146
|
+
// The WHERE clause excluded the row — someone else still holds.
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
var row = result.rows[0];
|
|
150
|
+
if (row.nodeId !== nodeId || row.leaseId !== leaseId) {
|
|
151
|
+
// Another node won the race (RETURNING showed their values, not ours).
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
nodeId: row.nodeId,
|
|
156
|
+
leaseId: row.leaseId,
|
|
157
|
+
acquiredAt: Number(row.acquiredAt),
|
|
158
|
+
expiresAt: Number(row.expiresAt),
|
|
159
|
+
fencingToken: Number(row.fencingToken),
|
|
160
|
+
endpoint: row.endpoint || null,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function renewLease(lease, opts) {
|
|
165
|
+
if (!lease || !lease.leaseId) throw _err("INVALID_LEASE", "lease required", true);
|
|
166
|
+
var nowMs = Date.now();
|
|
167
|
+
var newExpiresAt = nowMs + (lease.expiresAt - lease.acquiredAt);
|
|
168
|
+
// opts.endpoint, when provided, refreshes the stored endpoint so
|
|
169
|
+
// operators who hot-update their config see the discovery row catch
|
|
170
|
+
// up. Default = preserve whatever was stored at acquire time.
|
|
171
|
+
var endpoint = (opts && opts.endpoint !== undefined) ? opts.endpoint : lease.endpoint || null;
|
|
172
|
+
|
|
173
|
+
// Match on (nodeId, leaseId) so a takeover is detectable: if our
|
|
174
|
+
// leaseId is no longer in the row, 0 rows update and we throw
|
|
175
|
+
// NotLeader. Don't bump fencingToken on renewal — only on a fresh
|
|
176
|
+
// acquire.
|
|
177
|
+
var sql =
|
|
178
|
+
"UPDATE _blamejs_leader SET " +
|
|
179
|
+
" expiresAt = " + _placeholder(1) + "," +
|
|
180
|
+
" endpoint = " + _placeholder(2) + " " +
|
|
181
|
+
"WHERE scope = 'leader' AND nodeId = " + _placeholder(3) +
|
|
182
|
+
" AND leaseId = " + _placeholder(4) + " " +
|
|
183
|
+
"RETURNING nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint";
|
|
184
|
+
|
|
185
|
+
var result = await _q(sql, [newExpiresAt, endpoint, lease.nodeId, lease.leaseId]);
|
|
186
|
+
if (!result.rows || result.rows.length === 0) {
|
|
187
|
+
throw _err("LEASE_LOST",
|
|
188
|
+
"lease for node '" + lease.nodeId + "' was taken over (renewal rejected)",
|
|
189
|
+
false);
|
|
190
|
+
}
|
|
191
|
+
var row = result.rows[0];
|
|
192
|
+
return {
|
|
193
|
+
nodeId: row.nodeId,
|
|
194
|
+
leaseId: row.leaseId,
|
|
195
|
+
acquiredAt: Number(row.acquiredAt),
|
|
196
|
+
expiresAt: Number(row.expiresAt),
|
|
197
|
+
fencingToken: Number(row.fencingToken),
|
|
198
|
+
endpoint: row.endpoint || null,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function releaseLease(lease) {
|
|
203
|
+
if (!lease || !lease.leaseId) return;
|
|
204
|
+
// Clear our row so the next acquire wins immediately. Match on
|
|
205
|
+
// leaseId so a takeover-then-release race doesn't clear someone
|
|
206
|
+
// else's lease.
|
|
207
|
+
var sql =
|
|
208
|
+
"UPDATE _blamejs_leader SET " +
|
|
209
|
+
" expiresAt = 0 " +
|
|
210
|
+
"WHERE scope = 'leader' AND nodeId = " + _placeholder(1) +
|
|
211
|
+
" AND leaseId = " + _placeholder(2);
|
|
212
|
+
await _q(sql, [lease.nodeId, lease.leaseId]);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function currentLeader() {
|
|
216
|
+
var result = await _q(
|
|
217
|
+
"SELECT nodeId, expiresAt, fencingToken, endpoint FROM _blamejs_leader " +
|
|
218
|
+
"WHERE scope = 'leader'"
|
|
219
|
+
);
|
|
220
|
+
if (!result.rows || result.rows.length === 0) return null;
|
|
221
|
+
var row = result.rows[0];
|
|
222
|
+
if (Number(row.expiresAt) < Date.now()) return null;
|
|
223
|
+
return {
|
|
224
|
+
nodeId: row.nodeId,
|
|
225
|
+
leaseExpiresAt: Number(row.expiresAt),
|
|
226
|
+
fencingToken: Number(row.fencingToken),
|
|
227
|
+
endpoint: row.endpoint || null,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
kind: "db",
|
|
233
|
+
backendName: backendName,
|
|
234
|
+
dialect: dialect,
|
|
235
|
+
ensureSchema: ensureSchema,
|
|
236
|
+
acquireLease: acquireLease,
|
|
237
|
+
renewLease: renewLease,
|
|
238
|
+
releaseLease: releaseLease,
|
|
239
|
+
currentLeader: currentLeader,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = {
|
|
244
|
+
create: create,
|
|
245
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Framework-state SQL dispatch — runs against local SQLite in single-
|
|
4
|
+
* node mode and against external-db in cluster mode.
|
|
5
|
+
*
|
|
6
|
+
* audit / consent / sessions / queue / subject all read and write the
|
|
7
|
+
* framework's own tables (audit_log, consent_log, …). In single-node
|
|
8
|
+
* mode those tables live in the framework's own SQLite (lib/db.js).
|
|
9
|
+
* In cluster mode they live in the operator-supplied external-db with
|
|
10
|
+
* a `_blamejs_` prefix to avoid colliding with app tables.
|
|
11
|
+
*
|
|
12
|
+
* This module is the dispatch primitive. Callers write SQL once using
|
|
13
|
+
* unprefixed table names + `?` placeholders; the dispatcher translates
|
|
14
|
+
* to the active backend's flavor:
|
|
15
|
+
*
|
|
16
|
+
* single-node local SQLite via db().prepare(sql).run/get/all(...)
|
|
17
|
+
* cluster (sqlite) externalDb.query(sql, params) ? placeholders
|
|
18
|
+
* cluster (postgres) externalDb.query(translated, params) $1, $2, …
|
|
19
|
+
*
|
|
20
|
+
* Tables are translated through frameworkSchema.tableName so callers
|
|
21
|
+
* use logical names (audit_log) and the resolved name is automatically
|
|
22
|
+
* prefixed in cluster mode (_blamejs_audit_log).
|
|
23
|
+
*
|
|
24
|
+
* The dispatcher is async-only — the operator's external-db driver
|
|
25
|
+
* is async, and even local-SQLite calls return a resolved Promise to
|
|
26
|
+
* keep the call shape uniform. Callers `await` this module's methods;
|
|
27
|
+
* audit / consent / queue / session / subject all thread `async` /
|
|
28
|
+
* `await` through their own surfaces to match.
|
|
29
|
+
*
|
|
30
|
+
* Public API:
|
|
31
|
+
* await execute(sql, params?) { rows, rowCount }
|
|
32
|
+
* tableName(local) external-db prefixed name (or unchanged
|
|
33
|
+
* in single-node mode)
|
|
34
|
+
* placeholderize(sql, dialect) `?` to `$N` for postgres; passthrough
|
|
35
|
+
* for sqlite
|
|
36
|
+
* resolveTables(sql) rewrites bare unprefixed table names
|
|
37
|
+
* in cluster mode (only the framework's
|
|
38
|
+
* known tables are rewritten — operator
|
|
39
|
+
* app-data SQL is unaffected)
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
var cluster = require("./cluster");
|
|
43
|
+
var frameworkSchema = require("./framework-schema");
|
|
44
|
+
var externalDb = require("./external-db");
|
|
45
|
+
var lazyRequire = require("./lazy-require");
|
|
46
|
+
var { FrameworkError } = require("./framework-error");
|
|
47
|
+
|
|
48
|
+
class ClusterStorageError extends FrameworkError {
|
|
49
|
+
constructor(message, code) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "ClusterStorageError";
|
|
52
|
+
this.code = code || "cluster-storage/invalid";
|
|
53
|
+
this.isClusterStorageError = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---- Lazy db ref to avoid circular require ----
|
|
58
|
+
var _localDb = lazyRequire(function () { return require("./db"); });
|
|
59
|
+
|
|
60
|
+
// ---- Table-name resolution ----
|
|
61
|
+
|
|
62
|
+
function tableName(local) {
|
|
63
|
+
if (cluster.isClusterMode()) return frameworkSchema.tableName(local);
|
|
64
|
+
return local;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Rewrite bare table names in SQL when running in cluster mode. We only
|
|
68
|
+
// touch tokens that are exactly one of the framework's known table names
|
|
69
|
+
// (audit_log, consent_log, …) — anything else passes through unchanged
|
|
70
|
+
// so app-data SQL composed via this dispatcher (or operator-written
|
|
71
|
+
// migrations) isn't rewritten by accident.
|
|
72
|
+
function resolveTables(sql) {
|
|
73
|
+
if (!cluster.isClusterMode()) return sql;
|
|
74
|
+
var mapping = frameworkSchema.LOCAL_TO_EXTERNAL;
|
|
75
|
+
// Order longest-first so prefix matches don't collide (audit_log
|
|
76
|
+
// before audit). Match on word boundaries so we don't rewrite a
|
|
77
|
+
// suffix of a longer identifier.
|
|
78
|
+
var localNames = Object.keys(mapping).sort(function (a, b) { return b.length - a.length; });
|
|
79
|
+
var translated = sql;
|
|
80
|
+
for (var i = 0; i < localNames.length; i++) {
|
|
81
|
+
var local = localNames[i];
|
|
82
|
+
var external = mapping[local];
|
|
83
|
+
if (local === external) continue; // identity mapping — no-op
|
|
84
|
+
var re = new RegExp("\\b" + local.replace(/[\\.-]/g, "\\$&") + "\\b", "g");
|
|
85
|
+
translated = translated.replace(re, external);
|
|
86
|
+
}
|
|
87
|
+
return translated;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---- Placeholder translation ----
|
|
91
|
+
//
|
|
92
|
+
// SQLite and Postgres both accept `?` in some contexts, but Postgres
|
|
93
|
+
// only accepts `$N` for parameter binding. We translate at dispatch
|
|
94
|
+
// time so callers always write `?` and the right thing happens per
|
|
95
|
+
// dialect.
|
|
96
|
+
|
|
97
|
+
function placeholderize(sql, dialect) {
|
|
98
|
+
if (dialect !== "postgres") return sql;
|
|
99
|
+
// Walk the SQL and replace `?` with $1, $2, … but skip ones inside
|
|
100
|
+
// single-quoted string literals.
|
|
101
|
+
var out = "";
|
|
102
|
+
var n = 0;
|
|
103
|
+
var inStr = false;
|
|
104
|
+
for (var i = 0; i < sql.length; i++) {
|
|
105
|
+
var c = sql.charAt(i);
|
|
106
|
+
if (c === "'" && !inStr) { inStr = true; out += c; continue; }
|
|
107
|
+
if (c === "'" && inStr) {
|
|
108
|
+
// Handle escaped '' inside string
|
|
109
|
+
if (sql.charAt(i + 1) === "'") { out += "''"; i += 1; continue; }
|
|
110
|
+
inStr = false; out += c; continue;
|
|
111
|
+
}
|
|
112
|
+
if (!inStr && c === "?") { n += 1; out += "$" + n; continue; }
|
|
113
|
+
out += c;
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---- execute() ----
|
|
119
|
+
|
|
120
|
+
async function execute(sql, params) {
|
|
121
|
+
if (typeof sql !== "string") {
|
|
122
|
+
throw new ClusterStorageError("sql must be a string", "cluster-storage/bad-arg");
|
|
123
|
+
}
|
|
124
|
+
params = params || [];
|
|
125
|
+
|
|
126
|
+
if (cluster.isClusterMode()) {
|
|
127
|
+
var translated = placeholderize(resolveTables(sql), cluster.dialect());
|
|
128
|
+
var result = await externalDb.query(translated, params, {
|
|
129
|
+
backend: cluster.externalDbBackend(),
|
|
130
|
+
});
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Local SQLite path. node:sqlite is sync — wrap in a resolved Promise
|
|
135
|
+
// so callers always see the same shape regardless of mode.
|
|
136
|
+
var stmt = _localDb().prepare(sql);
|
|
137
|
+
// Heuristic: if the statement returns rows (SELECT or has RETURNING),
|
|
138
|
+
// use .all(); otherwise .run() and report changes as rowCount.
|
|
139
|
+
if (/^\s*SELECT\b/i.test(sql) || /\bRETURNING\b/i.test(sql)) {
|
|
140
|
+
var rows = stmt.all.apply(stmt, params);
|
|
141
|
+
return { rows: rows, rowCount: rows.length };
|
|
142
|
+
}
|
|
143
|
+
var info = stmt.run.apply(stmt, params);
|
|
144
|
+
return { rows: [], rowCount: info.changes };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Convenience wrappers for the two common patterns.
|
|
148
|
+
async function executeOne(sql, params) {
|
|
149
|
+
var result = await execute(sql, params);
|
|
150
|
+
return result.rows.length > 0 ? result.rows[0] : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function executeAll(sql, params) {
|
|
154
|
+
var result = await execute(sql, params);
|
|
155
|
+
return result.rows;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = {
|
|
159
|
+
execute: execute,
|
|
160
|
+
executeOne: executeOne,
|
|
161
|
+
executeAll: executeAll,
|
|
162
|
+
tableName: tableName,
|
|
163
|
+
resolveTables: resolveTables,
|
|
164
|
+
placeholderize: placeholderize,
|
|
165
|
+
ClusterStorageError: ClusterStorageError,
|
|
166
|
+
};
|