@blamejs/core 0.10.6 → 0.10.8

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/lib/safe-mime.js CHANGED
@@ -53,7 +53,15 @@ var DEFAULT_MAX_PARTS = 64; // allow:raw-byte-l
53
53
  var DEFAULT_MAX_NESTING_DEPTH = 16;
54
54
  var DEFAULT_MAX_BOUNDARY = 70; // RFC 2046 §5.1.1
55
55
  var DEFAULT_MAX_HEADER_BYTES = C.BYTES.kib(64);
56
- var DEFAULT_MAX_HEADER_LINE = 998; // allow:raw-byte-literal — RFC 5322 §2.1.1 line cap
56
+ // RFC 5322 §2.1.1 line cap. The spec defines TWO limits: a SHOULD of
57
+ // 78 bytes (the readability target) and a MUST of 998 bytes (the
58
+ // hard ceiling). The 78-byte SHOULD is intentionally NOT enforced
59
+ // here — modern senders routinely emit header lines longer than 78
60
+ // bytes (long URLs in List-Unsubscribe, EAI display names) and a
61
+ // strict 78-byte refusal would reject legitimate mail. We enforce
62
+ // only the 998-byte MUST. Future drift attempting to "fix" this to
63
+ // 78 would be a regression and should fail the audit gate.
64
+ var DEFAULT_MAX_HEADER_LINE = 998; // allow:raw-byte-literal — RFC 5322 §2.1.1 MUST (998); the SHOULD (78) is by design not enforced
57
65
  // Per-message header-count cap. RFC 5322 places no upper bound on
58
66
  // the number of headers in a message; without one, a sender can pack
59
67
  // tens of thousands of one-byte headers into the maxHeaderBytes budget
@@ -77,8 +85,15 @@ var DEFAULT_CHARSETS = Object.freeze([
77
85
  "euc-kr", "euc-jp",
78
86
  ]);
79
87
 
88
+ // RFC 3030 §3 — `binary` CTE on receive REQUIRES the receiving MTA
89
+ // to have advertised BINARYMIME during ESMTP negotiation. Inbound
90
+ // flows without explicit BINARYMIME wiring must refuse `binary`
91
+ // because consumers downstream (DKIM canonicalization, message
92
+ // rewriting) assume CRLF line structure that `binary` doesn't
93
+ // guarantee. Operators that wire BINARYMIME end-to-end opt back in
94
+ // via `transferEncodingAllowlist: ["7bit", ..., "binary"]`.
80
95
  var DEFAULT_TRANSFER_ENCODINGS = Object.freeze([
81
- "7bit", "8bit", "binary", "quoted-printable", "base64",
96
+ "7bit", "8bit", "quoted-printable", "base64",
82
97
  ]);
83
98
 
