@blamejs/core 0.8.13 → 0.8.16
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 +3 -1
- package/index.js +12 -1
- package/lib/a2a.js +272 -0
- package/lib/ai-input.js +151 -0
- package/lib/audit.js +6 -0
- package/lib/dark-patterns.js +357 -0
- package/lib/framework-error.js +34 -0
- package/lib/graphql-federation.js +176 -0
- package/lib/http-client.js +16 -0
- package/lib/mail-auth.js +33 -10
- package/lib/mail-dkim.js +44 -2
- package/lib/mcp.js +301 -0
- package/lib/middleware/sse.js +18 -20
- package/lib/network-smtp-policy.js +57 -5
- package/lib/network-tls.js +33 -0
- package/lib/request-helpers.js +34 -0
- package/lib/router.js +28 -0
- package/lib/sse.js +349 -0
- package/lib/vault/index.js +4 -0
- package/lib/vault/seal-pem-file.js +283 -0
- package/lib/websocket.js +15 -0
- package/package.json +2 -2
- package/sbom.cyclonedx.json +6 -6
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* vault/seal-pem-file — seal a PEM file at rest with file-watch auto-
|
|
4
|
+
* reseal.
|
|
5
|
+
*
|
|
6
|
+
* Operator workflow this primitive solves: ACME / Let's Encrypt
|
|
7
|
+
* renewals run on a 30-60 day cadence, write fresh certbot output to
|
|
8
|
+
* `/etc/letsencrypt/live/<domain>/privkey.pem`, and signal the
|
|
9
|
+
* application to reload. The fresh PEM lives unencrypted on disk
|
|
10
|
+
* between the renewal write and the next operator-driven re-seal.
|
|
11
|
+
* Auto-reseal closes that window: every renewal writes the plaintext
|
|
12
|
+
* PEM, the framework's watcher sees the mtime change, re-seals on the
|
|
13
|
+
* spot, and the in-process key material rotates without human
|
|
14
|
+
* intervention.
|
|
15
|
+
*
|
|
16
|
+
* Surface:
|
|
17
|
+
*
|
|
18
|
+
* var watcher = b.vault.sealPemFile({
|
|
19
|
+
* source: "/etc/letsencrypt/live/example.com/privkey.pem",
|
|
20
|
+
* destination: "/var/lib/blamejs/server.key.sealed",
|
|
21
|
+
* audit: true, // default
|
|
22
|
+
* pollInterval: b.constants.TIME.seconds(2), // fs.watchFile cadence
|
|
23
|
+
* onResealed: function (info) { ... }, // { srcPath, destPath, bytes,
|
|
24
|
+
* resealedAt, generation }
|
|
25
|
+
* onError: function (err) { ... }, // sealing failed
|
|
26
|
+
* });
|
|
27
|
+
* // watcher.stop()
|
|
28
|
+
* // watcher.generation — monotonically increases per reseal
|
|
29
|
+
* // watcher.lastResealedAt — Unix-ms of most recent successful reseal
|
|
30
|
+
* // watcher.lastError — most recent failure, or null
|
|
31
|
+
*
|
|
32
|
+
* Crash-safe write protocol:
|
|
33
|
+
*
|
|
34
|
+
* 1. Write `<destination>.tmp` with mode 0o600, fsync.
|
|
35
|
+
* 2. Create `<destination>.rewriting` marker (operator-visible).
|
|
36
|
+
* 3. Rename `<destination>.tmp` → `<destination>` (atomic on POSIX).
|
|
37
|
+
* 4. Remove `<destination>.rewriting` marker.
|
|
38
|
+
*
|
|
39
|
+
* If the framework crashes between steps 2 and 4, the marker remains
|
|
40
|
+
* on disk and the next sealPemFile() call detects it. Recovery: the
|
|
41
|
+
* sealedPath is either complete (rename happened) or still .tmp
|
|
42
|
+
* (rename did not happen). The recovery routine re-runs the seal from
|
|
43
|
+
* source — idempotent because the source PEM is the source of truth.
|
|
44
|
+
*
|
|
45
|
+
* fs.watchFile semantics:
|
|
46
|
+
*
|
|
47
|
+
* Node's fs.watchFile is a polling stat() loop with the configured
|
|
48
|
+
* pollInterval. It fires on mtime / size change. fs.watch (the
|
|
49
|
+
* inotify / kqueue backend) is more efficient but inconsistent across
|
|
50
|
+
* platforms — single rename events surface as multiple change events
|
|
51
|
+
* on Linux (events fire on the directory entry, the file, and the
|
|
52
|
+
* inode), and not at all on macOS for renamed-into files. Polling
|
|
53
|
+
* with watchFile is consistent everywhere and the latency cost (one
|
|
54
|
+
* pollInterval) is acceptable for renewal cadences measured in days.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
var fs = require("fs");
|
|
58
|
+
var path = require("path");
|
|
59
|
+
var atomicFile = require("../atomic-file");
|
|
60
|
+
var C = require("../constants");
|
|
61
|
+
var lazyRequire = require("../lazy-require");
|
|
62
|
+
var validateOpts = require("../validate-opts");
|
|
63
|
+
var { defineClass } = require("../framework-error");
|
|
64
|
+
var { boot } = require("../log");
|
|
65
|
+
|
|
66
|
+
var vault = lazyRequire(function () { return require("./index"); });
|
|
67
|
+
var audit = lazyRequire(function () { return require("../audit"); });
|
|
68
|
+
|
|
69
|
+
var log = boot("vault-seal-pem");
|
|
70
|
+
|
|
71
|
+
var SealPemFileError = defineClass("SealPemFileError", { alwaysPermanent: true });
|
|
72
|
+
|
|
73
|
+
// Default poll cadence balances latency against syscall pressure.
|
|
74
|
+
// At 2s, ACME renewals (which happen every ~60 days) experience a
|
|
75
|
+
// 2-second worst-case re-seal latency — negligible against the
|
|
76
|
+
// renewal cadence. Operators with sub-second-sensitive use cases
|
|
77
|
+
// override via opts.pollInterval.
|
|
78
|
+
var DEFAULT_POLL_MS = C.TIME.seconds(2);
|
|
79
|
+
|
|
80
|
+
function sealPemFile(opts) {
|
|
81
|
+
opts = opts || {};
|
|
82
|
+
validateOpts(opts, [
|
|
83
|
+
"source", "destination", "audit", "pollInterval",
|
|
84
|
+
"onResealed", "onError",
|
|
85
|
+
], "vault.sealPemFile");
|
|
86
|
+
|
|
87
|
+
validateOpts.requireNonEmptyString(opts.source,
|
|
88
|
+
"vault.sealPemFile: source must be a non-empty path",
|
|
89
|
+
SealPemFileError, "seal-pem-file/bad-source");
|
|
90
|
+
validateOpts.requireNonEmptyString(opts.destination,
|
|
91
|
+
"vault.sealPemFile: destination must be a non-empty path",
|
|
92
|
+
SealPemFileError, "seal-pem-file/bad-destination");
|
|
93
|
+
if (opts.source === opts.destination) {
|
|
94
|
+
throw new SealPemFileError("seal-pem-file/same-path",
|
|
95
|
+
"vault.sealPemFile: source and destination must differ — sealing in place would overwrite the plaintext");
|
|
96
|
+
}
|
|
97
|
+
validateOpts.optionalPositiveFinite(opts.pollInterval,
|
|
98
|
+
"vault.sealPemFile: pollInterval", SealPemFileError, "seal-pem-file/bad-poll-interval");
|
|
99
|
+
validateOpts.optionalFunction(opts.onResealed,
|
|
100
|
+
"vault.sealPemFile: onResealed", SealPemFileError, "seal-pem-file/bad-on-resealed");
|
|
101
|
+
validateOpts.optionalFunction(opts.onError,
|
|
102
|
+
"vault.sealPemFile: onError", SealPemFileError, "seal-pem-file/bad-on-error");
|
|
103
|
+
|
|
104
|
+
var source = opts.source;
|
|
105
|
+
var destination = opts.destination;
|
|
106
|
+
// optionalPositiveFinite above already threw on a bad-shaped opts.pollInterval;
|
|
107
|
+
// here only undefined / null / valid-positive-finite remain.
|
|
108
|
+
var pollInterval = opts.pollInterval || DEFAULT_POLL_MS;
|
|
109
|
+
var auditOn = opts.audit !== false;
|
|
110
|
+
var onResealed = typeof opts.onResealed === "function" ? opts.onResealed : null;
|
|
111
|
+
var onError = typeof opts.onError === "function" ? opts.onError : null;
|
|
112
|
+
|
|
113
|
+
var generation = 0;
|
|
114
|
+
var lastResealedAt = null;
|
|
115
|
+
var lastError = null;
|
|
116
|
+
var watching = false;
|
|
117
|
+
var listener = null;
|
|
118
|
+
var resealing = false;
|
|
119
|
+
var pendingMtime = null;
|
|
120
|
+
|
|
121
|
+
function _emitAudit(action, outcome, metadata) {
|
|
122
|
+
if (!auditOn) return;
|
|
123
|
+
try {
|
|
124
|
+
audit().safeEmit({
|
|
125
|
+
action: "vault.seal_pem_file." + action,
|
|
126
|
+
outcome: outcome,
|
|
127
|
+
metadata: metadata || {},
|
|
128
|
+
});
|
|
129
|
+
} catch (_e) { /* drop-silent */ }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function _writeSealed(plaintextBytes) {
|
|
133
|
+
// atomicFile.writeSync already does the .tmp + fsync + rename +
|
|
134
|
+
// fsyncDir sequence atomically. The marker is the framework's
|
|
135
|
+
// operator-visible crash-detection signal — created BEFORE the
|
|
136
|
+
// atomic rename, removed AFTER. If the framework crashes between
|
|
137
|
+
// marker create and marker remove, the marker remains on disk
|
|
138
|
+
// and _recoverIfNeeded() detects it on the next start().
|
|
139
|
+
var markerPath = destination + ".rewriting";
|
|
140
|
+
atomicFile.ensureDir(path.dirname(destination));
|
|
141
|
+
var sealed = vault().seal(plaintextBytes);
|
|
142
|
+
fs.writeFileSync(markerPath, String(Date.now()), { mode: 0o600 }); // allow:raw-byte-literal — POSIX file mode
|
|
143
|
+
try {
|
|
144
|
+
atomicFile.writeSync(destination, sealed, { fileMode: 0o600 }); // allow:raw-byte-literal — POSIX file mode
|
|
145
|
+
} catch (e) {
|
|
146
|
+
try { fs.unlinkSync(markerPath); } catch (_e) { /* best-effort */ }
|
|
147
|
+
throw e;
|
|
148
|
+
}
|
|
149
|
+
try { fs.unlinkSync(markerPath); } catch (_e) { /* marker cleanup best-effort */ }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function _resealNow() {
|
|
153
|
+
if (resealing) return;
|
|
154
|
+
resealing = true;
|
|
155
|
+
try {
|
|
156
|
+
var plaintext;
|
|
157
|
+
try { plaintext = fs.readFileSync(source); }
|
|
158
|
+
catch (e) {
|
|
159
|
+
var err = new SealPemFileError("seal-pem-file/source-read-failed",
|
|
160
|
+
"vault.sealPemFile: failed to read source '" + source + "': " + e.message);
|
|
161
|
+
lastError = err;
|
|
162
|
+
_emitAudit("read_failed", "failure", { source: source, error: e.message });
|
|
163
|
+
if (onError) { try { onError(err); } catch (_e) { /* drop-silent */ } }
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
_writeSealed(plaintext);
|
|
168
|
+
} catch (e2) {
|
|
169
|
+
var err2 = new SealPemFileError("seal-pem-file/seal-failed",
|
|
170
|
+
"vault.sealPemFile: failed to seal '" + source + "' to '" + destination + "': " + e2.message);
|
|
171
|
+
lastError = err2;
|
|
172
|
+
_emitAudit("seal_failed", "failure", {
|
|
173
|
+
source: source, destination: destination, error: e2.message,
|
|
174
|
+
});
|
|
175
|
+
if (onError) { try { onError(err2); } catch (_e) { /* drop-silent */ } }
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
generation += 1;
|
|
179
|
+
lastResealedAt = Date.now();
|
|
180
|
+
lastError = null;
|
|
181
|
+
_emitAudit("resealed", "success", {
|
|
182
|
+
source: source,
|
|
183
|
+
destination: destination,
|
|
184
|
+
bytes: plaintext.length,
|
|
185
|
+
generation: generation,
|
|
186
|
+
});
|
|
187
|
+
if (onResealed) {
|
|
188
|
+
try {
|
|
189
|
+
onResealed({
|
|
190
|
+
srcPath: source,
|
|
191
|
+
destPath: destination,
|
|
192
|
+
bytes: plaintext.length,
|
|
193
|
+
resealedAt: lastResealedAt,
|
|
194
|
+
generation: generation,
|
|
195
|
+
});
|
|
196
|
+
} catch (_e) { /* drop-silent */ }
|
|
197
|
+
}
|
|
198
|
+
} finally {
|
|
199
|
+
resealing = false;
|
|
200
|
+
if (pendingMtime) {
|
|
201
|
+
// A change event arrived while we were resealing — reseal again
|
|
202
|
+
// so the latest source bytes land. Single-flight: only one
|
|
203
|
+
// pending reseal is queued.
|
|
204
|
+
pendingMtime = null;
|
|
205
|
+
setImmediate(_resealNow);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Recover from a prior crash: if the marker is present, the previous
|
|
211
|
+
// reseal was interrupted. Re-seal from source idempotently.
|
|
212
|
+
function _recoverIfNeeded() {
|
|
213
|
+
var markerPath = destination + ".rewriting";
|
|
214
|
+
if (fs.existsSync(markerPath)) {
|
|
215
|
+
log.info("vault.sealPemFile: recovery — marker '" + markerPath +
|
|
216
|
+
"' present from prior crashed reseal; re-sealing from source");
|
|
217
|
+
_emitAudit("recovery_started", "success", {
|
|
218
|
+
source: source, destination: destination,
|
|
219
|
+
});
|
|
220
|
+
// Don't unlink the marker yet — _writeSealed will rewrite it
|
|
221
|
+
// and remove it as part of the normal sequence.
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function start() {
|
|
226
|
+
if (watching) return;
|
|
227
|
+
_recoverIfNeeded();
|
|
228
|
+
// Initial seal — operator gets the destination populated on
|
|
229
|
+
// start() even if the source's mtime never changes.
|
|
230
|
+
_resealNow();
|
|
231
|
+
listener = function (curr, prev) {
|
|
232
|
+
// mtime change OR the source appearing for the first time.
|
|
233
|
+
if (curr.mtimeMs !== prev.mtimeMs || curr.size !== prev.size) {
|
|
234
|
+
if (resealing) { pendingMtime = curr.mtimeMs; return; }
|
|
235
|
+
_resealNow();
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
fs.watchFile(source, { persistent: false, interval: pollInterval }, listener);
|
|
239
|
+
watching = true;
|
|
240
|
+
_emitAudit("watch_started", "success", {
|
|
241
|
+
source: source,
|
|
242
|
+
destination: destination,
|
|
243
|
+
pollInterval: pollInterval,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function stop() {
|
|
248
|
+
if (!watching) return;
|
|
249
|
+
fs.unwatchFile(source, listener);
|
|
250
|
+
listener = null;
|
|
251
|
+
watching = false;
|
|
252
|
+
_emitAudit("watch_stopped", "success", {
|
|
253
|
+
source: source,
|
|
254
|
+
destination: destination,
|
|
255
|
+
generation: generation,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Auto-start so the operator's `var watcher = sealPemFile(...)` call
|
|
260
|
+
// produces a populated destination immediately. Operators wiring it
|
|
261
|
+
// into a deferred lifecycle override by passing autoStart: false —
|
|
262
|
+
// not yet a frequent enough use case to surface, opens cleanly when
|
|
263
|
+
// the first operator surfaces it.
|
|
264
|
+
start();
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
stop: stop,
|
|
268
|
+
get generation() { return generation; },
|
|
269
|
+
get lastResealedAt() { return lastResealedAt; },
|
|
270
|
+
get lastError() { return lastError; },
|
|
271
|
+
get watching() { return watching; },
|
|
272
|
+
// Force a reseal — useful for tests and operator-triggered rotations
|
|
273
|
+
// (e.g. after a manual ACME renewal). Idempotent: produces an
|
|
274
|
+
// updated destination from the current source bytes.
|
|
275
|
+
forceReseal: _resealNow,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
module.exports = {
|
|
280
|
+
sealPemFile: sealPemFile,
|
|
281
|
+
SealPemFileError: SealPemFileError,
|
|
282
|
+
DEFAULT_POLL_MS: DEFAULT_POLL_MS,
|
|
283
|
+
};
|
package/lib/websocket.js
CHANGED
|
@@ -725,6 +725,21 @@ class WebSocketConnection extends EventEmitter {
|
|
|
725
725
|
return this._abort(CLOSE_PROTOCOL_ERROR, "RSV1 on continuation frame (must be on start)");
|
|
726
726
|
}
|
|
727
727
|
|
|
728
|
+
// RFC 6455 §5.5 — control frames (opcodes >= 0x8: CLOSE/PING/PONG)
|
|
729
|
+
// MUST have payload length ≤ 125 and MUST NOT be fragmented.
|
|
730
|
+
// Without the cap an attacker can send a 1 MiB PING and we echo it
|
|
731
|
+
// verbatim as PONG — a 2× outbound-bandwidth amplification DoS.
|
|
732
|
+
if (frame.opcode >= 0x8) {
|
|
733
|
+
if (frame.payload.length > 125) {
|
|
734
|
+
return this._abort(CLOSE_PROTOCOL_ERROR,
|
|
735
|
+
"control frame payload exceeds 125 bytes (RFC 6455 §5.5)");
|
|
736
|
+
}
|
|
737
|
+
if (!frame.fin) {
|
|
738
|
+
return this._abort(CLOSE_PROTOCOL_ERROR,
|
|
739
|
+
"control frame must not be fragmented (RFC 6455 §5.5)");
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
728
743
|
if (frame.opcode === OPCODE_CONTINUATION) {
|
|
729
744
|
if (this._fragOpcode === null) {
|
|
730
745
|
return this._abort(CLOSE_PROTOCOL_ERROR, "continuation without start");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/core",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.16",
|
|
4
4
|
"description": "The Node framework that owns its stack.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "blamejs contributors",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"owns-its-stack"
|
|
55
55
|
],
|
|
56
56
|
"engines": {
|
|
57
|
-
"node": ">=24.
|
|
57
|
+
"node": ">=24.4.0"
|
|
58
58
|
},
|
|
59
59
|
"files": [
|
|
60
60
|
"index.js",
|
package/sbom.cyclonedx.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.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:9d91dfe3-4449-4315-ac6c-d78f5dd92d0c",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-07T07:18:43.056Z",
|
|
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.8.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.8.16",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.16",
|
|
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.8.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.8.16",
|
|
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.8.
|
|
57
|
+
"ref": "@blamejs/core@0.8.16",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|