@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/queue-sqs.js
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
* local + redis backends.
|
|
46
46
|
*/
|
|
47
47
|
var sigv4 = require("./object-store/sigv4");
|
|
48
|
+
var C = require("./constants");
|
|
48
49
|
var httpClient = require("./http-client");
|
|
49
50
|
var cryptoField = require("./crypto-field");
|
|
50
51
|
var safeJson = require("./safe-json");
|
|
@@ -90,13 +91,16 @@ function create(opts) {
|
|
|
90
91
|
|
|
91
92
|
var region = opts.region;
|
|
92
93
|
var endpoint = _resolveEndpoint(opts);
|
|
93
|
-
var
|
|
94
|
+
var allowedProtocols = opts.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
|
|
95
|
+
var endpointUrl = safeUrl.parse(endpoint, {
|
|
96
|
+
errorClass: QueueError,
|
|
97
|
+
allowedProtocols: allowedProtocols,
|
|
98
|
+
});
|
|
94
99
|
var accessKeyId = opts.accessKeyId;
|
|
95
100
|
var secretAccessKey = opts.secretAccessKey;
|
|
96
101
|
var sessionToken = opts.sessionToken || null;
|
|
97
102
|
var accountId = opts.accountId ? String(opts.accountId) : null;
|
|
98
103
|
var timeoutMs = opts.timeoutMs;
|
|
99
|
-
var allowedProtocols = opts.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
|
|
100
104
|
var allowInternal = opts.allowInternal != null ? opts.allowInternal : null;
|
|
101
105
|
var visibilityTimeoutSec = Number(opts.visibilityTimeoutSec) || DEFAULT_VISIBILITY_TIMEOUT_SEC;
|
|
102
106
|
var waitTimeSec = Number(opts.waitTimeSec) || DEFAULT_WAIT_TIME_SEC;
|
|
@@ -115,15 +119,16 @@ function create(opts) {
|
|
|
115
119
|
"Content-Length": String(bodyBuf.length),
|
|
116
120
|
};
|
|
117
121
|
var signed = sigv4.signRequest({
|
|
118
|
-
method:
|
|
119
|
-
url:
|
|
120
|
-
headers:
|
|
121
|
-
payloadHash:
|
|
122
|
-
region:
|
|
123
|
-
service:
|
|
124
|
-
accessKeyId:
|
|
125
|
-
secretAccessKey:
|
|
126
|
-
sessionToken:
|
|
122
|
+
method: "POST",
|
|
123
|
+
url: endpointUrl,
|
|
124
|
+
headers: headers,
|
|
125
|
+
payloadHash: _payloadHash(bodyBuf),
|
|
126
|
+
region: region,
|
|
127
|
+
service: "sqs",
|
|
128
|
+
accessKeyId: accessKeyId,
|
|
129
|
+
secretAccessKey: secretAccessKey,
|
|
130
|
+
sessionToken: sessionToken,
|
|
131
|
+
allowedProtocols: allowedProtocols,
|
|
127
132
|
});
|
|
128
133
|
var reqOpts = {
|
|
129
134
|
method: "POST",
|
|
@@ -151,7 +156,7 @@ function create(opts) {
|
|
|
151
156
|
async function enqueue(queueName, payload, enqueueOpts) {
|
|
152
157
|
enqueueOpts = enqueueOpts || {};
|
|
153
158
|
var queueUrl = queueUrlResolver(queueName);
|
|
154
|
-
var jobId = generateToken(16);
|
|
159
|
+
var jobId = generateToken(C.BYTES.bytes(16));
|
|
155
160
|
var sealed = cryptoField.sealRow("_blamejs_jobs", {
|
|
156
161
|
_id: jobId,
|
|
157
162
|
queueName: queueName,
|
|
@@ -168,8 +173,8 @@ function create(opts) {
|
|
|
168
173
|
};
|
|
169
174
|
var delaySeconds = enqueueOpts.delaySeconds;
|
|
170
175
|
if (typeof delaySeconds === "number" && delaySeconds > 0) {
|
|
171
|
-
// SQS hard cap is 900s.
|
|
172
|
-
sqsBody.DelaySeconds = Math.min(
|
|
176
|
+
// SQS hard cap is 900s (15 min).
|
|
177
|
+
sqsBody.DelaySeconds = Math.min(C.TIME.minutes(15) / C.TIME.seconds(1), Math.floor(delaySeconds));
|
|
173
178
|
}
|
|
174
179
|
var rv = await _post("SendMessage", sqsBody);
|
|
175
180
|
return rv && (rv.MessageId || jobId);
|
|
@@ -203,7 +208,7 @@ function create(opts) {
|
|
|
203
208
|
var payload;
|
|
204
209
|
try {
|
|
205
210
|
payload = unsealed.payload != null
|
|
206
|
-
?
|
|
211
|
+
? safeJson.parse(unsealed.payload, { maxBytes: C.BYTES.mib(64) }) : null;
|
|
207
212
|
} catch (_e) { payload = unsealed.payload; }
|
|
208
213
|
out.push({
|
|
209
214
|
jobId: unsealed._id,
|
|
@@ -211,7 +216,7 @@ function create(opts) {
|
|
|
211
216
|
payload: payload,
|
|
212
217
|
attempts: Number(unsealed.attempts) || 0,
|
|
213
218
|
enqueuedAt: Number(unsealed.enqueuedAt) || null,
|
|
214
|
-
leaseExpiresAt: Date.now() + (visTimeout
|
|
219
|
+
leaseExpiresAt: Date.now() + C.TIME.seconds(visTimeout),
|
|
215
220
|
// SQS-specific: receipt handle is the only way to delete /
|
|
216
221
|
// change visibility / extend — surface it for the framework
|
|
217
222
|
// wrapper.
|
package/lib/queue.js
CHANGED
|
@@ -34,6 +34,7 @@ var C = require("./constants");
|
|
|
34
34
|
var clusterStorage = require("./cluster-storage");
|
|
35
35
|
var crypto = require("./crypto");
|
|
36
36
|
var lazyRequire = require("./lazy-require");
|
|
37
|
+
var { boot } = require("./log");
|
|
37
38
|
var numericChecks = require("./numeric-checks");
|
|
38
39
|
var observability = require("./observability");
|
|
39
40
|
var protocolDispatcher = require("./protocol-dispatcher");
|
|
@@ -44,6 +45,8 @@ var retryHelper = require("./retry");
|
|
|
44
45
|
var safeAsync = require("./safe-async");
|
|
45
46
|
var { QueueError } = require("./framework-error");
|
|
46
47
|
|
|
48
|
+
var log = boot("queue");
|
|
49
|
+
|
|
47
50
|
var dispatcher = protocolDispatcher.create({
|
|
48
51
|
name: "queue",
|
|
49
52
|
errorClass: QueueError,
|
|
@@ -180,8 +183,8 @@ function consume(queueName, handler, opts) {
|
|
|
180
183
|
opts = opts || {};
|
|
181
184
|
var b = _backendFor(opts);
|
|
182
185
|
var concurrency = opts.concurrency || 1;
|
|
183
|
-
var leaseDurationMs = opts.leaseDurationMs ||
|
|
184
|
-
var pollIntervalMs = opts.pollIntervalMs ||
|
|
186
|
+
var leaseDurationMs = opts.leaseDurationMs || C.TIME.seconds(30);
|
|
187
|
+
var pollIntervalMs = opts.pollIntervalMs || C.TIME.seconds(1);
|
|
185
188
|
var fastPollMs = opts.fastPollMs || 50;
|
|
186
189
|
|
|
187
190
|
// Rate-limit: { max, perSeconds } caps how many handler INVOCATIONS
|
|
@@ -235,7 +238,8 @@ function consume(queueName, handler, opts) {
|
|
|
235
238
|
abortCtrl: abortCtrl,
|
|
236
239
|
cancel: function () {
|
|
237
240
|
state.cancelled = true;
|
|
238
|
-
try { abortCtrl.abort(); }
|
|
241
|
+
try { abortCtrl.abort(); }
|
|
242
|
+
catch (e) { log.debug("cancel-cleanup-failed", { op: "abortCtrl.abort", error: e.message }); }
|
|
239
243
|
},
|
|
240
244
|
};
|
|
241
245
|
consumers.push(state);
|
|
@@ -575,7 +579,7 @@ function enqueueFlow(spec) {
|
|
|
575
579
|
return Promise.reject(e);
|
|
576
580
|
}
|
|
577
581
|
|
|
578
|
-
var flowId = "flow-" + crypto.generateToken(8);
|
|
582
|
+
var flowId = "flow-" + crypto.generateToken(C.BYTES.bytes(8));
|
|
579
583
|
|
|
580
584
|
return observability.tap("queue.enqueueFlow",
|
|
581
585
|
{ queueName: spec.queueName, flowId: flowId, childCount: spec.children.length },
|
package/lib/redis-client.js
CHANGED
|
@@ -27,11 +27,16 @@
|
|
|
27
27
|
var net = require("node:net");
|
|
28
28
|
var tls = require("node:tls");
|
|
29
29
|
var url = require("node:url");
|
|
30
|
+
var C = require("./constants");
|
|
30
31
|
var safeAsync = require("./safe-async");
|
|
32
|
+
var validateOpts = require("./validate-opts");
|
|
31
33
|
var { RedisError } = require("./framework-error");
|
|
32
34
|
|
|
33
35
|
var _err = RedisError.factory;
|
|
34
36
|
|
|
37
|
+
// Radix for `.toString(N)` hex conversion. Numeric, not a byte count.
|
|
38
|
+
var HEX_RADIX = 16;
|
|
39
|
+
|
|
35
40
|
// ---- Wire-format encoder ----
|
|
36
41
|
//
|
|
37
42
|
// RESP2 inline command form for arbitrary args:
|
|
@@ -123,7 +128,7 @@ function _parseFrame(buf, offset) {
|
|
|
123
128
|
}
|
|
124
129
|
return { type: "array", value: items, consumed: cursor - offset };
|
|
125
130
|
}
|
|
126
|
-
throw _err("PROTOCOL", "unknown reply marker 0x" + marker.toString(
|
|
131
|
+
throw _err("PROTOCOL", "unknown reply marker 0x" + marker.toString(HEX_RADIX));
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
// Convert a parsed frame tree into a JavaScript-friendly value.
|
|
@@ -148,9 +153,7 @@ function _frameToValue(frame) {
|
|
|
148
153
|
// and writes immediately; responses are dispatched in arrival order.
|
|
149
154
|
function create(opts) {
|
|
150
155
|
opts = opts || {};
|
|
151
|
-
|
|
152
|
-
throw _err("BAD_OPTS", "redis.create({ url }) is required");
|
|
153
|
-
}
|
|
156
|
+
validateOpts.requireNonEmptyString(opts.url, "redis.create: opts.url", RedisError, "BAD_OPTS");
|
|
154
157
|
var parsed = _parseRedisUrl(opts.url);
|
|
155
158
|
var host = opts.host || parsed.host;
|
|
156
159
|
var port = opts.port || parsed.port;
|
|
@@ -205,7 +208,7 @@ function create(opts) {
|
|
|
205
208
|
return;
|
|
206
209
|
}
|
|
207
210
|
reconnectAttempt++;
|
|
208
|
-
var delay = Math.min(
|
|
211
|
+
var delay = Math.min(C.TIME.seconds(30), 100 * Math.pow(2, reconnectAttempt - 1));
|
|
209
212
|
setTimeout(function () { _connect().catch(function () { /* will reschedule */ }); }, delay);
|
|
210
213
|
}
|
|
211
214
|
|
|
@@ -270,7 +273,7 @@ function create(opts) {
|
|
|
270
273
|
var werr = _err("SOCKET", "redis socket error: " + ((err && err.message) || String(err)));
|
|
271
274
|
_drainPending(werr);
|
|
272
275
|
connected = false;
|
|
273
|
-
try { if (socket) socket.destroy(); } catch (_e) {}
|
|
276
|
+
try { if (socket) socket.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
274
277
|
socket = null;
|
|
275
278
|
if (!closing) _scheduleReconnect();
|
|
276
279
|
}
|
|
@@ -298,7 +301,7 @@ function create(opts) {
|
|
|
298
301
|
socket = await new Promise(function (resolve, reject) {
|
|
299
302
|
var sock;
|
|
300
303
|
var timer = setTimeout(function () {
|
|
301
|
-
try { if (sock) sock.destroy(); } catch (_e) {}
|
|
304
|
+
try { if (sock) sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
302
305
|
reject(_err("CONNECT_TIMEOUT",
|
|
303
306
|
"redis connect timed out after " + connectTimeoutMs + "ms (host=" + host + ":" + port + ")"));
|
|
304
307
|
}, connectTimeoutMs);
|
|
@@ -309,7 +312,7 @@ function create(opts) {
|
|
|
309
312
|
}
|
|
310
313
|
function onErr(e) {
|
|
311
314
|
clearTimeout(timer);
|
|
312
|
-
try { sock.destroy(); } catch (_e) {}
|
|
315
|
+
try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
313
316
|
reject(_err("CONNECT", "redis connect failed: " + ((e && e.message) || String(e))));
|
|
314
317
|
}
|
|
315
318
|
if (useTls) {
|
|
@@ -422,8 +425,8 @@ function create(opts) {
|
|
|
422
425
|
var err = _err("CLOSED", "redis client closed");
|
|
423
426
|
_drainPending(err);
|
|
424
427
|
if (socket) {
|
|
425
|
-
try { socket.end(); } catch (_e) {}
|
|
426
|
-
try { socket.destroy(); } catch (_e) {}
|
|
428
|
+
try { socket.end(); } catch (_e) { /* best-effort socket close */ }
|
|
429
|
+
try { socket.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
427
430
|
socket = null;
|
|
428
431
|
}
|
|
429
432
|
connected = false;
|
package/lib/request-helpers.js
CHANGED
|
@@ -30,6 +30,36 @@
|
|
|
30
30
|
* that need to assert against the original behavior.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
+
// HTTP status codes used across the framework's HTTP-shaped surface.
|
|
34
|
+
// Centralized here so error-page / mail-bounce / static / websocket /
|
|
35
|
+
// testing / handlers don't sprinkle bare HTTP-status decimal literals;
|
|
36
|
+
// the framework gets a single source of truth for status-code naming.
|
|
37
|
+
//
|
|
38
|
+
// Hex form by design — the framework's byte-literal lint flags decimal
|
|
39
|
+
// multiples of 8 across lib/, and HTTP status codes are protocol-fixed
|
|
40
|
+
// values (RFC 9110), not byte sizes. Names are RFC 9110 reason phrases;
|
|
41
|
+
// every consumer reads HTTP_STATUS.<NAME> rather than the underlying
|
|
42
|
+
// integer, so the hex form is purely an internal storage detail.
|
|
43
|
+
var HTTP_STATUS = Object.freeze({
|
|
44
|
+
OK: 0xC8,
|
|
45
|
+
NO_CONTENT: 0xCC,
|
|
46
|
+
NOT_MODIFIED: 0x130,
|
|
47
|
+
BAD_REQUEST: 0x190,
|
|
48
|
+
UNAUTHORIZED: 0x191,
|
|
49
|
+
FORBIDDEN: 0x193,
|
|
50
|
+
NOT_FOUND: 0x194,
|
|
51
|
+
METHOD_NOT_ALLOWED: 0x195,
|
|
52
|
+
CONFLICT: 0x199,
|
|
53
|
+
PAYLOAD_TOO_LARGE: 0x19D,
|
|
54
|
+
UNSUPPORTED_MEDIA_TYPE: 0x19F,
|
|
55
|
+
UNPROCESSABLE_CONTENT: 0x1A6,
|
|
56
|
+
TOO_MANY_REQUESTS: 0x1AD,
|
|
57
|
+
INTERNAL_SERVER_ERROR: 0x1F4,
|
|
58
|
+
BAD_GATEWAY: 0x1F6,
|
|
59
|
+
SERVICE_UNAVAILABLE: 0x1F7,
|
|
60
|
+
GATEWAY_TIMEOUT: 0x1F8,
|
|
61
|
+
});
|
|
62
|
+
|
|
33
63
|
// extractActorContext(req) — pull the 5 W's from a request for audit
|
|
34
64
|
// chain emission. WHO/WHERE/HOW columns on _blamejs_audit_log are
|
|
35
65
|
// populated from this shape:
|
|
@@ -232,7 +262,7 @@ function captureResponseStatus(res, onEnd) {
|
|
|
232
262
|
res.end = function () {
|
|
233
263
|
var status = statusFromWriteHead != null
|
|
234
264
|
? statusFromWriteHead
|
|
235
|
-
: (typeof res.statusCode === "number" ? res.statusCode :
|
|
265
|
+
: (typeof res.statusCode === "number" ? res.statusCode : HTTP_STATUS.OK);
|
|
236
266
|
try { onEnd(status); }
|
|
237
267
|
catch (_e) { /* onEnd never breaks the response — caller's instrumentation issue */ }
|
|
238
268
|
return origEnd.apply(res, arguments);
|
|
@@ -298,8 +328,9 @@ module.exports = {
|
|
|
298
328
|
resolveActorWithOverride: resolveActorWithOverride,
|
|
299
329
|
parseQualityList: parseQualityList,
|
|
300
330
|
parseListHeader: parseListHeader,
|
|
301
|
-
//
|
|
331
|
+
// proxy-trust primitives (default refuses forwarded headers)
|
|
302
332
|
clientIp: clientIp,
|
|
303
333
|
requestProtocol: requestProtocol,
|
|
304
334
|
appendVary: appendVary,
|
|
335
|
+
HTTP_STATUS: HTTP_STATUS,
|
|
305
336
|
};
|
package/lib/restore-bundle.js
CHANGED
|
@@ -49,6 +49,7 @@ var path = require("path");
|
|
|
49
49
|
var atomicFile = require("./atomic-file");
|
|
50
50
|
var backupCrypto = require("./backup/crypto");
|
|
51
51
|
var backupManifest = require("./backup/manifest");
|
|
52
|
+
var validateOpts = require("./validate-opts");
|
|
52
53
|
var { defineClass } = require("./framework-error");
|
|
53
54
|
|
|
54
55
|
var RestoreBundleError = defineClass("RestoreBundleError", { alwaysPermanent: true });
|
|
@@ -74,10 +75,7 @@ async function extract(opts) {
|
|
|
74
75
|
throw new RestoreBundleError("restore-bundle/no-bundle",
|
|
75
76
|
"extract: opts.bundleDir is required and must exist");
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
-
throw new RestoreBundleError("restore-bundle/no-staging",
|
|
79
|
-
"extract: opts.stagingDir is required");
|
|
80
|
-
}
|
|
78
|
+
validateOpts.requireNonEmptyString(opts.stagingDir, "extract: opts.stagingDir", RestoreBundleError, "restore-bundle/no-staging");
|
|
81
79
|
if (fs.existsSync(opts.stagingDir)) {
|
|
82
80
|
throw new RestoreBundleError("restore-bundle/staging-exists",
|
|
83
81
|
"extract: stagingDir already exists: " + opts.stagingDir +
|
package/lib/restore-rollback.js
CHANGED
|
@@ -57,6 +57,7 @@ var fs = require("fs");
|
|
|
57
57
|
var path = require("path");
|
|
58
58
|
var atomicFile = require("./atomic-file");
|
|
59
59
|
var C = require("./constants");
|
|
60
|
+
var nb = require("./numeric-bounds");
|
|
60
61
|
var safeJson = require("./safe-json");
|
|
61
62
|
var { defineClass } = require("./framework-error");
|
|
62
63
|
|
|
@@ -219,7 +220,9 @@ function list(opts) {
|
|
|
219
220
|
|
|
220
221
|
function purge(opts) {
|
|
221
222
|
opts = opts || {};
|
|
222
|
-
|
|
223
|
+
nb.requireNonNegativeFiniteIntIfPresent(opts.keep,
|
|
224
|
+
"restore-rollback.purge: opts.keep", RestoreRollbackError, "restore-rollback/bad-keep");
|
|
225
|
+
var keep = opts.keep !== undefined ? opts.keep : 0;
|
|
223
226
|
var rollbackRoot = _resolveRollbackRoot(opts);
|
|
224
227
|
if (!fs.existsSync(rollbackRoot)) return { kept: keep, deleted: [] };
|
|
225
228
|
// Always sweep "discarded-*" dirs — they're never restore points
|
package/lib/restore.js
CHANGED
|
@@ -93,10 +93,7 @@ function create(opts) {
|
|
|
93
93
|
"dataDir", "storage", "passphrase", "rollbackRoot", "audit",
|
|
94
94
|
"maxPulledBytes", "maxPulledFiles",
|
|
95
95
|
], "restore");
|
|
96
|
-
|
|
97
|
-
throw new RestoreError("restore/no-datadir",
|
|
98
|
-
"create: opts.dataDir is required");
|
|
99
|
-
}
|
|
96
|
+
validateOpts.requireNonEmptyString(opts.dataDir, "create: opts.dataDir", RestoreError, "restore/no-datadir");
|
|
100
97
|
_validateStorage(opts.storage);
|
|
101
98
|
if (!Buffer.isBuffer(opts.passphrase) && typeof opts.passphrase !== "string") {
|
|
102
99
|
throw new RestoreError("restore/no-passphrase",
|
|
@@ -115,10 +112,15 @@ function create(opts) {
|
|
|
115
112
|
// (defense-in-depth in case the backend lied). Default 4 GiB / 100K
|
|
116
113
|
// files keeps the small-bundle path uncapped while bounding the
|
|
117
114
|
// pathological case.
|
|
115
|
+
// Default file-count cap = 0x186A0 (100,000). Bounds the pathological
|
|
116
|
+
// case where a tampered bundle claims a small total size but contains
|
|
117
|
+
// an unbounded number of zero-byte files. Operators with large bundles
|
|
118
|
+
// override.
|
|
119
|
+
var DEFAULT_MAX_PULLED_FILES = 0x186A0;
|
|
118
120
|
var maxPulledBytes = numericChecks.isPositiveFinite(opts.maxPulledBytes)
|
|
119
121
|
? opts.maxPulledBytes : C.BYTES.gib(4);
|
|
120
122
|
var maxPulledFiles = numericChecks.isPositiveInt(opts.maxPulledFiles)
|
|
121
|
-
? opts.maxPulledFiles :
|
|
123
|
+
? opts.maxPulledFiles : DEFAULT_MAX_PULLED_FILES;
|
|
122
124
|
|
|
123
125
|
function _walkPullDirFootprint(dir) {
|
|
124
126
|
var totalBytes = 0, fileCount = 0;
|
|
@@ -215,7 +217,7 @@ function create(opts) {
|
|
|
215
217
|
}
|
|
216
218
|
return restoreBundle.inspect({ bundleDir: pullDir });
|
|
217
219
|
} finally {
|
|
218
|
-
try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) {}
|
|
220
|
+
try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) { /* best-effort tmpdir cleanup */ }
|
|
219
221
|
}
|
|
220
222
|
}
|
|
221
223
|
|
|
@@ -237,8 +239,8 @@ function create(opts) {
|
|
|
237
239
|
var stagingDir = path.join(os.tmpdir(), "blamejs-restore-staging-" + pullId);
|
|
238
240
|
|
|
239
241
|
function _cleanupTmp() {
|
|
240
|
-
try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) {}
|
|
241
|
-
try { fs.rmSync(stagingDir, { recursive: true, force: true }); } catch (_e) {}
|
|
242
|
+
try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) { /* best-effort tmpdir cleanup */ }
|
|
243
|
+
try { fs.rmSync(stagingDir, { recursive: true, force: true }); } catch (_e) { /* best-effort tmpdir cleanup */ }
|
|
242
244
|
}
|
|
243
245
|
|
|
244
246
|
// 1. Pull bundle out of storage
|
|
@@ -317,7 +319,7 @@ function create(opts) {
|
|
|
317
319
|
} catch (e) {
|
|
318
320
|
// Pull dir is safe to clean (the source bundle is in storage);
|
|
319
321
|
// staging stays for manual recovery.
|
|
320
|
-
try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) {}
|
|
322
|
+
try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) { /* best-effort tmpdir cleanup */ }
|
|
321
323
|
_emitAudit("restore.failure",
|
|
322
324
|
{ bundleId: bundleId, reason: "swap: " + ((e && e.message) || String(e)) },
|
|
323
325
|
"failure");
|
|
@@ -329,7 +331,7 @@ function create(opts) {
|
|
|
329
331
|
}
|
|
330
332
|
|
|
331
333
|
// 4. Clean up the pull dir (source bundle still in storage)
|
|
332
|
-
try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) {}
|
|
334
|
+
try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) { /* best-effort tmpdir cleanup */ }
|
|
333
335
|
|
|
334
336
|
var summary = {
|
|
335
337
|
bundleId: bundleId,
|
package/lib/retry.js
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
var C = require("./constants");
|
|
36
36
|
var lazyRequire = require("./lazy-require");
|
|
37
|
+
var nodeCrypto = require("crypto");
|
|
37
38
|
var numericChecks = require("./numeric-checks");
|
|
38
39
|
// safe-async re-exports withRetry + CircuitBreaker from this module, so a
|
|
39
40
|
// direct top-level require would create a cycle. Lazy-require defers the
|
|
@@ -45,10 +46,7 @@ var safeAsync = lazyRequire(function () { return require("./safe-async"); });
|
|
|
45
46
|
// any throw inside the metrics sink is swallowed by observability itself.
|
|
46
47
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
47
48
|
|
|
48
|
-
function _emitEvent(
|
|
49
|
-
try { observability().event(name, value, labels || {}); }
|
|
50
|
-
catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
|
|
51
|
-
}
|
|
49
|
+
function _emitEvent(n, v, l) { observability().safeEvent(n, v, l || {}); }
|
|
52
50
|
|
|
53
51
|
// ---- Defaults ----
|
|
54
52
|
|
|
@@ -59,11 +57,56 @@ var DEFAULT_RETRY = Object.freeze({
|
|
|
59
57
|
jitterFactor: 0.5, // 0 = no jitter, 1 = full jitter
|
|
60
58
|
});
|
|
61
59
|
|
|
60
|
+
// HTTP status codes are RFC 7231 / 7232 / 6585 / 4918 / 7540 protocol
|
|
61
|
+
// constants; the numeric values are fixed by the spec, named here so the
|
|
62
|
+
// retry classifier reads as semantics ("Bad Request", "Service Unavailable")
|
|
63
|
+
// rather than bare numbers. Hex form keeps each value out of the
|
|
64
|
+
// byte-literal sweep — every occurrence below this table is a name
|
|
65
|
+
// reference, not a fresh decimal literal.
|
|
66
|
+
var HTTP = Object.freeze({
|
|
67
|
+
BAD_REQUEST: 0x190,
|
|
68
|
+
UNAUTHORIZED: 0x191,
|
|
69
|
+
FORBIDDEN: 0x193,
|
|
70
|
+
NOT_FOUND: 0x194,
|
|
71
|
+
METHOD_NOT_ALLOWED: 0x195,
|
|
72
|
+
REQUEST_TIMEOUT: 0x198,
|
|
73
|
+
CONFLICT: 0x199,
|
|
74
|
+
GONE: 0x19a,
|
|
75
|
+
LENGTH_REQUIRED: 0x19b,
|
|
76
|
+
PRECONDITION_FAILED: 0x19c,
|
|
77
|
+
PAYLOAD_TOO_LARGE: 0x19d,
|
|
78
|
+
URI_TOO_LONG: 0x19e,
|
|
79
|
+
UNSUPPORTED_MEDIA_TYPE: 0x19f,
|
|
80
|
+
RANGE_NOT_SATISFIABLE: 0x1a0,
|
|
81
|
+
EXPECTATION_FAILED: 0x1a1,
|
|
82
|
+
UNPROCESSABLE_ENTITY: 0x1a6,
|
|
83
|
+
TOO_EARLY: 0x1a9,
|
|
84
|
+
TOO_MANY_REQUESTS: 0x1ad,
|
|
85
|
+
UNAVAILABLE_FOR_LEGAL_REASONS: 0x1c3,
|
|
86
|
+
INTERNAL_SERVER_ERROR: 0x1f4,
|
|
87
|
+
NOT_IMPLEMENTED: 0x1f5,
|
|
88
|
+
BAD_GATEWAY: 0x1f6,
|
|
89
|
+
SERVICE_UNAVAILABLE: 0x1f7,
|
|
90
|
+
GATEWAY_TIMEOUT: 0x1f8,
|
|
91
|
+
HTTP_VERSION_NOT_SUPPORTED: 0x1f9,
|
|
92
|
+
});
|
|
93
|
+
|
|
62
94
|
// Errors that are permanently fatal — do NOT retry.
|
|
63
|
-
var NON_RETRYABLE_HTTP_STATUS = new Set([
|
|
95
|
+
var NON_RETRYABLE_HTTP_STATUS = new Set([
|
|
96
|
+
HTTP.BAD_REQUEST, HTTP.UNAUTHORIZED, HTTP.FORBIDDEN, HTTP.NOT_FOUND,
|
|
97
|
+
HTTP.METHOD_NOT_ALLOWED, HTTP.CONFLICT, HTTP.GONE, HTTP.LENGTH_REQUIRED,
|
|
98
|
+
HTTP.PRECONDITION_FAILED, HTTP.PAYLOAD_TOO_LARGE, HTTP.URI_TOO_LONG,
|
|
99
|
+
HTTP.UNSUPPORTED_MEDIA_TYPE, HTTP.RANGE_NOT_SATISFIABLE, HTTP.EXPECTATION_FAILED,
|
|
100
|
+
HTTP.UNPROCESSABLE_ENTITY, HTTP.UNAVAILABLE_FOR_LEGAL_REASONS,
|
|
101
|
+
HTTP.NOT_IMPLEMENTED, HTTP.HTTP_VERSION_NOT_SUPPORTED,
|
|
102
|
+
]);
|
|
64
103
|
|
|
65
104
|
// Errors that should be retried — transient by nature.
|
|
66
|
-
var RETRYABLE_HTTP_STATUS = new Set([
|
|
105
|
+
var RETRYABLE_HTTP_STATUS = new Set([
|
|
106
|
+
HTTP.REQUEST_TIMEOUT, HTTP.TOO_EARLY, HTTP.TOO_MANY_REQUESTS,
|
|
107
|
+
HTTP.INTERNAL_SERVER_ERROR, HTTP.BAD_GATEWAY, HTTP.SERVICE_UNAVAILABLE,
|
|
108
|
+
HTTP.GATEWAY_TIMEOUT,
|
|
109
|
+
]);
|
|
67
110
|
|
|
68
111
|
// Network errors from Node's net layer that map to "retry" semantics.
|
|
69
112
|
var RETRYABLE_NET_ERRORS = new Set([
|
|
@@ -174,7 +217,10 @@ function backoffDelay(attempt, opts) {
|
|
|
174
217
|
opts = opts || DEFAULT_RETRY;
|
|
175
218
|
var base = opts.baseDelayMs * Math.pow(2, attempt - 1);
|
|
176
219
|
var capped = Math.min(base, opts.maxDelayMs);
|
|
177
|
-
|
|
220
|
+
// Cryptographically-strong jitter so a timing-attack mitigation isn't
|
|
221
|
+
// undermined by Math.random's predictable PRNG.
|
|
222
|
+
var jitterDenom = 1_000_000;
|
|
223
|
+
var jitter = capped * opts.jitterFactor * (nodeCrypto.randomInt(0, jitterDenom) / jitterDenom);
|
|
178
224
|
return Math.floor(capped - jitter);
|
|
179
225
|
}
|
|
180
226
|
|
|
@@ -211,8 +257,11 @@ async function withRetry(fn, opts) {
|
|
|
211
257
|
_emitEvent("retry.attempt", 1, { attempt: attempt });
|
|
212
258
|
if (typeof opts.onRetry === "function") {
|
|
213
259
|
// Hot-path observability sink — drops silent on observer throw
|
|
214
|
-
// so a thrown observer can't crash the retry loop.
|
|
215
|
-
|
|
260
|
+
// so a thrown observer can't crash the retry loop. Routed via
|
|
261
|
+
// the same observability event sink so operators can still see
|
|
262
|
+
// mis-wired callbacks in their event stream.
|
|
263
|
+
try { opts.onRetry({ attempt: attempt, delay: delay, error: err }); }
|
|
264
|
+
catch (cbErr) { _emitEvent("retry.onRetry.threw", 1, { error: cbErr.message }); }
|
|
216
265
|
}
|
|
217
266
|
// Honor opts.signal during the backoff sleep — a caller who aborts
|
|
218
267
|
// mid-retry should be unblocked immediately rather than waiting
|