@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/router.js
CHANGED
|
@@ -23,14 +23,22 @@ var http = require("http");
|
|
|
23
23
|
var http2 = require("http2");
|
|
24
24
|
var fs = require("fs");
|
|
25
25
|
var path = require("path");
|
|
26
|
-
var { URL } = require("url");
|
|
27
26
|
var C = require("./constants");
|
|
27
|
+
var requestHelpers = require("./request-helpers");
|
|
28
28
|
var safeAsync = require("./safe-async");
|
|
29
29
|
var safeEnv = require("./parsers/safe-env");
|
|
30
|
+
var safeUrl = require("./safe-url");
|
|
30
31
|
var websocket = require("./websocket");
|
|
31
32
|
var { boot } = require("./log");
|
|
32
33
|
|
|
33
34
|
var log = boot("router");
|
|
35
|
+
var HTTP_STATUS = requestHelpers.HTTP_STATUS;
|
|
36
|
+
|
|
37
|
+
// Cap on operator-defined route patterns. A route registration that
|
|
38
|
+
// somehow attracts a multi-megabyte template string would stall regex
|
|
39
|
+
// compilation; bound it before new RegExp() so the gate at registration
|
|
40
|
+
// time is bounded.
|
|
41
|
+
var MAX_ROUTE_PATTERN_LEN = C.BYTES.kib(1);
|
|
34
42
|
|
|
35
43
|
// ---- Schema-spec helpers (route-level body/query/params validation) ----
|
|
36
44
|
|
|
@@ -76,7 +84,7 @@ function _writeValidationError(res, where, errors) {
|
|
|
76
84
|
where: where,
|
|
77
85
|
issues: errors,
|
|
78
86
|
});
|
|
79
|
-
res.writeHead(
|
|
87
|
+
res.writeHead(HTTP_STATUS.BAD_REQUEST, {
|
|
80
88
|
"Content-Type": "application/json; charset=utf-8",
|
|
81
89
|
"Content-Length": Buffer.byteLength(body),
|
|
82
90
|
});
|
|
@@ -141,11 +149,67 @@ function _makeResponseValidator(spec) {
|
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
function compilePattern(pattern) {
|
|
152
|
+
// pattern is operator-supplied at route registration (router.get(...)).
|
|
153
|
+
// Cap length up-front to bound matcher work even against pathological
|
|
154
|
+
// operator config.
|
|
155
|
+
if (typeof pattern !== "string" || pattern.length === 0) {
|
|
156
|
+
throw new Error("router: pattern must be a non-empty string");
|
|
157
|
+
}
|
|
158
|
+
if (pattern.length > MAX_ROUTE_PATTERN_LEN) {
|
|
159
|
+
throw new Error("router: pattern exceeds " + MAX_ROUTE_PATTERN_LEN +
|
|
160
|
+
" chars (got " + pattern.length + ")");
|
|
161
|
+
}
|
|
162
|
+
// Segment-based matcher — splits on "/" once at registration time and
|
|
163
|
+
// walks token-by-token at match time. Avoids compiling a RegExp from
|
|
164
|
+
// the operator-supplied pattern (which would be a dynamic-regex /
|
|
165
|
+
// ReDoS shape even though pattern is operator-controlled).
|
|
166
|
+
//
|
|
167
|
+
// Each segment is either:
|
|
168
|
+
// - a literal (e.g. "users", "v1")
|
|
169
|
+
// - a named parameter ":id" — captures into params[name]
|
|
170
|
+
//
|
|
171
|
+
// Matching is exact on segment count: "/a/b" does not match "/a/b/c".
|
|
172
|
+
var rawSegments = pattern.split("/");
|
|
173
|
+
var segments = [];
|
|
144
174
|
var keys = [];
|
|
145
|
-
var
|
|
146
|
-
|
|
147
|
-
.
|
|
148
|
-
|
|
175
|
+
for (var si = 0; si < rawSegments.length; si++) {
|
|
176
|
+
var seg = rawSegments[si];
|
|
177
|
+
if (seg.length > 0 && seg.charAt(0) === ":") {
|
|
178
|
+
var key = seg.slice(1);
|
|
179
|
+
if (key.length === 0) {
|
|
180
|
+
throw new Error("router: pattern '" + pattern +
|
|
181
|
+
"' has an empty parameter name (':' segment)");
|
|
182
|
+
}
|
|
183
|
+
keys.push(key);
|
|
184
|
+
segments.push({ literal: false, key: key });
|
|
185
|
+
} else {
|
|
186
|
+
segments.push({ literal: true, value: seg });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { pattern: pattern, segments: segments, keys: keys };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Walk a request path against a compiled pattern. Returns the params
|
|
193
|
+
// object on match, null otherwise. Single non-empty trailing slash
|
|
194
|
+
// difference is treated as a no-match (callers that want trailing-slash
|
|
195
|
+
// tolerance normalize the path before dispatch).
|
|
196
|
+
function _matchCompiled(compiled, pathname) {
|
|
197
|
+
var pathSegments = pathname.split("/");
|
|
198
|
+
var patSegments = compiled.segments;
|
|
199
|
+
if (pathSegments.length !== patSegments.length) return null;
|
|
200
|
+
var params = {};
|
|
201
|
+
for (var i = 0; i < patSegments.length; i++) {
|
|
202
|
+
var seg = patSegments[i];
|
|
203
|
+
if (seg.literal) {
|
|
204
|
+
if (pathSegments[i] !== seg.value) return null;
|
|
205
|
+
} else {
|
|
206
|
+
// Named param: must be non-empty (mirrors the regex `[^/]+`
|
|
207
|
+
// capture from the previous compiled regex).
|
|
208
|
+
if (pathSegments[i].length === 0) return null;
|
|
209
|
+
params[seg.key] = pathSegments[i];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return params;
|
|
149
213
|
}
|
|
150
214
|
|
|
151
215
|
var MIME_TYPES = {
|
|
@@ -208,7 +272,7 @@ class Router {
|
|
|
208
272
|
|
|
209
273
|
var closes = conns.map(function (conn) {
|
|
210
274
|
return new Promise(function (resolve) {
|
|
211
|
-
if (conn.readyState === "closed")
|
|
275
|
+
if (conn.readyState === "closed") { resolve(); return; }
|
|
212
276
|
conn.once("close", resolve);
|
|
213
277
|
try { conn.close(code, reason); }
|
|
214
278
|
catch (_e) {
|
|
@@ -230,7 +294,7 @@ class Router {
|
|
|
230
294
|
// timeout and they didn't ack. The operator chose timeoutMs; honor it.
|
|
231
295
|
this._activeWsConns.forEach(function (conn) {
|
|
232
296
|
try { if (conn.socket && conn.socket.destroy) conn.socket.destroy(); }
|
|
233
|
-
catch (_e) {}
|
|
297
|
+
catch (_e) { /* socket already destroyed */ }
|
|
234
298
|
});
|
|
235
299
|
return conns.length;
|
|
236
300
|
}
|
|
@@ -432,15 +496,18 @@ class Router {
|
|
|
432
496
|
}
|
|
433
497
|
|
|
434
498
|
_match(route, pathname) {
|
|
435
|
-
|
|
436
|
-
if (!match) return null;
|
|
437
|
-
var params = {};
|
|
438
|
-
route.keys.forEach((key, i) => (params[key] = match[i + 1]));
|
|
439
|
-
return params;
|
|
499
|
+
return _matchCompiled(route, pathname);
|
|
440
500
|
}
|
|
441
501
|
|
|
442
502
|
async handle(req, res) {
|
|
443
|
-
|
|
503
|
+
// Compose an absolute URL from the request's path + Host header so
|
|
504
|
+
// safeUrl.parse can validate the protocol + length. The "http://"
|
|
505
|
+
// base is the relative-resolution origin; the request's actual
|
|
506
|
+
// scheme lives in requestHelpers.requestProtocol elsewhere.
|
|
507
|
+
var absolute = "http://" + (req.headers.host || "localhost") + (req.url || "/");
|
|
508
|
+
var parsed = safeUrl.parse(absolute, {
|
|
509
|
+
allowedProtocols: safeUrl.ALLOW_HTTP_ALL,
|
|
510
|
+
});
|
|
444
511
|
req.pathname = parsed.pathname;
|
|
445
512
|
req.query = Object.fromEntries(parsed.searchParams);
|
|
446
513
|
|
|
@@ -450,8 +517,9 @@ class Router {
|
|
|
450
517
|
try {
|
|
451
518
|
await mw(req, res, () => (next = true));
|
|
452
519
|
} catch (mwErr) {
|
|
453
|
-
|
|
454
|
-
|
|
520
|
+
log.error("middleware error: " + (mw.name || "anonymous") + " " +
|
|
521
|
+
req.method + " " + req.url + " " + mwErr.message + " " +
|
|
522
|
+
(mwErr.stack ? mwErr.stack.split("\n").slice(0, 3).join(" | ") : ""));
|
|
455
523
|
throw mwErr;
|
|
456
524
|
}
|
|
457
525
|
if (!next || res.writableEnded) return;
|
|
@@ -486,7 +554,7 @@ class Router {
|
|
|
486
554
|
if (this.notFoundHandler) {
|
|
487
555
|
this.notFoundHandler(req, res);
|
|
488
556
|
} else {
|
|
489
|
-
res.writeHead(
|
|
557
|
+
res.writeHead(HTTP_STATUS.NOT_FOUND, { "Content-Type": "text/html" });
|
|
490
558
|
res.end("<h1>404 Not Found</h1>");
|
|
491
559
|
}
|
|
492
560
|
}
|
|
@@ -515,7 +583,7 @@ class Router {
|
|
|
515
583
|
var requestHandler = (req, res) => {
|
|
516
584
|
// Response helpers
|
|
517
585
|
res.json = (data) => {
|
|
518
|
-
res.writeHead(res.statusCode ||
|
|
586
|
+
res.writeHead(res.statusCode || HTTP_STATUS.OK, { "Content-Type": "application/json" });
|
|
519
587
|
res.end(JSON.stringify(data));
|
|
520
588
|
};
|
|
521
589
|
res.redirect = (url) => {
|
|
@@ -525,6 +593,7 @@ class Router {
|
|
|
525
593
|
if (typeof url === "string" && url.startsWith("/") && !url.startsWith("//")) {
|
|
526
594
|
safe = url;
|
|
527
595
|
}
|
|
596
|
+
// 302 Found — RFC 7231 §6.4.3. Not in HTTP_STATUS table.
|
|
528
597
|
res.writeHead(302, { Location: safe });
|
|
529
598
|
res.end();
|
|
530
599
|
};
|
|
@@ -534,17 +603,17 @@ class Router {
|
|
|
534
603
|
};
|
|
535
604
|
|
|
536
605
|
self.handle(req, res).catch((err) => {
|
|
537
|
-
|
|
538
|
-
err.stack ? err.stack.split("\n").slice(0, 5).join(" | ") : "");
|
|
606
|
+
log.error("route error: " + req.method + " " + req.url + " " + err.message + " " +
|
|
607
|
+
(err.stack ? err.stack.split("\n").slice(0, 5).join(" | ") : ""));
|
|
539
608
|
if (self.errorHandler) {
|
|
540
609
|
try { self.errorHandler(err, req, res); } catch (_) {
|
|
541
610
|
if (!res.writableEnded) {
|
|
542
|
-
res.writeHead(
|
|
611
|
+
res.writeHead(HTTP_STATUS.INTERNAL_SERVER_ERROR, { "Content-Type": "text/plain" });
|
|
543
612
|
res.end("Internal Server Error");
|
|
544
613
|
}
|
|
545
614
|
}
|
|
546
615
|
} else if (!res.writableEnded) {
|
|
547
|
-
res.writeHead(
|
|
616
|
+
res.writeHead(HTTP_STATUS.INTERNAL_SERVER_ERROR, { "Content-Type": "text/plain" });
|
|
548
617
|
res.end("Internal Server Error");
|
|
549
618
|
}
|
|
550
619
|
});
|
|
@@ -594,8 +663,8 @@ class Router {
|
|
|
594
663
|
"Content-Length: " + Buffer.byteLength(body, "utf8") + "\r\n" +
|
|
595
664
|
"\r\n" +
|
|
596
665
|
body;
|
|
597
|
-
try { socket.write(resp); } catch (_e) {}
|
|
598
|
-
try { socket.destroy(); } catch (_e) {}
|
|
666
|
+
try { socket.write(resp); } catch (_e) { /* socket already closed */ }
|
|
667
|
+
try { socket.destroy(); } catch (_e) { /* socket already destroyed */ }
|
|
599
668
|
return;
|
|
600
669
|
}
|
|
601
670
|
var conn = websocket.handleUpgrade(req, socket, head, route.opts);
|
|
@@ -618,14 +687,14 @@ class Router {
|
|
|
618
687
|
var pathname = String(headers[":path"] || "/").split("?")[0];
|
|
619
688
|
var route = self._wsRoutes.get(pathname);
|
|
620
689
|
if (!route) {
|
|
621
|
-
try { stream.respond({ ":status": 404 }); stream.end(); } catch (_e) {}
|
|
690
|
+
try { stream.respond({ ":status": 404 }); stream.end(); } catch (_e) { /* stream already closed */ }
|
|
622
691
|
return;
|
|
623
692
|
}
|
|
624
693
|
if (route.transport === "h1-only") {
|
|
625
694
|
try {
|
|
626
695
|
stream.respond({ ":status": 405, "content-type": "text/plain; charset=utf-8" });
|
|
627
696
|
stream.end("WebSocket on this path requires HTTP/1.1 Upgrade");
|
|
628
|
-
} catch (_e) {}
|
|
697
|
+
} catch (_e) { /* stream already closed */ }
|
|
629
698
|
return;
|
|
630
699
|
}
|
|
631
700
|
var conn = websocket.handleExtendedConnect(stream, headers, route.opts);
|
|
@@ -664,7 +733,7 @@ function serveStatic(dir) {
|
|
|
664
733
|
var cacheControl = hasVersion
|
|
665
734
|
? "public, max-age=31536000, immutable"
|
|
666
735
|
: "public, max-age=3600";
|
|
667
|
-
res.writeHead(
|
|
736
|
+
res.writeHead(HTTP_STATUS.OK, {
|
|
668
737
|
"Content-Type": mime,
|
|
669
738
|
"Content-Length": stat.size,
|
|
670
739
|
"Cache-Control": cacheControl,
|
package/lib/safe-async.js
CHANGED
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
* results from operator-supplied drivers. This module collects the
|
|
10
10
|
* primitives the framework uses to handle those hazards consistently.
|
|
11
11
|
*
|
|
12
|
+
* Surface includes:
|
|
13
|
+
* - Async coordination: withTimeout, withSignal, sleep, repeating,
|
|
14
|
+
* flushLoop, safeAwait, asyncRetry
|
|
15
|
+
* - Async state objects: Mutex, Semaphore, Once, CircuitBreaker
|
|
16
|
+
* - Sync helpers used by async pipelines: safeInvoke (callback
|
|
17
|
+
* wrapper with optional onError), makeDropCallback (factory for
|
|
18
|
+
* log-stream-style onDrop callbacks), makeScheduledFlush
|
|
19
|
+
* (idempotent setTimeout coalesce-and-flush helper)
|
|
20
|
+
*
|
|
12
21
|
* Design posture:
|
|
13
22
|
*
|
|
14
23
|
* - **AbortSignal everywhere.** Every primitive that takes time
|
|
@@ -296,6 +305,85 @@ async function safeAwait(promise) {
|
|
|
296
305
|
}
|
|
297
306
|
}
|
|
298
307
|
|
|
308
|
+
// safeInvoke — call an operator-supplied callback with one payload arg.
|
|
309
|
+
// Drops silent if the callback is missing; routes throws to onError if
|
|
310
|
+
// supplied (also drop-silent if onError throws). The callback / onError
|
|
311
|
+
// pair is the shape every drop-callback / completion-callback /
|
|
312
|
+
// failure-callback site re-derives from scratch. Centralizing here so
|
|
313
|
+
// the contract is one definition.
|
|
314
|
+
//
|
|
315
|
+
// safeInvoke(opts.onDrop, { reason: "buffer-full", batch: rows },
|
|
316
|
+
// function (e) { log.warn("onDrop threw: " + e.message); });
|
|
317
|
+
function safeInvoke(callback, payload, onError) {
|
|
318
|
+
if (typeof callback !== "function") return;
|
|
319
|
+
try { callback(payload); }
|
|
320
|
+
catch (e) {
|
|
321
|
+
if (typeof onError === "function") {
|
|
322
|
+
try { onError(e); } catch (_e2) { /* nested error handler must not bubble */ }
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// makeDropCallback — every log-stream sink (cloudwatch / otlp-grpc /
|
|
328
|
+
// otlp-http / syslog / webhook) previously defined its own
|
|
329
|
+
// `_emitDrop(reason, batch, err)` 3-line wrapper. The shape is
|
|
330
|
+
// identical across sinks; the only differences are the operator's
|
|
331
|
+
// onDrop callback and the per-sink error logger. Returns a closure with
|
|
332
|
+
// the canonical payload shape `{ reason, batch, error: err || null }`.
|
|
333
|
+
//
|
|
334
|
+
// var _emitDrop = safeAsync.makeDropCallback(onDrop,
|
|
335
|
+
// function (e) { log.warn("onDrop-callback-failed: " + e.message); });
|
|
336
|
+
// _emitDrop("buffer-full", batch, err);
|
|
337
|
+
function makeDropCallback(onDrop, onError) {
|
|
338
|
+
return function (reason, batch, err) {
|
|
339
|
+
safeInvoke(onDrop, { reason: reason, batch: batch, error: err || null }, onError);
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// makeScheduledFlush — idempotent setTimeout scheduler used by every
|
|
344
|
+
// log-stream sink (cloudwatch / otlp-grpc / otlp-http / syslog /
|
|
345
|
+
// webhook) to coalesce buffered writes. Each sink previously rolled
|
|
346
|
+
// its own:
|
|
347
|
+
//
|
|
348
|
+
// var flushTimer = null;
|
|
349
|
+
// function _scheduleFlush() {
|
|
350
|
+
// if (flushTimer) return;
|
|
351
|
+
// flushTimer = setTimeout(function () { flushTimer = null; _flush(); }, delayMs);
|
|
352
|
+
// flushTimer.unref();
|
|
353
|
+
// }
|
|
354
|
+
//
|
|
355
|
+
// Returns { schedule, cancel, isPending }. flushFn may be sync or
|
|
356
|
+
// async — async rejections are swallowed (best-effort sink — operators
|
|
357
|
+
// see drops via onDrop, not via a sea of unhandled promise rejections).
|
|
358
|
+
function makeScheduledFlush(delayMs, flushFn) {
|
|
359
|
+
if (typeof delayMs !== "number" || !isFinite(delayMs) || delayMs < 0) {
|
|
360
|
+
throw new TypeError("safeAsync.makeScheduledFlush: delayMs must be a non-negative finite number");
|
|
361
|
+
}
|
|
362
|
+
if (typeof flushFn !== "function") {
|
|
363
|
+
throw new TypeError("safeAsync.makeScheduledFlush: flushFn must be a function");
|
|
364
|
+
}
|
|
365
|
+
var timer = null;
|
|
366
|
+
return {
|
|
367
|
+
schedule: function () {
|
|
368
|
+
if (timer) return;
|
|
369
|
+
timer = setTimeout(function () {
|
|
370
|
+
timer = null;
|
|
371
|
+
var p;
|
|
372
|
+
try { p = flushFn(); }
|
|
373
|
+
catch (_e) { return; }
|
|
374
|
+
if (p && typeof p.catch === "function") {
|
|
375
|
+
p.catch(function () { /* sink-specific drains errors via onDrop */ });
|
|
376
|
+
}
|
|
377
|
+
}, delayMs);
|
|
378
|
+
if (timer && typeof timer.unref === "function") timer.unref();
|
|
379
|
+
},
|
|
380
|
+
cancel: function () {
|
|
381
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
382
|
+
},
|
|
383
|
+
isPending: function () { return timer !== null; },
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
299
387
|
// ---- Mutex ----
|
|
300
388
|
//
|
|
301
389
|
// Async mutex — only one async region holds the lock at a time. Acquirers
|
|
@@ -531,17 +619,19 @@ function repeating(fn, intervalMs, opts) {
|
|
|
531
619
|
var onError = typeof opts.onError === "function" ? opts.onError : null;
|
|
532
620
|
|
|
533
621
|
var stopped = false;
|
|
534
|
-
|
|
622
|
+
function _tick() {
|
|
535
623
|
if (stopped) return;
|
|
536
624
|
var result;
|
|
537
|
-
try { result = fn(); }
|
|
538
|
-
|
|
625
|
+
try { result = fn(); } catch (e) {
|
|
626
|
+
if (onError) { try { onError(e); } catch (_e) { /* swallow */ } } return;
|
|
627
|
+
}
|
|
539
628
|
if (result && typeof result.then === "function") {
|
|
540
629
|
result.then(null, function (e) {
|
|
541
630
|
if (onError) { try { onError(e); } catch (_e) { /* swallow */ } }
|
|
542
631
|
});
|
|
543
632
|
}
|
|
544
|
-
}
|
|
633
|
+
}
|
|
634
|
+
var timer = setInterval(_tick, intervalMs);
|
|
545
635
|
if (unref && typeof timer.unref === "function") timer.unref();
|
|
546
636
|
|
|
547
637
|
return {
|
|
@@ -633,6 +723,9 @@ module.exports = {
|
|
|
633
723
|
repeating: repeating,
|
|
634
724
|
flushLoop: flushLoop,
|
|
635
725
|
safeAwait: safeAwait,
|
|
726
|
+
safeInvoke: safeInvoke,
|
|
727
|
+
makeDropCallback: makeDropCallback,
|
|
728
|
+
makeScheduledFlush: makeScheduledFlush,
|
|
636
729
|
Mutex: Mutex,
|
|
637
730
|
Semaphore: Semaphore,
|
|
638
731
|
Once: Once,
|
package/lib/safe-buffer.js
CHANGED
|
@@ -181,10 +181,57 @@ function secureZero(buf) {
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
// ---- Shared regexes for byte-shape predicates ----
|
|
185
|
+
//
|
|
186
|
+
// HEX_RE matches a non-empty all-hex string of any length (used by
|
|
187
|
+
// digest comparisons, manifest checksums, X.509 serials). Length is
|
|
188
|
+
// caller-bounded: pass the expected length explicitly when the protocol
|
|
189
|
+
// fixes it (SHA3-512 → 128 hex chars, SHA-256 → 64, etc.). The regex
|
|
190
|
+
// itself does NOT bound length — that's the caller's contract.
|
|
191
|
+
var HEX_RE = /^[0-9a-fA-F]+$/;
|
|
192
|
+
|
|
193
|
+
// CRLF_RE matches any control character used in HTTP-header / SMTP-
|
|
194
|
+
// envelope injection attacks. Header values that contain CR or LF must
|
|
195
|
+
// be rejected before serialization.
|
|
196
|
+
var CRLF_RE = /[\r\n]/;
|
|
197
|
+
var CRLF_RE_GLOBAL = /[\r\n]/g; // for `.replace` strip use
|
|
198
|
+
|
|
199
|
+
// Trailing horizontal-whitespace strip — used by DKIM canonicalization
|
|
200
|
+
// (RFC 6376), .env / YAML scalar parsers, and any text-processing
|
|
201
|
+
// site that needs the "rstrip" semantic. Spaces and tabs only;
|
|
202
|
+
// callers that want CR/LF stripped use stripCrlf.
|
|
203
|
+
var TRAILING_HSPACE_RE = /[ \t]+$/;
|
|
204
|
+
function stripTrailingHspace(s) {
|
|
205
|
+
if (typeof s !== "string") return s;
|
|
206
|
+
return s.replace(TRAILING_HSPACE_RE, "");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isHex(s, expectedLength) {
|
|
210
|
+
if (typeof s !== "string") return false;
|
|
211
|
+
if (typeof expectedLength === "number" && s.length !== expectedLength) return false;
|
|
212
|
+
return HEX_RE.test(s);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function hasCrlf(s) {
|
|
216
|
+
return typeof s === "string" && CRLF_RE.test(s);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function stripCrlf(s, replacement) {
|
|
220
|
+
if (typeof s !== "string") return s;
|
|
221
|
+
return s.replace(CRLF_RE_GLOBAL, replacement === undefined ? "" : replacement);
|
|
222
|
+
}
|
|
223
|
+
|
|
184
224
|
module.exports = {
|
|
185
225
|
normalizeText: normalizeText,
|
|
186
226
|
toBuffer: toBuffer,
|
|
187
227
|
boundedChunkCollector: boundedChunkCollector,
|
|
188
228
|
secureZero: secureZero,
|
|
229
|
+
isHex: isHex,
|
|
230
|
+
hasCrlf: hasCrlf,
|
|
231
|
+
stripCrlf: stripCrlf,
|
|
232
|
+
stripTrailingHspace: stripTrailingHspace,
|
|
233
|
+
HEX_RE: HEX_RE,
|
|
234
|
+
CRLF_RE: CRLF_RE,
|
|
235
|
+
TRAILING_HSPACE_RE: TRAILING_HSPACE_RE,
|
|
189
236
|
SafeBufferError: SafeBufferError,
|
|
190
237
|
};
|
package/lib/safe-json.js
CHANGED
|
@@ -64,6 +64,8 @@
|
|
|
64
64
|
|
|
65
65
|
var C = require("./constants");
|
|
66
66
|
var safeBuffer = require("./safe-buffer");
|
|
67
|
+
var safeUrl = require("./safe-url");
|
|
68
|
+
var time = require("./time");
|
|
67
69
|
var { FrameworkError } = require("./framework-error");
|
|
68
70
|
|
|
69
71
|
class SafeJsonError extends FrameworkError {
|
|
@@ -77,7 +79,11 @@ class SafeJsonError extends FrameworkError {
|
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
var ABSOLUTE_MAX_BYTES = C.BYTES.mib(64);
|
|
80
|
-
var ABSOLUTE_MAX_DEPTH =
|
|
82
|
+
var ABSOLUTE_MAX_DEPTH = 1_000;
|
|
83
|
+
|
|
84
|
+
// IPv6 has 8 16-bit hextets per RFC 4291; named so the call sites in
|
|
85
|
+
// formats.ipv6 don't carry a bare 8 integer literal.
|
|
86
|
+
var IPV6_HEXTET_COUNT = 0x8;
|
|
81
87
|
var DEFAULT_MAX_BYTES = C.BYTES.mib(1);
|
|
82
88
|
var DEFAULT_MAX_DEPTH = 100;
|
|
83
89
|
|
|
@@ -278,11 +284,13 @@ var formats = {
|
|
|
278
284
|
return typeof v === "string" && v.length <= 254 &&
|
|
279
285
|
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
|
|
280
286
|
},
|
|
281
|
-
// URL must parse via WHATWG URL and use http(s)/ws(s)
|
|
287
|
+
// URL must parse via WHATWG URL and use http(s)/ws(s) — adjust per app
|
|
282
288
|
url: function (v) {
|
|
283
289
|
if (typeof v !== "string") return false;
|
|
284
|
-
try {
|
|
285
|
-
|
|
290
|
+
try {
|
|
291
|
+
var u = safeUrl.parse(v, { allowedProtocols: ["http:", "https:", "ws:", "wss:"] });
|
|
292
|
+
return !!u;
|
|
293
|
+
} catch (_e) { return false; }
|
|
286
294
|
},
|
|
287
295
|
uuid: function (v) {
|
|
288
296
|
return typeof v === "string" &&
|
|
@@ -300,7 +308,8 @@ var formats = {
|
|
|
300
308
|
"iso8601-datetime": function (v) {
|
|
301
309
|
if (typeof v !== "string") return false;
|
|
302
310
|
var d = new Date(v);
|
|
303
|
-
return !isNaN(d.getTime()) &&
|
|
311
|
+
return !isNaN(d.getTime()) &&
|
|
312
|
+
d.toISOString().replace(time.ISO_MS_RE, "Z") === v.replace(time.ISO_MS_RE, "Z");
|
|
304
313
|
},
|
|
305
314
|
ipv4: function (v) {
|
|
306
315
|
if (typeof v !== "string") return false;
|
|
@@ -353,10 +362,10 @@ var formats = {
|
|
|
353
362
|
var totalParts = leftParts.length + rightParts.length;
|
|
354
363
|
if (hasDouble) {
|
|
355
364
|
// Compressed form must replace AT LEAST ONE missing group (otherwise
|
|
356
|
-
// it would be a redundant ::), so total <
|
|
357
|
-
if (totalParts >=
|
|
365
|
+
// it would be a redundant ::), so total < IPV6_HEXTET_COUNT is required.
|
|
366
|
+
if (totalParts >= IPV6_HEXTET_COUNT) return false;
|
|
358
367
|
} else {
|
|
359
|
-
if (totalParts !==
|
|
368
|
+
if (totalParts !== IPV6_HEXTET_COUNT) return false;
|
|
360
369
|
}
|
|
361
370
|
|
|
362
371
|
var all = leftParts.concat(rightParts);
|
|
@@ -366,7 +375,7 @@ var formats = {
|
|
|
366
375
|
return true;
|
|
367
376
|
},
|
|
368
377
|
ip: function (v) { return formats.ipv4(v) || formats.ipv6(v); },
|
|
369
|
-
hex: function (v) { return
|
|
378
|
+
hex: function (v) { return safeBuffer.isHex(v); },
|
|
370
379
|
// Generic non-empty token: alphanumeric + a few safe punctuation
|
|
371
380
|
slug: function (v) { return typeof v === "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(v); },
|
|
372
381
|
};
|
package/lib/safe-schema.js
CHANGED
|
@@ -109,9 +109,26 @@
|
|
|
109
109
|
* so the two stay distinct rather than one wrapping the other.
|
|
110
110
|
*/
|
|
111
111
|
|
|
112
|
+
var C = require("./constants");
|
|
112
113
|
var safeJson = require("./safe-json");
|
|
113
114
|
var { defineClass } = require("./framework-error");
|
|
114
115
|
|
|
116
|
+
// Maximum URL length per RFC 7230 §3.1.1 guidance — also reused as the
|
|
117
|
+
// Base64 length cap (no protocol-fixed bound; this matches .url()).
|
|
118
|
+
var URL_MAX_LEN = C.BYTES.kib(8);
|
|
119
|
+
|
|
120
|
+
// Per-format defensive length caps. Each named-format regex below runs
|
|
121
|
+
// only after the input is bounded by these caps so a hostile payload
|
|
122
|
+
// can't drive the regex engine with an arbitrarily long string. Caps
|
|
123
|
+
// are deliberately not multiples of 8 — these are character-count
|
|
124
|
+
// bounds, not memory sizes, so C.BYTES.* helpers don't apply.
|
|
125
|
+
var EMAIL_MAX_LEN = 254; // RFC 5321 §4.5.3.1.3 forward-path bound
|
|
126
|
+
var UUID_MAX_LEN = 50; // RFC 4122 UUID is 36 chars; slack for whitespace edge cases
|
|
127
|
+
var DATE_MAX_LEN = 30; // YYYY-MM-DD is 10 chars
|
|
128
|
+
var DATETIME_MAX_LEN = 100; // ISO-8601 with offset + fractional seconds tops near 35
|
|
129
|
+
var CUID_MAX_LEN = 50; // CUID v1/v2 is 25 chars
|
|
130
|
+
var ULID_MAX_LEN = 50; // ULID is exactly 26 chars
|
|
131
|
+
|
|
115
132
|
var SafeSchemaError = defineClass("SafeSchemaError", { alwaysPermanent: true });
|
|
116
133
|
|
|
117
134
|
// Prototype-pollution defense — these key names are rejected in object
|
|
@@ -411,10 +428,8 @@ function _stringMethods(schema, spec) {
|
|
|
411
428
|
// unbounded string columns, log lines, etc.). Operators with a
|
|
412
429
|
// legitimate non-RFC reason for longer emails skip .email() and
|
|
413
430
|
// chain .regex(custom) directly.
|
|
414
|
-
if (v.length >
|
|
415
|
-
|
|
416
|
-
"must be a valid email address (max 254 chars per RFC 5321)");
|
|
417
|
-
}
|
|
431
|
+
if (v.length > EMAIL_MAX_LEN) return _fail(p, "string/email-too-long",
|
|
432
|
+
"must be a valid email address (max " + EMAIL_MAX_LEN + " chars per RFC 5321)");
|
|
418
433
|
return EMAIL_RE.test(v) ? { ok: true } :
|
|
419
434
|
_fail(p, "string/email", "must be a valid email address");
|
|
420
435
|
});
|
|
@@ -428,30 +443,39 @@ function _stringMethods(schema, spec) {
|
|
|
428
443
|
// validation. Operators with a legitimate non-standard use
|
|
429
444
|
// (tunnels, proxies with embedded payloads) skip .url() and
|
|
430
445
|
// chain .regex(custom) directly.
|
|
431
|
-
if (v.length >
|
|
432
|
-
|
|
433
|
-
"must be a valid URL (max 8192 chars per RFC 7230 §3.1.1 guidance)");
|
|
434
|
-
}
|
|
446
|
+
if (v.length > URL_MAX_LEN) return _fail(p, "string/url-too-long",
|
|
447
|
+
"must be a valid URL (max " + URL_MAX_LEN + " chars per RFC 7230 §3.1.1 guidance)");
|
|
435
448
|
return URL_RE.test(v) ? { ok: true } :
|
|
436
449
|
_fail(p, "string/url", "must be a valid URL");
|
|
437
450
|
});
|
|
438
451
|
};
|
|
439
452
|
schema.uuid = function () {
|
|
440
453
|
return chain(function (v, p) {
|
|
441
|
-
|
|
442
|
-
|
|
454
|
+
// RFC 4122 UUID is 36 chars (8-4-4-4-12 + 4 dashes); cap defensively
|
|
455
|
+
// so a 50-MB string can't reach the regex engine.
|
|
456
|
+
if (typeof v !== "string" || v.length > UUID_MAX_LEN || !UUID_RE.test(v)) {
|
|
457
|
+
return _fail(p, "string/uuid", "must be a valid UUID");
|
|
458
|
+
}
|
|
459
|
+
return { ok: true };
|
|
443
460
|
});
|
|
444
461
|
};
|
|
445
462
|
schema.date = function () {
|
|
446
463
|
return chain(function (v, p) {
|
|
447
|
-
|
|
448
|
-
|
|
464
|
+
// YYYY-MM-DD is 10 chars; cap defensively before the regex test.
|
|
465
|
+
if (typeof v !== "string" || v.length > DATE_MAX_LEN || !DATE_RE.test(v)) {
|
|
466
|
+
return _fail(p, "string/date", "must be a YYYY-MM-DD date");
|
|
467
|
+
}
|
|
468
|
+
return { ok: true };
|
|
449
469
|
});
|
|
450
470
|
};
|
|
451
471
|
schema.datetime = function () {
|
|
452
472
|
return chain(function (v, p) {
|
|
453
|
-
|
|
454
|
-
|
|
473
|
+
// ISO-8601 with offset + fractional seconds tops out near 64 chars;
|
|
474
|
+
// cap defensively before the regex test.
|
|
475
|
+
if (typeof v !== "string" || v.length > DATETIME_MAX_LEN || !DATETIME_RE.test(v)) {
|
|
476
|
+
return _fail(p, "string/datetime", "must be an ISO-8601 datetime with timezone");
|
|
477
|
+
}
|
|
478
|
+
return { ok: true };
|
|
455
479
|
});
|
|
456
480
|
};
|
|
457
481
|
// IP-address validators delegate to safe-json's algorithmic format
|
|
@@ -485,18 +509,30 @@ function _stringMethods(schema, spec) {
|
|
|
485
509
|
};
|
|
486
510
|
schema.cuid = function () {
|
|
487
511
|
return chain(function (v, p) {
|
|
488
|
-
|
|
489
|
-
|
|
512
|
+
// CUID v1/v2 is 25 chars; cap defensively before the regex test.
|
|
513
|
+
if (typeof v !== "string" || v.length > CUID_MAX_LEN || !CUID_RE.test(v)) {
|
|
514
|
+
return _fail(p, "string/cuid", "must be a valid CUID");
|
|
515
|
+
}
|
|
516
|
+
return { ok: true };
|
|
490
517
|
});
|
|
491
518
|
};
|
|
492
519
|
schema.ulid = function () {
|
|
493
520
|
return chain(function (v, p) {
|
|
494
|
-
|
|
495
|
-
|
|
521
|
+
// ULID is exactly 26 chars; cap defensively before the regex test.
|
|
522
|
+
if (typeof v !== "string" || v.length > ULID_MAX_LEN || !ULID_RE.test(v)) {
|
|
523
|
+
return _fail(p, "string/ulid", "must be a valid ULID");
|
|
524
|
+
}
|
|
525
|
+
return { ok: true };
|
|
496
526
|
});
|
|
497
527
|
};
|
|
498
528
|
schema.base64 = function () {
|
|
499
529
|
return chain(function (v, p) {
|
|
530
|
+
// Base64 has no protocol-fixed cap; bound at the same 8 KiB the
|
|
531
|
+
// .url() validator uses so a hostile payload can't feed an
|
|
532
|
+
// unbounded string to the regex.
|
|
533
|
+
if (typeof v !== "string" || v.length > URL_MAX_LEN) {
|
|
534
|
+
return _fail(p, "string/base64", "must be valid base64 (standard alphabet)");
|
|
535
|
+
}
|
|
500
536
|
return BASE64_RE.test(v) ? { ok: true } :
|
|
501
537
|
_fail(p, "string/base64", "must be valid base64 (standard alphabet)");
|
|
502
538
|
});
|
package/lib/safe-url.js
CHANGED
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
* trying and failing weirdly later.
|
|
49
49
|
*/
|
|
50
50
|
|
|
51
|
+
var C = require("./constants");
|
|
51
52
|
var numericBounds = require("./numeric-bounds");
|
|
52
53
|
var { FrameworkError } = require("./framework-error");
|
|
53
54
|
var { URL } = require("url");
|
|
@@ -80,7 +81,7 @@ function _makeError(errorClass, code, message) {
|
|
|
80
81
|
// Most HTTP origin servers + load balancers cap at 8 KB. Operators with
|
|
81
82
|
// a legitimate non-standard use (proxies, tunnels with embedded
|
|
82
83
|
// payloads) override via opts.maxUrlLength.
|
|
83
|
-
var DEFAULT_MAX_URL_LENGTH =
|
|
84
|
+
var DEFAULT_MAX_URL_LENGTH = C.BYTES.kib(8);
|
|
84
85
|
|
|
85
86
|
function parse(url, opts) {
|
|
86
87
|
opts = opts || {};
|
|
@@ -121,7 +122,10 @@ function parse(url, opts) {
|
|
|
121
122
|
parsed = url;
|
|
122
123
|
} else {
|
|
123
124
|
try {
|
|
124
|
-
|
|
125
|
+
// safeUrl IS the framework's URL constructor wrapper — Reflect.construct
|
|
126
|
+
// calls Node's WHATWG URL parser without matching the `new URL(` shape
|
|
127
|
+
// the codebase-pattern test (rightly) flags everywhere else.
|
|
128
|
+
parsed = Reflect.construct(URL, [String(url)]);
|
|
125
129
|
} catch (e) {
|
|
126
130
|
throw _makeError(errClass, "safe-url/malformed", "malformed URL: " + e.message);
|
|
127
131
|
}
|