@blamejs/core 0.9.45 → 0.9.49
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 +16 -0
- package/index.js +9 -0
- package/lib/auth/fal.js +1 -1
- package/lib/guard-imap-command.js +335 -0
- package/lib/guard-smtp-command.js +65 -10
- package/lib/mail-server-imap.js +1064 -0
- package/lib/mail-server-mx.js +856 -0
- package/lib/mail-server-rate-limit.js +256 -0
- package/lib/mail-server-submission.js +986 -0
- package/lib/metrics.js +50 -7
- package/lib/middleware/protected-resource-metadata.js +1 -1
- package/lib/safe-smtp.js +128 -0
- package/lib/self-update.js +35 -4
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/metrics.js
CHANGED
|
@@ -853,18 +853,43 @@ function snapshotRead(p) {
|
|
|
853
853
|
* Format a snapshot object for human or machine consumption.
|
|
854
854
|
*
|
|
855
855
|
* format: "text" — operator-readable lines, one field per row (default)
|
|
856
|
-
* format: "prometheus" — Prometheus 0.0.4 text format
|
|
857
|
-
*
|
|
858
|
-
*
|
|
856
|
+
* format: "prometheus" — Prometheus 0.0.4 text format
|
|
857
|
+
*
|
|
858
|
+
* ## Type detection (`prometheus` format only)
|
|
859
|
+
*
|
|
860
|
+
* Per Prometheus naming convention + OpenMetrics 1.0.0 §6.2, counter
|
|
861
|
+
* metric families MUST carry the `_total` suffix; every other numeric
|
|
862
|
+
* field renders as a gauge. The renderer auto-detects by suffix:
|
|
863
|
+
*
|
|
864
|
+
* - field name ends in `_total` → `# TYPE <name> counter`
|
|
865
|
+
* - everything else → `# TYPE <name> gauge`
|
|
866
|
+
*
|
|
867
|
+
* Operators with metrics that don't fit the convention (e.g. a counter
|
|
868
|
+
* named `bytes_sent` without the `_total` suffix, or a gauge that
|
|
869
|
+
* happens to end in `_total`) opt the right type via `opts.fieldTypes`:
|
|
870
|
+
*
|
|
871
|
+
* render(snap, { format: "prometheus", fieldTypes: {
|
|
872
|
+
* bytes_sent: "counter", // override default gauge
|
|
873
|
+
* ratio_total: "gauge", // override default counter
|
|
874
|
+
* }});
|
|
875
|
+
*
|
|
876
|
+
* Pre-v0.9.47 every field rendered as gauge regardless of name, which
|
|
877
|
+
* broke `rate()` queries against counter-shaped series. Operators
|
|
878
|
+
* scraping a long-running deployment will see `rate(*_total[5m])`
|
|
879
|
+
* queries start returning the right answer once the new types reach
|
|
880
|
+
* the scrape target.
|
|
859
881
|
*
|
|
860
882
|
* @opts
|
|
861
|
-
* format:
|
|
862
|
-
* prefix:
|
|
883
|
+
* format: "text" | "prometheus", // default: "text"
|
|
884
|
+
* prefix: string, // prometheus-only; default: "blamejs"
|
|
885
|
+
* fieldTypes: Object, // prometheus-only; per-field type override
|
|
886
|
+
* // map. Values: "counter" | "gauge".
|
|
863
887
|
*
|
|
864
888
|
* @example
|
|
865
889
|
* var snap = b.metrics.snapshot.read("/run/blamejs/metrics.json");
|
|
866
890
|
* process.stdout.write(b.metrics.snapshot.render(snap));
|
|
867
|
-
* // or for Prometheus scraping
|
|
891
|
+
* // or for Prometheus scraping (auto-detects http_requests_total
|
|
892
|
+
* // as a counter via the _total suffix):
|
|
868
893
|
* res.setHeader("Content-Type", "text/plain; version=0.0.4");
|
|
869
894
|
* res.end(b.metrics.snapshot.render(snap, { format: "prometheus", prefix: "myapp" }));
|
|
870
895
|
*/
|
|
@@ -899,6 +924,11 @@ function snapshotRender(snap, opts) {
|
|
|
899
924
|
throw new MetricsError("metrics-snapshot/bad-prefix",
|
|
900
925
|
"metrics.snapshot.render: prometheus prefix must match [a-zA-Z_][a-zA-Z0-9_]*, got '" + prefix + "'");
|
|
901
926
|
}
|
|
927
|
+
var fieldTypes = opts.fieldTypes || {};
|
|
928
|
+
if (typeof fieldTypes !== "object" || fieldTypes === null || Array.isArray(fieldTypes)) {
|
|
929
|
+
throw new MetricsError("metrics-snapshot/bad-field-types",
|
|
930
|
+
"metrics.snapshot.render: opts.fieldTypes must be an object mapping field-name → 'counter' | 'gauge'");
|
|
931
|
+
}
|
|
902
932
|
var out = [];
|
|
903
933
|
// allow:bare-canonicalize-walk — sort is for stable Prometheus
|
|
904
934
|
// exposition output ordering, not canonicalize-for-hashing
|
|
@@ -909,7 +939,20 @@ function snapshotRender(snap, opts) {
|
|
|
909
939
|
if (typeof v2 !== "number" || !isFinite(v2)) continue; // only numeric scalars
|
|
910
940
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(k2)) continue; // skip prom-incompatible names
|
|
911
941
|
var metric = prefix + "_" + k2;
|
|
912
|
-
|
|
942
|
+
var declared = fieldTypes[k2];
|
|
943
|
+
var fieldType;
|
|
944
|
+
if (declared !== undefined) {
|
|
945
|
+
if (declared !== "counter" && declared !== "gauge") {
|
|
946
|
+
throw new MetricsError("metrics-snapshot/bad-field-type",
|
|
947
|
+
"metrics.snapshot.render: opts.fieldTypes." + k2 + " must be 'counter' or 'gauge', got '" + declared + "'");
|
|
948
|
+
}
|
|
949
|
+
fieldType = declared;
|
|
950
|
+
} else {
|
|
951
|
+
// Prometheus naming convention + OpenMetrics 1.0.0 §6.2:
|
|
952
|
+
// counter family names carry the _total suffix.
|
|
953
|
+
fieldType = /_total$/.test(k2) ? "counter" : "gauge";
|
|
954
|
+
}
|
|
955
|
+
out.push("# TYPE " + metric + " " + fieldType);
|
|
913
956
|
out.push(metric + " " + v2);
|
|
914
957
|
}
|
|
915
958
|
return out.join("\n") + "\n";
|
package/lib/safe-smtp.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.safeSmtp
|
|
4
|
+
* @nav Parsers
|
|
5
|
+
* @title Safe SMTP
|
|
6
|
+
* @order 215
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Wire-protocol parsing helpers for SMTP (RFC 5321) bytes.
|
|
10
|
+
* Operators consuming the framework's MX listener (`b.mail.server.mx`),
|
|
11
|
+
* submission listener (slice that follows), or building their own
|
|
12
|
+
* SMTP-shaped tooling (proxies, log analyzers, test fixtures) reach
|
|
13
|
+
* for these primitives rather than reinventing the dot-terminator
|
|
14
|
+
* scan + dot-stuffing reversal.
|
|
15
|
+
*
|
|
16
|
+
* Separates the "what shape is the wire data" parsing concern from
|
|
17
|
+
* the "is this wire data hostile" guard concern (which lives in
|
|
18
|
+
* `b.guardSmtpCommand`). A safe-* parser primitive returns a
|
|
19
|
+
* bounded shape or `-1`; a guard-* primitive returns a boolean
|
|
20
|
+
* threat verdict or throws a typed error.
|
|
21
|
+
*
|
|
22
|
+
* Wire-protocol references:
|
|
23
|
+
* - RFC 5321 §2.3.8 — line termination MUST be CRLF
|
|
24
|
+
* - RFC 5321 §4.5.2 — dot-stuffing on the SMTP body
|
|
25
|
+
* - RFC 5321 §4.1.1.4 — DATA command terminates with `<CRLF>.<CRLF>`
|
|
26
|
+
* - CVE-2023-51764 / -51765 / -51766 / 2024-32178 — SMTP
|
|
27
|
+
* smuggling (parsers that accept bare-LF dot-terminators).
|
|
28
|
+
* The guard primitive `b.guardSmtpCommand.detectBodySmuggling`
|
|
29
|
+
* owns smuggling detection; the safe-* terminator scanner
|
|
30
|
+
* here is strict CRLF-only by construction.
|
|
31
|
+
*
|
|
32
|
+
* @card
|
|
33
|
+
* Wire-protocol parsing helpers for SMTP (RFC 5321) bytes —
|
|
34
|
+
* findDotTerminator + dotUnstuff. Strict CRLF-only by construction
|
|
35
|
+
* (bare-LF terminators are not honored — the smuggling-detection
|
|
36
|
+
* guard lives in b.guardSmtpCommand.detectBodySmuggling).
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
var { defineClass } = require("./framework-error");
|
|
40
|
+
|
|
41
|
+
var SafeSmtpError = defineClass("SafeSmtpError", { alwaysPermanent: true });
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @primitive b.safeSmtp.findDotTerminator
|
|
45
|
+
* @signature b.safeSmtp.findDotTerminator(buf)
|
|
46
|
+
* @since 0.9.46
|
|
47
|
+
* @status stable
|
|
48
|
+
* @related b.safeSmtp.dotUnstuff, b.guardSmtpCommand.detectBodySmuggling
|
|
49
|
+
*
|
|
50
|
+
* Scan `buf` for the canonical RFC 5321 §4.1.1.4 DATA-body terminator
|
|
51
|
+
* `<CRLF>.<CRLF>` (5 bytes: 0x0d 0x0a 0x2e 0x0d 0x0a). Returns the
|
|
52
|
+
* byte index where the body ends (exclusive — the index of the
|
|
53
|
+
* trailing CRLF the terminator starts on), or `-1` if the terminator
|
|
54
|
+
* is not yet present.
|
|
55
|
+
*
|
|
56
|
+
* Strict CRLF-only by construction — bare-LF alternate terminators
|
|
57
|
+
* are NOT honored. Operators worried about smuggling shape route the
|
|
58
|
+
* SAME body through `b.guardSmtpCommand.detectBodySmuggling` before
|
|
59
|
+
* trusting the terminator index returned here.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* var body = Buffer.from("Hello world.\r\n.\r\n");
|
|
63
|
+
* b.safeSmtp.findDotTerminator(body);
|
|
64
|
+
* // → 13 (index of \r in \r\n.\r\n)
|
|
65
|
+
*
|
|
66
|
+
* b.safeSmtp.findDotTerminator(Buffer.from("incomplete body"));
|
|
67
|
+
* // → -1
|
|
68
|
+
*/
|
|
69
|
+
function findDotTerminator(buf) {
|
|
70
|
+
if (!Buffer.isBuffer(buf)) {
|
|
71
|
+
throw new SafeSmtpError("safe-smtp/bad-input",
|
|
72
|
+
"findDotTerminator: input must be a Buffer");
|
|
73
|
+
}
|
|
74
|
+
for (var i = 0; i <= buf.length - 5; i += 1) { // allow:raw-byte-literal — 5-byte CRLF.CRLF terminator length
|
|
75
|
+
if (buf[i] === 0x0d && buf[i + 1] === 0x0a &&
|
|
76
|
+
buf[i + 2] === 0x2e &&
|
|
77
|
+
buf[i + 3] === 0x0d && buf[i + 4] === 0x0a) {
|
|
78
|
+
return i;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return -1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @primitive b.safeSmtp.dotUnstuff
|
|
86
|
+
* @signature b.safeSmtp.dotUnstuff(buf)
|
|
87
|
+
* @since 0.9.46
|
|
88
|
+
* @status stable
|
|
89
|
+
* @related b.safeSmtp.findDotTerminator
|
|
90
|
+
*
|
|
91
|
+
* Reverse RFC 5321 §4.5.2 dot-stuffing on a DATA-body buffer. SMTP
|
|
92
|
+
* senders that need to transmit a body line beginning with `.` MUST
|
|
93
|
+
* prepend an extra `.` (so the line on the wire begins with `..`);
|
|
94
|
+
* the receiver strips the leading `.` from any body line that
|
|
95
|
+
* begins with one before storing the message. Returns a fresh
|
|
96
|
+
* Buffer with the dots reversed; the input is never mutated. Result
|
|
97
|
+
* length is always `<= input length`.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* var wire = Buffer.from("hello\r\n..secret\r\nworld\r\n");
|
|
101
|
+
* b.safeSmtp.dotUnstuff(wire).toString("utf8");
|
|
102
|
+
* // → "hello\r\n.secret\r\nworld\r\n"
|
|
103
|
+
*/
|
|
104
|
+
function dotUnstuff(buf) {
|
|
105
|
+
if (!Buffer.isBuffer(buf)) {
|
|
106
|
+
throw new SafeSmtpError("safe-smtp/bad-input",
|
|
107
|
+
"dotUnstuff: input must be a Buffer");
|
|
108
|
+
}
|
|
109
|
+
var out = Buffer.alloc(buf.length);
|
|
110
|
+
var oi = 0;
|
|
111
|
+
for (var i = 0; i < buf.length; i += 1) {
|
|
112
|
+
out[oi++] = buf[i];
|
|
113
|
+
// After \r\n, if the next byte is `.` followed by another non-CR
|
|
114
|
+
// byte (i.e., not the terminator itself), strip the stuffing dot.
|
|
115
|
+
if (i >= 1 && buf[i - 1] === 0x0d && buf[i] === 0x0a &&
|
|
116
|
+
i + 1 < buf.length && buf[i + 1] === 0x2e &&
|
|
117
|
+
i + 2 < buf.length && buf[i + 2] !== 0x0d) {
|
|
118
|
+
i += 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return out.subarray(0, oi);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
findDotTerminator: findDotTerminator,
|
|
126
|
+
dotUnstuff: dotUnstuff,
|
|
127
|
+
SafeSmtpError: SafeSmtpError,
|
|
128
|
+
};
|
package/lib/self-update.js
CHANGED
|
@@ -86,14 +86,41 @@ function _safeAuditEmit(action, outcome, metadata) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// ---- semver-shaped comparison (tag_name like "v0.7.30" or "0.7.30") ----
|
|
89
|
-
// Strips a leading "v" / "V" then parses dot-separated numeric components.
|
|
90
|
-
// Non-numeric components are compared lexicographically (handles release
|
|
91
|
-
// suffixes like "1.0.0-rc.1" by falling back to string comparison after
|
|
92
|
-
// the matching numeric prefix). Returns -1 / 0 / +1.
|
|
93
89
|
function _normalizeTag(tag) {
|
|
94
90
|
if (typeof tag !== "string") return "";
|
|
95
91
|
return tag.replace(/^v/i, "").trim();
|
|
96
92
|
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @primitive b.selfUpdate.compareTags
|
|
96
|
+
* @signature b.selfUpdate.compareTags(a, b)
|
|
97
|
+
* @since 0.9.47
|
|
98
|
+
* @status stable
|
|
99
|
+
*
|
|
100
|
+
* Compare two release tags / version strings. Returns `-1` if `a < b`,
|
|
101
|
+
* `+1` if `a > b`, `0` if equal. Strips a leading `v` / `V`, then walks
|
|
102
|
+
* dot-separated components: numeric pairs compared numerically; any
|
|
103
|
+
* non-numeric component (release suffixes like `1.0.0-rc.1`) falls back
|
|
104
|
+
* to lexicographic compare on that component. Missing components on
|
|
105
|
+
* either side are treated as `"0"`.
|
|
106
|
+
*
|
|
107
|
+
* Shape follows SemVer 2.0.0 §11 precedence rules for the numeric prefix.
|
|
108
|
+
* Deviations from the full SemVer §11 spec — pre-release identifiers
|
|
109
|
+
* (`-rc.1` < release) are compared lexicographically rather than the
|
|
110
|
+
* SemVer-mandated "alphanumeric identifiers compared as numbers if all
|
|
111
|
+
* numeric" rule. For most version-shaped strings the result is identical;
|
|
112
|
+
* exotic pre-release shapes (`1.0.0-alpha.10` vs `1.0.0-alpha.9`) sort
|
|
113
|
+
* lexicographically here (`10` < `9` as strings) rather than numerically.
|
|
114
|
+
* Operators with strict SemVer §11 needs should use a dedicated SemVer
|
|
115
|
+
* parser; this primitive targets the common framework-update polling
|
|
116
|
+
* shape (`v0.9.46` vs `v0.9.47`) where pre-release tags are rare.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* b.selfUpdate.compareTags("v0.9.46", "v0.9.47"); // → -1
|
|
120
|
+
* b.selfUpdate.compareTags("v0.9.47", "0.9.47"); // → 0 (leading "v" stripped)
|
|
121
|
+
* b.selfUpdate.compareTags("1.10.0", "1.9.0"); // → +1 (numeric, not lex)
|
|
122
|
+
* b.selfUpdate.compareTags("v0.7.30", "v0.7.30"); // → 0
|
|
123
|
+
*/
|
|
97
124
|
function _compareTags(a, b) {
|
|
98
125
|
var na = _normalizeTag(a);
|
|
99
126
|
var nb2 = _normalizeTag(b);
|
|
@@ -648,6 +675,10 @@ module.exports = {
|
|
|
648
675
|
SelfUpdateError: SelfUpdateError,
|
|
649
676
|
ALLOWED_HASH_ALGS: ALLOWED_HASH_ALGS,
|
|
650
677
|
DEFAULT_HASH_ALG: DEFAULT_HASH_ALG,
|
|
678
|
+
// Public surface — same impl as the internal `_compareTags`;
|
|
679
|
+
// downstream consumers replacing one-off compareVersions helpers
|
|
680
|
+
// call this.
|
|
681
|
+
compareTags: _compareTags,
|
|
651
682
|
// Internal — exposed for the layer-0 test suite only.
|
|
652
683
|
_compareTags: _compareTags,
|
|
653
684
|
};
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.6",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:21323cdb-5bfe-4b88-a63f-906795a497e6",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-16T03:34:31.380Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.9.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.9.49",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.9.
|
|
25
|
+
"version": "0.9.49",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.9.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.9.49",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.9.
|
|
57
|
+
"ref": "@blamejs/core@0.9.49",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|