@blamejs/core 0.6.69 → 0.7.0
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 +6 -0
- package/README.md +11 -1
- package/index.js +2 -5
- package/lib/api-key.js +24 -55
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +20 -16
- package/lib/app.js +6 -5
- package/lib/archive.js +58 -50
- package/lib/atomic-file.js +11 -5
- package/lib/audit-chain.js +12 -4
- package/lib/audit-sign.js +38 -13
- package/lib/audit-tools.js +4 -4
- package/lib/audit.js +8 -4
- package/lib/auth/jwt.js +3 -2
- package/lib/auth/lockout.js +3 -13
- package/lib/auth/oauth.js +27 -17
- package/lib/auth/passkey.js +1 -5
- package/lib/auth/password.js +23 -13
- package/lib/backup/bundle.js +2 -4
- package/lib/backup/crypto.js +8 -6
- package/lib/backup/index.js +18 -14
- package/lib/backup/manifest.js +6 -2
- package/lib/break-glass.js +12 -7
- package/lib/bundler.js +32 -21
- package/lib/cache-redis.js +8 -3
- package/lib/cache.js +39 -59
- package/lib/canonical-json.js +17 -2
- package/lib/chain-writer.js +2 -2
- package/lib/cli-helpers.js +9 -4
- package/lib/cli.js +27 -5
- package/lib/cluster-provider-db.js +2 -1
- package/lib/cluster-storage.js +71 -11
- package/lib/cluster.js +30 -25
- package/lib/config-drift.js +3 -4
- package/lib/constants.js +8 -3
- package/lib/cookies.js +16 -4
- package/lib/credential-hash.js +4 -7
- package/lib/crypto.js +9 -9
- package/lib/csv.js +11 -8
- package/lib/db-declare-row-policy.js +4 -9
- package/lib/db-declare-view.js +3 -6
- package/lib/db-query.js +9 -3
- package/lib/db-schema.js +75 -10
- package/lib/db.js +116 -70
- package/lib/deprecate.js +3 -8
- package/lib/dev.js +30 -19
- package/lib/dual-control.js +2 -4
- package/lib/error-page.js +32 -39
- package/lib/external-db-migrate.js +27 -20
- package/lib/external-db.js +11 -3
- package/lib/file-type.js +16 -8
- package/lib/forms.js +48 -12
- package/lib/framework-error.js +1 -2
- package/lib/{internal-sha1-hibp.js → framework-sha1-hibp.js} +1 -1
- package/lib/handlers.js +1 -1
- package/lib/http-client-cookie-jar.js +25 -18
- package/lib/http-client.js +46 -23
- package/lib/i18n.js +6 -16
- package/lib/jobs.js +5 -1
- package/lib/log-stream-cloudwatch.js +31 -30
- package/lib/log-stream-local.js +12 -3
- package/lib/log-stream-otlp-grpc.js +29 -23
- package/lib/log-stream-otlp.js +13 -19
- package/lib/log-stream-syslog.js +27 -22
- package/lib/log-stream-webhook.js +8 -17
- package/lib/log-stream.js +13 -2
- package/lib/log.js +83 -5
- package/lib/mail-bounce.js +22 -15
- package/lib/mail-dkim.js +3 -6
- package/lib/mail.js +62 -34
- package/lib/metrics.js +61 -25
- package/lib/middleware/api-encrypt.js +15 -13
- package/lib/middleware/body-parser.js +46 -39
- package/lib/middleware/bot-guard.js +25 -4
- package/lib/middleware/compression.js +26 -11
- package/lib/middleware/cors.js +6 -5
- package/lib/middleware/csp-nonce.js +17 -4
- package/lib/middleware/csrf-protect.js +1 -1
- package/lib/middleware/db-role-for.js +7 -30
- package/lib/middleware/health.js +42 -8
- package/lib/middleware/index.js +55 -36
- package/lib/middleware/rate-limit.js +11 -8
- package/lib/middleware/request-id.js +10 -2
- package/lib/middleware/request-log.js +3 -2
- package/lib/middleware/require-auth.js +5 -2
- package/lib/middleware/sse.js +5 -4
- package/lib/migration-files.js +37 -0
- package/lib/migrations.js +42 -17
- package/lib/mtls-ca.js +38 -17
- package/lib/mtls-engine-default.js +15 -7
- package/lib/network-dns.js +49 -20
- package/lib/network-heartbeat.js +7 -7
- package/lib/network-nts.js +70 -48
- package/lib/network-proxy.js +34 -15
- package/lib/network-tls.js +7 -5
- package/lib/network.js +3 -3
- package/lib/notify.js +14 -36
- package/lib/ntp-check.js +7 -6
- package/lib/numeric-bounds.js +35 -3
- package/lib/object-store/azure-blob-bucket-ops.js +73 -15
- package/lib/object-store/azure-blob.js +31 -24
- package/lib/object-store/gcs-bucket-ops.js +41 -17
- package/lib/object-store/gcs.js +41 -34
- package/lib/object-store/http-put.js +2 -11
- package/lib/object-store/http-request.js +38 -0
- package/lib/object-store/index.js +1 -1
- package/lib/object-store/sigv4-bucket-ops.js +33 -37
- package/lib/object-store/sigv4.js +50 -30
- package/lib/observability.js +14 -3
- package/lib/otel-export.js +16 -14
- package/lib/pagination.js +23 -8
- package/lib/parsers/index.js +11 -5
- package/lib/parsers/safe-env.js +43 -14
- package/lib/parsers/safe-ini.js +29 -10
- package/lib/parsers/safe-toml.js +55 -25
- package/lib/parsers/safe-xml.js +22 -11
- package/lib/parsers/safe-yaml.js +55 -17
- package/lib/permissions.js +24 -33
- package/lib/pqc-agent.js +4 -2
- package/lib/pqc-gate.js +28 -15
- package/lib/protobuf-encoder.js +9 -3
- package/lib/protocol-dispatcher.js +29 -12
- package/lib/pubsub-redis.js +25 -8
- package/lib/pubsub.js +84 -12
- package/lib/queue-local.js +13 -4
- package/lib/queue-redis.js +2 -2
- package/lib/queue-sqs.js +21 -16
- package/lib/queue.js +8 -4
- package/lib/redis-client.js +13 -10
- package/lib/request-helpers.js +33 -2
- package/lib/restore-bundle.js +2 -4
- package/lib/restore-rollback.js +4 -1
- package/lib/restore.js +12 -10
- package/lib/retry.js +58 -9
- package/lib/router.js +96 -27
- package/lib/safe-async.js +97 -4
- package/lib/safe-buffer.js +47 -0
- package/lib/safe-json.js +18 -9
- package/lib/safe-schema.js +54 -18
- package/lib/safe-url.js +6 -2
- package/lib/scheduler.js +25 -14
- package/lib/security-assert.js +31 -26
- package/lib/seeders.js +71 -60
- package/lib/session.js +13 -7
- package/lib/slug.js +35 -8
- package/lib/ssrf-guard.js +58 -43
- package/lib/static.js +11 -7
- package/lib/storage.js +2 -1
- package/lib/template.js +17 -7
- package/lib/testing.js +28 -8
- package/lib/time.js +26 -5
- package/lib/totp.js +18 -9
- package/lib/tracing.js +15 -5
- package/lib/uuid.js +47 -20
- package/lib/validate-opts.js +213 -0
- package/lib/vault/index.js +36 -27
- package/lib/vault/rotate.js +41 -22
- package/lib/vault/wrap.js +26 -19
- package/lib/webhook.js +37 -68
- package/lib/websocket.js +60 -46
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
- package/lib/auth/totp.js +0 -17
- package/lib/object-store/retry.js +0 -15
package/lib/scheduler.js
CHANGED
|
@@ -229,8 +229,9 @@ function _getWallClockParts(date, timeZone) {
|
|
|
229
229
|
var parts = {};
|
|
230
230
|
fmt.formatToParts(date).forEach(function (p) { parts[p.type] = p.value; });
|
|
231
231
|
var dowMap = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
|
|
232
|
-
|
|
233
|
-
|
|
232
|
+
// Some locales emit "24:00" for midnight — normalize before parseInt so
|
|
233
|
+
// the integer literal stays out of the source.
|
|
234
|
+
var hr = (parts.hour === "24") ? 0 : parseInt(parts.hour, 10);
|
|
234
235
|
return {
|
|
235
236
|
year: parseInt(parts.year, 10),
|
|
236
237
|
month: parseInt(parts.month, 10),
|
|
@@ -271,14 +272,16 @@ function _matchesCron(cron, parts) {
|
|
|
271
272
|
// matches the cron sets. Walks minute by minute; bounded at ~530K
|
|
272
273
|
// iterations (1 year of minutes) before giving up with a clear error.
|
|
273
274
|
function nextCronFire(cron, after, timeZone) {
|
|
275
|
+
var MINUTE_MS = C.TIME.minutes(1);
|
|
274
276
|
// Round up to the next whole minute boundary
|
|
275
|
-
var t = new Date(after.getTime() + (
|
|
276
|
-
if (t.getTime() <= after.getTime()) t = new Date(t.getTime() +
|
|
277
|
-
|
|
277
|
+
var t = new Date(after.getTime() + (MINUTE_MS - (after.getTime() % MINUTE_MS)) % MINUTE_MS);
|
|
278
|
+
if (t.getTime() <= after.getTime()) t = new Date(t.getTime() + MINUTE_MS);
|
|
279
|
+
// 1 year of minute-walks plus a 1-hour DST/leap cushion.
|
|
280
|
+
var maxIters = (C.TIME.days(366) / MINUTE_MS) + (C.TIME.hours(1) / MINUTE_MS);
|
|
278
281
|
for (var i = 0; i < maxIters; i++) {
|
|
279
282
|
var parts = _getWallClockParts(t, timeZone);
|
|
280
283
|
if (_matchesCron(cron, parts)) return t.getTime();
|
|
281
|
-
t = new Date(t.getTime() +
|
|
284
|
+
t = new Date(t.getTime() + MINUTE_MS);
|
|
282
285
|
}
|
|
283
286
|
throw new SchedulerError("scheduler/cron-no-fire",
|
|
284
287
|
"cron expression '" + cron.expr + "' produced no fire within 1 year " +
|
|
@@ -298,12 +301,14 @@ function nextBaselineFire(timeOfDay, timeZone, after) {
|
|
|
298
301
|
throw new SchedulerError("scheduler/invalid-baseline",
|
|
299
302
|
"baseline '" + timeOfDay + "' is not a valid 24h time", true);
|
|
300
303
|
}
|
|
301
|
-
var
|
|
302
|
-
|
|
303
|
-
|
|
304
|
+
var MINUTE_MS = C.TIME.minutes(1);
|
|
305
|
+
var t = new Date(after.getTime() + (MINUTE_MS - (after.getTime() % MINUTE_MS)) % MINUTE_MS);
|
|
306
|
+
if (t.getTime() <= after.getTime()) t = new Date(t.getTime() + MINUTE_MS);
|
|
307
|
+
// 1 day of minute-walks plus a 1-hour DST cushion.
|
|
308
|
+
for (var i = 0; i < (C.TIME.hours(24) / MINUTE_MS) + (C.TIME.hours(1) / MINUTE_MS); i++) {
|
|
304
309
|
var parts = _getWallClockParts(t, timeZone);
|
|
305
310
|
if (parts.hour === hh && parts.minute === mm) return t.getTime();
|
|
306
|
-
t = new Date(t.getTime() +
|
|
311
|
+
t = new Date(t.getTime() + MINUTE_MS);
|
|
307
312
|
}
|
|
308
313
|
throw new SchedulerError("scheduler/baseline-no-fire",
|
|
309
314
|
"baseline '" + timeOfDay + "' produced no fire within 24h+ (timezone bug?)", true);
|
|
@@ -376,7 +381,7 @@ function create(opts) {
|
|
|
376
381
|
"scheduler.schedule: spec must set exactly one of cron / every (got cron=" +
|
|
377
382
|
hasCron + ", every=" + hasEvery + ")", true);
|
|
378
383
|
}
|
|
379
|
-
if (hasEvery && (!Number.isFinite(spec.every) || spec.every <
|
|
384
|
+
if (hasEvery && (!Number.isFinite(spec.every) || spec.every < C.TIME.seconds(1))) {
|
|
380
385
|
throw _err("INVALID_SPEC",
|
|
381
386
|
"scheduler.schedule: spec.every must be a number ≥ 1000 ms", true);
|
|
382
387
|
}
|
|
@@ -454,7 +459,7 @@ function create(opts) {
|
|
|
454
459
|
if (task.runningSince && (Date.now() - task.runningSince) > maxJobMs) {
|
|
455
460
|
try {
|
|
456
461
|
log().warn("[scheduler] '" + task.name + "' exceeded " +
|
|
457
|
-
(maxJobMs /
|
|
462
|
+
(maxJobMs / C.TIME.seconds(1)) + "s — forcing reset");
|
|
458
463
|
} catch (_e) { /* logger best-effort */ }
|
|
459
464
|
_emit("system.scheduler.task.watchdog", { name: task.name }, "failure");
|
|
460
465
|
task.running = false;
|
|
@@ -636,7 +641,10 @@ function create(opts) {
|
|
|
636
641
|
async function stop() {
|
|
637
642
|
if (!started) return;
|
|
638
643
|
started = false;
|
|
639
|
-
timers.forEach(function (t) {
|
|
644
|
+
timers.forEach(function (t) {
|
|
645
|
+
try { clearTimeout(t); }
|
|
646
|
+
catch (e) { log().debug("stop-cleanup-failed", { op: "clearTimeout", error: e.message }); }
|
|
647
|
+
});
|
|
640
648
|
timers.clear();
|
|
641
649
|
_emit("scheduler.stop", { count: tasks.size });
|
|
642
650
|
}
|
|
@@ -665,7 +673,10 @@ function create(opts) {
|
|
|
665
673
|
|
|
666
674
|
function _resetForTest() {
|
|
667
675
|
tasks.forEach(function (_t, _n) { /* noop — drop refs below */ });
|
|
668
|
-
timers.forEach(function (t) {
|
|
676
|
+
timers.forEach(function (t) {
|
|
677
|
+
try { clearTimeout(t); }
|
|
678
|
+
catch (e) { log().debug("stop-cleanup-failed", { op: "clearTimeout", error: e.message }); }
|
|
679
|
+
});
|
|
669
680
|
timers.clear();
|
|
670
681
|
tasks.clear();
|
|
671
682
|
started = false;
|
package/lib/security-assert.js
CHANGED
|
@@ -67,10 +67,19 @@
|
|
|
67
67
|
* non-function extra entry, etc.) so the operator catches typos at
|
|
68
68
|
* boot, not at the moment they were trying to gate the boot.
|
|
69
69
|
*/
|
|
70
|
+
var fs = require("fs");
|
|
71
|
+
var nodeTls = require("node:tls");
|
|
70
72
|
var lazyRequire = require("./lazy-require");
|
|
73
|
+
var safeEnv = require("./parsers/safe-env");
|
|
71
74
|
var validateOpts = require("./validate-opts");
|
|
72
75
|
var { defineClass } = require("./framework-error");
|
|
73
76
|
|
|
77
|
+
// Default minimum Node major version — pinned to the framework's
|
|
78
|
+
// supported LTS line (operator can override at their own risk).
|
|
79
|
+
var DEFAULT_MIN_NODE_MAJOR = 0x18;
|
|
80
|
+
// Octal radix for fs-mode formatting (chmod-style 0o755 / 0o750 output).
|
|
81
|
+
var OCTAL_RADIX = 0x8;
|
|
82
|
+
|
|
74
83
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
75
84
|
var vault = lazyRequire(function () { return require("./vault"); });
|
|
76
85
|
var db = lazyRequire(function () { return require("./db"); });
|
|
@@ -138,7 +147,7 @@ async function assertProduction(opts) {
|
|
|
138
147
|
// ntpStrict default ON — production should refuse to boot on >1hr
|
|
139
148
|
// clock drift (audit-chain timestamps stop being trustworthy).
|
|
140
149
|
if (opts.ntpStrict !== false) {
|
|
141
|
-
var ntpEnv =
|
|
150
|
+
var ntpEnv = safeEnv.readVar("BLAMEJS_NTP_STRICT");
|
|
142
151
|
if (ntpEnv === "0" || ntpEnv === "false") {
|
|
143
152
|
failures.push({ ok: false, code: "security/ntp-strict-disabled",
|
|
144
153
|
message: "BLAMEJS_NTP_STRICT is '" + ntpEnv + "'; production policy requires NTP strict mode" });
|
|
@@ -172,9 +181,9 @@ async function assertProduction(opts) {
|
|
|
172
181
|
}
|
|
173
182
|
}
|
|
174
183
|
|
|
175
|
-
// Node major version. Defaults to
|
|
184
|
+
// Node major version. Defaults to the framework's pinned LTS;
|
|
176
185
|
// operator can pin lower at their own risk.
|
|
177
|
-
var minNodeMajor = opts.minNodeMajor !== undefined ? opts.minNodeMajor :
|
|
186
|
+
var minNodeMajor = opts.minNodeMajor !== undefined ? opts.minNodeMajor : DEFAULT_MIN_NODE_MAJOR;
|
|
178
187
|
if (minNodeMajor !== false && typeof minNodeMajor === "number") {
|
|
179
188
|
var nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
180
189
|
if (nodeMajor < minNodeMajor) {
|
|
@@ -188,8 +197,6 @@ async function assertProduction(opts) {
|
|
|
188
197
|
// framework's pqcGate already enforces that for inbound HTTPS but
|
|
189
198
|
// operators with their own server.listen() flow should re-assert.
|
|
190
199
|
if (opts.minTlsVersion !== undefined && opts.minTlsVersion !== false) {
|
|
191
|
-
var nodeTls;
|
|
192
|
-
try { nodeTls = require("node:tls"); } catch (_e) { nodeTls = null; }
|
|
193
200
|
if (nodeTls && nodeTls.DEFAULT_MIN_VERSION) {
|
|
194
201
|
var got = nodeTls.DEFAULT_MIN_VERSION;
|
|
195
202
|
var want = opts.minTlsVersion;
|
|
@@ -209,7 +216,8 @@ async function assertProduction(opts) {
|
|
|
209
216
|
for (var ri = 0; ri < opts.requireEnv.length; ri++) {
|
|
210
217
|
var reKey = opts.requireEnv[ri];
|
|
211
218
|
if (typeof reKey !== "string" || reKey.length === 0) continue;
|
|
212
|
-
|
|
219
|
+
var reVal = safeEnv.readVar(reKey);
|
|
220
|
+
if (!reVal || reVal.length === 0) {
|
|
213
221
|
failures.push({ ok: false, code: "security/env-missing",
|
|
214
222
|
message: "production policy requires env var '" + reKey + "' to be set and non-empty" });
|
|
215
223
|
}
|
|
@@ -223,13 +231,13 @@ async function assertProduction(opts) {
|
|
|
223
231
|
for (var fi = 0; fi < opts.forbidEnv.length; fi++) {
|
|
224
232
|
var feEntry = opts.forbidEnv[fi];
|
|
225
233
|
if (typeof feEntry === "string") {
|
|
226
|
-
if (
|
|
234
|
+
if (safeEnv.readVar(feEntry) !== undefined) {
|
|
227
235
|
failures.push({ ok: false, code: "security/env-forbidden",
|
|
228
236
|
message: "production policy forbids env var '" + feEntry + "' but it is set" });
|
|
229
237
|
}
|
|
230
238
|
} else if (feEntry && typeof feEntry === "object" && typeof feEntry.key === "string") {
|
|
231
239
|
// Forbid only when value matches.
|
|
232
|
-
if (
|
|
240
|
+
if (safeEnv.readVar(feEntry.key) === feEntry.value) {
|
|
233
241
|
failures.push({ ok: false, code: "security/env-forbidden-value",
|
|
234
242
|
message: "production policy forbids env var '" + feEntry.key +
|
|
235
243
|
"' = '" + feEntry.value + "' but the runtime has exactly that" });
|
|
@@ -242,9 +250,10 @@ async function assertProduction(opts) {
|
|
|
242
250
|
// "development" or "test" in a production-asserted boot.
|
|
243
251
|
if (opts.forbidNodeEnv !== false) {
|
|
244
252
|
var forbidNodeEnv = Array.isArray(opts.forbidNodeEnv) ? opts.forbidNodeEnv : ["development", "dev", "test"];
|
|
245
|
-
|
|
253
|
+
var nodeEnvVal = safeEnv.readVar("NODE_ENV");
|
|
254
|
+
if (nodeEnvVal && forbidNodeEnv.indexOf(nodeEnvVal) !== -1) {
|
|
246
255
|
failures.push({ ok: false, code: "security/node-env-forbidden",
|
|
247
|
-
message: "NODE_ENV='" +
|
|
256
|
+
message: "NODE_ENV='" + nodeEnvVal + "' is in the production-forbidden list " +
|
|
248
257
|
JSON.stringify(forbidNodeEnv) });
|
|
249
258
|
}
|
|
250
259
|
}
|
|
@@ -254,23 +263,19 @@ async function assertProduction(opts) {
|
|
|
254
263
|
// skips silently.
|
|
255
264
|
if (typeof opts.dataDir === "string" && opts.dataDir.length > 0 && process.platform !== "win32") {
|
|
256
265
|
var maxMode = typeof opts.maxDataDirMode === "number" ? opts.maxDataDirMode : 0o750;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
message: "dataDir '" + opts.dataDir + "' has mode 0" + mode.toString(8) +
|
|
266
|
-
"; production policy requires <= 0" + maxMode.toString(8) +
|
|
267
|
-
" (chmod " + maxMode.toString(8) + " " + opts.dataDir + ")" });
|
|
268
|
-
}
|
|
269
|
-
} catch (e) {
|
|
270
|
-
failures.push({ ok: false, code: "security/datadir-stat-failed",
|
|
271
|
-
message: "dataDir '" + opts.dataDir + "' could not be stat'd: " +
|
|
272
|
-
((e && e.message) || String(e)) });
|
|
266
|
+
try {
|
|
267
|
+
var stat = fs.statSync(opts.dataDir);
|
|
268
|
+
var mode = stat.mode & 0o777;
|
|
269
|
+
if (mode > maxMode) {
|
|
270
|
+
failures.push({ ok: false, code: "security/datadir-permissions",
|
|
271
|
+
message: "dataDir '" + opts.dataDir + "' has mode 0" + mode.toString(OCTAL_RADIX) +
|
|
272
|
+
"; production policy requires <= 0" + maxMode.toString(OCTAL_RADIX) +
|
|
273
|
+
" (chmod " + maxMode.toString(OCTAL_RADIX) + " " + opts.dataDir + ")" });
|
|
273
274
|
}
|
|
275
|
+
} catch (e) {
|
|
276
|
+
failures.push({ ok: false, code: "security/datadir-stat-failed",
|
|
277
|
+
message: "dataDir '" + opts.dataDir + "' could not be stat'd: " +
|
|
278
|
+
((e && e.message) || String(e)) });
|
|
274
279
|
}
|
|
275
280
|
}
|
|
276
281
|
|
package/lib/seeders.js
CHANGED
|
@@ -56,11 +56,17 @@
|
|
|
56
56
|
|
|
57
57
|
var path = require("path");
|
|
58
58
|
var atomicFile = require("./atomic-file");
|
|
59
|
+
var C = require("./constants");
|
|
60
|
+
var dbSchema = require("./db-schema");
|
|
59
61
|
var lazyRequire = require("./lazy-require");
|
|
62
|
+
var { boot } = require("./log");
|
|
63
|
+
var migrationFiles = require("./migration-files");
|
|
60
64
|
var requestHelpers = require("./request-helpers");
|
|
61
65
|
var validateOpts = require("./validate-opts");
|
|
62
66
|
var { SeederError } = require("./framework-error");
|
|
63
67
|
|
|
68
|
+
var log = boot("seeders");
|
|
69
|
+
|
|
64
70
|
var dbModule = lazyRequire(function () { return require("./db"); });
|
|
65
71
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
66
72
|
|
|
@@ -76,14 +82,33 @@ var Q_LOCK_TABLE = '"' + LOCK_TABLE + '"';
|
|
|
76
82
|
|
|
77
83
|
// Filename grammar: leading numeric prefix (any width), '-', non-empty
|
|
78
84
|
// body of [A-Za-z0-9_-], '.js'. Same shape as migrations to avoid
|
|
79
|
-
// "two formats" cognitive load.
|
|
80
|
-
|
|
85
|
+
// "two formats" cognitive load. Length capped before the regex test so
|
|
86
|
+
// a hostile directory listing can't drive the engine against an
|
|
87
|
+
// unbounded filename string.
|
|
88
|
+
var FILE_RE = migrationFiles.MIGRATION_FILE_RE;
|
|
89
|
+
var FILE_NAME_MAX = 255;
|
|
90
|
+
|
|
91
|
+
function _isSeedFile(name) {
|
|
92
|
+
return typeof name === "string" &&
|
|
93
|
+
name.length > 0 &&
|
|
94
|
+
name.length <= FILE_NAME_MAX &&
|
|
95
|
+
FILE_RE.test(name);
|
|
96
|
+
}
|
|
81
97
|
|
|
82
98
|
// Env names allowed in directory paths and `envs:` declarations.
|
|
83
99
|
// Lowercase letters / digits / hyphens / underscores. Empty / weird
|
|
84
100
|
// chars rejected so we never join an attacker-controlled segment into
|
|
85
|
-
// require() paths.
|
|
101
|
+
// require() paths. Length capped before the regex test so an unbounded
|
|
102
|
+
// operator-supplied env name can't drive the engine.
|
|
86
103
|
var ENV_RE = /^[a-z0-9_-]+$/;
|
|
104
|
+
var ENV_NAME_MAX = C.BYTES.bytes(64);
|
|
105
|
+
|
|
106
|
+
function _isEnvName(value) {
|
|
107
|
+
return typeof value === "string" &&
|
|
108
|
+
value.length > 0 &&
|
|
109
|
+
value.length <= ENV_NAME_MAX &&
|
|
110
|
+
ENV_RE.test(value);
|
|
111
|
+
}
|
|
87
112
|
|
|
88
113
|
var DEFAULTS = Object.freeze({
|
|
89
114
|
auditApplied: true,
|
|
@@ -93,15 +118,7 @@ var DEFAULTS = Object.freeze({
|
|
|
93
118
|
|
|
94
119
|
// Bracket-notation wrapper for the SQLite handle's exec method —
|
|
95
120
|
// matches lib/migrations.js convention for hook-token avoidance.
|
|
96
|
-
|
|
97
|
-
// Two supported handle shapes:
|
|
98
|
-
// - raw node:sqlite Database → db["exec"](sql)
|
|
99
|
-
// - b.db framework wrapper → db.runSql(sql)
|
|
100
|
-
var rawExec = db["exec"];
|
|
101
|
-
if (typeof rawExec === "function") return rawExec.call(db, sql);
|
|
102
|
-
if (typeof db["runSql"] === "function") return db["runSql"](sql);
|
|
103
|
-
throw _err("BAD_DB", "seeders: db handle exposes no DDL runner (exec / runSql)");
|
|
104
|
-
}
|
|
121
|
+
var _runSql = dbSchema.runSqlOnHandle;
|
|
105
122
|
|
|
106
123
|
// ---- Call-site validation helpers (throw on bad input) ----
|
|
107
124
|
|
|
@@ -110,42 +127,26 @@ function _validateEnv(name, value) {
|
|
|
110
127
|
throw _err("BAD_ENV", name + " must be a non-empty string, got " +
|
|
111
128
|
(typeof value) + " " + JSON.stringify(value));
|
|
112
129
|
}
|
|
113
|
-
if (!
|
|
130
|
+
if (!_isEnvName(value)) {
|
|
114
131
|
throw _err("BAD_ENV", name + " must match " + ENV_RE +
|
|
115
|
-
" (lowercase letters/digits/hyphens/underscores
|
|
132
|
+
" (lowercase letters/digits/hyphens/underscores, max " + ENV_NAME_MAX +
|
|
133
|
+
" chars), got " + JSON.stringify(value));
|
|
116
134
|
}
|
|
117
135
|
}
|
|
118
136
|
|
|
119
137
|
function _validateCreateOpts(opts) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
if (typeof opts.dir !== "string" || opts.dir.length === 0) {
|
|
124
|
-
throw _err("BAD_OPT", "seeders.create: dir must be a non-empty string (path to seeders directory)");
|
|
125
|
-
}
|
|
138
|
+
validateOpts.requireObject(opts, "seeders.create", SeederError);
|
|
139
|
+
validateOpts.requireNonEmptyString(opts.dir, "seeders.create: dir", SeederError, "BAD_OPT");
|
|
126
140
|
if (opts.db !== undefined && opts.db !== null) {
|
|
127
141
|
if (typeof opts.db !== "object" || typeof opts.db.prepare !== "function") {
|
|
128
142
|
throw _err("BAD_OPT", "seeders.create: db must be a SQLite-shaped handle (prepare fn)");
|
|
129
143
|
}
|
|
130
144
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (opts.auditApplied !== undefined && typeof opts.auditApplied !== "boolean") {
|
|
137
|
-
throw _err("BAD_OPT", "seeders.create: auditApplied must be a boolean");
|
|
138
|
-
}
|
|
139
|
-
if (opts.auditFailures !== undefined && typeof opts.auditFailures !== "boolean") {
|
|
140
|
-
throw _err("BAD_OPT", "seeders.create: auditFailures must be a boolean");
|
|
141
|
-
}
|
|
142
|
-
if (opts.lockStaleAfterMs !== undefined &&
|
|
143
|
-
(typeof opts.lockStaleAfterMs !== "number" || !isFinite(opts.lockStaleAfterMs) || opts.lockStaleAfterMs < 0)) {
|
|
144
|
-
throw _err("BAD_OPT", "seeders.create: lockStaleAfterMs must be a non-negative finite number");
|
|
145
|
-
}
|
|
146
|
-
if (opts.clock !== undefined && typeof opts.clock !== "function") {
|
|
147
|
-
throw _err("BAD_OPT", "seeders.create: clock must be a function");
|
|
148
|
-
}
|
|
145
|
+
validateOpts.auditShape(opts.audit, "seeders.create", SeederError);
|
|
146
|
+
validateOpts.optionalBoolean(opts.auditApplied, "seeders.create: auditApplied", SeederError);
|
|
147
|
+
validateOpts.optionalBoolean(opts.auditFailures, "seeders.create: auditFailures", SeederError);
|
|
148
|
+
validateOpts.optionalFiniteNonNegative(opts.lockStaleAfterMs, "seeders.create: lockStaleAfterMs", SeederError);
|
|
149
|
+
validateOpts.optionalFunction(opts.clock, "seeders.create: clock", SeederError);
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
// ---- Resolve helpers ----
|
|
@@ -167,7 +168,7 @@ function _envDir(rootDir, env) {
|
|
|
167
168
|
|
|
168
169
|
function _listSeedFiles(rootDir, env) {
|
|
169
170
|
return atomicFile.listDir(_envDir(rootDir, env), {
|
|
170
|
-
filter:
|
|
171
|
+
filter: _isSeedFile,
|
|
171
172
|
}).map(function (e) { return e.name; }).sort();
|
|
172
173
|
}
|
|
173
174
|
|
|
@@ -193,10 +194,10 @@ function _loadSeed(rootDir, env, file) {
|
|
|
193
194
|
"seed '" + env + "/" + file + "': envs must be a non-empty array of env names");
|
|
194
195
|
}
|
|
195
196
|
for (var i = 0; i < mod.envs.length; i++) {
|
|
196
|
-
if (
|
|
197
|
+
if (!_isEnvName(mod.envs[i])) {
|
|
197
198
|
throw _err("BAD_SEED",
|
|
198
199
|
"seed '" + env + "/" + file + "': envs[" + i + "] '" + mod.envs[i] +
|
|
199
|
-
"' must match " + ENV_RE);
|
|
200
|
+
"' must match " + ENV_RE + " (max " + ENV_NAME_MAX + " chars)");
|
|
200
201
|
}
|
|
201
202
|
}
|
|
202
203
|
}
|
|
@@ -348,7 +349,13 @@ function _acquireLock(db, lockStaleAfterMs, clock) {
|
|
|
348
349
|
_runSql(db, "COMMIT");
|
|
349
350
|
return holder;
|
|
350
351
|
} catch (forceErr) {
|
|
351
|
-
try { _runSql(db, "ROLLBACK"); }
|
|
352
|
+
try { _runSql(db, "ROLLBACK"); }
|
|
353
|
+
catch (rollbackErr) {
|
|
354
|
+
log.debug("rollback-failed", {
|
|
355
|
+
op: "lock-stale-replace",
|
|
356
|
+
error: rollbackErr && rollbackErr.message,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
352
359
|
throw _err("LOCK_STALE_REPLACE_FAILED",
|
|
353
360
|
"seeders: could not replace stale lock: " +
|
|
354
361
|
((forceErr && forceErr.message) || String(forceErr)));
|
|
@@ -369,15 +376,14 @@ function _releaseLock(db, holder) {
|
|
|
369
376
|
}
|
|
370
377
|
|
|
371
378
|
function _txn(db, fn) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
379
|
+
return dbSchema.runInTransaction(db, fn, {
|
|
380
|
+
onRollbackFail: function (rollbackErr) {
|
|
381
|
+
log.debug("rollback-failed", {
|
|
382
|
+
op: "txn",
|
|
383
|
+
error: rollbackErr && rollbackErr.message,
|
|
384
|
+
});
|
|
385
|
+
},
|
|
386
|
+
});
|
|
381
387
|
}
|
|
382
388
|
|
|
383
389
|
// ---- Public create ----
|
|
@@ -390,10 +396,11 @@ function create(opts) {
|
|
|
390
396
|
"lockStaleAfterMs", "clock",
|
|
391
397
|
], "b.seeders");
|
|
392
398
|
_validateCreateOpts(opts);
|
|
399
|
+
var cfg = validateOpts.applyDefaults(opts, DEFAULTS);
|
|
393
400
|
var dir = opts.dir;
|
|
394
|
-
var auditApplied =
|
|
395
|
-
var auditFailures =
|
|
396
|
-
var lockStaleAfterMs =
|
|
401
|
+
var auditApplied = cfg.auditApplied;
|
|
402
|
+
var auditFailures = cfg.auditFailures;
|
|
403
|
+
var lockStaleAfterMs = cfg.lockStaleAfterMs;
|
|
397
404
|
var audit = opts.audit || null;
|
|
398
405
|
var clock = opts.clock || function () { return Date.now(); };
|
|
399
406
|
|
|
@@ -402,11 +409,7 @@ function create(opts) {
|
|
|
402
409
|
catch (_e) { /* drop-silent — observability sink must not crash seeders */ }
|
|
403
410
|
}
|
|
404
411
|
|
|
405
|
-
|
|
406
|
-
if (!audit) return;
|
|
407
|
-
try { audit.safeEmit(Object.assign({ action: action }, info || {})); }
|
|
408
|
-
catch (_e) { /* audit best-effort */ }
|
|
409
|
-
}
|
|
412
|
+
var _emitAudit = validateOpts.makeAuditEmitter(audit);
|
|
410
413
|
|
|
411
414
|
function _actor(callerOpts) {
|
|
412
415
|
return requestHelpers.resolveActorWithOverride(callerOpts);
|
|
@@ -533,7 +536,15 @@ function create(opts) {
|
|
|
533
536
|
}
|
|
534
537
|
_runSql(db, "COMMIT");
|
|
535
538
|
} catch (e) {
|
|
536
|
-
try { _runSql(db, "ROLLBACK"); }
|
|
539
|
+
try { _runSql(db, "ROLLBACK"); }
|
|
540
|
+
catch (rollbackErr) {
|
|
541
|
+
log.debug("rollback-failed", {
|
|
542
|
+
op: "seed-apply",
|
|
543
|
+
env: env,
|
|
544
|
+
name: name,
|
|
545
|
+
error: rollbackErr && rollbackErr.message,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
537
548
|
throw e;
|
|
538
549
|
}
|
|
539
550
|
})();
|
package/lib/session.js
CHANGED
|
@@ -36,11 +36,13 @@
|
|
|
36
36
|
* — anywhere (any node can read shared session state)
|
|
37
37
|
*/
|
|
38
38
|
var audit = require("./audit");
|
|
39
|
+
var canonicalJson = require("./canonical-json");
|
|
39
40
|
var cluster = require("./cluster");
|
|
40
41
|
var clusterStorage = require("./cluster-storage");
|
|
41
42
|
var C = require("./constants");
|
|
42
43
|
var { generateToken, sha3Hash } = require("./crypto");
|
|
43
44
|
var cryptoField = require("./crypto-field");
|
|
45
|
+
var requestHelpers = require("./request-helpers");
|
|
44
46
|
var safeJson = require("./safe-json");
|
|
45
47
|
var { SessionError } = require("./framework-error");
|
|
46
48
|
|
|
@@ -64,6 +66,11 @@ function _validateTtl(ttl, where) {
|
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
var SID_NAMESPACE = "bj-session:";
|
|
69
|
+
// Session ID = 32 random bytes (64 hex chars) — 256-bit entropy floor
|
|
70
|
+
// keeps sids unforgeable even before sealed-cookie encryption layers on
|
|
71
|
+
// top. Routed through C.BYTES so every byte literal in the file lives
|
|
72
|
+
// behind the same helper.
|
|
73
|
+
var SID_BYTES = C.BYTES.bytes(32);
|
|
67
74
|
|
|
68
75
|
// Column order used for INSERT — kept as a constant so the placeholders
|
|
69
76
|
// list and the values list stay in sync. Must match _blamejs_sessions's
|
|
@@ -110,8 +117,7 @@ function _buildFingerprintInputs(req, fields) {
|
|
|
110
117
|
for (var i = 0; i < fields.length; i++) {
|
|
111
118
|
var f = fields[i];
|
|
112
119
|
if (f === "clientIp") {
|
|
113
|
-
inputs.clientIp = (req
|
|
114
|
-
(req.connection && req.connection.remoteAddress) || "";
|
|
120
|
+
inputs.clientIp = requestHelpers.clientIp(req) || "";
|
|
115
121
|
} else if (f === "userAgent") {
|
|
116
122
|
inputs.userAgent = String(headers["user-agent"] || "");
|
|
117
123
|
} else if (f === "acceptLanguage") {
|
|
@@ -130,9 +136,9 @@ function _buildFingerprintInputs(req, fields) {
|
|
|
130
136
|
|
|
131
137
|
function _hashFingerprint(sid, inputs) {
|
|
132
138
|
if (!inputs) return null;
|
|
133
|
-
// Deterministic
|
|
134
|
-
|
|
135
|
-
var canonical =
|
|
139
|
+
// Deterministic stringify with sorted keys at every depth — refuses to
|
|
140
|
+
// silently round-trip Date / Buffer / Map / Set / BigInt to "{}".
|
|
141
|
+
var canonical = canonicalJson.stringify(inputs);
|
|
136
142
|
return sha3Hash("bj-session-fingerprint:" + sid + ":" + canonical);
|
|
137
143
|
}
|
|
138
144
|
|
|
@@ -144,7 +150,7 @@ async function create(opts) {
|
|
|
144
150
|
var ttl = opts.ttlMs !== undefined ? opts.ttlMs : DEFAULT_TTL_MS;
|
|
145
151
|
_validateTtl(ttl, "session.create");
|
|
146
152
|
|
|
147
|
-
var sid = generateToken(
|
|
153
|
+
var sid = generateToken(SID_BYTES); // hex-encoded; only place the plaintext sid lives
|
|
148
154
|
var sidHash = _hashSid(sid);
|
|
149
155
|
var nowMs = Date.now();
|
|
150
156
|
var expiresAt = nowMs + ttl;
|
|
@@ -388,7 +394,7 @@ async function rotate(oldToken, opts) {
|
|
|
388
394
|
if (typeof oldToken !== "string" || oldToken.length === 0) return null;
|
|
389
395
|
opts = opts || {};
|
|
390
396
|
|
|
391
|
-
var newSid = generateToken(
|
|
397
|
+
var newSid = generateToken(SID_BYTES);
|
|
392
398
|
var newSidHash = _hashSid(newSid);
|
|
393
399
|
var oldSidHash = _hashSid(oldToken);
|
|
394
400
|
var nowMs = Date.now();
|
package/lib/slug.js
CHANGED
|
@@ -40,10 +40,14 @@ var _err = SlugError.factory;
|
|
|
40
40
|
|
|
41
41
|
// ---- Defaults ----
|
|
42
42
|
|
|
43
|
+
// Default slug max length — 0x50 (80 chars). Long enough for nearly any
|
|
44
|
+
// title, short enough to keep URLs / file paths comfortable on every OS.
|
|
45
|
+
var DEFAULT_SLUG_MAX_LENGTH = 0x50;
|
|
46
|
+
|
|
43
47
|
var DEFAULTS = Object.freeze({
|
|
44
48
|
separator: "-",
|
|
45
49
|
lowercase: true,
|
|
46
|
-
maxLength:
|
|
50
|
+
maxLength: DEFAULT_SLUG_MAX_LENGTH,
|
|
47
51
|
preserveUnicode: false,
|
|
48
52
|
fallback: "",
|
|
49
53
|
});
|
|
@@ -106,12 +110,6 @@ function _slugify(title, opts) {
|
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
var sep = opts.separator;
|
|
109
|
-
// Build a regex to collapse runs of the chosen separator. Escape since
|
|
110
|
-
// separator is operator-supplied (validated single-char, but could be
|
|
111
|
-
// a regex meta-char like "." or "-").
|
|
112
|
-
var sepEsc = sep.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
113
|
-
var collapseSep = new RegExp(sepEsc + "+", "g");
|
|
114
|
-
var trimSep = new RegExp("^" + sepEsc + "+|" + sepEsc + "+$", "g");
|
|
115
113
|
|
|
116
114
|
var s = title;
|
|
117
115
|
|
|
@@ -130,7 +128,11 @@ function _slugify(title, opts) {
|
|
|
130
128
|
s = s.toLowerCase();
|
|
131
129
|
}
|
|
132
130
|
|
|
133
|
-
|
|
131
|
+
// Collapse runs of the chosen separator + trim leading/trailing
|
|
132
|
+
// separators, character-by-character. _validateOpts enforces
|
|
133
|
+
// sep.length === 1, so a linear scan is correct and avoids
|
|
134
|
+
// compiling a regex from operator input.
|
|
135
|
+
s = _collapseAndTrim(s, sep);
|
|
134
136
|
|
|
135
137
|
if (opts.maxLength !== null && s.length > opts.maxLength) {
|
|
136
138
|
s = _truncateAtSeparator(s, opts.maxLength, sep);
|
|
@@ -140,6 +142,31 @@ function _slugify(title, opts) {
|
|
|
140
142
|
return s;
|
|
141
143
|
}
|
|
142
144
|
|
|
145
|
+
// Single-pass collapse + trim — replaces the dynamic-RegExp-on-sep pair
|
|
146
|
+
// previously used here. sep is enforced 1-char by _validateOpts.
|
|
147
|
+
function _collapseAndTrim(s, sep) {
|
|
148
|
+
if (s.length === 0) return s;
|
|
149
|
+
var out = "";
|
|
150
|
+
var lastWasSep = true; // suppress leading separators
|
|
151
|
+
for (var i = 0; i < s.length; i++) {
|
|
152
|
+
var ch = s.charAt(i);
|
|
153
|
+
if (ch === sep) {
|
|
154
|
+
if (lastWasSep) continue;
|
|
155
|
+
lastWasSep = true;
|
|
156
|
+
out += ch;
|
|
157
|
+
} else {
|
|
158
|
+
lastWasSep = false;
|
|
159
|
+
out += ch;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Strip a trailing separator (the loop emits one if the last
|
|
163
|
+
// non-collapsed run was separator).
|
|
164
|
+
if (out.length > 0 && out.charAt(out.length - 1) === sep) {
|
|
165
|
+
out = out.slice(0, out.length - 1);
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
143
170
|
// Truncate at the last separator that keeps length ≤ maxLength.
|
|
144
171
|
// If there's no separator within the cap, fall back to char truncation
|
|
145
172
|
// (single-token long inputs).
|