@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/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,11 +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");
|
|
47
|
+
var numericBounds = require("./numeric-bounds");
|
|
44
48
|
var dbModule = lazyRequire(function () { return require("./db"); });
|
|
45
49
|
var validateOpts = require("./validate-opts");
|
|
46
50
|
var { FrameworkError } = require("./framework-error");
|
|
47
51
|
|
|
52
|
+
var log = boot("migrations");
|
|
53
|
+
|
|
48
54
|
class MigrationError extends FrameworkError {
|
|
49
55
|
constructor(code, message, permanent) {
|
|
50
56
|
super(message, code);
|
|
@@ -63,12 +69,22 @@ var Q_MIGRATIONS_TABLE = '"' + MIGRATIONS_TABLE + '"';
|
|
|
63
69
|
// non-empty body, then '.js'. Numeric prefix orders execution. Letters
|
|
64
70
|
// in the body include hyphens, underscores, and alphanumerics; anything
|
|
65
71
|
// else is rejected so an operator can't sneak path tricks into require.
|
|
66
|
-
|
|
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
|
+
}
|
|
67
83
|
|
|
68
84
|
// Bracket-notation wrapper for the SQLite handle's exec method. The
|
|
69
85
|
// rest of the framework uses db["exec"] to avoid hook false-positives
|
|
70
86
|
// matching the literal '.exec(' token; mirror that here.
|
|
71
|
-
|
|
87
|
+
var _runSql = dbSchema.runSqlOnHandle;
|
|
72
88
|
|
|
73
89
|
function _ensureTable(db) {
|
|
74
90
|
_runSql(db,
|
|
@@ -108,8 +124,21 @@ function _acquireLock(db, opts) {
|
|
|
108
124
|
_ensureLockTable(db);
|
|
109
125
|
var holder = _lockHolderId();
|
|
110
126
|
var nowMs = Date.now();
|
|
111
|
-
|
|
112
|
-
|
|
127
|
+
// staleAfterMs gates whether an existing lock is considered stale.
|
|
128
|
+
// 0 (the default) means "never replace — wait for the operator".
|
|
129
|
+
// Pre-fix Infinity was accepted but degenerate `(now - lockedAt) >
|
|
130
|
+
// Infinity` to always-false, identical to 0 but with an obscured
|
|
131
|
+
// typo. Reject Infinity / NaN / non-integer / negative — operators
|
|
132
|
+
// wanting "never" pass 0 explicitly.
|
|
133
|
+
var staleAfterMs;
|
|
134
|
+
if (!opts || opts.staleAfterMs === undefined) {
|
|
135
|
+
staleAfterMs = 0;
|
|
136
|
+
} else if (!numericBounds.isNonNegativeFiniteInt(opts.staleAfterMs)) {
|
|
137
|
+
throw new Error("migrations.acquireLock: staleAfterMs must be a " +
|
|
138
|
+
"non-negative finite integer; got " + numericBounds.shape(opts.staleAfterMs));
|
|
139
|
+
} else {
|
|
140
|
+
staleAfterMs = opts.staleAfterMs;
|
|
141
|
+
}
|
|
113
142
|
// Try to insert; if there's a stale lock, optionally force-replace it.
|
|
114
143
|
try {
|
|
115
144
|
db.prepare(
|
|
@@ -148,7 +177,13 @@ function _acquireLock(db, opts) {
|
|
|
148
177
|
_runSql(db, "COMMIT");
|
|
149
178
|
return holder;
|
|
150
179
|
} catch (forceErr) {
|
|
151
|
-
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
|
+
}
|
|
152
187
|
throw new MigrationError("migrations/lock-stale-replace-failed",
|
|
153
188
|
"could not replace stale lock: " + ((forceErr && forceErr.message) || String(forceErr)),
|
|
154
189
|
true);
|
|
@@ -181,7 +216,7 @@ function _withLock(db, opts, fn) {
|
|
|
181
216
|
|
|
182
217
|
function _list(dir) {
|
|
183
218
|
return atomicFile.listDir(dir, {
|
|
184
|
-
filter:
|
|
219
|
+
filter: _isMigrationFile,
|
|
185
220
|
}).map(function (e) { return e.name; }).sort();
|
|
186
221
|
}
|
|
187
222
|
|
|
@@ -219,17 +254,7 @@ function _loadMigration(file, dir) {
|
|
|
219
254
|
return mod;
|
|
220
255
|
}
|
|
221
256
|
|
|
222
|
-
|
|
223
|
-
_runSql(db, "BEGIN");
|
|
224
|
-
try {
|
|
225
|
-
var v = fn();
|
|
226
|
-
_runSql(db, "COMMIT");
|
|
227
|
-
return v;
|
|
228
|
-
} catch (e) {
|
|
229
|
-
try { _runSql(db, "ROLLBACK"); } catch (_e) { /* already in error path */ }
|
|
230
|
-
throw e;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
257
|
+
var _txn = dbSchema.runInTransaction;
|
|
233
258
|
|
|
234
259
|
function create(opts) {
|
|
235
260
|
opts = opts || {};
|