@blamejs/core 0.5.17 → 0.6.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 +2 -0
- package/README.md +29 -1
- package/lib/backup/index.js +5 -0
- package/lib/cluster.js +17 -20
- package/lib/db.js +8 -3
- package/lib/deprecate.js +3 -2
- package/lib/error-page.js +2 -1
- package/lib/http-client-cookie-jar.js +2 -1
- package/lib/log.js +2 -1
- package/lib/middleware/api-encrypt.js +3 -2
- package/lib/notify.js +1 -1
- package/lib/pagination.js +2 -1
- package/lib/parsers/safe-env.js +1 -2
- package/lib/queue-local.js +2 -1
- package/lib/queue.js +2 -2
- package/lib/router.js +5 -3
- package/lib/testing.js +1 -1
- package/lib/totp.js +2 -2
- package/lib/webhook.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.5.x
|
|
10
10
|
|
|
11
|
+
- **0.5.18** (2026-05-01) — bypass-fix sweep: route through existing primitives
|
|
12
|
+
- **0.5.17** (2026-05-01) — primitive-drift sweep: csv unify + 3 new shared primitives
|
|
11
13
|
- **0.5.16** (2026-04-30) — b.otelExport: OTLP/HTTP exporter for b.observability
|
|
12
14
|
- **0.5.15** (2026-04-30) — b.archive: ZIP creation
|
|
13
15
|
- **0.5.14** (2026-04-30) — b.time: timezone-aware datetime arithmetic + formatting
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ The modern Node app is a 1,200-package supply-chain liability with no LTS calend
|
|
|
17
17
|
|
|
18
18
|
## Status
|
|
19
19
|
|
|
20
|
-
Pre-1.0.
|
|
20
|
+
Pre-1.0. Usable end-to-end — operators can build production apps on it today; the surface is still subject to change before 1.0. Recent line is **v0.6.0** ([releases](https://github.com/blamejs/blamejs/releases) · [npm](https://www.npmjs.com/package/@blamejs/core) · [container](https://github.com/blamejs/blamejs/pkgs/container/blamejs-wiki)).
|
|
21
21
|
|
|
22
22
|
```js
|
|
23
23
|
var b = require("@blamejs/core");
|
|
@@ -37,6 +37,34 @@ var b = require("@blamejs/core");
|
|
|
37
37
|
|
|
38
38
|
**Requirements:** Node.js 24+ (current active LTS).
|
|
39
39
|
|
|
40
|
+
## What ships in the box
|
|
41
|
+
|
|
42
|
+
The framework bundles the surface a typical Node app reaches for. Every primitive listed is callable today; nothing is a stub.
|
|
43
|
+
|
|
44
|
+
- **Data layer** — SQLite with sealed-by-default columns (`b.db`), migrations, seeders, atomic-file writes; S3 / R2 / B2 / GCS / Azure object store with multipart upload + SSE + bucket ops (`b.storage`, `b.objectStore`); durable queue with priority + cron + flows (`b.queue`, `b.jobs`); cluster-shared cache (`b.cache`).
|
|
45
|
+
- **Identity & access** — passwords (Argon2id), passkeys (WebAuthn), TOTP, JWT (PQ-default), OAuth, sessions, brute-force lockout (`b.auth.*`, `b.session`); RBAC (`b.permissions`); API keys with rotation (`b.apiKey`); break-glass column gates with second-factor + audit (`b.breakGlass`).
|
|
46
|
+
- **Crypto** — envelope-versioned PQC at rest (ML-KEM-1024 + P-384 hybrid, XChaCha20-Poly1305, SHAKE256), vault sealing, field-level crypto, signed webhooks (SLH-DSA-SHAKE-256f), ECIES API encryption (`b.crypto`, `b.vault`, `b.webhook`); pure-JS mTLS CA, PQC TLS gates inbound + outbound (`b.mtlsCa`, `b.pqcGate`, `b.pqcAgent`).
|
|
47
|
+
- **HTTP** — router with schema-validated routes + OpenAPI publication; full middleware stack (CSRF, CORS, rate-limit, security headers, CSP nonce, body parser, compression, SSE, request log) wired by `createApp`; HTTP/1.1 + HTTP/2 outbound client with SSRF gate, redirects, multipart, interceptors, progress, encrypted cookie jar (`b.httpClient`, `b.ssrfGuard`, `b.safeUrl`).
|
|
48
|
+
- **Defensive parsers** — `b.safeJson`, `b.safeBuffer`, `b.safeSql`, `b.safeSchema`, `b.parsers` (XML / TOML / YAML / .env), `b.config` (schema-validated env).
|
|
49
|
+
- **Communication** — WebSockets with channel/room fan-out across cluster replicas (`b.websocket`, `b.websocketChannels`); mail with multipart + attachments + DKIM + calendar invites + bounce intake (`b.mail`, `b.mailBounce`); generic notification dispatcher with operator-supplied transports (`b.notify`).
|
|
50
|
+
- **Observability** — tamper-evident audit chain with SLH-DSA-signed checkpoints, metrics, tracing (OTel pass-through when wired), PII redaction, log-stream sinks, OTLP/HTTP-JSON exporter for any OTel-compatible backend (`b.audit`, `b.metrics`, `b.tracing`, `b.redact`, `b.logStream`, `b.otelExport`).
|
|
51
|
+
- **i18n** — CLDR plural rules, Accept-Language negotiation, Intl formatters, RTL (`b.i18n`).
|
|
52
|
+
- **Format helpers** — RFC 4180 CSV with Excel formula-injection prevention (`b.csv`), RFC 9562 UUID v4 + v7 (`b.uuid`), URL-safe slugs (`b.slug`), TZ-aware datetime (`b.time`), ZIP creation (`b.archive`), HMAC-signed cursor pagination (`b.pagination`), HTML form rendering + validation + CSRF (`b.forms`).
|
|
53
|
+
- **Production** — cluster leader election with fenced leases over Postgres/SQLite (`b.cluster`); cron + interval scheduler that runs exactly-once globally (`b.scheduler`); retry with full-jitter backoff + circuit breaker (`b.retry`); graceful shutdown (`b.appShutdown`); NTP boot check (`b.ntpCheck`); end-to-end-encrypted backup bundles (`b.backup`).
|
|
54
|
+
|
|
55
|
+
## Documentation
|
|
56
|
+
|
|
57
|
+
Full primitive-by-primitive docs live at [blamejs.com](https://blamejs.com), which is itself the `examples/wiki/` app running in production. The wiki is organized by concern:
|
|
58
|
+
|
|
59
|
+
- **Data** — [Database](https://blamejs.com/database) · [Object Store](https://blamejs.com/object-store) · [Queue & Cache](https://blamejs.com/queue-cache)
|
|
60
|
+
- **Identity** — [Authentication](https://blamejs.com/auth) · [Access Control](https://blamejs.com/access-control)
|
|
61
|
+
- **Crypto** — [Crypto & Vault](https://blamejs.com/crypto-vault) · [Network Crypto](https://blamejs.com/network-crypto)
|
|
62
|
+
- **HTTP** — [Routing](https://blamejs.com/routing) · [Middleware](https://blamejs.com/middleware) · [Outbound HTTP](https://blamejs.com/outbound-http)
|
|
63
|
+
- **Validation** — [Safe Parsers](https://blamejs.com/safe-parsers)
|
|
64
|
+
- **Communication** — [WebSockets](https://blamejs.com/websockets) · [Mail](https://blamejs.com/mail) · [Notifications](https://blamejs.com/notifications)
|
|
65
|
+
- **Tools** — [Observability](https://blamejs.com/observability) · [Testing](https://blamejs.com/testing) · [i18n & Locale](https://blamejs.com/i18n-locale) · [Format Helpers](https://blamejs.com/format-helpers)
|
|
66
|
+
- **Production** — [Cluster Mode](https://blamejs.com/cluster) · [Reliability](https://blamejs.com/reliability) · [Backup & Restore](https://blamejs.com/backup-restore)
|
|
67
|
+
|
|
40
68
|
## CLI
|
|
41
69
|
|
|
42
70
|
`blamejs` ships an operator-facing CLI for the recurring ops work. Each subcommand boots a headless app instance from `--data-dir` (no HTTP listener), runs the operation, and shuts down. Same vault + DB + audit chain the running app uses.
|
package/lib/backup/index.js
CHANGED
|
@@ -243,6 +243,11 @@ function create(opts) {
|
|
|
243
243
|
// running backup against an external db handle still work).
|
|
244
244
|
if (flushBeforeBackup === null && opts.flushBeforeBackup !== false) {
|
|
245
245
|
try {
|
|
246
|
+
// Inline require: backup is a leaf module the framework re-exports
|
|
247
|
+
// without forcing operators to also bring in lib/db. Hoisting to
|
|
248
|
+
// top-of-file would require everyone using `b.backup` from
|
|
249
|
+
// outside the framework to have ./db loadable in the same module
|
|
250
|
+
// graph, which CLI tools and stand-alone backup runners don't.
|
|
246
251
|
var dbModule = require("../db");
|
|
247
252
|
if (typeof dbModule.flushToDisk === "function") {
|
|
248
253
|
flushBeforeBackup = function () { dbModule.flushToDisk(); };
|
package/lib/cluster.js
CHANGED
|
@@ -51,11 +51,21 @@
|
|
|
51
51
|
* await cluster.shutdown() releases lease, stops heartbeat
|
|
52
52
|
*/
|
|
53
53
|
var C = require("./constants");
|
|
54
|
+
var clusterProviderDb = require("./cluster-provider-db");
|
|
55
|
+
var crypto = require("./crypto");
|
|
56
|
+
var lazyRequire = require("./lazy-require");
|
|
54
57
|
var { boot } = require("./log");
|
|
55
58
|
var safeAsync = require("./safe-async");
|
|
59
|
+
var safeJson = require("./safe-json");
|
|
56
60
|
var safeUrl = require("./safe-url");
|
|
57
61
|
var { FrameworkError, ClusterError } = require("./framework-error");
|
|
58
62
|
|
|
63
|
+
// Lazy: vault → db → cluster forms a load-time chain, and external-db is
|
|
64
|
+
// loaded before its init has run; both are safe to call once cluster
|
|
65
|
+
// reaches runtime, but eager require here would deadlock the load order.
|
|
66
|
+
var externalDb = lazyRequire(function () { return require("./external-db"); });
|
|
67
|
+
var vault = lazyRequire(function () { return require("./vault"); });
|
|
68
|
+
|
|
59
69
|
var DEFAULT_LEASE_TTL = C.TIME.seconds(30);
|
|
60
70
|
var DEFAULT_HEARTBEAT = C.TIME.seconds(10);
|
|
61
71
|
var MIN_LEASE_TTL = C.TIME.seconds(5);
|
|
@@ -185,8 +195,7 @@ async function init(opts) {
|
|
|
185
195
|
throw _err("INVALID_CONFIG",
|
|
186
196
|
"cluster.init requires either { provider } or { externalDbBackend }", true);
|
|
187
197
|
}
|
|
188
|
-
|
|
189
|
-
provider = dbProvider.create({
|
|
198
|
+
provider = clusterProviderDb.create({
|
|
190
199
|
externalDbBackend: opts.externalDbBackend,
|
|
191
200
|
dialect: opts.dialect,
|
|
192
201
|
});
|
|
@@ -264,15 +273,9 @@ async function init(opts) {
|
|
|
264
273
|
// hash → FATAL via process.exit(1). Same posture as the
|
|
265
274
|
// single-node audit.tip sidecar rollback check.
|
|
266
275
|
async function _checkChainTipRollback(chainName, logTable, tipTable) {
|
|
267
|
-
// Lazy require because external-db isn't available before
|
|
268
|
-
// cluster.init (and cluster is required from many subsystems —
|
|
269
|
-
// a top-of-file require would form a circular load with
|
|
270
|
-
// external-db's audit emit path).
|
|
271
|
-
var externalDb = require("./external-db");
|
|
272
|
-
|
|
273
276
|
var tipRows;
|
|
274
277
|
try {
|
|
275
|
-
tipRows = await externalDb.query(
|
|
278
|
+
tipRows = await externalDb().query(
|
|
276
279
|
"SELECT atMonotonicCounter, rowHash FROM " + tipTable +
|
|
277
280
|
" WHERE scope = '" + chainName + "'",
|
|
278
281
|
[],
|
|
@@ -294,7 +297,7 @@ async function _checkChainTipRollback(chainName, logTable, tipTable) {
|
|
|
294
297
|
var tipCounter = Number(tip.atMonotonicCounter);
|
|
295
298
|
var tipHash = tip.rowHash;
|
|
296
299
|
|
|
297
|
-
var currentRows = await externalDb.query(
|
|
300
|
+
var currentRows = await externalDb().query(
|
|
298
301
|
"SELECT MAX(monotonicCounter) AS m FROM " + logTable,
|
|
299
302
|
[],
|
|
300
303
|
{ backend: configuredExternalDbBackend }
|
|
@@ -313,7 +316,7 @@ async function _checkChainTipRollback(chainName, logTable, tipTable) {
|
|
|
313
316
|
}
|
|
314
317
|
|
|
315
318
|
if (tipHash) {
|
|
316
|
-
var hashRows = await externalDb.query(
|
|
319
|
+
var hashRows = await externalDb().query(
|
|
317
320
|
"SELECT rowHash FROM " + logTable + " WHERE monotonicCounter = " +
|
|
318
321
|
(configuredDialect === "postgres" ? "$1" : "?"),
|
|
319
322
|
[tipCounter],
|
|
@@ -347,14 +350,9 @@ async function _checkChainTipRollback(chainName, logTable, tipTable) {
|
|
|
347
350
|
// posture as the audit-tip rollback check skipping when there's no
|
|
348
351
|
// audit-tip table.
|
|
349
352
|
function _vaultKeyFingerprint() {
|
|
350
|
-
// Lazy require to avoid the circular load risk: vault → db → cluster
|
|
351
|
-
// (the audit module pulls cluster in, db loads audit at init).
|
|
352
|
-
var vault = require("./vault");
|
|
353
|
-
var crypto = require("./crypto");
|
|
354
|
-
var safeJson = require("./safe-json");
|
|
355
353
|
var keysJson;
|
|
356
354
|
try {
|
|
357
|
-
keysJson = vault.getKeysJson();
|
|
355
|
+
keysJson = vault().getKeysJson();
|
|
358
356
|
} catch (e) {
|
|
359
357
|
// vault.init() not called — gates-only mode. Skip silently.
|
|
360
358
|
if (/vault.init\(\) must be awaited/.test((e && e.message) || "")) {
|
|
@@ -380,7 +378,6 @@ async function _checkVaultKeyConsistency() {
|
|
|
380
378
|
log("vault not initialized — skipping vault-key consistency check (cluster gates-only mode)");
|
|
381
379
|
return;
|
|
382
380
|
}
|
|
383
|
-
var externalDb = require("./external-db");
|
|
384
381
|
var nowMs = Date.now();
|
|
385
382
|
var ph = configuredDialect === "postgres";
|
|
386
383
|
|
|
@@ -390,7 +387,7 @@ async function _checkVaultKeyConsistency() {
|
|
|
390
387
|
// compares — any mismatch (including ours after a losing race)
|
|
391
388
|
// surfaces the drift.
|
|
392
389
|
try {
|
|
393
|
-
await externalDb.query(
|
|
390
|
+
await externalDb().query(
|
|
394
391
|
"INSERT INTO _blamejs_cluster_state " +
|
|
395
392
|
" (scope, vaultKeyFp, recordedAt, recordedByNode) " +
|
|
396
393
|
"VALUES ('state', " +
|
|
@@ -413,7 +410,7 @@ async function _checkVaultKeyConsistency() {
|
|
|
413
410
|
|
|
414
411
|
// Read whatever fingerprint is canonical (ours if first boot,
|
|
415
412
|
// someone else's if we lost the race or are joining an existing cluster).
|
|
416
|
-
var rows = await externalDb.query(
|
|
413
|
+
var rows = await externalDb().query(
|
|
417
414
|
"SELECT vaultKeyFp, recordedByNode, recordedAt FROM _blamejs_cluster_state " +
|
|
418
415
|
"WHERE scope = 'state'",
|
|
419
416
|
[],
|
package/lib/db.js
CHANGED
|
@@ -55,11 +55,17 @@ var cryptoField = require("./crypto-field");
|
|
|
55
55
|
var { Query } = require("./db-query");
|
|
56
56
|
var dbSchema = require("./db-schema");
|
|
57
57
|
var { boot } = require("./log");
|
|
58
|
+
var lazyRequire = require("./lazy-require");
|
|
58
59
|
var safeAsync = require("./safe-async");
|
|
59
60
|
var safeEnv = require("./parsers/safe-env");
|
|
60
61
|
var safeJson = require("./safe-json");
|
|
61
62
|
var vault = require("./vault");
|
|
62
63
|
|
|
64
|
+
// Lazy: cluster-storage's _localDb pulls db back in, so eager require
|
|
65
|
+
// would deadlock the load order. cluster-storage is only used on the
|
|
66
|
+
// purge-audit-chain external-db path, which always runs after init.
|
|
67
|
+
var clusterStorage = lazyRequire(function () { return require("./cluster-storage"); });
|
|
68
|
+
|
|
63
69
|
var AUDIT_TIP_SCHEMA = {
|
|
64
70
|
type: "object",
|
|
65
71
|
required: ["atMonotonicCounter"],
|
|
@@ -1119,10 +1125,9 @@ module.exports = {
|
|
|
1119
1125
|
if (!Number.isFinite(lastPurgedCounter) || lastPurgedCounter < 0) {
|
|
1120
1126
|
throw new Error("purgeAuditChain: lastPurgedCounter must be a non-negative number");
|
|
1121
1127
|
}
|
|
1122
|
-
|
|
1123
|
-
if (c.isClusterMode()) {
|
|
1128
|
+
if (cluster.isClusterMode()) {
|
|
1124
1129
|
// External-db has no append-only triggers; ordinary DELETE works.
|
|
1125
|
-
var cs =
|
|
1130
|
+
var cs = clusterStorage();
|
|
1126
1131
|
var d = await cs.execute(
|
|
1127
1132
|
"DELETE FROM audit_log WHERE monotonicCounter <= ?", [lastPurgedCounter]
|
|
1128
1133
|
);
|
package/lib/deprecate.js
CHANGED
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
* call counter is still incremented so dep.list() shows usage volume.
|
|
51
51
|
*/
|
|
52
52
|
|
|
53
|
+
var safeEnv = require("./parsers/safe-env");
|
|
53
54
|
var { FrameworkError } = require("./framework-error");
|
|
54
55
|
|
|
55
56
|
class DeprecateError extends FrameworkError {
|
|
@@ -65,12 +66,12 @@ class DeprecateError extends FrameworkError {
|
|
|
65
66
|
var _seen = new Map();
|
|
66
67
|
|
|
67
68
|
function _modeFromEnv() {
|
|
68
|
-
var env =
|
|
69
|
+
var env = safeEnv.readVar("BLAMEJS_DEPRECATIONS");
|
|
69
70
|
if (typeof env === "string" && env.length > 0) {
|
|
70
71
|
var v = env.toLowerCase();
|
|
71
72
|
if (v === "warn" || v === "silent" || v === "error") return v;
|
|
72
73
|
}
|
|
73
|
-
if (
|
|
74
|
+
if (safeEnv.readVar("NODE_ENV") === "production") return "silent";
|
|
74
75
|
return "warn";
|
|
75
76
|
}
|
|
76
77
|
|
package/lib/error-page.js
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
|
|
41
41
|
var lazyRequire = require("./lazy-require");
|
|
42
42
|
var requestHelpers = require("./request-helpers");
|
|
43
|
+
var safeEnv = require("./parsers/safe-env");
|
|
43
44
|
var template = require("./template");
|
|
44
45
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
45
46
|
|
|
@@ -273,7 +274,7 @@ function create(opts) {
|
|
|
273
274
|
if (modeOpt === "dev" || modeOpt === "prod") {
|
|
274
275
|
mode = modeOpt;
|
|
275
276
|
} else {
|
|
276
|
-
var nodeEnv =
|
|
277
|
+
var nodeEnv = safeEnv.readVar("NODE_ENV");
|
|
277
278
|
mode = (nodeEnv === "production") ? "prod" : "dev";
|
|
278
279
|
}
|
|
279
280
|
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
* }
|
|
44
44
|
*/
|
|
45
45
|
|
|
46
|
+
var C = require("./constants");
|
|
46
47
|
var safeUrl = require("./safe-url");
|
|
47
48
|
var { defineClass } = require("./framework-error");
|
|
48
49
|
|
|
@@ -186,7 +187,7 @@ function create(opts) {
|
|
|
186
187
|
if (attrs["max-age"] !== undefined) {
|
|
187
188
|
var maxAge = parseInt(attrs["max-age"], 10);
|
|
188
189
|
if (!isNaN(maxAge)) {
|
|
189
|
-
expiresAt = maxAge <= 0 ? 0 : (now + maxAge
|
|
190
|
+
expiresAt = maxAge <= 0 ? 0 : (now + C.TIME.seconds(maxAge));
|
|
190
191
|
}
|
|
191
192
|
} else if (attrs.expires) {
|
|
192
193
|
expiresAt = _parseHttpDate(attrs.expires);
|
package/lib/log.js
CHANGED
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
*/
|
|
61
61
|
|
|
62
62
|
var { AsyncLocalStorage } = require("node:async_hooks");
|
|
63
|
+
var nodeCrypto = require("node:crypto");
|
|
63
64
|
var redact = require("./redact");
|
|
64
65
|
var validateOpts = require("./validate-opts");
|
|
65
66
|
var { FrameworkError } = require("./framework-error");
|
|
@@ -315,7 +316,7 @@ function create(opts) {
|
|
|
315
316
|
? mwOpts.generate
|
|
316
317
|
: function () {
|
|
317
318
|
// 16 random hex chars — short, sufficient correlation entropy
|
|
318
|
-
return
|
|
319
|
+
return nodeCrypto.randomBytes(8).toString("hex");
|
|
319
320
|
};
|
|
320
321
|
return function logRequestIdMiddleware(req, res, next) {
|
|
321
322
|
var inbound = req.headers && req.headers[headerName];
|
|
@@ -174,7 +174,7 @@ function create(opts) {
|
|
|
174
174
|
validateOpts(opts, [
|
|
175
175
|
"keypair", "keypairs", "replayWindowMs", "pruneIntervalMs",
|
|
176
176
|
"nonceStore", "exemptPaths", "contentTypes", "audit",
|
|
177
|
-
"maxDecryptedBytes",
|
|
177
|
+
"maxDecryptedBytes", "trustProxy",
|
|
178
178
|
], "middleware.apiEncrypt");
|
|
179
179
|
var keypairs = _resolveKeypairs(opts);
|
|
180
180
|
var activeKeypair = keypairs[0];
|
|
@@ -205,6 +205,7 @@ function create(opts) {
|
|
|
205
205
|
? opts.contentTypes.slice()
|
|
206
206
|
: DEFAULT_CONTENT_TYPES.slice());
|
|
207
207
|
var auditOn = opts.audit !== false;
|
|
208
|
+
var trustProxy = opts.trustProxy === true;
|
|
208
209
|
var lastPruneAt = 0;
|
|
209
210
|
|
|
210
211
|
function _isExempt(req) {
|
|
@@ -233,7 +234,7 @@ function create(opts) {
|
|
|
233
234
|
function _emitFailure(req, reason) {
|
|
234
235
|
var info = {
|
|
235
236
|
reason: reason,
|
|
236
|
-
ip: (req
|
|
237
|
+
ip: requestHelpers.clientIp(req, { trustProxy: trustProxy }),
|
|
237
238
|
path: req.pathname || (req.url || "/").split("?")[0],
|
|
238
239
|
method: req.method,
|
|
239
240
|
ts: new Date().toISOString(),
|
package/lib/notify.js
CHANGED
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
*/
|
|
49
49
|
|
|
50
50
|
var lazyRequire = require("./lazy-require");
|
|
51
|
+
var bootLog = require("./log");
|
|
51
52
|
var requestHelpers = require("./request-helpers");
|
|
52
53
|
var safeAsync = require("./safe-async");
|
|
53
54
|
var safeUrl = require("./safe-url");
|
|
@@ -274,7 +275,6 @@ function logTransport(opts) {
|
|
|
274
275
|
if (opts.logger && typeof opts.logger.info === "function") {
|
|
275
276
|
logger = opts.logger;
|
|
276
277
|
} else {
|
|
277
|
-
var bootLog = require("./log");
|
|
278
278
|
logger = bootLog.boot("notify.log");
|
|
279
279
|
// boot returns a fn-with-fields shape; tolerate both.
|
|
280
280
|
if (typeof logger === "function") {
|
package/lib/pagination.js
CHANGED
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
*/
|
|
90
90
|
|
|
91
91
|
var nodeCrypto = require("node:crypto");
|
|
92
|
+
var crypto = require("./crypto");
|
|
92
93
|
var { defineClass } = require("./framework-error");
|
|
93
94
|
|
|
94
95
|
var PaginationError = defineClass("PaginationError", { alwaysPermanent: true });
|
|
@@ -171,7 +172,7 @@ function decodeCursor(token, secret) {
|
|
|
171
172
|
throw new PaginationError("pagination/bad-cursor", "cursor base64 decode failed");
|
|
172
173
|
}
|
|
173
174
|
var expected = _tag(sb, json);
|
|
174
|
-
if (
|
|
175
|
+
if (!crypto.timingSafeEqual(tag, expected)) {
|
|
175
176
|
throw new PaginationError("pagination/cursor-tag-mismatch",
|
|
176
177
|
"cursor HMAC verification failed (tampered or wrong secret)");
|
|
177
178
|
}
|
package/lib/parsers/safe-env.js
CHANGED
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
var C = require("../constants");
|
|
58
58
|
var atomicFile = require("../atomic-file");
|
|
59
59
|
var safeBuffer = require("../safe-buffer");
|
|
60
|
+
var safeJson = require("../safe-json");
|
|
60
61
|
var { FrameworkError } = require("../framework-error");
|
|
61
62
|
var { boot } = require("../log");
|
|
62
63
|
|
|
@@ -275,7 +276,6 @@ function _coerceType(rawValue, type, key) {
|
|
|
275
276
|
);
|
|
276
277
|
}
|
|
277
278
|
if (type === "json") {
|
|
278
|
-
var safeJson = require("./../safe-json");
|
|
279
279
|
try { return safeJson.parse(rawValue); }
|
|
280
280
|
catch (e) {
|
|
281
281
|
throw new SafeEnvError("invalid JSON for key '" + key + "': " + e.message,
|
|
@@ -437,7 +437,6 @@ function load(filepath, opts) {
|
|
|
437
437
|
if (snapshotPath && atomicFile.exists(snapshotPath)) {
|
|
438
438
|
try {
|
|
439
439
|
var snapBuf = atomicFile.readSync(snapshotPath);
|
|
440
|
-
var safeJson = require("./../safe-json");
|
|
441
440
|
prevValues = safeJson.parse(snapBuf) || {};
|
|
442
441
|
} catch (_e) { /* missing/corrupt snapshot → treat as empty */ }
|
|
443
442
|
}
|
package/lib/queue-local.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
*/
|
|
32
32
|
var cluster = require("./cluster");
|
|
33
33
|
var clusterStorage = require("./cluster-storage");
|
|
34
|
+
var C = require("./constants");
|
|
34
35
|
var { generateToken } = require("./crypto");
|
|
35
36
|
var cryptoField = require("./crypto-field");
|
|
36
37
|
var lazyRequire = require("./lazy-require");
|
|
@@ -108,7 +109,7 @@ function create(_config) {
|
|
|
108
109
|
cluster.requireLeader();
|
|
109
110
|
opts = opts || {};
|
|
110
111
|
var nowMs = Date.now();
|
|
111
|
-
var availableAt = nowMs + (opts.delaySeconds ? opts.delaySeconds
|
|
112
|
+
var availableAt = nowMs + (opts.delaySeconds ? C.TIME.seconds(opts.delaySeconds) : 0);
|
|
112
113
|
|
|
113
114
|
var priority = (typeof opts.priority === "number" && isFinite(opts.priority))
|
|
114
115
|
? Math.floor(opts.priority) : 0;
|
package/lib/queue.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* failed (status='failed')
|
|
32
32
|
*/
|
|
33
33
|
var C = require("./constants");
|
|
34
|
+
var clusterStorage = require("./cluster-storage");
|
|
34
35
|
var crypto = require("./crypto");
|
|
35
36
|
var lazyRequire = require("./lazy-require");
|
|
36
37
|
var observability = require("./observability");
|
|
@@ -196,7 +197,7 @@ function consume(queueName, handler, opts) {
|
|
|
196
197
|
}
|
|
197
198
|
rateLimit = {
|
|
198
199
|
max: opts.rateLimit.max,
|
|
199
|
-
windowMs: opts.rateLimit.perSeconds
|
|
200
|
+
windowMs: C.TIME.seconds(opts.rateLimit.perSeconds),
|
|
200
201
|
timestamps: [],
|
|
201
202
|
};
|
|
202
203
|
}
|
|
@@ -602,7 +603,6 @@ function enqueueFlow(spec) {
|
|
|
602
603
|
}
|
|
603
604
|
// Second pass: write dependsOn (translated to jobIds) for children
|
|
604
605
|
// that need it, and parking-lot their availableAt to MAX_SAFE_INTEGER.
|
|
605
|
-
var clusterStorage = require("./cluster-storage");
|
|
606
606
|
for (var q = 0; q < jobs.length; q++) {
|
|
607
607
|
var j = jobs[q];
|
|
608
608
|
if (j.dependsOn.length === 0) continue;
|
package/lib/router.js
CHANGED
|
@@ -26,6 +26,7 @@ var path = require("path");
|
|
|
26
26
|
var { URL } = require("url");
|
|
27
27
|
var C = require("./constants");
|
|
28
28
|
var safeAsync = require("./safe-async");
|
|
29
|
+
var safeEnv = require("./parsers/safe-env");
|
|
29
30
|
var websocket = require("./websocket");
|
|
30
31
|
var { boot } = require("./log");
|
|
31
32
|
|
|
@@ -112,7 +113,7 @@ function _makeResponseValidator(spec) {
|
|
|
112
113
|
// - BLAMEJS_VALIDATE_RESPONSES=warn (or per-route validateResponse: "warn")
|
|
113
114
|
// → log a warning; ship the response as-is (prod-safe).
|
|
114
115
|
var perRoute = spec.validateResponse;
|
|
115
|
-
var globalMode =
|
|
116
|
+
var globalMode = safeEnv.readVar("BLAMEJS_VALIDATE_RESPONSES");
|
|
116
117
|
var mode = (perRoute === "throw" || perRoute === "warn") ? perRoute :
|
|
117
118
|
(globalMode === "throw" || globalMode === "warn") ? globalMode : null;
|
|
118
119
|
if (!mode) return function passthrough(_req, _res, next) { next(); };
|
|
@@ -260,9 +261,10 @@ class Router {
|
|
|
260
261
|
// route dispatch) but before any route-specific handler.
|
|
261
262
|
handlers = [_makeSchemaValidator(split.spec)].concat(handlers);
|
|
262
263
|
// Response validation (dev/opt-in via env or per-route opt).
|
|
264
|
+
var globalValidateMode = safeEnv.readVar("BLAMEJS_VALIDATE_RESPONSES");
|
|
263
265
|
if (split.spec.response &&
|
|
264
|
-
(
|
|
265
|
-
|
|
266
|
+
(globalValidateMode === "throw" ||
|
|
267
|
+
globalValidateMode === "warn" ||
|
|
266
268
|
split.spec.validateResponse)) {
|
|
267
269
|
handlers = [_makeResponseValidator(split.spec)].concat(handlers);
|
|
268
270
|
}
|
package/lib/testing.js
CHANGED
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
*/
|
|
61
61
|
|
|
62
62
|
var fs = require("node:fs");
|
|
63
|
+
var http = require("node:http");
|
|
63
64
|
var os = require("node:os");
|
|
64
65
|
var nodePath = require("node:path");
|
|
65
66
|
var EventEmitter = require("node:events").EventEmitter;
|
|
@@ -567,7 +568,6 @@ function listenOnRandomPort(server, host) {
|
|
|
567
568
|
// production traffic takes. Server is closed automatically when the
|
|
568
569
|
// promise resolves or rejects.
|
|
569
570
|
function request(target) {
|
|
570
|
-
var http = require("node:http");
|
|
571
571
|
// Resolve target → request listener
|
|
572
572
|
var server;
|
|
573
573
|
var ownsServer = false;
|
package/lib/totp.js
CHANGED
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
* 1Password, Bitwarden, Aegis, Microsoft Authenticator all do).
|
|
61
61
|
*/
|
|
62
62
|
var nodeCrypto = require("crypto");
|
|
63
|
+
var crypto = require("./crypto");
|
|
63
64
|
var { generateBytes, generateToken } = require("./crypto");
|
|
64
65
|
var { AuthError } = require("./framework-error");
|
|
65
66
|
|
|
@@ -214,8 +215,7 @@ function verify(secret, code, opts) {
|
|
|
214
215
|
try { expected = compute(secret, step, opts); }
|
|
215
216
|
catch (_e) { return false; }
|
|
216
217
|
var expectedBuf = Buffer.from(expected);
|
|
217
|
-
if (expectedBuf
|
|
218
|
-
nodeCrypto.timingSafeEqual(expectedBuf, userBuf)) {
|
|
218
|
+
if (crypto.timingSafeEqual(expectedBuf, userBuf)) {
|
|
219
219
|
return step;
|
|
220
220
|
}
|
|
221
221
|
}
|
package/lib/webhook.js
CHANGED
|
@@ -539,7 +539,7 @@ function verifier(opts) {
|
|
|
539
539
|
|
|
540
540
|
// Timestamp window: signed in seconds, compare to ms clock.
|
|
541
541
|
var nowMs = nowFn();
|
|
542
|
-
var ageMs = nowMs - (ts
|
|
542
|
+
var ageMs = nowMs - C.TIME.seconds(ts);
|
|
543
543
|
if (ageMs > toleranceMs) {
|
|
544
544
|
throw _failure("EXPIRED", "webhook: timestamp older than toleranceMs (age=" + ageMs + "ms)", "expired", ctxReq);
|
|
545
545
|
}
|
|
@@ -570,7 +570,7 @@ function verifier(opts) {
|
|
|
570
570
|
}
|
|
571
571
|
|
|
572
572
|
if (nonceStore) {
|
|
573
|
-
var expireAt = (ts
|
|
573
|
+
var expireAt = C.TIME.seconds(ts) + toleranceMs;
|
|
574
574
|
var fresh = await nonceStore.checkAndInsert(parsed.id, expireAt);
|
|
575
575
|
if (!fresh) {
|
|
576
576
|
throw _failure("REPLAY", "webhook: id '" + parsed.id + "' has been seen before", "replay", ctxReq);
|