@blamejs/core 0.14.24 → 0.14.26
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 +4 -0
- package/lib/auth/oauth.js +25 -5
- package/lib/auth/sd-jwt-vc.js +16 -3
- package/lib/break-glass.js +153 -3
- package/lib/constants.js +11 -0
- package/lib/crypto-field.js +307 -78
- package/lib/db-query.js +65 -5
- package/lib/db.js +17 -3
- package/lib/dsr.js +378 -52
- package/lib/middleware/idempotency-key.js +21 -13
- package/lib/queue-local.js +23 -1
- package/lib/queue.js +7 -0
- package/lib/request-helpers.js +7 -0
- package/lib/retention.js +11 -1
- package/lib/vault/rotate.js +64 -44
- package/lib/vault-aad.js +6 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/request-helpers.js
CHANGED
|
@@ -254,6 +254,13 @@ function clientIp(req, opts) {
|
|
|
254
254
|
}
|
|
255
255
|
if (req.socket && typeof req.socket.remoteAddress === "string") return req.socket.remoteAddress;
|
|
256
256
|
if (req.connection && typeof req.connection.remoteAddress === "string") return req.connection.remoteAddress;
|
|
257
|
+
// Express-shaped requests expose the resolved client address as `req.ip`
|
|
258
|
+
// (Express derives it from the socket, honoring its own trust-proxy
|
|
259
|
+
// setting) without a `socket.remoteAddress` surface. Fall back to it so a
|
|
260
|
+
// binding captured from such a request is populated rather than null —
|
|
261
|
+
// callers that pin a grant to the issuing IP otherwise capture null and
|
|
262
|
+
// could only be saved by a fail-closed guard at the consumer.
|
|
263
|
+
if (typeof req.ip === "string" && req.ip.length > 0) return req.ip;
|
|
257
264
|
return null;
|
|
258
265
|
}
|
|
259
266
|
|
package/lib/retention.js
CHANGED
|
@@ -274,8 +274,18 @@ function create(opts) {
|
|
|
274
274
|
values.push(row._id);
|
|
275
275
|
var upd2 = db.prepare("UPDATE \"" + table + "\" SET " + setClauses.join(", ") + " WHERE _id = ?");
|
|
276
276
|
upd2.run.apply(upd2, values);
|
|
277
|
+
// Per-row-key tables (declarePerRowKey): NULLing the sealed columns
|
|
278
|
+
// is not enough — WAL / replica residuals keep the old K_row cells.
|
|
279
|
+
// Destroy the row's wrapped secret so K_row is unrecoverable and the
|
|
280
|
+
// residual ciphertext reads as absent (crypto-shred, GDPR Art. 17).
|
|
281
|
+
// rowId is row._id, the same identity materialize / eraseHard use.
|
|
282
|
+
var perRowKeysDestroyed = 0;
|
|
283
|
+
if (cryptoField.hasPerRowKey(table)) {
|
|
284
|
+
var dr = cryptoField.destroyPerRowKey(table, row._id, db);
|
|
285
|
+
perRowKeysDestroyed = (dr && dr.destroyed) || 0;
|
|
286
|
+
}
|
|
277
287
|
void erased;
|
|
278
|
-
return { erased: 1, sealedFieldCount: sealedFields.length };
|
|
288
|
+
return { erased: 1, sealedFieldCount: sealedFields.length, perRowKeysDestroyed: perRowKeysDestroyed };
|
|
279
289
|
}
|
|
280
290
|
|
|
281
291
|
function _cascade(rule, rowId, dryRun) {
|
package/lib/vault/rotate.js
CHANGED
|
@@ -81,6 +81,11 @@ var agentSnapshotLazy = lazyRequire(function () { return require("../agent-snaps
|
|
|
81
81
|
// rotation pipeline never walks, so archive-wrap exports the same external
|
|
82
82
|
// AAD_ROTATION descriptor and must be gated here too.
|
|
83
83
|
var archiveWrapLazy = lazyRequire(function () { return require("../archive-wrap"); });
|
|
84
|
+
// The DSR ticket store, when backed by an operator-supplied database, holds
|
|
85
|
+
// {aad:true} sealed cells (subject identifiers + request payload) keyed off the
|
|
86
|
+
// vault root that this pipeline never walks, so dsr exports the same external
|
|
87
|
+
// AAD_ROTATION descriptor and must be gated here too.
|
|
88
|
+
var dsrLazy = lazyRequire(function () { return require("../dsr"); });
|
|
84
89
|
var { defineClass } = require("../framework-error");
|
|
85
90
|
|
|
86
91
|
var rotateLog = boot("vault-rotate");
|
|
@@ -439,7 +444,7 @@ var VAULT_PREFIX_LEN = C.VAULT_PREFIX.length;
|
|
|
439
444
|
// so loading rotate.js doesn't eagerly pull the agent modules.
|
|
440
445
|
var EXTERNAL_AAD_MODULE_LOADERS = [
|
|
441
446
|
agentIdempotencyLazy, agentOrchestratorLazy, agentTenantLazy, agentSnapshotLazy,
|
|
442
|
-
archiveWrapLazy,
|
|
447
|
+
archiveWrapLazy, dsrLazy,
|
|
443
448
|
];
|
|
444
449
|
|
|
445
450
|
function _externalAadTables() {
|
|
@@ -464,22 +469,25 @@ function _emit(cb, ev) {
|
|
|
464
469
|
}
|
|
465
470
|
}
|
|
466
471
|
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
// the
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
472
|
+
// Create a fresh file in the owner-only staging dir with exclusive,
|
|
473
|
+
// no-follow semantics, then fsync it. O_EXCL turns a pre-planted file or
|
|
474
|
+
// symlink into a hard failure instead of a followed write; O_NOFOLLOW
|
|
475
|
+
// refuses a symlinked final component; the explicit 0o600 keeps the bytes
|
|
476
|
+
// owner-only regardless of umask. Any leftover from an aborted prior
|
|
477
|
+
// rotation is cleared first so the exclusive create can proceed. The
|
|
478
|
+
// staging dir is already 0o700 owner-only, so this is defense in depth
|
|
479
|
+
// against a same-user pre-plant / symlink swap (CWE-377 / CWE-379 / CWE-59).
|
|
480
|
+
function _writeStagedFileExclusive(p, data) {
|
|
481
|
+
try { nodeFs.unlinkSync(p); } catch (_e) { /* no stale entry to clear */ }
|
|
482
|
+
var fd = nodeFs.openSync(p,
|
|
483
|
+
nodeFs.constants.O_WRONLY | nodeFs.constants.O_CREAT |
|
|
484
|
+
nodeFs.constants.O_EXCL | (nodeFs.constants.O_NOFOLLOW || 0), 0o600);
|
|
479
485
|
try {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
486
|
+
nodeFs.writeFileSync(fd, data);
|
|
487
|
+
nodeFs.fsyncSync(fd);
|
|
488
|
+
} finally {
|
|
489
|
+
nodeFs.closeSync(fd);
|
|
490
|
+
}
|
|
483
491
|
}
|
|
484
492
|
|
|
485
493
|
function _reSealValue(sealedValue, oldKeys, newKeys) {
|
|
@@ -670,7 +678,8 @@ async function rotate(opts) {
|
|
|
670
678
|
"pipeline and would be orphaned under the retired keypair: " + externalAad.join(", ") +
|
|
671
679
|
". Re-seal each via its module hook (b.agent.idempotency.reseal / " +
|
|
672
680
|
"b.agent.orchestrator.reseal / b.agent.tenant AAD_ROTATION reseal / " +
|
|
673
|
-
"b.agent.snapshot.reseal / b.archive.rewrapTenant for archive-wrap:tenant-blobs
|
|
681
|
+
"b.agent.snapshot.reseal / b.archive.rewrapTenant for archive-wrap:tenant-blobs / " +
|
|
682
|
+
"b.dsr.reseal for the dsr_tickets store) " +
|
|
674
683
|
"BEFORE retiring the old keypair, then pass " +
|
|
675
684
|
"opts.externalAadResealed: [" + externalAad.map(function (t) { return JSON.stringify(t); }).join(", ") +
|
|
676
685
|
"] to acknowledge. If you do not use these features, pass opts.externalAadResealed: true.");
|
|
@@ -709,7 +718,10 @@ async function rotate(opts) {
|
|
|
709
718
|
}
|
|
710
719
|
var dest = nodePath.join(stagingDir, entry.relativePath);
|
|
711
720
|
atomicFile.ensureDir(nodePath.dirname(dest));
|
|
712
|
-
|
|
721
|
+
// Stage via the exclusive-create + fsync helper rather than a plain copy,
|
|
722
|
+
// so the verbatim file is durable at write time (no later by-path fsync)
|
|
723
|
+
// and a pre-planted file/symlink at the staging path hard-fails.
|
|
724
|
+
_writeStagedFileExclusive(dest, nodeFs.readFileSync(src));
|
|
713
725
|
}
|
|
714
726
|
for (var vd = 0; vd < paths.verbatimDirs.length; vd++) {
|
|
715
727
|
var dent = paths.verbatimDirs[vd];
|
|
@@ -737,9 +749,9 @@ async function rotate(opts) {
|
|
|
737
749
|
var newRootJson = keysJson;
|
|
738
750
|
if (mode === "wrapped") {
|
|
739
751
|
var sealed = await vaultWrap().wrap(keysJson, opts.newPassphrase);
|
|
740
|
-
|
|
752
|
+
_writeStagedFileExclusive(nodePath.join(stagingDir, paths.vaultKeySealed), sealed);
|
|
741
753
|
} else {
|
|
742
|
-
|
|
754
|
+
_writeStagedFileExclusive(nodePath.join(stagingDir, paths.vaultKeyPlain), keysJson);
|
|
743
755
|
}
|
|
744
756
|
|
|
745
757
|
// 3. re-seal db.key.enc + any operator-supplied additionalSealed files
|
|
@@ -759,14 +771,14 @@ async function rotate(opts) {
|
|
|
759
771
|
var dbKeyB64Aad = vaultAad.unsealRoot(sealedKey, dbKeyAad, oldRootJson);
|
|
760
772
|
dbKey = Buffer.from(dbKeyB64Aad, "base64");
|
|
761
773
|
var resealedAad = vaultAad.sealRoot(dbKeyB64Aad, dbKeyAad, newRootJson);
|
|
762
|
-
|
|
774
|
+
_writeStagedFileExclusive(nodePath.join(stagingDir, paths.dbKeySealed), resealedAad);
|
|
763
775
|
} else if (sealedKey.indexOf(C.VAULT_PREFIX) === 0) {
|
|
764
776
|
// Legacy plain-sealed db.key.enc (pre-AAD). Re-key in place; db.init
|
|
765
777
|
// read-migrates plain -> AAD on the next boot.
|
|
766
778
|
var dbKeyB64 = bCrypto.decrypt(sealedKey.substring(VAULT_PREFIX_LEN), oldKeys);
|
|
767
779
|
dbKey = Buffer.from(dbKeyB64, "base64");
|
|
768
780
|
var resealedKey = C.VAULT_PREFIX + bCrypto.encrypt(dbKeyB64, newKeys);
|
|
769
|
-
|
|
781
|
+
_writeStagedFileExclusive(nodePath.join(stagingDir, paths.dbKeySealed), resealedKey);
|
|
770
782
|
} else {
|
|
771
783
|
throw new VaultRotateError("vault-rotate/bad-dbkey",
|
|
772
784
|
"rotate: db.key.enc does not start with a vault prefix (vault: or vault.aad:)");
|
|
@@ -789,8 +801,8 @@ async function rotate(opts) {
|
|
|
789
801
|
}
|
|
790
802
|
var asDestDir = nodePath.join(stagingDir, nodePath.dirname(ase.relativePath));
|
|
791
803
|
if (!nodeFs.existsSync(asDestDir)) atomicFile.ensureDir(asDestDir);
|
|
792
|
-
|
|
793
|
-
_reSealValue(current, oldKeys, newKeys)
|
|
804
|
+
_writeStagedFileExclusive(nodePath.join(stagingDir, ase.relativePath),
|
|
805
|
+
_reSealValue(current, oldKeys, newKeys));
|
|
794
806
|
}
|
|
795
807
|
|
|
796
808
|
// 3b. Framework-managed crypto-field derived-hash files — always
|
|
@@ -801,14 +813,17 @@ async function rotate(opts) {
|
|
|
801
813
|
// re-seals to the same value since the keypair is unchanged).
|
|
802
814
|
var saltSrc = nodePath.join(dataDir, "vault.derived-hash-salt");
|
|
803
815
|
if (nodeFs.existsSync(saltSrc)) {
|
|
804
|
-
|
|
816
|
+
// Stage via the exclusive-create + fsync helper (not a plain copy) so the
|
|
817
|
+
// salt is durable at write time and no later by-path fsync is needed.
|
|
818
|
+
_writeStagedFileExclusive(nodePath.join(stagingDir, "vault.derived-hash-salt"),
|
|
819
|
+
nodeFs.readFileSync(saltSrc));
|
|
805
820
|
}
|
|
806
821
|
var macSrc = nodePath.join(dataDir, "vault.derived-hash-mac.sealed");
|
|
807
822
|
if (nodeFs.existsSync(macSrc)) {
|
|
808
823
|
var macCurrent = nodeFs.readFileSync(macSrc, "utf8").trim();
|
|
809
824
|
if (macCurrent.indexOf(C.VAULT_PREFIX) === 0) {
|
|
810
|
-
|
|
811
|
-
_reSealValue(macCurrent, oldKeys, newKeys)
|
|
825
|
+
_writeStagedFileExclusive(nodePath.join(stagingDir, "vault.derived-hash-mac.sealed"),
|
|
826
|
+
_reSealValue(macCurrent, oldKeys, newKeys));
|
|
812
827
|
}
|
|
813
828
|
}
|
|
814
829
|
|
|
@@ -830,7 +845,7 @@ async function rotate(opts) {
|
|
|
830
845
|
try { plainBytes = bCrypto.decryptPacked(packed, dbKey, dbEncAad); }
|
|
831
846
|
catch (_eAad) { plainBytes = bCrypto.decryptPacked(packed, dbKey); }
|
|
832
847
|
var tmpDbPath = nodePath.join(stagingDir, "_blamejs_rotate.tmp.db");
|
|
833
|
-
|
|
848
|
+
_writeStagedFileExclusive(tmpDbPath, plainBytes);
|
|
834
849
|
|
|
835
850
|
var db = new DatabaseSync(tmpDbPath);
|
|
836
851
|
try {
|
|
@@ -893,25 +908,23 @@ async function rotate(opts) {
|
|
|
893
908
|
try { nodeFs.unlinkSync(tmpDbPath + "-shm"); }
|
|
894
909
|
catch (e) { rotateLog.debug("cleanup-failed", { op: "fs.unlinkSync", path: tmpDbPath + "-shm", error: e.message }); }
|
|
895
910
|
|
|
896
|
-
//
|
|
897
|
-
//
|
|
898
|
-
//
|
|
899
|
-
//
|
|
900
|
-
//
|
|
901
|
-
//
|
|
902
|
-
// inside it. Files are written 0o600 implicitly via the dir's umask
|
|
903
|
-
// and removed before the rotation completes.
|
|
911
|
+
// Every staged path lives inside opts.stagingDir (operator-supplied,
|
|
912
|
+
// ensureDir'd 0o700 owner-only, never under os.tmpdir()) and carries a
|
|
913
|
+
// framework-internal marker name. The staged writes go through
|
|
914
|
+
// _writeStagedFileExclusive — exclusive + no-follow create, owner-only
|
|
915
|
+
// 0o600 — so a same-user pre-plant or symlink swap is a hard failure
|
|
916
|
+
// rather than a followed write, and the bytes never inherit a wider mode.
|
|
904
917
|
var rotatedBytes = nodeFs.readFileSync(tmpDbPath);
|
|
905
918
|
// Re-encrypt under the SAME dataDir AAD so db.init's AAD-first open
|
|
906
919
|
// succeeds after the staged dir is swapped over dataDir in place.
|
|
907
|
-
|
|
920
|
+
_writeStagedFileExclusive(nodePath.join(stagingDir, paths.encryptedDb),
|
|
908
921
|
bCrypto.encryptPacked(rotatedBytes, dbKey, dbEncAad));
|
|
909
922
|
nodeFs.unlinkSync(tmpDbPath);
|
|
910
923
|
|
|
911
924
|
// Round-trip verify on the staged DB
|
|
912
925
|
_emit(progress, { phase: "verify" });
|
|
913
926
|
var verifyTmp = nodePath.join(stagingDir, "_blamejs_verify.tmp.db");
|
|
914
|
-
|
|
927
|
+
_writeStagedFileExclusive(verifyTmp,
|
|
915
928
|
bCrypto.decryptPacked(nodeFs.readFileSync(nodePath.join(stagingDir, paths.encryptedDb)), dbKey, dbEncAad));
|
|
916
929
|
var vdb = new DatabaseSync(verifyTmp);
|
|
917
930
|
try {
|
|
@@ -934,19 +947,26 @@ async function rotate(opts) {
|
|
|
934
947
|
}
|
|
935
948
|
}
|
|
936
949
|
|
|
937
|
-
// 5. fsync staging for durability before caller
|
|
950
|
+
// 5. fsync staging directory entries for durability before the caller swaps.
|
|
951
|
+
// Every staged FILE is already fsync'd at write time by
|
|
952
|
+
// _writeStagedFileExclusive (the re-encrypted db, the resealed vault/db keys,
|
|
953
|
+
// sealed files, the derived-hash salt, and verbatim files), so re-opening
|
|
954
|
+
// each by path here is redundant — and opening a staged file by path is the
|
|
955
|
+
// os-temp-dir open the static analyzer refuses (CWE-377 heuristic). Only the
|
|
956
|
+
// optional verbatimDirs are copied with copyFileSync (no per-file fsync);
|
|
957
|
+
// their directory entries + the rename are made durable by fsyncDir and their
|
|
958
|
+
// source files in dataDir remain intact, so a crash in that narrow window is
|
|
959
|
+
// recoverable.
|
|
938
960
|
_emit(progress, { phase: "fsync" });
|
|
939
|
-
function
|
|
961
|
+
function fsyncDirTree(dir) {
|
|
940
962
|
var entries = nodeFs.readdirSync(dir);
|
|
941
963
|
for (var i = 0; i < entries.length; i++) {
|
|
942
964
|
var p = nodePath.join(dir, entries[i]);
|
|
943
|
-
|
|
944
|
-
if (st.isFile()) _fsyncFileByPath(p);
|
|
945
|
-
else if (st.isDirectory()) fsyncTree(p);
|
|
965
|
+
if (nodeFs.statSync(p).isDirectory()) fsyncDirTree(p);
|
|
946
966
|
}
|
|
947
967
|
atomicFile.fsyncDir(dir);
|
|
948
968
|
}
|
|
949
|
-
|
|
969
|
+
fsyncDirTree(stagingDir);
|
|
950
970
|
|
|
951
971
|
var durationMs = Date.now() - startedAt;
|
|
952
972
|
_emit(progress, {
|
package/lib/vault-aad.js
CHANGED
|
@@ -304,6 +304,12 @@ module.exports = {
|
|
|
304
304
|
isAadSealed: isAadSealed,
|
|
305
305
|
buildColumnAad: buildColumnAad,
|
|
306
306
|
buildContextAad: buildContextAad,
|
|
307
|
+
// canonicalizeAad — the length-prefixed, sorted-keys AAD-bytes
|
|
308
|
+
// encoder. Exported (internal) so a sibling primitive that runs its
|
|
309
|
+
// own AEAD (crypto-field's per-row K_row cells) threads byte-identical
|
|
310
|
+
// AAD into encryptPacked/decryptPacked as this module does for its
|
|
311
|
+
// own seal/unseal — one canonical encoder, no drift.
|
|
312
|
+
canonicalizeAad: _canonicalize,
|
|
307
313
|
AAD_PREFIX: AAD_PREFIX,
|
|
308
314
|
AAD_VERSION: AAD_VERSION,
|
|
309
315
|
VaultAadError: VaultAadError,
|
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.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:e0214cf9-5d77-475a-af9a-e3cedff9f6d7",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-06-
|
|
8
|
+
"timestamp": "2026-06-06T21:14:16.419Z",
|
|
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.14.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.14.26",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.14.
|
|
25
|
+
"version": "0.14.26",
|
|
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.14.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.14.26",
|
|
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.14.
|
|
57
|
+
"ref": "@blamejs/core@0.14.26",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|