@blamejs/core 0.8.40 → 0.8.42
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 +4 -0
- package/lib/audit-tools.js +23 -5
- package/lib/audit.js +2 -0
- package/lib/auth/password.js +51 -7
- package/lib/backup/index.js +63 -0
- package/lib/canonical-json.js +35 -7
- package/lib/config.js +118 -7
- package/lib/constants.js +10 -1
- package/lib/crypto-field.js +17 -5
- package/lib/crypto.js +51 -14
- package/lib/db.js +91 -2
- package/lib/external-db.js +74 -31
- package/lib/mail-auth.js +16 -4
- package/lib/network-smtp-policy.js +11 -4
- package/lib/network-tls.js +15 -0
- package/lib/pqc-software.js +42 -0
- package/lib/process-spawn.js +122 -0
- package/lib/resource-access-lock.js +116 -0
- package/lib/vault/index.js +64 -3
- package/lib/vault/seal-pem-file.js +38 -2
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.resourceAccessLock — three-mode access-lock for arbitrary
|
|
4
|
+
* resources (data exports, scheduled jobs, file paths, queue
|
|
5
|
+
* partitions). Different from b.auth.accessLock — that one gates
|
|
6
|
+
* HTTP request flow; this one gates non-HTTP-shaped operator
|
|
7
|
+
* actions. Both share the open / read-only / locked vocabulary.
|
|
8
|
+
*
|
|
9
|
+
* var exportLock = b.resourceAccessLock.create({
|
|
10
|
+
* resource: "data-export-jobs",
|
|
11
|
+
* startMode: "open",
|
|
12
|
+
* audit: b.audit,
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* if (!exportLock.permits("write")) {
|
|
16
|
+
* throw new b.resourceAccessLock.ResourceAccessLockError(
|
|
17
|
+
* "resource-access-lock/refused",
|
|
18
|
+
* "data export refused: lock mode is " + exportLock.mode());
|
|
19
|
+
* }
|
|
20
|
+
* await runExportJob();
|
|
21
|
+
*
|
|
22
|
+
* exportLock.set("locked", { actor: "alice", reason: "incident-42 freeze" });
|
|
23
|
+
*
|
|
24
|
+
* Mode semantics:
|
|
25
|
+
* open — every action permitted
|
|
26
|
+
* read-only — actions tagged "read" permitted; "write" refused
|
|
27
|
+
* locked — every action refused
|
|
28
|
+
*
|
|
29
|
+
* Audit shape:
|
|
30
|
+
* resourceaccesslock.mode_changed — {resource, from, to, actor, reason}
|
|
31
|
+
* resourceaccesslock.refused — {resource, action, mode, actor}
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
var lazyRequire = require("./lazy-require");
|
|
35
|
+
var validateOpts = require("./validate-opts");
|
|
36
|
+
var { defineClass } = require("./framework-error");
|
|
37
|
+
|
|
38
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
39
|
+
|
|
40
|
+
var ResourceAccessLockError = defineClass("ResourceAccessLockError",
|
|
41
|
+
{ alwaysPermanent: true });
|
|
42
|
+
|
|
43
|
+
var VALID_MODES = Object.freeze({ open: 1, "read-only": 1, locked: 1 });
|
|
44
|
+
var READ_ACTIONS = Object.freeze({ read: 1, list: 1, get: 1, query: 1, "read-only": 1 });
|
|
45
|
+
|
|
46
|
+
function create(opts) {
|
|
47
|
+
opts = opts || {};
|
|
48
|
+
validateOpts(opts, ["resource", "startMode", "audit"], "resourceAccessLock.create");
|
|
49
|
+
validateOpts.requireNonEmptyString(opts.resource, "resource",
|
|
50
|
+
ResourceAccessLockError, "resource-access-lock/no-resource");
|
|
51
|
+
var startMode = opts.startMode || "open";
|
|
52
|
+
if (!VALID_MODES[startMode]) {
|
|
53
|
+
throw new ResourceAccessLockError(
|
|
54
|
+
"resource-access-lock/bad-start-mode",
|
|
55
|
+
"startMode must be one of: " + Object.keys(VALID_MODES).join(" / "));
|
|
56
|
+
}
|
|
57
|
+
var auditOn = opts.audit !== false;
|
|
58
|
+
var resource = opts.resource;
|
|
59
|
+
var mode = startMode;
|
|
60
|
+
|
|
61
|
+
function _emit(action, outcome, meta) {
|
|
62
|
+
if (!auditOn) return;
|
|
63
|
+
try {
|
|
64
|
+
audit().safeEmit({
|
|
65
|
+
action: action, outcome: outcome,
|
|
66
|
+
metadata: Object.assign({ resource: resource }, meta || {}),
|
|
67
|
+
});
|
|
68
|
+
} catch (_e) { /* audit best-effort */ }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function permits(action) {
|
|
72
|
+
if (mode === "open") return true;
|
|
73
|
+
if (mode === "locked") return false;
|
|
74
|
+
return !!READ_ACTIONS[action];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function set(newMode, ctx) {
|
|
78
|
+
ctx = ctx || {};
|
|
79
|
+
if (!VALID_MODES[newMode]) {
|
|
80
|
+
throw new ResourceAccessLockError(
|
|
81
|
+
"resource-access-lock/bad-mode",
|
|
82
|
+
"set: mode must be one of: " + Object.keys(VALID_MODES).join(" / "));
|
|
83
|
+
}
|
|
84
|
+
var prev = mode;
|
|
85
|
+
mode = newMode;
|
|
86
|
+
_emit("resourceaccesslock.mode_changed", "success", {
|
|
87
|
+
from: prev, to: newMode,
|
|
88
|
+
actor: ctx.actor || null, reason: ctx.reason || null,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function assertPermits(action, ctx) {
|
|
93
|
+
if (permits(action)) return;
|
|
94
|
+
_emit("resourceaccesslock.refused", "failure", {
|
|
95
|
+
action: action, mode: mode,
|
|
96
|
+
actor: (ctx && ctx.actor) || null,
|
|
97
|
+
});
|
|
98
|
+
throw new ResourceAccessLockError(
|
|
99
|
+
"resource-access-lock/refused",
|
|
100
|
+
resource + " refuses '" + action + "': lock mode is '" + mode + "'");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
resource: resource,
|
|
105
|
+
mode: function () { return mode; },
|
|
106
|
+
set: set,
|
|
107
|
+
permits: permits,
|
|
108
|
+
assertPermits: assertPermits,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
create: create,
|
|
114
|
+
VALID_MODES: Object.freeze(Object.keys(VALID_MODES)),
|
|
115
|
+
ResourceAccessLockError: ResourceAccessLockError,
|
|
116
|
+
};
|
package/lib/vault/index.js
CHANGED
|
@@ -66,12 +66,49 @@ var log = boot("vault");
|
|
|
66
66
|
|
|
67
67
|
function resolvePaths(dataDir) {
|
|
68
68
|
return {
|
|
69
|
-
dataDir:
|
|
70
|
-
plaintext:
|
|
71
|
-
sealed:
|
|
69
|
+
dataDir: dataDir,
|
|
70
|
+
plaintext: path.join(dataDir, "vault.key"),
|
|
71
|
+
sealed: path.join(dataDir, "vault.key.sealed"),
|
|
72
|
+
derivedHashSalt: path.join(dataDir, "vault.derived-hash-salt"),
|
|
72
73
|
};
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
// derivedHashSalt — per-deployment salt for crypto-field
|
|
77
|
+
// derivedHashes (D-H1). Pre-v0.8.42 the deterministic
|
|
78
|
+
// sha3(namespace + plaintext) shape allowed cross-deployment
|
|
79
|
+
// rainbow + cross-table correlation; binding a 32-byte
|
|
80
|
+
// per-deployment salt closes that class without breaking
|
|
81
|
+
// indexed-lookup determinism inside one deployment. The salt
|
|
82
|
+
// persists across vault rotations (different file from vault.key)
|
|
83
|
+
// so existing derivedHash columns survive a passphrase change.
|
|
84
|
+
function _readOrCreateDerivedHashSalt() {
|
|
85
|
+
if (!paths) {
|
|
86
|
+
throw new VaultError("vault/not-initialized",
|
|
87
|
+
"vault.derivedHashSalt() requires init()");
|
|
88
|
+
}
|
|
89
|
+
if (fs.existsSync(paths.derivedHashSalt)) {
|
|
90
|
+
var raw = atomicFile.readSync(paths.derivedHashSalt);
|
|
91
|
+
if (raw.length !== 32) { // allow:raw-byte-literal — 32-byte (256-bit) salt
|
|
92
|
+
throw new VaultError("vault/derived-hash-salt-corrupted",
|
|
93
|
+
"vault.derived-hash-salt must be exactly 32 bytes; got " + raw.length);
|
|
94
|
+
}
|
|
95
|
+
return raw;
|
|
96
|
+
}
|
|
97
|
+
var nodeCrypto = require("node:crypto");
|
|
98
|
+
var salt = nodeCrypto.randomBytes(32); // allow:raw-byte-literal — 32-byte salt
|
|
99
|
+
atomicFile.writeSync(paths.derivedHashSalt, salt, { fileMode: 0o600 });
|
|
100
|
+
log("generated per-deployment derivedHash salt at " + paths.derivedHashSalt);
|
|
101
|
+
return salt;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
var _cachedDerivedHashSalt = null;
|
|
105
|
+
function getDerivedHashSalt() {
|
|
106
|
+
if (_cachedDerivedHashSalt === null) {
|
|
107
|
+
_cachedDerivedHashSalt = _readOrCreateDerivedHashSalt();
|
|
108
|
+
}
|
|
109
|
+
return _cachedDerivedHashSalt;
|
|
110
|
+
}
|
|
111
|
+
|
|
75
112
|
// ---- Init dispatch ----
|
|
76
113
|
|
|
77
114
|
async function init(opts) {
|
|
@@ -294,10 +331,34 @@ var vaultAad = require("../vault-aad");
|
|
|
294
331
|
|
|
295
332
|
var sealPemFileModule = require("./seal-pem-file");
|
|
296
333
|
|
|
334
|
+
// _zeroizeAndReplace — best-effort secureZero of prior in-memory keys
|
|
335
|
+
// before a swap. V8 strings can't be reliably overwritten (string
|
|
336
|
+
// interning + GC managed), so the pre-swap pass converts each PEM
|
|
337
|
+
// string to a Buffer, secureZeros the Buffer, and rebinds the
|
|
338
|
+
// property to "ZEROED" before the new keys land. The string copy
|
|
339
|
+
// inside V8 may still linger until GC; this just removes the
|
|
340
|
+
// largest-window heap copy (the ones held by `keys`).
|
|
341
|
+
function _zeroizeAndReplace(replacement) {
|
|
342
|
+
if (!keys) { keys = replacement; return; }
|
|
343
|
+
Object.keys(keys).forEach(function (k) {
|
|
344
|
+
var v = keys[k];
|
|
345
|
+
if (typeof v === "string" && v.length > 0) {
|
|
346
|
+
try {
|
|
347
|
+
var buf = Buffer.from(v, "utf8");
|
|
348
|
+
safeBuffer.secureZero(buf);
|
|
349
|
+
} catch (_e) { /* best-effort */ }
|
|
350
|
+
keys[k] = "ZEROED";
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
keys = replacement;
|
|
354
|
+
}
|
|
355
|
+
|
|
297
356
|
module.exports = {
|
|
298
357
|
init: init,
|
|
299
358
|
seal: seal,
|
|
300
359
|
unseal: unseal,
|
|
360
|
+
getDerivedHashSalt: getDerivedHashSalt,
|
|
361
|
+
_zeroizeAndReplace: _zeroizeAndReplace,
|
|
301
362
|
aad: vaultAad,
|
|
302
363
|
getKeysJson: getKeysJson,
|
|
303
364
|
getCurrentPassphrase: getCurrentPassphrase,
|
|
@@ -76,7 +76,12 @@ var SealPemFileError = defineClass("SealPemFileError", { alwaysPermanent: true }
|
|
|
76
76
|
// 2-second worst-case re-seal latency — negligible against the
|
|
77
77
|
// renewal cadence. Operators with sub-second-sensitive use cases
|
|
78
78
|
// override via opts.pollInterval.
|
|
79
|
-
|
|
79
|
+
// H6 #6 — fs.watchFile default cadence reduced from 2s to 500ms so a
|
|
80
|
+
// fast renewal-then-revert (mtime bump then second bump within ~2s)
|
|
81
|
+
// doesn't sneak past the watcher. Operators with extremely-quiet
|
|
82
|
+
// renewal cycles can override via opts.pollInterval; the cost of
|
|
83
|
+
// 500ms polling on an idle PEM file is ~2 stat() syscalls/sec.
|
|
84
|
+
var DEFAULT_POLL_MS = 500; // allow:raw-time-literal — 500ms watchFile cadence (sub-second)
|
|
80
85
|
|
|
81
86
|
// PEM files are tiny — 4 KiB for an ECDSA key, ~8 KiB for a 4096-bit
|
|
82
87
|
// RSA key, ~64 KiB for a long cert chain. Cap at 1 MiB so an operator
|
|
@@ -148,7 +153,28 @@ function sealPemFile(opts) {
|
|
|
148
153
|
// marker create and marker remove, the marker remains on disk
|
|
149
154
|
// and _recoverIfNeeded() detects it on the next start().
|
|
150
155
|
var markerPath = destination + ".rewriting";
|
|
151
|
-
|
|
156
|
+
var destDir = path.dirname(destination);
|
|
157
|
+
atomicFile.ensureDir(destDir);
|
|
158
|
+
// H6 #4 — assert parent-dir mode. If the directory is world-
|
|
159
|
+
// writable, an attacker can swap the destination file or the
|
|
160
|
+
// .rewriting marker between our writeFileSync and the atomic
|
|
161
|
+
// rename. Refuse on group-/other-writable parent dirs (POSIX
|
|
162
|
+
// mode bits 0o022). On Windows the stat mode is synthetic;
|
|
163
|
+
// skip the check there.
|
|
164
|
+
if (process.platform !== "win32") {
|
|
165
|
+
try {
|
|
166
|
+
var dirStat = fs.statSync(destDir);
|
|
167
|
+
if ((dirStat.mode & 0o022) !== 0) { // allow:raw-byte-literal — POSIX mode mask
|
|
168
|
+
throw new SealPemFileError("seal-pem-file/parent-dir-writable",
|
|
169
|
+
"destination parent dir '" + destDir + "' is group/other-writable " +
|
|
170
|
+
"(mode " + (dirStat.mode & 0o777).toString(8) + // allow:raw-byte-literal — POSIX mode mask
|
|
171
|
+
") — refuse to seal; chmod 0700 the dir");
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {
|
|
174
|
+
if (e && e.code === "seal-pem-file/parent-dir-writable") throw e;
|
|
175
|
+
// stat itself failing is not fatal — the writeFileSync below will surface it.
|
|
176
|
+
}
|
|
177
|
+
}
|
|
152
178
|
var sealed = vault().seal(plaintextBytes);
|
|
153
179
|
fs.writeFileSync(markerPath, String(Date.now()), { mode: 0o600 }); // allow:raw-byte-literal — POSIX file mode
|
|
154
180
|
try {
|
|
@@ -158,6 +184,16 @@ function sealPemFile(opts) {
|
|
|
158
184
|
throw e;
|
|
159
185
|
}
|
|
160
186
|
try { fs.unlinkSync(markerPath); } catch (_e) { /* marker cleanup best-effort */ }
|
|
187
|
+
// H6 #5 — fsync the destination directory so the rename + marker
|
|
188
|
+
// unlink survive a power loss. Crash + backup-snapshot edge case:
|
|
189
|
+
// without dir-fsync, a journaled fs may have the new file inode
|
|
190
|
+
// but not the directory entry update by the time the snapshot
|
|
191
|
+
// reads.
|
|
192
|
+
try {
|
|
193
|
+
var dirFd = fs.openSync(destDir, "r");
|
|
194
|
+
try { fs.fsyncSync(dirFd); }
|
|
195
|
+
finally { fs.closeSync(dirFd); }
|
|
196
|
+
} catch (_e) { /* dir fsync best-effort — Windows / non-POSIX may refuse */ }
|
|
161
197
|
}
|
|
162
198
|
|
|
163
199
|
function _resealNow(actor) {
|
package/package.json
CHANGED
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:6b316a2e-756a-4aa8-90a8-b6c414dbdc39",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-07T17:47:29.804Z",
|
|
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.42",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.42",
|
|
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.42",
|
|
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.42",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|