84
99
  /**
@@ -453,12 +468,18 @@ function _parseHeaders(buf, ctx) {
453
468
  // Refuse NUL, CR, LF, and other C0 control chars in header values.
454
469
  // Tab (0x09) is allowed (header folding). C1 control range
455
470
  // (0x80-0x9F) NOT refused — legitimate non-ASCII via EAI/RFC 2047
456
- // decoded-words can produce bytes in that range.
471
+ // decoded-words can produce bytes in that range. Error metadata
472
+ // surfaces the BYTE offset (via `Buffer.byteLength` on the JS
473
+ // string prefix) rather than the UTF-16 code-unit index, so the
474
+ // operator audit log lines up with the wire-level byte stream
475
+ // they're inspecting.
457
476
  for (var hci = 0; hci < value.length; hci += 1) {
458
477
  var hcc = value.charCodeAt(hci);
459
478
  if ((hcc < 0x20 && hcc !== 0x09) || hcc === 0x7F) { // allow:raw-byte-literal — C0 control char + DEL refusal
479
+ var byteOffset = Buffer.byteLength(value.slice(0, hci), "utf8");
460
480
  throw new SafeMimeError("safe-mime/control-char-in-header",
461
- "safeMime.parse: header '" + name + "' contains control char 0x" + hcc.toString(16));
481
+ "safeMime.parse: header '" + name + "' contains control char 0x" +
482
+ hcc.toString(16) + " at byte offset " + byteOffset); // allow:raw-byte-literal — toString radix 16 hex, not bytes
462
483
  }
463
484
  }
464
485
  value = _decodeRfc2047Words(value);
@@ -673,9 +694,35 @@ function _decodeBufferAs(buf, charset) {
673
694
  if (c === "us-ascii" || c === "ascii") return buf.toString("ascii");
674
695
  if (c === "iso-8859-1" || c === "latin1") return buf.toString("latin1");
675
696
  if (c === "utf-16le") return buf.toString("utf16le");
697
+ if (c === "utf-16be") return _decodeUtf16BE(buf);
698
+ if (c === "utf-16") {
699
+ // RFC 2781 §3.3 — `utf-16` with a leading BOM (FE FF = BE, FF FE
700
+ // = LE). When no BOM is present the spec defaults to BE; Node
701
+ // doesn't speak BE natively so we transcode either way.
702
+ if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) {
703
+ return buf.subarray(2).toString("utf16le");
704
+ }
705
+ if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff) {
706
+ return _decodeUtf16BE(buf.subarray(2));
707
+ }
708
+ return _decodeUtf16BE(buf); // RFC 2781 §3.3 BE default with no BOM
709
+ }
676
710
  return buf.toString("utf8");
677
711
  }
678
712
 
713
+ // utf-16be → utf-16le swap (Node has no direct utf-16be decoder).
714
+ // Byte-pair endian flip into a temporary buffer, then decode as
715
+ // utf-16le. Allocates a single buffer (no per-character churn).
716
+ function _decodeUtf16BE(buf) {
717
+ var n = buf.length & ~1; // allow:raw-byte-literal — pair alignment mask
718
+ var swapped = Buffer.alloc(n);
719
+ for (var i = 0; i < n; i += 2) {
720
+ swapped[i] = buf[i + 1];
721
+ swapped[i + 1] = buf[i];
722
+ }
723
+ return swapped.toString("utf16le");
724
+ }
725
+
679
726
  function _materializeText(part) {
680
727
  return {
681
728
  contentType: part.leaf.contentType,
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.sdNotify
4
+ * @nav Process
5
+ * @title systemd Notify
6
+ *
7
+ * @intro
8
+ * `sd_notify` protocol surface for daemons running under
9
+ * `Type=notify` systemd units. Composes the standard lifecycle
10
+ * messages — `READY=1` on boot, `WATCHDOG=1` on heartbeat,
11
+ * `STOPPING=1` on shutdown, `RELOADING=1` on hot-reload — against
12
+ * the `$NOTIFY_SOCKET` env var systemd populates for the child
13
+ * process.
14
+ *
15
+ * Transport: Node has no unix-DGRAM socket support in its `dgram`
16
+ * module, so the v1 path shells out to `systemd-notify(1)` via
17
+ * `execFile` (NOT `exec` — no shell-string parsing on the
18
+ * message bytes). Operators running under systemd already have
19
+ * `systemd-tools` installed by definition, so the dependency is
20
+ * no expansion of the trust surface.
21
+ *
22
+ * Compose with `b.appShutdown` for the STOPPING signal: register a
23
+ * priority-0 phase that calls `b.sdNotify.stopping()` so systemd
24
+ * sees the shutdown intent before the framework tears anything
25
+ * down. Compose with a periodic `WATCHDOG=1` against the unit's
26
+ * `WatchdogSec=` interval so systemd auto-restarts the daemon if
27
+ * the event loop wedges.
28
+ *
29
+ * When `$NOTIFY_SOCKET` is unset (process running outside systemd
30
+ * — bare invocation, foreground dev, container without
31
+ * `--notify-ready`), every call is a no-op that surfaces a single
32
+ * boot-time audit entry. Operators get observability of the
33
+ * degraded state without per-call log noise.
34
+ *
35
+ * @card
36
+ * sd_notify protocol for systemd Type=notify daemons — READY / WATCHDOG / STOPPING / RELOADING. Composes b.appShutdown for shutdown signaling.
37
+ */
38
+
39
+ var { execFile } = require("node:child_process");
40
+ var C = require("./constants");
41
+ var safeEnv = require("./parsers/safe-env");
42
+ var audit = require("./audit");
43
+ var { defineClass } = require("./framework-error");
44
+
45
+ var SdNotifyError = defineClass("SdNotifyError", { alwaysPermanent: true });
46
+
47
+ // Whitelist of sd_notify state= values we ship as named helpers. The
48
+ // underlying `send({ state })` accepts any string but the helpers are
49
+ // the operator-facing surface — `READY=1` etc. — and the audit log
50
+ // records the named state, not arbitrary payload bytes.
51
+ var KNOWN_STATES = Object.freeze({
52
+ ready: "READY=1",
53
+ stopping: "STOPPING=1",
54
+ reloading: "RELOADING=1",
55
+ watchdog: "WATCHDOG=1",
56
+ });
57
+
58
+ function _notifySocketPath() {
59
+ var p = safeEnv.readVar("NOTIFY_SOCKET");
60
+ if (typeof p !== "string" || p.length === 0) return null;
61
+ // Abstract namespace socket (Linux-only) prefixed with `@` —
62
+ // systemd-notify(1) accepts the same form, so we don't normalize.
63
+ return p;
64
+ }
65
+
66
+ function _runNotify(payload) {
67
+ return new Promise(function (resolve, reject) {
68
+ var args = [];
69
+ var lines = String(payload).split("\n");
70
+ for (var i = 0; i < lines.length; i += 1) {
71
+ if (lines[i].length > 0) args.push(lines[i]);
72
+ }
73
+ if (args.length === 0) { resolve(); return; }
74
+ // execFile (not exec) — no shell evaluation; the message bytes
75
+ // pass through argv exactly. systemd-notify accepts one or more
76
+ // KEY=VALUE arguments. The `--no-block` flag returns immediately
77
+ // without waiting for the notification to be processed.
78
+ execFile("systemd-notify", ["--no-block"].concat(args),
79
+ { timeout: C.TIME.seconds(5), windowsHide: true },
80
+ function (err) {
81
+ if (err) reject(err);
82
+ else resolve();
83
+ });
84
+ });
85
+ }
86
+
87
+ /**
88
+ * @primitive b.sdNotify.send
89
+ * @signature b.sdNotify.send(opts)
90
+ * @since 0.10.8
91
+ * @status stable
92
+ * @related b.sdNotify.ready, b.sdNotify.stopping, b.appShutdown.create
93
+ *
94
+ * Generic sd_notify dispatch. Sends one or more `KEY=VALUE` payload
95
+ * lines to systemd via `systemd-notify(1)`. No-op when
96
+ * `$NOTIFY_SOCKET` is unset (foreground / container without
97
+ * `--notify-ready` / non-systemd init). Returns a Promise resolving
98
+ * on dispatch success.
99
+ *
100
+ * @opts
101
+ * state: string, // e.g. "READY=1" / "STOPPING=1"
102
+ * status: string, // free-form status text → `STATUS=`
103
+ * mainpid: number, // PID override → `MAINPID=`
104
+ * audit: boolean, // default true
105
+ *
106
+ * @example
107
+ * await b.sdNotify.send({ state: "READY=1", status: "Listening on :8080" });
108
+ */
109
+ function send(opts) {
110
+ opts = opts || {};
111
+ var lines = [];
112
+ if (typeof opts.state === "string" && opts.state.length > 0) lines.push(opts.state);
113
+ if (typeof opts.status === "string" && opts.status.length > 0) {
114
+ // STATUS= permits arbitrary UTF-8 except newline — refuse newline
115
+ // so a hostile status string can't smuggle a second key.
116
+ if (opts.status.indexOf("\n") !== -1 || opts.status.indexOf("\r") !== -1) {
117
+ throw new SdNotifyError("sd-notify/control-char-in-status",
118
+ "send: status field must not contain CR/LF (sd_notify framing)");
119
+ }
120
+ lines.push("STATUS=" + opts.status);
121
+ }
122
+ if (opts.mainpid !== undefined) {
123
+ if (typeof opts.mainpid !== "number" || !isFinite(opts.mainpid) ||
124
+ Math.floor(opts.mainpid) !== opts.mainpid || opts.mainpid < 1) {
125
+ throw new SdNotifyError("sd-notify/bad-mainpid",
126
+ "send: mainpid must be a positive integer");
127
+ }
128
+ lines.push("MAINPID=" + opts.mainpid);
129
+ }
130
+ if (lines.length === 0) return Promise.resolve();
131
+
132
+ var socketPath = _notifySocketPath();
133
+ if (socketPath === null) {
134
+ if (opts.audit !== false) {
135
+ try {
136
+ audit.safeEmit({
137
+ action: "sdnotify.send.skipped",
138
+ outcome: "denied",
139
+ metadata: { reason: "no-notify-socket", state: opts.state || null },
140
+ });
141
+ } catch (_e) { /* drop-silent */ }
142
+ }
143
+ return Promise.resolve();
144
+ }
145
+ var auditOn = opts.audit !== false;
146
+ return _runNotify(lines.join("\n")).then(function () {
147
+ if (auditOn) {
148
+ try {
149
+ audit.safeEmit({
150
+ action: "sdnotify.send",
151
+ outcome: "success",
152
+ metadata: { state: opts.state || null, status: opts.status || null },
153
+ });
154
+ } catch (_e) { /* drop-silent */ }
155
+ }
156
+ }).catch(function (err) {
157
+ if (auditOn) {
158
+ try {
159
+ audit.safeEmit({
160
+ action: "sdnotify.send",
161
+ outcome: "failure",
162
+ metadata: { state: opts.state || null, error: (err && err.message) || String(err) },
163
+ });
164
+ } catch (_e2) { /* drop-silent */ }
165
+ }
166
+ throw new SdNotifyError("sd-notify/dispatch-failed",
167
+ "send: systemd-notify dispatch failed: " + ((err && err.message) || String(err)));
168
+ });
169
+ }
170
+
171
+ /**
172
+ * @primitive b.sdNotify.ready
173
+ * @signature b.sdNotify.ready(opts?)
174
+ * @since 0.10.8
175
+ * @status stable
176
+ * @related b.sdNotify.send, b.sdNotify.stopping
177
+ *
178
+ * Send `READY=1` to systemd, signaling boot complete. Use once the
179
+ * listener is bound and the daemon is accepting work.
180
+ *
181
+ * @opts
182
+ * status: string, // free-form status text → STATUS=
183
+ *
184
+ * @opts
185
+ * status: string, // free-form status text → STATUS=
186
+ * audit: boolean, // default true
187
+ *
188
+ * @example
189
+ * await b.sdNotify.ready({ status: "Listening on :8080" });
190
+ */
191
+ function ready(opts) {
192
+ return send(Object.assign({}, opts || {}, { state: KNOWN_STATES.ready }));
193
+ }
194
+
195
+ /**
196
+ * @primitive b.sdNotify.stopping
197
+ * @signature b.sdNotify.stopping(opts?)
198
+ * @since 0.10.8
199
+ * @status stable
200
+ * @related b.sdNotify.send, b.appShutdown.create
201
+ *
202
+ * Send `STOPPING=1`. Operators wire this into `b.appShutdown` as the
203
+ * earliest shutdown phase (priority 0) so systemd sees the shutdown
204
+ * intent before any teardown begins.
205
+ *
206
+ * @opts
207
+ * status: string, // free-form status text → STATUS=
208
+ * audit: boolean, // default true
209
+ *
210
+ * @example
211
+ * b.appShutdown.create({ name: "sd-notify-stopping", priority: 0,
212
+ * run: function () { return b.sdNotify.stopping(); } });
213
+ */
214
+ function stopping(opts) {
215
+ return send(Object.assign({}, opts || {}, { state: KNOWN_STATES.stopping }));
216
+ }
217
+
218
+ /**
219
+ * @primitive b.sdNotify.reloading
220
+ * @signature b.sdNotify.reloading(opts?)
221
+ * @since 0.10.8
222
+ * @status stable
223
+ *
224
+ * Send `RELOADING=1` then (after the reload completes) `READY=1`.
225
+ * Use during hot-config-reload paths; systemd treats the unit as
226
+ * "reloading" until the next `READY=1`.
227
+ *
228
+ * @opts
229
+ * status: string, // free-form status text → STATUS=
230
+ * audit: boolean, // default true
231
+ *
232
+ * @example
233
+ * await b.sdNotify.reloading();
234
+ * await reloadConfig();
235
+ * await b.sdNotify.ready();
236
+ */
237
+ function reloading(opts) {
238
+ return send(Object.assign({}, opts || {}, { state: KNOWN_STATES.reloading }));
239
+ }
240
+
241
+ /**
242
+ * @primitive b.sdNotify.watchdog
243
+ * @signature b.sdNotify.watchdog(opts?)
244
+ * @since 0.10.8
245
+ * @status stable
246
+ *
247
+ * Send `WATCHDOG=1`. Operators with `WatchdogSec=` configured on
248
+ * their unit call this periodically (e.g. every `WatchdogSec/2`)
249
+ * so systemd auto-restarts the daemon when the event loop wedges.
250
+ *
251
+ * @opts
252
+ * audit: boolean, // default true
253
+ *
254
+ * @example
255
+ * setInterval(function () { b.sdNotify.watchdog(); }, 15000);
256
+ */
257
+ function watchdog(opts) {
258
+ return send(Object.assign({}, opts || {}, { state: KNOWN_STATES.watchdog }));
259
+ }
260
+
261
+ module.exports = {
262
+ send: send,
263
+ ready: ready,
264
+ stopping: stopping,
265
+ reloading: reloading,
266
+ watchdog: watchdog,
267
+ isAvailable: function () { return _notifySocketPath() !== null; },
268
+ SdNotifyError: SdNotifyError,
269
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.10.6",
3
+ "version": "0.10.8",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:eeaab12e-3641-448e-8bad-29a2842f802b",
5
+ "serialNumber": "urn:uuid:2db3bd59-b835-4672-aac5-7c874f2f9276",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-17T18:41:38.896Z",
8
+ "timestamp": "2026-05-18T00:17:19.942Z",
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.10.6",
22
+ "bom-ref": "@blamejs/core@0.10.8",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.10.6",
25
+ "version": "0.10.8",
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.10.6",
29
+ "purl": "pkg:npm/%40blamejs/core@0.10.8",
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.10.6",
57
+ "ref": "@blamejs/core@0.10.8",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]