@blamejs/core 0.6.70 → 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 +4 -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 +23 -26
- 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 +4 -3
- 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 +26 -15
- 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/middleware/health.js
CHANGED
|
@@ -99,15 +99,22 @@
|
|
|
99
99
|
* fails as { ok: false, error: "timeout" } and the others complete.
|
|
100
100
|
*/
|
|
101
101
|
|
|
102
|
+
var C = require("../constants");
|
|
103
|
+
var nb = require("../numeric-bounds");
|
|
104
|
+
var requestHelpers = require("../request-helpers");
|
|
102
105
|
var safeAsync = require("../safe-async");
|
|
103
106
|
var validateOpts = require("../validate-opts");
|
|
104
107
|
var { defineClass } = require("../framework-error");
|
|
105
108
|
|
|
109
|
+
var HTTP_STATUS = requestHelpers.HTTP_STATUS;
|
|
110
|
+
|
|
106
111
|
var HealthError = defineClass("HealthError", { alwaysPermanent: true });
|
|
107
112
|
|
|
108
113
|
var TIERS = Object.freeze(["liveness", "readiness", "startup"]);
|
|
109
114
|
var TIER_SET = new Set(TIERS);
|
|
110
115
|
|
|
116
|
+
var DEFAULT_TIMEOUT_MS = C.TIME.seconds(5);
|
|
117
|
+
|
|
111
118
|
function create(opts) {
|
|
112
119
|
opts = opts || {};
|
|
113
120
|
validateOpts(opts, [
|
|
@@ -123,9 +130,26 @@ function create(opts) {
|
|
|
123
130
|
"detailLevel must be 'minimal' or 'detailed'");
|
|
124
131
|
}
|
|
125
132
|
var detailPredicate = typeof opts.detailPredicate === "function" ? opts.detailPredicate : null;
|
|
126
|
-
var defaultTimeoutMs
|
|
127
|
-
|
|
128
|
-
|
|
133
|
+
var defaultTimeoutMs;
|
|
134
|
+
if (opts.defaultTimeoutMs === undefined) {
|
|
135
|
+
defaultTimeoutMs = DEFAULT_TIMEOUT_MS;
|
|
136
|
+
} else if (nb.isPositiveFiniteInt(opts.defaultTimeoutMs)) {
|
|
137
|
+
defaultTimeoutMs = opts.defaultTimeoutMs;
|
|
138
|
+
} else {
|
|
139
|
+
throw new HealthError("health/bad-opt",
|
|
140
|
+
"defaultTimeoutMs must be a positive finite integer; got " +
|
|
141
|
+
nb.shape(opts.defaultTimeoutMs));
|
|
142
|
+
}
|
|
143
|
+
var cacheMs;
|
|
144
|
+
if (opts.cacheMs === undefined) {
|
|
145
|
+
cacheMs = 0;
|
|
146
|
+
} else if (nb.isNonNegativeFiniteInt(opts.cacheMs)) {
|
|
147
|
+
cacheMs = opts.cacheMs;
|
|
148
|
+
} else {
|
|
149
|
+
throw new HealthError("health/bad-opt",
|
|
150
|
+
"cacheMs must be a non-negative finite integer; got " +
|
|
151
|
+
nb.shape(opts.cacheMs));
|
|
152
|
+
}
|
|
129
153
|
var includeMeta = opts.includeMeta !== false;
|
|
130
154
|
var version = opts.version || null;
|
|
131
155
|
|
|
@@ -164,8 +188,16 @@ function create(opts) {
|
|
|
164
188
|
}
|
|
165
189
|
}
|
|
166
190
|
}
|
|
167
|
-
var timeoutMs
|
|
168
|
-
|
|
191
|
+
var timeoutMs;
|
|
192
|
+
if (copts.timeoutMs === undefined) {
|
|
193
|
+
timeoutMs = defaultTimeoutMs;
|
|
194
|
+
} else if (nb.isPositiveFiniteInt(copts.timeoutMs)) {
|
|
195
|
+
timeoutMs = copts.timeoutMs;
|
|
196
|
+
} else {
|
|
197
|
+
throw new HealthError("health/bad-opt",
|
|
198
|
+
"registerCheck: timeoutMs must be a positive finite integer; got " +
|
|
199
|
+
nb.shape(copts.timeoutMs));
|
|
200
|
+
}
|
|
169
201
|
checks.push({
|
|
170
202
|
name: name,
|
|
171
203
|
fn: fn,
|
|
@@ -267,7 +299,8 @@ function create(opts) {
|
|
|
267
299
|
}
|
|
268
300
|
|
|
269
301
|
function _writeResponse(res, result, includeDetail) {
|
|
270
|
-
var status = (result.status === "ok" || result.status === "degraded")
|
|
302
|
+
var status = (result.status === "ok" || result.status === "degraded")
|
|
303
|
+
? HTTP_STATUS.OK : HTTP_STATUS.SERVICE_UNAVAILABLE;
|
|
271
304
|
var payload;
|
|
272
305
|
if (includeDetail) {
|
|
273
306
|
payload = { status: result.status, checks: result.checks };
|
|
@@ -309,7 +342,8 @@ function create(opts) {
|
|
|
309
342
|
var result = await runChecks(tier);
|
|
310
343
|
if (req.method === "HEAD") {
|
|
311
344
|
// HEAD: status code only, no body.
|
|
312
|
-
var status = (result.status === "ok" || result.status === "degraded")
|
|
345
|
+
var status = (result.status === "ok" || result.status === "degraded")
|
|
346
|
+
? HTTP_STATUS.OK : HTTP_STATUS.SERVICE_UNAVAILABLE;
|
|
313
347
|
res.writeHead(status, { "Cache-Control": "no-store" });
|
|
314
348
|
res.end();
|
|
315
349
|
return;
|
|
@@ -319,7 +353,7 @@ function create(opts) {
|
|
|
319
353
|
// Catastrophic — write 503 with minimal detail and never leak
|
|
320
354
|
// internal error info on a public endpoint.
|
|
321
355
|
var body = JSON.stringify({ status: "fail", error: "health-check-internal" });
|
|
322
|
-
res.writeHead(
|
|
356
|
+
res.writeHead(HTTP_STATUS.SERVICE_UNAVAILABLE, {
|
|
323
357
|
"Content-Type": "application/json; charset=utf-8",
|
|
324
358
|
"Content-Length": Buffer.byteLength(body),
|
|
325
359
|
"Cache-Control": "no-store",
|
package/lib/middleware/index.js
CHANGED
|
@@ -16,45 +16,64 @@
|
|
|
16
16
|
* 6. (your auth + business middleware + routes)
|
|
17
17
|
* 7. errorHandler — must be LAST so it catches everything that throws
|
|
18
18
|
*/
|
|
19
|
+
var apiEncrypt = require("./api-encrypt");
|
|
20
|
+
var attachUser = require("./attach-user");
|
|
21
|
+
var bodyParser = require("./body-parser");
|
|
22
|
+
var botGuard = require("./bot-guard");
|
|
23
|
+
var compression = require("./compression");
|
|
24
|
+
var cors = require("./cors");
|
|
25
|
+
var cspNonce = require("./csp-nonce");
|
|
26
|
+
var csrfProtect = require("./csrf-protect");
|
|
27
|
+
var dbRoleFor = require("./db-role-for");
|
|
28
|
+
var errorHandler = require("./error-handler");
|
|
29
|
+
var health = require("./health");
|
|
30
|
+
var networkAllowlist = require("./network-allowlist");
|
|
31
|
+
var rateLimit = require("./rate-limit");
|
|
32
|
+
var requestId = require("./request-id");
|
|
33
|
+
var requestLog = require("./request-log");
|
|
34
|
+
var requireAuth = require("./require-auth");
|
|
35
|
+
var securityHeaders = require("./security-headers");
|
|
36
|
+
var sse = require("./sse");
|
|
37
|
+
|
|
19
38
|
module.exports = {
|
|
20
|
-
requestId:
|
|
21
|
-
securityHeaders:
|
|
22
|
-
errorHandler:
|
|
23
|
-
botGuard:
|
|
24
|
-
cors:
|
|
25
|
-
rateLimit:
|
|
26
|
-
attachUser:
|
|
27
|
-
requireAuth:
|
|
28
|
-
csrfProtect:
|
|
29
|
-
bodyParser:
|
|
30
|
-
health:
|
|
31
|
-
compression:
|
|
32
|
-
cspNonce:
|
|
33
|
-
sse:
|
|
34
|
-
requestLog:
|
|
35
|
-
apiEncrypt:
|
|
36
|
-
dbRoleFor:
|
|
37
|
-
networkAllowlist:
|
|
39
|
+
requestId: requestId.create,
|
|
40
|
+
securityHeaders: securityHeaders.create,
|
|
41
|
+
errorHandler: errorHandler.create,
|
|
42
|
+
botGuard: botGuard.create,
|
|
43
|
+
cors: cors.create,
|
|
44
|
+
rateLimit: rateLimit.create,
|
|
45
|
+
attachUser: attachUser.create,
|
|
46
|
+
requireAuth: requireAuth.create,
|
|
47
|
+
csrfProtect: csrfProtect.create,
|
|
48
|
+
bodyParser: bodyParser.create,
|
|
49
|
+
health: health.create,
|
|
50
|
+
compression: compression.create,
|
|
51
|
+
cspNonce: cspNonce.create,
|
|
52
|
+
sse: sse.create,
|
|
53
|
+
requestLog: requestLog.create,
|
|
54
|
+
apiEncrypt: apiEncrypt,
|
|
55
|
+
dbRoleFor: dbRoleFor.create,
|
|
56
|
+
networkAllowlist: networkAllowlist.create,
|
|
38
57
|
|
|
39
58
|
// Module exports for advanced use (constants, raw factory access)
|
|
40
59
|
_modules: {
|
|
41
|
-
requestId:
|
|
42
|
-
securityHeaders:
|
|
43
|
-
errorHandler:
|
|
44
|
-
botGuard:
|
|
45
|
-
cors:
|
|
46
|
-
rateLimit:
|
|
47
|
-
attachUser:
|
|
48
|
-
requireAuth:
|
|
49
|
-
csrfProtect:
|
|
50
|
-
bodyParser:
|
|
51
|
-
health:
|
|
52
|
-
compression:
|
|
53
|
-
cspNonce:
|
|
54
|
-
sse:
|
|
55
|
-
requestLog:
|
|
56
|
-
apiEncrypt:
|
|
57
|
-
dbRoleFor:
|
|
58
|
-
networkAllowlist:
|
|
60
|
+
requestId: requestId,
|
|
61
|
+
securityHeaders: securityHeaders,
|
|
62
|
+
errorHandler: errorHandler,
|
|
63
|
+
botGuard: botGuard,
|
|
64
|
+
cors: cors,
|
|
65
|
+
rateLimit: rateLimit,
|
|
66
|
+
attachUser: attachUser,
|
|
67
|
+
requireAuth: requireAuth,
|
|
68
|
+
csrfProtect: csrfProtect,
|
|
69
|
+
bodyParser: bodyParser,
|
|
70
|
+
health: health,
|
|
71
|
+
compression: compression,
|
|
72
|
+
cspNonce: cspNonce,
|
|
73
|
+
sse: sse,
|
|
74
|
+
requestLog: requestLog,
|
|
75
|
+
apiEncrypt: apiEncrypt,
|
|
76
|
+
dbRoleFor: dbRoleFor,
|
|
77
|
+
networkAllowlist: networkAllowlist,
|
|
59
78
|
},
|
|
60
79
|
};
|
|
@@ -77,7 +77,8 @@ function _requirePositiveNumber(name, value) {
|
|
|
77
77
|
// ---- Memory backend (token bucket) ----
|
|
78
78
|
|
|
79
79
|
function _memoryBackend(opts) {
|
|
80
|
-
|
|
80
|
+
// Default 1-per-second tokens fully refilled in 1 minute = 60 tokens.
|
|
81
|
+
var burst = opts.burst != null ? opts.burst : C.TIME.minutes(1) / C.TIME.seconds(1);
|
|
81
82
|
var refillPerSecond = opts.refillPerSecond != null ? opts.refillPerSecond : 10;
|
|
82
83
|
_requirePositiveNumber("burst", burst);
|
|
83
84
|
_requirePositiveNumber("refillPerSecond", refillPerSecond);
|
|
@@ -102,7 +103,7 @@ function _memoryBackend(opts) {
|
|
|
102
103
|
b = { tokens: burst, lastRefillAt: now };
|
|
103
104
|
buckets.set(key, b);
|
|
104
105
|
} else {
|
|
105
|
-
var elapsed = (now - b.lastRefillAt) /
|
|
106
|
+
var elapsed = (now - b.lastRefillAt) / C.TIME.seconds(1);
|
|
106
107
|
b.tokens = Math.min(burst, b.tokens + elapsed * refillPerSecond);
|
|
107
108
|
b.lastRefillAt = now;
|
|
108
109
|
}
|
|
@@ -116,12 +117,12 @@ function _memoryBackend(opts) {
|
|
|
116
117
|
};
|
|
117
118
|
}
|
|
118
119
|
var deficit = 1 - b.tokens;
|
|
119
|
-
var waitMs = Math.ceil((deficit / refillPerSecond)
|
|
120
|
+
var waitMs = Math.ceil(C.TIME.seconds(deficit / refillPerSecond));
|
|
120
121
|
return {
|
|
121
122
|
allowed: false,
|
|
122
123
|
limit: burst,
|
|
123
124
|
remaining: 0,
|
|
124
|
-
retryAfter: Math.ceil(waitMs /
|
|
125
|
+
retryAfter: Math.ceil(waitMs / C.TIME.seconds(1)),
|
|
125
126
|
};
|
|
126
127
|
}
|
|
127
128
|
|
|
@@ -130,7 +131,7 @@ function _memoryBackend(opts) {
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
function close() {
|
|
133
|
-
try { gcInterval.stop(); } catch (_e) {}
|
|
134
|
+
try { gcInterval.stop(); } catch (_e) { /* timer already stopped */ }
|
|
134
135
|
buckets.clear();
|
|
135
136
|
}
|
|
136
137
|
|
|
@@ -140,7 +141,9 @@ function _memoryBackend(opts) {
|
|
|
140
141
|
// ---- Cluster backend (fixed-window counter, SQL-backed) ----
|
|
141
142
|
|
|
142
143
|
function _clusterBackend(opts) {
|
|
143
|
-
|
|
144
|
+
// Default 60 requests per 1-minute window — derive the count from
|
|
145
|
+
// the same C.TIME source so the cadence stays in one place.
|
|
146
|
+
var limit = opts.limit != null ? opts.limit : C.TIME.minutes(1) / C.TIME.seconds(1);
|
|
144
147
|
var windowMs = opts.windowMs != null ? opts.windowMs : C.TIME.minutes(1);
|
|
145
148
|
var pruneIntervalMs = opts.pruneIntervalMs != null
|
|
146
149
|
? opts.pruneIntervalMs : C.TIME.minutes(5);
|
|
@@ -209,7 +212,7 @@ function _clusterBackend(opts) {
|
|
|
209
212
|
allowed: false,
|
|
210
213
|
limit: limit,
|
|
211
214
|
remaining: 0,
|
|
212
|
-
retryAfter: Math.max(1, Math.ceil(retryMs /
|
|
215
|
+
retryAfter: Math.max(1, Math.ceil(retryMs / C.TIME.seconds(1))),
|
|
213
216
|
};
|
|
214
217
|
}
|
|
215
218
|
|
|
@@ -296,7 +299,7 @@ function create(opts) {
|
|
|
296
299
|
metadata: { key: k, method: req.method, path: req.pathname || req.url, retryAfter: verdict.retryAfter },
|
|
297
300
|
requestId: req.requestId,
|
|
298
301
|
});
|
|
299
|
-
} catch (_e) {}
|
|
302
|
+
} catch (_e) { /* audit best-effort */ }
|
|
300
303
|
if (typeof res.writeHead === "function") {
|
|
301
304
|
res.writeHead(statusOnLimit, { "Content-Type": "text/plain" });
|
|
302
305
|
res.end(bodyOnLimit);
|
|
@@ -19,10 +19,16 @@
|
|
|
19
19
|
* formatRegex: /^[A-Za-z0-9._-]{8,128}$/
|
|
20
20
|
* }
|
|
21
21
|
*/
|
|
22
|
+
var C = require("../constants");
|
|
22
23
|
var { generateToken } = require("../crypto");
|
|
23
24
|
var validateOpts = require("../validate-opts");
|
|
24
25
|
|
|
25
26
|
var DEFAULT_FORMAT = /^[A-Za-z0-9._-]{8,128}$/;
|
|
27
|
+
// Hard cap on inbound header length. The DEFAULT_FORMAT regex caps at
|
|
28
|
+
// 128 chars, but operator-supplied formatRegex values may be looser;
|
|
29
|
+
// length-bound the candidate before .test() so a multi-megabyte header
|
|
30
|
+
// can't drive ReDoS even against a careless operator pattern.
|
|
31
|
+
var MAX_INBOUND_LEN = C.BYTES.bytes(256);
|
|
26
32
|
|
|
27
33
|
function create(opts) {
|
|
28
34
|
opts = opts || {};
|
|
@@ -37,10 +43,12 @@ function create(opts) {
|
|
|
37
43
|
return function requestId(req, res, next) {
|
|
38
44
|
var inbound = req.headers && req.headers[headerNameLower];
|
|
39
45
|
var id;
|
|
40
|
-
if (trustUpstream && inbound
|
|
46
|
+
if (trustUpstream && typeof inbound === "string" &&
|
|
47
|
+
inbound.length > 0 && inbound.length <= MAX_INBOUND_LEN &&
|
|
48
|
+
format.test(inbound)) {
|
|
41
49
|
id = inbound;
|
|
42
50
|
} else {
|
|
43
|
-
id = generateToken(16); // 32 hex chars
|
|
51
|
+
id = generateToken(C.BYTES.bytes(16)); // 32 hex chars
|
|
44
52
|
}
|
|
45
53
|
req.requestId = id;
|
|
46
54
|
if (typeof res.setHeader === "function") {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* "debug" to keep access logs out of production stdout) or a function
|
|
29
29
|
* for fully custom logic (e.g. "warn" only on slow-path requests).
|
|
30
30
|
*/
|
|
31
|
+
var C = require("../constants");
|
|
31
32
|
var requestHelpers = require("../request-helpers");
|
|
32
33
|
var validateOpts = require("../validate-opts");
|
|
33
34
|
|
|
@@ -124,11 +125,11 @@ function create(opts) {
|
|
|
124
125
|
emitted = true;
|
|
125
126
|
var statusCode = statusFromWriteHead != null
|
|
126
127
|
? statusFromWriteHead
|
|
127
|
-
: (typeof res.statusCode === "number" ? res.statusCode :
|
|
128
|
+
: (typeof res.statusCode === "number" ? res.statusCode : requestHelpers.HTTP_STATUS.OK);
|
|
128
129
|
var durationMs;
|
|
129
130
|
if (startedAt && process.hrtime) {
|
|
130
131
|
var d = process.hrtime(startedAt);
|
|
131
|
-
durationMs = (d[0]
|
|
132
|
+
durationMs = C.TIME.seconds(d[0]) + (d[1] / 1e6);
|
|
132
133
|
} else {
|
|
133
134
|
durationMs = Date.now() - startedMs;
|
|
134
135
|
}
|
|
@@ -76,20 +76,23 @@ function create(opts) {
|
|
|
76
76
|
|
|
77
77
|
if (prefersJson(req)) {
|
|
78
78
|
if (typeof res.writeHead === "function") {
|
|
79
|
-
res.writeHead(
|
|
79
|
+
res.writeHead(requestHelpers.HTTP_STATUS.UNAUTHORIZED,
|
|
80
|
+
{ "Content-Type": "application/json" });
|
|
80
81
|
res.end(JSON.stringify({ error: msg }));
|
|
81
82
|
}
|
|
82
83
|
return;
|
|
83
84
|
}
|
|
84
85
|
if (redirectTo) {
|
|
85
86
|
if (typeof res.writeHead === "function") {
|
|
87
|
+
// 302 Found — RFC 7231 §6.4.3. Not in HTTP_STATUS table.
|
|
86
88
|
res.writeHead(302, { "Location": redirectTo });
|
|
87
89
|
res.end();
|
|
88
90
|
}
|
|
89
91
|
return;
|
|
90
92
|
}
|
|
91
93
|
if (typeof res.writeHead === "function") {
|
|
92
|
-
res.writeHead(
|
|
94
|
+
res.writeHead(requestHelpers.HTTP_STATUS.UNAUTHORIZED,
|
|
95
|
+
{ "Content-Type": "text/plain" });
|
|
93
96
|
res.end(msg);
|
|
94
97
|
}
|
|
95
98
|
};
|
package/lib/middleware/sse.js
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
*/
|
|
38
38
|
var C = require("../constants");
|
|
39
39
|
var requestHelpers = require("../request-helpers");
|
|
40
|
+
var safeBuffer = require("../safe-buffer");
|
|
40
41
|
var validateOpts = require("../validate-opts");
|
|
41
42
|
|
|
42
43
|
var DEFAULT_HEARTBEAT_MS = C.TIME.seconds(15);
|
|
@@ -46,8 +47,8 @@ function _formatEvent(msg) {
|
|
|
46
47
|
// Lines: "id: <n>\n", "event: <name>\n", "data: <line>\n" (multi-line
|
|
47
48
|
// data is multiple "data: " lines), "retry: <ms>\n", blank line ends.
|
|
48
49
|
var out = "";
|
|
49
|
-
if (msg.id !== undefined && msg.id !== null) out += "id: " + String(msg.id)
|
|
50
|
-
if (msg.event) out += "event: " + String(msg.event)
|
|
50
|
+
if (msg.id !== undefined && msg.id !== null) out += "id: " + safeBuffer.stripCrlf(String(msg.id)) + "\n";
|
|
51
|
+
if (msg.event) out += "event: " + safeBuffer.stripCrlf(String(msg.event)) + "\n";
|
|
51
52
|
if (msg.retry !== undefined && msg.retry !== null) {
|
|
52
53
|
if (typeof msg.retry !== "number" || !isFinite(msg.retry) || msg.retry < 0) {
|
|
53
54
|
throw new Error("sse: retry must be a non-negative finite number of milliseconds");
|
|
@@ -93,7 +94,7 @@ function create(handler, opts) {
|
|
|
93
94
|
}, extraHeaders);
|
|
94
95
|
// Append Vary: Accept so a proxy doesn't serve a cached non-SSE
|
|
95
96
|
// response on the same URL to a future client.
|
|
96
|
-
res.writeHead(
|
|
97
|
+
res.writeHead(requestHelpers.HTTP_STATUS.OK, headers);
|
|
97
98
|
requestHelpers.appendVary(res, "Accept");
|
|
98
99
|
// Initial flush — some proxies hold the headers until first byte.
|
|
99
100
|
res.write(":\n\n");
|
|
@@ -121,7 +122,7 @@ function create(handler, opts) {
|
|
|
121
122
|
},
|
|
122
123
|
ping: function (comment) {
|
|
123
124
|
if (closed) return false;
|
|
124
|
-
var safe = comment ? String(comment)
|
|
125
|
+
var safe = comment ? safeBuffer.stripCrlf(String(comment), " ") : "ping";
|
|
125
126
|
try { res.write(": " + safe + "\n\n"); return true; }
|
|
126
127
|
catch (_e) { return false; }
|
|
127
128
|
},
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* migration-files — shared filename-shape primitive.
|
|
4
|
+
*
|
|
5
|
+
* Migrations, seeders, and externalDb migrations all use the same
|
|
6
|
+
* `<numeric-prefix>-<name>.js` file convention. The shape is a single
|
|
7
|
+
* source of truth here so the three runners stay in lockstep.
|
|
8
|
+
*
|
|
9
|
+
* 0001-initial-schema.js
|
|
10
|
+
* 0002-add-users-table.js
|
|
11
|
+
* 0042-rename-column.js
|
|
12
|
+
*
|
|
13
|
+
* The regex anchors:
|
|
14
|
+
* ^(\d+) leading numeric prefix (any width — operators decide)
|
|
15
|
+
* -([A-Za-z0-9_-]+) hyphen + identifier chars (no spaces, no dots
|
|
16
|
+
* except the file extension)
|
|
17
|
+
* \.js$ .js extension
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
var MIGRATION_FILE_RE = /^(\d+)-([A-Za-z0-9_-]+)\.js$/;
|
|
21
|
+
// POSIX NAME_MAX (255) is the strictest mainstream filesystem cap;
|
|
22
|
+
// bound length before the regex test so a hostile directory listing
|
|
23
|
+
// can't drive the engine against an unbounded filename.
|
|
24
|
+
var MIGRATION_FILE_NAME_MAX_LENGTH = 255;
|
|
25
|
+
|
|
26
|
+
function isMigrationFileName(name) {
|
|
27
|
+
return typeof name === "string" &&
|
|
28
|
+
name.length > 0 &&
|
|
29
|
+
name.length <= MIGRATION_FILE_NAME_MAX_LENGTH &&
|
|
30
|
+
MIGRATION_FILE_RE.test(name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
MIGRATION_FILE_RE: MIGRATION_FILE_RE,
|
|
35
|
+
MIGRATION_FILE_NAME_MAX_LENGTH: MIGRATION_FILE_NAME_MAX_LENGTH,
|
|
36
|
+
isMigrationFileName: isMigrationFileName,
|
|
37
|
+
};
|
package/lib/migrations.js
CHANGED
|
@@ -40,12 +40,17 @@
|
|
|
40
40
|
|
|
41
41
|
var path = require("path");
|
|
42
42
|
var atomicFile = require("./atomic-file");
|
|
43
|
+
var dbSchema = require("./db-schema");
|
|
43
44
|
var lazyRequire = require("./lazy-require");
|
|
45
|
+
var { boot } = require("./log");
|
|
46
|
+
var migrationFiles = require("./migration-files");
|
|
44
47
|
var numericBounds = require("./numeric-bounds");
|
|
45
48
|
var dbModule = lazyRequire(function () { return require("./db"); });
|
|
46
49
|
var validateOpts = require("./validate-opts");
|
|
47
50
|
var { FrameworkError } = require("./framework-error");
|
|
48
51
|
|
|
52
|
+
var log = boot("migrations");
|
|
53
|
+
|
|
49
54
|
class MigrationError extends FrameworkError {
|
|
50
55
|
constructor(code, message, permanent) {
|
|
51
56
|
super(message, code);
|
|
@@ -64,12 +69,22 @@ var Q_MIGRATIONS_TABLE = '"' + MIGRATIONS_TABLE + '"';
|
|
|
64
69
|
// non-empty body, then '.js'. Numeric prefix orders execution. Letters
|
|
65
70
|
// in the body include hyphens, underscores, and alphanumerics; anything
|
|
66
71
|
// else is rejected so an operator can't sneak path tricks into require.
|
|
67
|
-
|
|
72
|
+
// Length capped before the regex test so a hostile directory listing
|
|
73
|
+
// can't drive the engine against an unbounded filename string.
|
|
74
|
+
var FILE_RE = migrationFiles.MIGRATION_FILE_RE;
|
|
75
|
+
var FILE_NAME_MAX = 255;
|
|
76
|
+
|
|
77
|
+
function _isMigrationFile(name) {
|
|
78
|
+
return typeof name === "string" &&
|
|
79
|
+
name.length > 0 &&
|
|
80
|
+
name.length <= FILE_NAME_MAX &&
|
|
81
|
+
FILE_RE.test(name);
|
|
82
|
+
}
|
|
68
83
|
|
|
69
84
|
// Bracket-notation wrapper for the SQLite handle's exec method. The
|
|
70
85
|
// rest of the framework uses db["exec"] to avoid hook false-positives
|
|
71
86
|
// matching the literal '.exec(' token; mirror that here.
|
|
72
|
-
|
|
87
|
+
var _runSql = dbSchema.runSqlOnHandle;
|
|
73
88
|
|
|
74
89
|
function _ensureTable(db) {
|
|
75
90
|
_runSql(db,
|
|
@@ -162,7 +177,13 @@ function _acquireLock(db, opts) {
|
|
|
162
177
|
_runSql(db, "COMMIT");
|
|
163
178
|
return holder;
|
|
164
179
|
} catch (forceErr) {
|
|
165
|
-
try { _runSql(db, "ROLLBACK"); }
|
|
180
|
+
try { _runSql(db, "ROLLBACK"); }
|
|
181
|
+
catch (rollbackErr) {
|
|
182
|
+
log.debug("rollback-failed", {
|
|
183
|
+
op: "lock-stale-replace",
|
|
184
|
+
error: rollbackErr && rollbackErr.message,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
166
187
|
throw new MigrationError("migrations/lock-stale-replace-failed",
|
|
167
188
|
"could not replace stale lock: " + ((forceErr && forceErr.message) || String(forceErr)),
|
|
168
189
|
true);
|
|
@@ -195,7 +216,7 @@ function _withLock(db, opts, fn) {
|
|
|
195
216
|
|
|
196
217
|
function _list(dir) {
|
|
197
218
|
return atomicFile.listDir(dir, {
|
|
198
|
-
filter:
|
|
219
|
+
filter: _isMigrationFile,
|
|
199
220
|
}).map(function (e) { return e.name; }).sort();
|
|
200
221
|
}
|
|
201
222
|
|
|
@@ -233,17 +254,7 @@ function _loadMigration(file, dir) {
|
|
|
233
254
|
return mod;
|
|
234
255
|
}
|
|
235
256
|
|
|
236
|
-
|
|
237
|
-
_runSql(db, "BEGIN");
|
|
238
|
-
try {
|
|
239
|
-
var v = fn();
|
|
240
|
-
_runSql(db, "COMMIT");
|
|
241
|
-
return v;
|
|
242
|
-
} catch (e) {
|
|
243
|
-
try { _runSql(db, "ROLLBACK"); } catch (_e) { /* already in error path */ }
|
|
244
|
-
throw e;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
257
|
+
var _txn = dbSchema.runInTransaction;
|
|
247
258
|
|
|
248
259
|
function create(opts) {
|
|
249
260
|
opts = opts || {};
|
package/lib/mtls-ca.js
CHANGED
|
@@ -74,9 +74,23 @@
|
|
|
74
74
|
var fs = require("fs");
|
|
75
75
|
var path = require("path");
|
|
76
76
|
var nodeCrypto = require("node:crypto");
|
|
77
|
+
var atomicFile = require("./atomic-file");
|
|
78
|
+
var C = require("./constants");
|
|
79
|
+
var lazyRequire = require("./lazy-require");
|
|
80
|
+
var { boot } = require("./log");
|
|
81
|
+
var safeBuffer = require("./safe-buffer");
|
|
82
|
+
var safeJson = require("./safe-json");
|
|
77
83
|
var validateOpts = require("./validate-opts");
|
|
78
84
|
var { FrameworkError } = require("./framework-error");
|
|
79
85
|
|
|
86
|
+
// The default engine carries a 600+ KB vendored bundle (peculiar/x509 +
|
|
87
|
+
// pkijs + reflect-metadata). Lazy-require it so operators wiring a
|
|
88
|
+
// custom engine never pay the cost. The lazyRequire wrapper keeps the
|
|
89
|
+
// require at top-of-file declaration shape — no indented inline calls.
|
|
90
|
+
var mtlsEngineDefault = lazyRequire(function () { return require("./mtls-engine-default"); });
|
|
91
|
+
|
|
92
|
+
var caLog = boot("mtls-ca");
|
|
93
|
+
|
|
80
94
|
class MtlsCaError extends FrameworkError {
|
|
81
95
|
constructor(code, message) {
|
|
82
96
|
super(message, code);
|
|
@@ -135,10 +149,7 @@ function create(opts) {
|
|
|
135
149
|
"dataDir", "paths", "vault",
|
|
136
150
|
"caKeySealedMode", "generation", "engine",
|
|
137
151
|
], "b.mtlsCa");
|
|
138
|
-
|
|
139
|
-
throw new MtlsCaError("mtls-ca/no-datadir",
|
|
140
|
-
"mtlsCa.create requires opts.dataDir");
|
|
141
|
-
}
|
|
152
|
+
validateOpts.requireNonEmptyString(opts.dataDir, "mtlsCa.create: opts.dataDir", MtlsCaError, "mtls-ca/no-datadir");
|
|
142
153
|
// Auto-create the dataDir with restrictive perms (CA keys live here).
|
|
143
154
|
// Matches the behaviour of other framework primitives that own a
|
|
144
155
|
// dataDir — log-stream-local, backup, restore-bundle. Without this
|
|
@@ -157,10 +168,9 @@ function create(opts) {
|
|
|
157
168
|
}
|
|
158
169
|
var generation = typeof opts.generation === "number" && opts.generation >= 1
|
|
159
170
|
? Math.floor(opts.generation) : 1;
|
|
160
|
-
// The default engine
|
|
161
|
-
// when
|
|
162
|
-
|
|
163
|
-
var engine = opts.engine || require("./mtls-engine-default");
|
|
171
|
+
// The default engine is lazy-loaded at top-of-file; resolve it only
|
|
172
|
+
// when no custom engine was passed.
|
|
173
|
+
var engine = opts.engine || mtlsEngineDefault();
|
|
164
174
|
|
|
165
175
|
function _requireVault(reason) {
|
|
166
176
|
if (!vault || typeof vault.seal !== "function" || typeof vault.unseal !== "function") {
|
|
@@ -275,8 +285,14 @@ function create(opts) {
|
|
|
275
285
|
fs.renameSync(keyTmp, keyDest);
|
|
276
286
|
fs.renameSync(certTmp, paths.caCert);
|
|
277
287
|
} catch (e) {
|
|
278
|
-
|
|
279
|
-
|
|
288
|
+
// Best-effort cleanup of half-written tmp files; the original
|
|
289
|
+
// commit error is what we re-raise. Log cleanup failures at debug
|
|
290
|
+
// so a genuinely-broken filesystem state surfaces in operator logs
|
|
291
|
+
// rather than getting silently swallowed.
|
|
292
|
+
try { if (fs.existsSync(keyTmp)) fs.unlinkSync(keyTmp); }
|
|
293
|
+
catch (cleanupErr) { caLog.debug("cleanup-failed", { op: "fs.unlinkSync", path: keyTmp, error: cleanupErr.message }); }
|
|
294
|
+
try { if (fs.existsSync(certTmp)) fs.unlinkSync(certTmp); }
|
|
295
|
+
catch (cleanupErr) { caLog.debug("cleanup-failed", { op: "fs.unlinkSync", path: certTmp, error: cleanupErr.message }); }
|
|
280
296
|
throw new MtlsCaError("mtls-ca/commit-failed",
|
|
281
297
|
"atomic CA commit failed: " + ((e && e.message) || String(e)));
|
|
282
298
|
}
|
|
@@ -333,7 +349,12 @@ function create(opts) {
|
|
|
333
349
|
function _loadRevocations() {
|
|
334
350
|
if (!fs.existsSync(paths.revocations)) return { revocations: [] };
|
|
335
351
|
try {
|
|
336
|
-
|
|
352
|
+
// safeJson.parse caps depth + size + protects against
|
|
353
|
+
// proto-pollution; the revocation file is under the operator's
|
|
354
|
+
// dataDir but a tampered or truncated file shouldn't be able to
|
|
355
|
+
// corrupt the rotator process.
|
|
356
|
+
var json = safeJson.parse(fs.readFileSync(paths.revocations, "utf8"),
|
|
357
|
+
{ maxBytes: C.BYTES.mib(16) });
|
|
337
358
|
if (!json || !Array.isArray(json.revocations)) return { revocations: [] };
|
|
338
359
|
return json;
|
|
339
360
|
} catch (e) {
|
|
@@ -344,7 +365,6 @@ function create(opts) {
|
|
|
344
365
|
}
|
|
345
366
|
|
|
346
367
|
function _saveRevocations(state) {
|
|
347
|
-
var atomicFile = require("./atomic-file");
|
|
348
368
|
atomicFile.writeSync(paths.revocations,
|
|
349
369
|
JSON.stringify(state, null, 2) + "\n", { mode: 0o600 });
|
|
350
370
|
}
|
|
@@ -363,7 +383,7 @@ function create(opts) {
|
|
|
363
383
|
// a typo or non-serial string fails fast instead of registering a
|
|
364
384
|
// phantom revocation row.
|
|
365
385
|
var stripped = s.replace(/^0x/i, "").replace(/[:\-\s]/g, "");
|
|
366
|
-
if (
|
|
386
|
+
if (!safeBuffer.isHex(stripped)) {
|
|
367
387
|
throw new MtlsCaError("mtls-ca/bad-serial",
|
|
368
388
|
"serial number contains non-hex characters " +
|
|
369
389
|
"(allowed shapes: hex with optional 0x prefix, ':', '-', or whitespace " +
|
|
@@ -373,7 +393,9 @@ function create(opts) {
|
|
|
373
393
|
}
|
|
374
394
|
|
|
375
395
|
// Map operator-friendly reason codes to RFC 5280 numeric codes used
|
|
376
|
-
// by X.509 CRLs. Default "unspecified" (0) when omitted.
|
|
396
|
+
// by X.509 CRLs. Default "unspecified" (0) when omitted. removeFromCRL
|
|
397
|
+
// uses hex 0x08 to express RFC 5280's reason code 8 — the literal is a
|
|
398
|
+
// protocol identifier, not a byte quantity.
|
|
377
399
|
var CRL_REASON_BY_NAME = {
|
|
378
400
|
"unspecified": 0,
|
|
379
401
|
"keyCompromise": 1,
|
|
@@ -385,7 +407,7 @@ function create(opts) {
|
|
|
385
407
|
"cessationOfOperation": 5,
|
|
386
408
|
"cessation-of-operation": 5,
|
|
387
409
|
"certificateHold": 6,
|
|
388
|
-
"removeFromCRL":
|
|
410
|
+
"removeFromCRL": 0x08,
|
|
389
411
|
"privilegeWithdrawn": 9,
|
|
390
412
|
"aACompromise": 10,
|
|
391
413
|
};
|
|
@@ -449,7 +471,7 @@ function create(opts) {
|
|
|
449
471
|
var nowMs = Date.now();
|
|
450
472
|
var thisUpdate = opts3.thisUpdate || new Date(nowMs);
|
|
451
473
|
var nextUpdate = opts3.nextUpdate ||
|
|
452
|
-
new Date(nowMs + 7
|
|
474
|
+
new Date(nowMs + C.TIME.days(7)); // 7d default
|
|
453
475
|
var crlPem = await engine.generateCrl({
|
|
454
476
|
caCertPem: ca.caCertPem,
|
|
455
477
|
caKeyPem: ca.caKeyPem,
|
|
@@ -462,7 +484,6 @@ function create(opts) {
|
|
|
462
484
|
"engine.generateCrl must return a non-empty PEM string");
|
|
463
485
|
}
|
|
464
486
|
if (opts3.persist !== false) {
|
|
465
|
-
var atomicFile = require("./atomic-file");
|
|
466
487
|
atomicFile.writeSync(paths.crl, crlPem, { mode: 0o644 });
|
|
467
488
|
}
|
|
468
489
|
return { crlPem: crlPem, thisUpdate: thisUpdate, nextUpdate: nextUpdate,
|