@blamejs/core 0.5.15 → 0.5.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/index.js +2 -0
- package/lib/backup/index.js +2 -2
- package/lib/cache.js +5 -9
- package/lib/cli.js +4 -3
- package/lib/cluster.js +4 -4
- package/lib/csv.js +208 -162
- package/lib/db.js +5 -5
- package/lib/external-db.js +4 -3
- package/lib/forms.js +3 -6
- package/lib/http-client.js +2 -2
- package/lib/mail.js +2 -2
- package/lib/middleware/body-parser.js +2 -1
- package/lib/middleware/compression.js +3 -2
- package/lib/middleware/cors.js +1 -1
- package/lib/middleware/rate-limit.js +4 -4
- package/lib/mtls-engine-default.js +3 -2
- package/lib/nonce-store.js +4 -4
- package/lib/otel-export.js +267 -0
- package/lib/parsers/index.js +3 -7
- package/lib/queue.js +6 -7
- package/lib/request-helpers.js +35 -3
- package/lib/restore.js +3 -3
- package/lib/safe-async.js +122 -0
- package/lib/tracing.js +3 -3
- package/lib/webhook.js +3 -3
- package/lib/websocket.js +6 -4
- package/package.json +1 -1
- package/lib/parsers/safe-csv.js +0 -224
package/lib/external-db.js
CHANGED
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
var retryHelper = require("./retry");
|
|
54
54
|
var C = require("./constants");
|
|
55
55
|
var lazyRequire = require("./lazy-require");
|
|
56
|
+
var safeAsync = require("./safe-async");
|
|
56
57
|
var { ExternalDbError } = require("./framework-error");
|
|
57
58
|
|
|
58
59
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
@@ -78,8 +79,8 @@ class Pool {
|
|
|
78
79
|
this.idle = []; // [{ client, lastUsedAt }]
|
|
79
80
|
this.active = 0; // count of in-use clients
|
|
80
81
|
this.waiters = []; // queued acquisitions when at max
|
|
81
|
-
this._reaper =
|
|
82
|
-
|
|
82
|
+
this._reaper = safeAsync.repeating(this._reapIdle.bind(this),
|
|
83
|
+
C.TIME.seconds(10), { name: "external-db-reaper" });
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
async acquire() {
|
|
@@ -141,7 +142,7 @@ class Pool {
|
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
async drain() {
|
|
144
|
-
if (this._reaper) {
|
|
145
|
+
if (this._reaper) { this._reaper.stop(); this._reaper = null; }
|
|
145
146
|
var idleClients = this.idle.map(function (e) { return e.client; });
|
|
146
147
|
this.idle = [];
|
|
147
148
|
var self = this;
|
package/lib/forms.js
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* forms.escapeAttribute(value) → string (double-quoted-attr context)
|
|
36
36
|
* forms.escapeHtml = template.escapeHtml (re-export for convenience)
|
|
37
37
|
*/
|
|
38
|
-
var
|
|
38
|
+
var crypto = require("./crypto");
|
|
39
39
|
var safeSchema = require("./safe-schema");
|
|
40
40
|
var safeUrl = require("./safe-url");
|
|
41
41
|
var template = require("./template");
|
|
@@ -47,16 +47,13 @@ var template = require("./template");
|
|
|
47
47
|
var CSRF_TOKEN_BYTES = 32;
|
|
48
48
|
|
|
49
49
|
function generateCsrfToken() {
|
|
50
|
-
return
|
|
50
|
+
return crypto.generateToken(CSRF_TOKEN_BYTES);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function verifyCsrfToken(submitted, expected) {
|
|
54
54
|
if (typeof submitted !== "string" || typeof expected !== "string") return false;
|
|
55
55
|
if (submitted.length === 0 || submitted.length !== expected.length) return false;
|
|
56
|
-
|
|
57
|
-
var b = Buffer.from(expected, "utf8");
|
|
58
|
-
if (a.length !== b.length) return false;
|
|
59
|
-
return nodeCrypto.timingSafeEqual(a, b);
|
|
56
|
+
return crypto.timingSafeEqual(submitted, expected);
|
|
60
57
|
}
|
|
61
58
|
|
|
62
59
|
// ============================================================
|
package/lib/http-client.js
CHANGED
|
@@ -64,10 +64,10 @@
|
|
|
64
64
|
var http = require("http");
|
|
65
65
|
var https = require("https");
|
|
66
66
|
var http2 = require("http2");
|
|
67
|
-
var nodeCrypto = require("node:crypto");
|
|
68
67
|
var nodeStream = require("node:stream");
|
|
69
68
|
var { URL } = require("url");
|
|
70
69
|
var C = require("./constants");
|
|
70
|
+
var crypto = require("./crypto");
|
|
71
71
|
var pqcAgent = require("./pqc-agent");
|
|
72
72
|
var safeAsync = require("./safe-async");
|
|
73
73
|
var safeBuffer = require("./safe-buffer");
|
|
@@ -356,7 +356,7 @@ function _attachJarCookie(headers, jar, url) {
|
|
|
356
356
|
// parser accepts so round-trip from one blamejs app's outbound to
|
|
357
357
|
// another's inbound is exact.
|
|
358
358
|
function _buildMultipartBody(spec) {
|
|
359
|
-
var boundary = "----blamejs-mp-" +
|
|
359
|
+
var boundary = "----blamejs-mp-" + crypto.generateToken(16);
|
|
360
360
|
var CRLF = "\r\n";
|
|
361
361
|
var parts = [];
|
|
362
362
|
|
package/lib/mail.js
CHANGED
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
* diagnostic logs identify the provider that rejected the message.
|
|
64
64
|
*/
|
|
65
65
|
var C = require("./constants");
|
|
66
|
+
var crypto = require("./crypto");
|
|
66
67
|
var lazyRequire = require("./lazy-require");
|
|
67
|
-
var nodeCrypto = require("node:crypto");
|
|
68
68
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
69
69
|
var httpClient = lazyRequire(function () { return require("./http-client"); });
|
|
70
70
|
var net = lazyRequire(function () { return require("net"); });
|
|
@@ -297,7 +297,7 @@ function _newBoundary(label) {
|
|
|
297
297
|
// convention. RFC 5322 only requires uniqueness within a message,
|
|
298
298
|
// but consistency with how every other identifier in lib/ is built
|
|
299
299
|
// wins over premature differentiation.
|
|
300
|
-
return "blamejs-" + label + "-" + Date.now() + "-" +
|
|
300
|
+
return "blamejs-" + label + "-" + Date.now() + "-" + crypto.generateToken(8);
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
// base64-encode the buffer with line wrapping at 76 chars (RFC 2045
|
|
@@ -113,6 +113,7 @@ var os = require("os");
|
|
|
113
113
|
var path = require("path");
|
|
114
114
|
var nodeCrypto = require("node:crypto");
|
|
115
115
|
var atomicFile = require("../atomic-file");
|
|
116
|
+
var crypto = require("../crypto");
|
|
116
117
|
var safeJson = require("../safe-json");
|
|
117
118
|
var validateOpts = require("../validate-opts");
|
|
118
119
|
var C = require("../constants");
|
|
@@ -708,7 +709,7 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
708
709
|
|
|
709
710
|
// Generate the tmp path — never derived from the
|
|
710
711
|
// operator-supplied filename.
|
|
711
|
-
var unique =
|
|
712
|
+
var unique = crypto.generateToken(16);
|
|
712
713
|
currentTmpPath = path.join(tmpDir, "blamejs-up-" + unique);
|
|
713
714
|
try {
|
|
714
715
|
currentFd = fs.openSync(currentTmpPath, "wx", 0o600);
|
|
@@ -209,9 +209,10 @@ function _appendVary(existing, token) {
|
|
|
209
209
|
if (!existing) return token;
|
|
210
210
|
var lc = String(existing).toLowerCase();
|
|
211
211
|
if (lc === "*") return "*"; // Vary: * already stops caches; don't dilute.
|
|
212
|
-
var parts =
|
|
212
|
+
var parts = requestHelpers.parseListHeader(existing);
|
|
213
|
+
var lcToken = token.toLowerCase();
|
|
213
214
|
for (var i = 0; i < parts.length; i++) {
|
|
214
|
-
if (parts[i].toLowerCase() ===
|
|
215
|
+
if (parts[i].toLowerCase() === lcToken) return String(existing);
|
|
215
216
|
}
|
|
216
217
|
parts.push(token);
|
|
217
218
|
return parts.join(", ");
|
package/lib/middleware/cors.js
CHANGED
|
@@ -236,7 +236,7 @@ function create(opts) {
|
|
|
236
236
|
if (refuseUnknown) {
|
|
237
237
|
var requestedHdrs = req.headers["access-control-request-headers"];
|
|
238
238
|
if (requestedHdrs) {
|
|
239
|
-
var asked =
|
|
239
|
+
var asked = requestHelpers.parseListHeader(requestedHdrs, { lowercase: true });
|
|
240
240
|
for (var ah = 0; ah < asked.length; ah++) {
|
|
241
241
|
if (allowedHeadersSet.indexOf(asked[ah]) === -1) {
|
|
242
242
|
if (typeof res.writeHead === "function") {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
var C = require("../constants");
|
|
47
47
|
var lazyRequire = require("../lazy-require");
|
|
48
48
|
var requestHelpers = require("../request-helpers");
|
|
49
|
+
var safeAsync = require("../safe-async");
|
|
49
50
|
var validateOpts = require("../validate-opts");
|
|
50
51
|
var clusterStorage = require("../cluster-storage");
|
|
51
52
|
|
|
@@ -71,13 +72,12 @@ function _memoryBackend(opts) {
|
|
|
71
72
|
var buckets = new Map();
|
|
72
73
|
|
|
73
74
|
// Periodic GC of stale buckets so the map doesn't grow unbounded.
|
|
74
|
-
var gcInterval =
|
|
75
|
+
var gcInterval = safeAsync.repeating(function () {
|
|
75
76
|
var cutoff = Date.now() - C.TIME.hours(1);
|
|
76
77
|
for (var k of buckets.keys()) {
|
|
77
78
|
if (buckets.get(k).lastRefillAt < cutoff) buckets.delete(k);
|
|
78
79
|
}
|
|
79
|
-
}, C.TIME.minutes(5));
|
|
80
|
-
if (typeof gcInterval.unref === "function") gcInterval.unref();
|
|
80
|
+
}, C.TIME.minutes(5), { name: "rate-limit-gc" });
|
|
81
81
|
|
|
82
82
|
// Memory backend returns verdicts SYNCHRONOUSLY — no awaitable.
|
|
83
83
|
// The middleware checks `.then` to decide whether to chain. Keeping
|
|
@@ -118,7 +118,7 @@ function _memoryBackend(opts) {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
function close() {
|
|
121
|
-
try {
|
|
121
|
+
try { gcInterval.stop(); } catch (_e) {}
|
|
122
122
|
buckets.clear();
|
|
123
123
|
}
|
|
124
124
|
|
|
@@ -32,6 +32,7 @@ var nodeCrypto = require("node:crypto");
|
|
|
32
32
|
var pki = require("./vendor/pki.cjs");
|
|
33
33
|
|
|
34
34
|
var C = require("./constants");
|
|
35
|
+
var crypto = require("./crypto");
|
|
35
36
|
var { FrameworkError } = require("./framework-error");
|
|
36
37
|
|
|
37
38
|
var x509 = pki.x509;
|
|
@@ -108,7 +109,7 @@ async function generateCa(opts) {
|
|
|
108
109
|
var keys = await webcrypto.subtle.generateKey(CA_KEY_ALG, true, CA_KEY_USAGES);
|
|
109
110
|
var now = new Date();
|
|
110
111
|
var ca = await x509.X509CertificateGenerator.createSelfSigned({
|
|
111
|
-
serialNumber:
|
|
112
|
+
serialNumber: crypto.generateToken(16),
|
|
112
113
|
name: "CN=" + caName + ",OU=CAv" + generation,
|
|
113
114
|
notBefore: now,
|
|
114
115
|
notAfter: new Date(now.getTime() + C.TIME.days(CA_VALIDITY_DAYS)),
|
|
@@ -143,7 +144,7 @@ async function signClientCert(opts) {
|
|
|
143
144
|
var now = new Date();
|
|
144
145
|
var notAfter = new Date(now.getTime() + C.TIME.days(validityDays));
|
|
145
146
|
var clientCert = await x509.X509CertificateGenerator.create({
|
|
146
|
-
serialNumber:
|
|
147
|
+
serialNumber: crypto.generateToken(16),
|
|
147
148
|
subject: "CN=" + cn,
|
|
148
149
|
issuer: caCert.subject,
|
|
149
150
|
notBefore: now,
|
package/lib/nonce-store.js
CHANGED
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
|
|
44
44
|
var clusterStorage = require("./cluster-storage");
|
|
45
45
|
var C = require("./constants");
|
|
46
|
+
var safeAsync = require("./safe-async");
|
|
46
47
|
var { defineClass } = require("./framework-error");
|
|
47
48
|
|
|
48
49
|
var NonceStoreError = defineClass("NonceStoreError");
|
|
@@ -59,13 +60,12 @@ function _memoryBackend(opts) {
|
|
|
59
60
|
var sweepIntervalMs = opts.sweepIntervalMs || DEFAULT_SWEEP_INTERVAL_MS;
|
|
60
61
|
var seen = new Map(); // nonce -> expireAt
|
|
61
62
|
|
|
62
|
-
var sweepTimer =
|
|
63
|
+
var sweepTimer = safeAsync.repeating(function () {
|
|
63
64
|
var now = Date.now();
|
|
64
65
|
for (var entry of seen) {
|
|
65
66
|
if (entry[1] <= now) seen.delete(entry[0]);
|
|
66
67
|
}
|
|
67
|
-
}, sweepIntervalMs);
|
|
68
|
-
if (typeof sweepTimer.unref === "function") sweepTimer.unref();
|
|
68
|
+
}, sweepIntervalMs, { name: "nonce-sweep" });
|
|
69
69
|
|
|
70
70
|
function checkAndInsert(nonce, expireAt) {
|
|
71
71
|
if (typeof nonce !== "string" || nonce.length === 0) {
|
|
@@ -92,7 +92,7 @@ function _memoryBackend(opts) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
function close() {
|
|
95
|
-
if (sweepTimer) {
|
|
95
|
+
if (sweepTimer) { sweepTimer.stop(); sweepTimer = null; }
|
|
96
96
|
seen.clear();
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* otel-export — OTLP/HTTP-JSON exporter for `b.observability` events.
|
|
4
|
+
*
|
|
5
|
+
* Bridges the framework's `observability.event(name, value, attrs)`
|
|
6
|
+
* surface to any OTel-compatible backend (Honeycomb, Datadog, Jaeger
|
|
7
|
+
* Collector, AWS Distro, Grafana, NewRelic — anything that speaks
|
|
8
|
+
* OTLP/HTTP). Spec: opentelemetry.io/docs/specs/otlp.
|
|
9
|
+
*
|
|
10
|
+
* var otel = b.otelExport.create({
|
|
11
|
+
* endpoint: "https://otel.honeycomb.io/v1/metrics",
|
|
12
|
+
* headers: { "X-Honeycomb-Team": env("HONEYCOMB_API_KEY") },
|
|
13
|
+
* serviceName: "wiki",
|
|
14
|
+
* intervalMs: b.constants.TIME.seconds(15), // auto-flush cadence
|
|
15
|
+
* httpClient: b.httpClient, // for testing
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // Counter — accumulates per (name, attrs) tuple, flushed in batches.
|
|
19
|
+
* otel.recordCounter("http.requests", 1, { method: "GET", status: 200 });
|
|
20
|
+
*
|
|
21
|
+
* // Histogram — operator-bucketed observation. Less common; operators
|
|
22
|
+
* // wanting full histogram support build on top.
|
|
23
|
+
* otel.recordObservation("http.duration_ms", 142, { route: "/api/x" });
|
|
24
|
+
*
|
|
25
|
+
* await otel.flush(); // manual flush
|
|
26
|
+
* otel.close(); // cancels interval, final flush
|
|
27
|
+
*
|
|
28
|
+
* Wiring with `b.observability`:
|
|
29
|
+
*
|
|
30
|
+
* // Option A: replace the metrics tap entirely
|
|
31
|
+
* b.observability._setTap(otel.tapHandler);
|
|
32
|
+
*
|
|
33
|
+
* // Option B: alongside b.metrics — operators write their own
|
|
34
|
+
* // multi-tap fan-out (or pick one or the other for v1).
|
|
35
|
+
*
|
|
36
|
+
* Endpoint must accept OTLP/HTTP with Content-Type: application/json
|
|
37
|
+
* (the JSON variant of the OTLP protobuf — every modern collector
|
|
38
|
+
* supports it). The framework refuses to ship the binary protobuf
|
|
39
|
+
* encoding because it requires either a vendored proto runtime or
|
|
40
|
+
* hand-rolled wire format with no compelling benefit at this scope.
|
|
41
|
+
*/
|
|
42
|
+
var C = require("./constants");
|
|
43
|
+
var safeAsync = require("./safe-async");
|
|
44
|
+
var validateOpts = require("./validate-opts");
|
|
45
|
+
var { defineClass } = require("./framework-error");
|
|
46
|
+
|
|
47
|
+
var OtelExportError = defineClass("OtelExportError", { alwaysPermanent: false });
|
|
48
|
+
|
|
49
|
+
var DEFAULT_INTERVAL_MS = C.TIME.seconds(15);
|
|
50
|
+
|
|
51
|
+
// OTLP aggregation temporality:
|
|
52
|
+
// 1 = DELTA — counters report deltas since last export
|
|
53
|
+
// 2 = CUMULATIVE — counters report running totals
|
|
54
|
+
// DELTA is what most exporters do for short-lived processes; the
|
|
55
|
+
// receiving backend handles the running sum.
|
|
56
|
+
var TEMPORALITY_DELTA = 1;
|
|
57
|
+
|
|
58
|
+
// ---- attribute encoding ----
|
|
59
|
+
// OTLP attributes are KeyValue with typed `value` fields:
|
|
60
|
+
// { key, value: { stringValue | intValue | doubleValue | boolValue } }
|
|
61
|
+
function _attrsToOtlp(attrs) {
|
|
62
|
+
if (!attrs || typeof attrs !== "object") return [];
|
|
63
|
+
var out = [];
|
|
64
|
+
for (var k in attrs) {
|
|
65
|
+
if (!Object.prototype.hasOwnProperty.call(attrs, k)) continue;
|
|
66
|
+
var v = attrs[k];
|
|
67
|
+
var kv;
|
|
68
|
+
if (typeof v === "string") kv = { stringValue: v };
|
|
69
|
+
else if (typeof v === "number") {
|
|
70
|
+
kv = Number.isInteger(v) ? { intValue: String(v) } : { doubleValue: v };
|
|
71
|
+
}
|
|
72
|
+
else if (typeof v === "boolean") kv = { boolValue: v };
|
|
73
|
+
else if (v == null) continue;
|
|
74
|
+
else kv = { stringValue: String(v) };
|
|
75
|
+
out.push({ key: k, value: kv });
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Stable key per (name, attrs) so tap calls aggregate.
|
|
81
|
+
function _bucketKey(name, attrs) {
|
|
82
|
+
if (!attrs) return name + "|";
|
|
83
|
+
var ks = Object.keys(attrs).sort();
|
|
84
|
+
var parts = [name];
|
|
85
|
+
for (var i = 0; i < ks.length; i++) parts.push(ks[i] + "=" + String(attrs[ks[i]]));
|
|
86
|
+
return parts.join("|");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function create(opts) {
|
|
90
|
+
opts = opts || {};
|
|
91
|
+
validateOpts(opts, [
|
|
92
|
+
"endpoint", "headers", "serviceName", "intervalMs",
|
|
93
|
+
"httpClient", "resourceAttributes", "scope",
|
|
94
|
+
], "otelExport.create");
|
|
95
|
+
if (typeof opts.endpoint !== "string" || opts.endpoint.length === 0) {
|
|
96
|
+
throw new OtelExportError("otel-export/bad-endpoint",
|
|
97
|
+
"create: endpoint must be a non-empty URL");
|
|
98
|
+
}
|
|
99
|
+
if (typeof opts.serviceName !== "string" || opts.serviceName.length === 0) {
|
|
100
|
+
throw new OtelExportError("otel-export/bad-service-name",
|
|
101
|
+
"create: serviceName must be a non-empty string");
|
|
102
|
+
}
|
|
103
|
+
var endpoint = opts.endpoint;
|
|
104
|
+
var serviceName = opts.serviceName;
|
|
105
|
+
var headers = opts.headers || {};
|
|
106
|
+
var intervalMs = opts.intervalMs != null ? opts.intervalMs : DEFAULT_INTERVAL_MS;
|
|
107
|
+
if (typeof intervalMs !== "number" || !isFinite(intervalMs) || intervalMs < 0) {
|
|
108
|
+
throw new OtelExportError("otel-export/bad-interval",
|
|
109
|
+
"create: intervalMs must be a non-negative finite number");
|
|
110
|
+
}
|
|
111
|
+
var httpClient = opts.httpClient || require("./http-client");
|
|
112
|
+
var scopeName = (opts.scope && opts.scope.name) || "blamejs";
|
|
113
|
+
var scopeVersion = (opts.scope && opts.scope.version) || "0.5.x";
|
|
114
|
+
var resourceAttrs = Object.assign({ "service.name": serviceName },
|
|
115
|
+
opts.resourceAttributes || {});
|
|
116
|
+
|
|
117
|
+
// Buckets: counters and observations keyed by (name, sorted-attrs).
|
|
118
|
+
var counters = new Map(); // bucketKey → { name, attrs, value, startUnixNano }
|
|
119
|
+
var observations = new Map(); // bucketKey → { name, attrs, sum, count, min, max, startUnixNano }
|
|
120
|
+
var startUnixNano = String(Date.now() * 1e6);
|
|
121
|
+
var loop = null;
|
|
122
|
+
var closed = false;
|
|
123
|
+
|
|
124
|
+
function recordCounter(name, value, attrs) {
|
|
125
|
+
if (closed) return;
|
|
126
|
+
if (typeof name !== "string" || name.length === 0) return;
|
|
127
|
+
var v = typeof value === "number" && isFinite(value) ? value : 1;
|
|
128
|
+
var key = _bucketKey(name, attrs);
|
|
129
|
+
var b = counters.get(key);
|
|
130
|
+
if (!b) {
|
|
131
|
+
b = { name: name, attrs: attrs || {}, value: 0, startUnixNano: startUnixNano };
|
|
132
|
+
counters.set(key, b);
|
|
133
|
+
}
|
|
134
|
+
b.value += v;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function recordObservation(name, value, attrs) {
|
|
138
|
+
if (closed) return;
|
|
139
|
+
if (typeof name !== "string" || name.length === 0) return;
|
|
140
|
+
if (typeof value !== "number" || !isFinite(value)) return;
|
|
141
|
+
var key = _bucketKey(name, attrs);
|
|
142
|
+
var b = observations.get(key);
|
|
143
|
+
if (!b) {
|
|
144
|
+
b = { name: name, attrs: attrs || {}, sum: 0, count: 0, min: value, max: value, startUnixNano: startUnixNano };
|
|
145
|
+
observations.set(key, b);
|
|
146
|
+
}
|
|
147
|
+
b.sum += value;
|
|
148
|
+
b.count += 1;
|
|
149
|
+
if (value < b.min) b.min = value;
|
|
150
|
+
if (value > b.max) b.max = value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Operators wire this as the observability tap. event(name, value, labels)
|
|
154
|
+
// → recordCounter for value=1 fire-and-forget shapes.
|
|
155
|
+
function tapHandler(name, value, labels) {
|
|
156
|
+
recordCounter(name, value, labels);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function _drainAndEncode() {
|
|
160
|
+
var nowUnixNano = String(Date.now() * 1e6);
|
|
161
|
+
var metrics = [];
|
|
162
|
+
var c, o;
|
|
163
|
+
|
|
164
|
+
counters.forEach(function (entry) {
|
|
165
|
+
metrics.push({
|
|
166
|
+
name: entry.name,
|
|
167
|
+
sum: {
|
|
168
|
+
dataPoints: [{
|
|
169
|
+
attributes: _attrsToOtlp(entry.attrs),
|
|
170
|
+
startTimeUnixNano: entry.startUnixNano,
|
|
171
|
+
timeUnixNano: nowUnixNano,
|
|
172
|
+
asDouble: entry.value,
|
|
173
|
+
}],
|
|
174
|
+
aggregationTemporality: TEMPORALITY_DELTA,
|
|
175
|
+
isMonotonic: true,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
void c;
|
|
180
|
+
observations.forEach(function (entry) {
|
|
181
|
+
metrics.push({
|
|
182
|
+
name: entry.name,
|
|
183
|
+
summary: {
|
|
184
|
+
dataPoints: [{
|
|
185
|
+
attributes: _attrsToOtlp(entry.attrs),
|
|
186
|
+
startTimeUnixNano: entry.startUnixNano,
|
|
187
|
+
timeUnixNano: nowUnixNano,
|
|
188
|
+
count: String(entry.count),
|
|
189
|
+
sum: entry.sum,
|
|
190
|
+
quantileValues: [
|
|
191
|
+
{ quantile: 0, value: entry.min },
|
|
192
|
+
{ quantile: 1, value: entry.max },
|
|
193
|
+
],
|
|
194
|
+
}],
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
void o;
|
|
199
|
+
|
|
200
|
+
// Reset buckets (DELTA temporality — each export is the delta).
|
|
201
|
+
counters.clear();
|
|
202
|
+
observations.clear();
|
|
203
|
+
startUnixNano = nowUnixNano;
|
|
204
|
+
if (metrics.length === 0) return null;
|
|
205
|
+
return {
|
|
206
|
+
resourceMetrics: [{
|
|
207
|
+
resource: { attributes: _attrsToOtlp(resourceAttrs) },
|
|
208
|
+
scopeMetrics: [{
|
|
209
|
+
scope: { name: scopeName, version: scopeVersion },
|
|
210
|
+
metrics: metrics,
|
|
211
|
+
}],
|
|
212
|
+
}],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function flush() {
|
|
217
|
+
var payload = _drainAndEncode();
|
|
218
|
+
if (!payload) return { sent: false, reason: "no-data" };
|
|
219
|
+
var body = JSON.stringify(payload);
|
|
220
|
+
try {
|
|
221
|
+
var res = await httpClient.request({
|
|
222
|
+
method: "POST",
|
|
223
|
+
url: endpoint,
|
|
224
|
+
headers: Object.assign({ "Content-Type": "application/json" }, headers),
|
|
225
|
+
body: body,
|
|
226
|
+
});
|
|
227
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
228
|
+
throw new OtelExportError("otel-export/upstream-rejected",
|
|
229
|
+
"OTLP endpoint returned " + res.statusCode);
|
|
230
|
+
}
|
|
231
|
+
return { sent: true, statusCode: res.statusCode, bodyLength: body.length };
|
|
232
|
+
} catch (e) {
|
|
233
|
+
if (e && e.isOtelExportError) throw e;
|
|
234
|
+
throw new OtelExportError("otel-export/send-failed",
|
|
235
|
+
"OTLP send failed: " + ((e && e.message) || String(e)));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (intervalMs > 0) {
|
|
240
|
+
loop = safeAsync.flushLoop(flush, intervalMs, { name: "otel-flush" });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function close() {
|
|
244
|
+
if (closed) return;
|
|
245
|
+
closed = true;
|
|
246
|
+
if (loop) { loop.stop(); loop = null; }
|
|
247
|
+
return flush().catch(function (_e) { /* close path swallows final-flush errors */ });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
recordCounter: recordCounter,
|
|
252
|
+
recordObservation: recordObservation,
|
|
253
|
+
tapHandler: tapHandler,
|
|
254
|
+
flush: flush,
|
|
255
|
+
close: close,
|
|
256
|
+
get bufferedCounters() { return counters.size; },
|
|
257
|
+
get bufferedObservations() { return observations.size; },
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
create: create,
|
|
263
|
+
OtelExportError: OtelExportError,
|
|
264
|
+
// Test-only encoders for unit-testing the OTLP shape without an HTTP client.
|
|
265
|
+
_attrsToOtlpForTest: _attrsToOtlp,
|
|
266
|
+
_bucketKeyForTest: _bucketKey,
|
|
267
|
+
};
|
package/lib/parsers/index.js
CHANGED
|
@@ -7,9 +7,6 @@
|
|
|
7
7
|
* xml — RFC-compliant subset; XXE / DOCTYPE / billion-laughs blocked
|
|
8
8
|
* by default; depth + element + attribute count limits;
|
|
9
9
|
* numeric-character-ref bounds checked
|
|
10
|
-
* csv — RFC 4180 parsing + writer with formula-injection prevention
|
|
11
|
-
* (cells starting with =/+/-/@/tab/CR get a single-quote
|
|
12
|
-
* prefix on stringify so Excel doesn't execute them)
|
|
13
10
|
* toml — TOML 1.0 parsing with depth + size limits;
|
|
14
11
|
* prototype-pollution rejection on dotted-key path segments;
|
|
15
12
|
* strict same-key redefinition (silent overwrite would mask
|
|
@@ -49,15 +46,14 @@
|
|
|
49
46
|
*
|
|
50
47
|
* Public API:
|
|
51
48
|
* parsers.xml.parse(input, opts?) → object
|
|
52
|
-
*
|
|
53
|
-
*
|
|
49
|
+
*
|
|
50
|
+
* (CSV moved to top-level `b.csv` in v0.5.17 — same surface unified.)
|
|
54
51
|
*
|
|
55
52
|
* Error types: each parser exports its own *SafeError class with .code
|
|
56
|
-
* matching the format (xml/...,
|
|
53
|
+
* matching the format (xml/..., toml/...).
|
|
57
54
|
*/
|
|
58
55
|
module.exports = {
|
|
59
56
|
xml: require("./safe-xml"),
|
|
60
|
-
csv: require("./safe-csv"),
|
|
61
57
|
toml: require("./safe-toml"),
|
|
62
58
|
yaml: require("./safe-yaml"),
|
|
63
59
|
env: require("./safe-env"),
|
package/lib/queue.js
CHANGED
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
* failed (status='failed')
|
|
32
32
|
*/
|
|
33
33
|
var C = require("./constants");
|
|
34
|
+
var crypto = require("./crypto");
|
|
34
35
|
var lazyRequire = require("./lazy-require");
|
|
35
|
-
var nodeCrypto = require("node:crypto");
|
|
36
36
|
var observability = require("./observability");
|
|
37
37
|
var protocolDispatcher = require("./protocol-dispatcher");
|
|
38
38
|
var localProto = require("./queue-local");
|
|
@@ -125,14 +125,13 @@ function init(opts) {
|
|
|
125
125
|
|
|
126
126
|
// Sweep expired leases periodically (every 30s) so crashed-handler jobs
|
|
127
127
|
// get re-pended.
|
|
128
|
-
sweepTimer =
|
|
128
|
+
sweepTimer = safeAsync.repeating(function () {
|
|
129
129
|
Object.keys(backends).forEach(function (n) {
|
|
130
130
|
if (backends[n].sweepExpired) {
|
|
131
131
|
backends[n].sweepExpired().catch(function () { /* best effort */ });
|
|
132
132
|
}
|
|
133
133
|
});
|
|
134
|
-
},
|
|
135
|
-
sweepTimer.unref();
|
|
134
|
+
}, C.TIME.seconds(30), { name: "queue-sweep" });
|
|
136
135
|
|
|
137
136
|
initialized = true;
|
|
138
137
|
}
|
|
@@ -466,7 +465,7 @@ async function shutdown(opts) {
|
|
|
466
465
|
await safeAsync.sleep(50);
|
|
467
466
|
}
|
|
468
467
|
consumers = [];
|
|
469
|
-
if (sweepTimer) {
|
|
468
|
+
if (sweepTimer) { sweepTimer.stop(); sweepTimer = null; }
|
|
470
469
|
}
|
|
471
470
|
|
|
472
471
|
function listBackends() {
|
|
@@ -485,7 +484,7 @@ function _requireInit() {
|
|
|
485
484
|
}
|
|
486
485
|
|
|
487
486
|
function _resetForTest() {
|
|
488
|
-
if (sweepTimer) {
|
|
487
|
+
if (sweepTimer) { sweepTimer.stop(); sweepTimer = null; }
|
|
489
488
|
consumers.forEach(function (c) { c.cancel(); });
|
|
490
489
|
consumers = [];
|
|
491
490
|
backends = {};
|
|
@@ -571,7 +570,7 @@ function enqueueFlow(spec) {
|
|
|
571
570
|
return Promise.reject(e);
|
|
572
571
|
}
|
|
573
572
|
|
|
574
|
-
var flowId = "flow-" +
|
|
573
|
+
var flowId = "flow-" + crypto.generateToken(8);
|
|
575
574
|
|
|
576
575
|
return observability.tap("queue.enqueueFlow",
|
|
577
576
|
{ queueName: spec.queueName, flowId: flowId, childCount: spec.children.length },
|
package/lib/request-helpers.js
CHANGED
|
@@ -136,7 +136,7 @@ function clientIp(req, opts) {
|
|
|
136
136
|
if (trust && req.headers) {
|
|
137
137
|
var xff = req.headers["x-forwarded-for"];
|
|
138
138
|
if (xff) {
|
|
139
|
-
var hops =
|
|
139
|
+
var hops = parseListHeader(xff);
|
|
140
140
|
if (trust === true) return hops[0];
|
|
141
141
|
if (typeof trust === "number" && trust >= 1 && hops.length >= trust) {
|
|
142
142
|
return hops[hops.length - trust];
|
|
@@ -154,7 +154,8 @@ function requestProtocol(req, opts) {
|
|
|
154
154
|
if (trust && req.headers) {
|
|
155
155
|
var fwd = req.headers["x-forwarded-proto"];
|
|
156
156
|
if (typeof fwd === "string" && fwd.length > 0) {
|
|
157
|
-
|
|
157
|
+
var hops = parseListHeader(fwd, { lowercase: true });
|
|
158
|
+
if (hops.length > 0) return hops[0];
|
|
158
159
|
}
|
|
159
160
|
}
|
|
160
161
|
if (req.socket && req.socket.encrypted) return "https";
|
|
@@ -162,6 +163,36 @@ function requestProtocol(req, opts) {
|
|
|
162
163
|
return "http";
|
|
163
164
|
}
|
|
164
165
|
|
|
166
|
+
// parseListHeader — split a comma-separated header / opt value into a
|
|
167
|
+
// list of trimmed non-empty tokens. Replaces the
|
|
168
|
+
// `String(x).split(",").map(s => s.trim()).filter(Boolean)` chain that
|
|
169
|
+
// was duplicated across cors / compression / scheduler / webhook /
|
|
170
|
+
// websocket / db-schema / cli before v0.5.17.
|
|
171
|
+
//
|
|
172
|
+
// parseListHeader("a, b , ,c") → ["a", "b", "c"]
|
|
173
|
+
// parseListHeader("Foo, Bar", { lowercase: true })
|
|
174
|
+
// → ["foo", "bar"]
|
|
175
|
+
// parseListHeader(undefined) → []
|
|
176
|
+
// parseListHeader("") → []
|
|
177
|
+
//
|
|
178
|
+
// Tier-C: tolerant of non-string input (returns []) — these are read
|
|
179
|
+
// from request headers that the network might omit. Callers needing
|
|
180
|
+
// stricter checks layer their own validation on the result.
|
|
181
|
+
function parseListHeader(value, opts) {
|
|
182
|
+
if (value == null) return [];
|
|
183
|
+
opts = opts || {};
|
|
184
|
+
var s = typeof value === "string" ? value : String(value);
|
|
185
|
+
if (s.length === 0) return [];
|
|
186
|
+
var parts = s.split(",");
|
|
187
|
+
var out = [];
|
|
188
|
+
for (var i = 0; i < parts.length; i++) {
|
|
189
|
+
var t = parts[i].trim();
|
|
190
|
+
if (t.length === 0) continue;
|
|
191
|
+
out.push(opts.lowercase ? t.toLowerCase() : t);
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
165
196
|
// Append a token to a `Vary` response header without dropping prior
|
|
166
197
|
// values (compression middleware sets `Vary: Accept-Encoding`, an
|
|
167
198
|
// auth helper might set `Vary: Authorization`, etc.). Idempotent —
|
|
@@ -170,7 +201,7 @@ function appendVary(res, value) {
|
|
|
170
201
|
if (!res || typeof res.getHeader !== "function" || typeof res.setHeader !== "function") return;
|
|
171
202
|
var existing = res.getHeader("Vary");
|
|
172
203
|
if (existing == null || existing === "") { res.setHeader("Vary", value); return; }
|
|
173
|
-
var tokens =
|
|
204
|
+
var tokens = parseListHeader(existing);
|
|
174
205
|
var lower = value.toLowerCase();
|
|
175
206
|
for (var i = 0; i < tokens.length; i++) if (tokens[i].toLowerCase() === lower) return;
|
|
176
207
|
tokens.push(value);
|
|
@@ -266,6 +297,7 @@ module.exports = {
|
|
|
266
297
|
extractActorContext: extractActorContext,
|
|
267
298
|
resolveActorWithOverride: resolveActorWithOverride,
|
|
268
299
|
parseQualityList: parseQualityList,
|
|
300
|
+
parseListHeader: parseListHeader,
|
|
269
301
|
// v0.5.3 — proxy-trust primitives (default refuses forwarded headers)
|
|
270
302
|
clientIp: clientIp,
|
|
271
303
|
requestProtocol: requestProtocol,
|