@blamejs/core 0.15.8 → 0.15.10
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/README.md +4 -3
- package/lib/archive-read.js +2 -1
- package/lib/archive-tar-read.js +2 -1
- package/lib/atomic-file.js +5 -0
- package/lib/bundler.js +2 -7
- package/lib/cli.js +8 -1
- package/lib/config-drift.js +19 -4
- package/lib/db-schema.js +29 -0
- package/lib/db.js +15 -2
- package/lib/http-client.js +5 -2
- package/lib/local-db-thin.js +26 -3
- package/lib/log-stream-local.js +1 -1
- package/lib/mail-scan.js +2 -5
- package/lib/middleware/clear-site-data.js +36 -11
- package/lib/mtls-ca.js +2 -2
- package/lib/numeric-bounds.js +32 -0
- package/lib/object-store/azure-blob.js +12 -1
- package/lib/object-store/gcs.js +12 -1
- package/lib/object-store/http-put.js +11 -1
- package/lib/object-store/index.js +4 -0
- package/lib/object-store/local.js +11 -1
- package/lib/object-store/sigv4.js +86 -5
- package/lib/restore-rollback.js +5 -5
- package/lib/safe-decompress.js +3 -12
- package/lib/seeders.js +33 -39
- package/lib/self-update.js +1 -1
- package/lib/session.js +64 -0
- package/lib/storage.js +71 -7
- package/lib/vault/passphrase-ops.js +3 -3
- package/lib/vault/rotate.js +4 -13
- package/lib/watcher.js +8 -0
- package/package.json +2 -2
- package/sbom.cdx.json +7 -7
|
@@ -101,7 +101,17 @@ function create(config) {
|
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
function deleteKey(key) {
|
|
104
|
+
function deleteKey(key, opts) {
|
|
105
|
+
opts = opts || {};
|
|
106
|
+
// A filesystem has no object versions; a versioned-delete request can only
|
|
107
|
+
// be a caller mistake. Refuse loudly rather than unlink the single on-disk
|
|
108
|
+
// file and report a version was erased.
|
|
109
|
+
if (opts.versionId) {
|
|
110
|
+
throw _err("VERSIONID_UNSUPPORTED",
|
|
111
|
+
"deleteKey: versioned delete (opts.versionId) is not supported on the " +
|
|
112
|
+
"filesystem backend — a local file has no version history. Use a sigv4 " +
|
|
113
|
+
"(S3 Object-Lock) backend for version erasure.", true);
|
|
114
|
+
}
|
|
105
115
|
cluster.requireLeader();
|
|
106
116
|
var full = _resolveSafe(rootDir, key);
|
|
107
117
|
if (!nodeFs.existsSync(full)) return Promise.resolve(false);
|
|
@@ -499,7 +499,15 @@ function create(config) {
|
|
|
499
499
|
var headers = _makeSigned("PUT", url, payloadHash, extra);
|
|
500
500
|
return _request("PUT", url, headers, buf, reqOpts).then(function (res) {
|
|
501
501
|
_verifySseResponse(sseRequested, res.headers);
|
|
502
|
-
return {
|
|
502
|
+
return {
|
|
503
|
+
size: buf.length,
|
|
504
|
+
etag: res.headers.etag,
|
|
505
|
+
// On a versioning-enabled (Object-Lock) bucket S3/MinIO returns the
|
|
506
|
+
// version this PUT created. Surface it so callers can target the
|
|
507
|
+
// exact version for a later versioned delete / erasure — without it
|
|
508
|
+
// the only way to find the version is a separate listVersions() call.
|
|
509
|
+
versionId: res.headers && res.headers["x-amz-version-id"] || null,
|
|
510
|
+
};
|
|
503
511
|
});
|
|
504
512
|
}
|
|
505
513
|
|
|
@@ -601,7 +609,12 @@ function create(config) {
|
|
|
601
609
|
// response may or may not echo the header depending on vendor;
|
|
602
610
|
// re-verifying here would double-fault on otherwise-fine setups.
|
|
603
611
|
var result = completeDoc.CompleteMultipartUploadResult || {};
|
|
604
|
-
return {
|
|
612
|
+
return {
|
|
613
|
+
size: totalSize,
|
|
614
|
+
etag: result.ETag || completeRes.headers.etag,
|
|
615
|
+
multipart: true,
|
|
616
|
+
versionId: completeRes.headers && completeRes.headers["x-amz-version-id"] || null,
|
|
617
|
+
};
|
|
605
618
|
} catch (e) {
|
|
606
619
|
// Abort cleans up server-side storage for the partial upload.
|
|
607
620
|
// Failures here are silently swallowed — the caller's original
|
|
@@ -639,6 +652,11 @@ function create(config) {
|
|
|
639
652
|
function getResponse(key, opts) {
|
|
640
653
|
opts = opts || {};
|
|
641
654
|
var url = _keyToUrl(key);
|
|
655
|
+
// Reading a specific version (opts.versionId) is the read half of the
|
|
656
|
+
// WORM erasure workflow — verify a protected version is present before /
|
|
657
|
+
// gone after a versioned delete. Set it before signing so the query
|
|
658
|
+
// param is in the SigV4 canonical request.
|
|
659
|
+
if (opts.versionId) url.searchParams.set("versionId", opts.versionId);
|
|
642
660
|
var headers = _makeSigned("GET", url, sha256Hex(Buffer.alloc(0)));
|
|
643
661
|
if (opts.range) {
|
|
644
662
|
headers["Range"] = "bytes=" + opts.range.start + "-" + opts.range.end;
|
|
@@ -674,8 +692,10 @@ function create(config) {
|
|
|
674
692
|
});
|
|
675
693
|
}
|
|
676
694
|
|
|
677
|
-
function head(key) {
|
|
695
|
+
function head(key, opts) {
|
|
696
|
+
opts = opts || {};
|
|
678
697
|
var url = _keyToUrl(key);
|
|
698
|
+
if (opts.versionId) url.searchParams.set("versionId", opts.versionId);
|
|
679
699
|
var headers = _makeSigned("HEAD", url, sha256Hex(Buffer.alloc(0)));
|
|
680
700
|
return _request("HEAD", url, headers, null, reqOpts).then(function (res) {
|
|
681
701
|
return {
|
|
@@ -696,9 +716,25 @@ function create(config) {
|
|
|
696
716
|
});
|
|
697
717
|
}
|
|
698
718
|
|
|
699
|
-
|
|
719
|
+
// deleteKey(key, opts?) — opts.versionId targets a specific version;
|
|
720
|
+
// opts.bypassGovernanceRetention signs x-amz-bypass-governance-retention so
|
|
721
|
+
// a GOVERNANCE-mode retention can be lifted by a caller with the permission
|
|
722
|
+
// (COMPLIANCE mode is immutable to everyone and stays refused).
|
|
723
|
+
//
|
|
724
|
+
// WORM-awareness: an UNVERSIONED delete on a versioning-enabled bucket only
|
|
725
|
+
// writes a delete-marker — the data version survives and the call still
|
|
726
|
+
// resolves true. To actually erase a version (e.g. crypto-shred / GDPR Art.
|
|
727
|
+
// 17 on an Object-Lock bucket) pass the versionId from put()/listVersions().
|
|
728
|
+
// A delete refused by an active retention surfaces as a thrown error (S3 403
|
|
729
|
+
// / MinIO 400), never a silent success, so the caller learns the version is
|
|
730
|
+
// still protected.
|
|
731
|
+
function deleteKey(key, opts) {
|
|
732
|
+
opts = opts || {};
|
|
700
733
|
var url = _keyToUrl(key);
|
|
701
|
-
|
|
734
|
+
if (opts.versionId) url.searchParams.set("versionId", opts.versionId);
|
|
735
|
+
var extra = {};
|
|
736
|
+
if (opts.bypassGovernanceRetention) extra["x-amz-bypass-governance-retention"] = "true";
|
|
737
|
+
var headers = _makeSigned("DELETE", url, sha256Hex(Buffer.alloc(0)), extra);
|
|
702
738
|
return _request("DELETE", url, headers, null, reqOpts).then(
|
|
703
739
|
function () { return true; },
|
|
704
740
|
function (e) { if (e.statusCode === 404) return false; throw e; }
|
|
@@ -953,6 +989,50 @@ function create(config) {
|
|
|
953
989
|
});
|
|
954
990
|
}
|
|
955
991
|
|
|
992
|
+
// listVersions(prefix, opts?) — enumerate every object VERSION and
|
|
993
|
+
// delete-marker under prefix (S3 ListObjectVersions / the ?versions
|
|
994
|
+
// subresource). Plain list() only sees current versions; to erase prior
|
|
995
|
+
// versions on a versioning / Object-Lock bucket you first need their
|
|
996
|
+
// versionIds, which only this call surfaces. Each item carries
|
|
997
|
+
// { key, versionId, isLatest, deleteMarker, size, lastModified, etag };
|
|
998
|
+
// deleteMarker:true rows are tombstones (no data, size null). Pagination
|
|
999
|
+
// walks (keyMarker, versionIdMarker) the way list() walks continuationToken.
|
|
1000
|
+
function listVersions(prefix, opts) {
|
|
1001
|
+
opts = opts || {};
|
|
1002
|
+
var params = { versions: "" };
|
|
1003
|
+
if (prefix) params["prefix"] = prefix;
|
|
1004
|
+
if (opts.maxResults) params["max-keys"] = String(opts.maxResults);
|
|
1005
|
+
if (opts.keyMarker) params["key-marker"] = opts.keyMarker;
|
|
1006
|
+
if (opts.versionIdMarker) params["version-id-marker"] = opts.versionIdMarker;
|
|
1007
|
+
|
|
1008
|
+
var url = _bucketUrl(params);
|
|
1009
|
+
var headers = _makeSigned("GET", url, sha256Hex(Buffer.alloc(0)));
|
|
1010
|
+
return _request("GET", url, headers, null, reqOpts).then(function (res) {
|
|
1011
|
+
var doc = safeXml.parse(res.body, LIST_PARSE_OPTS);
|
|
1012
|
+
var result = doc.ListVersionsResult || {};
|
|
1013
|
+
function _mapEntry(e, isDeleteMarker) {
|
|
1014
|
+
return {
|
|
1015
|
+
key: e.Key,
|
|
1016
|
+
versionId: e.VersionId != null ? String(e.VersionId) : null,
|
|
1017
|
+
isLatest: e.IsLatest === "true",
|
|
1018
|
+
deleteMarker: isDeleteMarker,
|
|
1019
|
+
size: isDeleteMarker ? null : (e.Size != null ? parseInt(e.Size, 10) : null),
|
|
1020
|
+
lastModified: e.LastModified ? Date.parse(e.LastModified) : null,
|
|
1021
|
+
etag: isDeleteMarker ? null : (e.ETag || null),
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
var versions = _arrayify(result.Version).map(function (v) { return _mapEntry(v, false); });
|
|
1025
|
+
var markers = _arrayify(result.DeleteMarker).map(function (m) { return _mapEntry(m, true); });
|
|
1026
|
+
var items = versions.concat(markers).filter(function (it) { return it.key; });
|
|
1027
|
+
return {
|
|
1028
|
+
items: items,
|
|
1029
|
+
truncated: result.IsTruncated === "true",
|
|
1030
|
+
keyMarker: result.NextKeyMarker || null,
|
|
1031
|
+
versionIdMarker: result.NextVersionIdMarker || null,
|
|
1032
|
+
};
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
|
|
956
1036
|
return {
|
|
957
1037
|
protocol: "sigv4",
|
|
958
1038
|
endpoint: endpoint,
|
|
@@ -966,6 +1046,7 @@ function create(config) {
|
|
|
966
1046
|
head: head,
|
|
967
1047
|
delete: deleteKey,
|
|
968
1048
|
list: list,
|
|
1049
|
+
listVersions: listVersions,
|
|
969
1050
|
presignedUploadUrl: presignedUploadUrl,
|
|
970
1051
|
presignedDownloadUrl: presignedDownloadUrl,
|
|
971
1052
|
presignedUploadPolicy: presignedUploadPolicy,
|
package/lib/restore-rollback.js
CHANGED
|
@@ -119,7 +119,7 @@ function swap(opts) {
|
|
|
119
119
|
// Step 1: rename current dataDir → rollback nodePath. Skipped on first
|
|
120
120
|
// restore (no existing dataDir).
|
|
121
121
|
if (hadDataDir) {
|
|
122
|
-
try {
|
|
122
|
+
try { atomicFile.renameWithRetry(opts.dataDir, rollbackPath); }
|
|
123
123
|
catch (e) {
|
|
124
124
|
throw new RestoreRollbackError("restore-rollback/rename-existing-failed",
|
|
125
125
|
"swap: could not move existing dataDir to rollback: " + ((e && e.message) || String(e)));
|
|
@@ -127,11 +127,11 @@ function swap(opts) {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
// Step 2: rename staging → dataDir
|
|
130
|
-
try {
|
|
130
|
+
try { atomicFile.renameWithRetry(opts.stagingDir, opts.dataDir); }
|
|
131
131
|
catch (e) {
|
|
132
132
|
// Step 2 failed — try to undo step 1 so the operator's dataDir is back
|
|
133
133
|
if (hadDataDir) {
|
|
134
|
-
try {
|
|
134
|
+
try { atomicFile.renameWithRetry(rollbackPath, opts.dataDir); }
|
|
135
135
|
catch (_e) { /* dataDir is now in rollbackPath; operator must recover manually */ }
|
|
136
136
|
}
|
|
137
137
|
throw new RestoreRollbackError("restore-rollback/rename-staging-failed",
|
|
@@ -220,7 +220,7 @@ async function rollback(opts) {
|
|
|
220
220
|
atomicFile.ensureDir(rollbackRoot);
|
|
221
221
|
discardedAt = atomicFile.pathTimestamp();
|
|
222
222
|
var discardedPath = nodePath.join(rollbackRoot, "discarded-" + discardedAt);
|
|
223
|
-
try {
|
|
223
|
+
try { atomicFile.renameWithRetry(opts.dataDir, discardedPath); }
|
|
224
224
|
catch (e) {
|
|
225
225
|
throw new RestoreRollbackError("restore-rollback/rename-existing-failed",
|
|
226
226
|
"rollback: could not move current dataDir aside: " + ((e && e.message) || String(e)));
|
|
@@ -229,7 +229,7 @@ async function rollback(opts) {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
// Rename the rollback dir back into dataDir's place
|
|
232
|
-
try {
|
|
232
|
+
try { atomicFile.renameWithRetry(opts.rollbackPath, opts.dataDir); }
|
|
233
233
|
catch (e) {
|
|
234
234
|
throw new RestoreRollbackError("restore-rollback/rollback-rename-failed",
|
|
235
235
|
"rollback: could not move rollback into dataDir: " + ((e && e.message) || String(e)) +
|
package/lib/safe-decompress.js
CHANGED
|
@@ -171,18 +171,9 @@ function safeDecompress(input, opts) {
|
|
|
171
171
|
JSON.stringify(opts.algorithm));
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
// maxOutputBytes — required, positive finite integer.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// when undefined). The numericBounds.requirePositiveFiniteInt helper
|
|
178
|
-
// would fit, but the existing call surface across the framework
|
|
179
|
-
// uses the inline shape for required-opt validation.
|
|
180
|
-
if (!numericBounds.isPositiveFiniteInt(opts.maxOutputBytes)) { // allow:inline-numeric-bounds-cascade — required (non-optional) opt; requirePositiveFiniteIntIfPresent skips when undefined
|
|
181
|
-
throw new SafeDecompressError(
|
|
182
|
-
"safe-decompress/bad-arg",
|
|
183
|
-
"safeDecompress: maxOutputBytes must be a positive finite integer; got " +
|
|
184
|
-
numericBounds.shape(opts.maxOutputBytes));
|
|
185
|
-
}
|
|
174
|
+
// maxOutputBytes — required, positive finite integer.
|
|
175
|
+
numericBounds.requirePositiveFiniteInt(opts.maxOutputBytes,
|
|
176
|
+
"safeDecompress: maxOutputBytes", SafeDecompressError, "safe-decompress/bad-arg");
|
|
186
177
|
|
|
187
178
|
// Input shape
|
|
188
179
|
var buf;
|
package/lib/seeders.js
CHANGED
|
@@ -546,46 +546,40 @@ function create(opts) {
|
|
|
546
546
|
|
|
547
547
|
try {
|
|
548
548
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
appliedAt: nowIso, rerunnable: mod.rerunnable ? 1 : 0 })
|
|
571
|
-
.toSql();
|
|
572
|
-
}
|
|
573
|
-
var writeStmt = db.prepare(writeBuilt.sql);
|
|
574
|
-
writeStmt.run.apply(writeStmt, writeBuilt.params);
|
|
575
|
-
_runSql(db, "COMMIT");
|
|
576
|
-
} catch (e) {
|
|
577
|
-
try { _runSql(db, "ROLLBACK"); }
|
|
578
|
-
catch (rollbackErr) {
|
|
579
|
-
log.debug("rollback-failed", {
|
|
580
|
-
op: "seed-apply",
|
|
581
|
-
env: env,
|
|
582
|
-
name: name,
|
|
583
|
-
error: rollbackErr && rollbackErr.message,
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
throw e;
|
|
549
|
+
// Per-seed transaction: SQLite txns are sync, but the seed's
|
|
550
|
+
// run() may be async — runInTransactionAsync wraps BEGIN/COMMIT
|
|
551
|
+
// around the awaited body and rolls back this seed only on failure.
|
|
552
|
+
await dbSchema.runInTransactionAsync(db, async function () {
|
|
553
|
+
await mod.run(db, ctx);
|
|
554
|
+
var nowIso = new Date(clock()).toISOString();
|
|
555
|
+
var writeBuilt;
|
|
556
|
+
if (alreadyApplied && mod.rerunnable) {
|
|
557
|
+
writeBuilt = sql.update(_seedersTable(), _sqlOpts(db))
|
|
558
|
+
.set({ appliedAt: nowIso, description: mod.description || "",
|
|
559
|
+
rerunnable: mod.rerunnable ? 1 : 0 })
|
|
560
|
+
.where("env", env).where("name", name).toSql();
|
|
561
|
+
} else if (alreadyApplied && force) {
|
|
562
|
+
writeBuilt = sql.update(_seedersTable(), _sqlOpts(db))
|
|
563
|
+
.set({ appliedAt: nowIso, description: mod.description || "" })
|
|
564
|
+
.where("env", env).where("name", name).toSql();
|
|
565
|
+
} else {
|
|
566
|
+
writeBuilt = sql.insert(_seedersTable(), _sqlOpts(db))
|
|
567
|
+
.values({ env: env, name: name, description: mod.description || "",
|
|
568
|
+
appliedAt: nowIso, rerunnable: mod.rerunnable ? 1 : 0 })
|
|
569
|
+
.toSql();
|
|
587
570
|
}
|
|
588
|
-
|
|
571
|
+
var writeStmt = db.prepare(writeBuilt.sql);
|
|
572
|
+
writeStmt.run.apply(writeStmt, writeBuilt.params);
|
|
573
|
+
}, {
|
|
574
|
+
onRollbackFail: function (rollbackErr) {
|
|
575
|
+
log.debug("rollback-failed", {
|
|
576
|
+
op: "seed-apply",
|
|
577
|
+
env: env,
|
|
578
|
+
name: name,
|
|
579
|
+
error: rollbackErr && rollbackErr.message,
|
|
580
|
+
});
|
|
581
|
+
},
|
|
582
|
+
});
|
|
589
583
|
applied.push(name);
|
|
590
584
|
appliedSet.add(name);
|
|
591
585
|
|
package/lib/self-update.js
CHANGED
|
@@ -676,7 +676,7 @@ async function swap(opts) {
|
|
|
676
676
|
// don't silently lose both binaries (the prior best-effort comment
|
|
677
677
|
// swallowed the rollback exception — SSDF RV.1 violation).
|
|
678
678
|
try {
|
|
679
|
-
|
|
679
|
+
atomicFile.renameWithRetry(from, to);
|
|
680
680
|
} catch (e) {
|
|
681
681
|
if (e && e.code === "EXDEV") {
|
|
682
682
|
// Cross-device — copy + unlink. Use atomicFile.copy for the safety
|
package/lib/session.js
CHANGED
|
@@ -65,6 +65,9 @@ var { SessionError } = require("./framework-error");
|
|
|
65
65
|
// the cookie-side sid so the wire token is ciphertext rather than
|
|
66
66
|
// plaintext (sealed-cookie default since v0.8.61).
|
|
67
67
|
var vault = lazyRequire(function () { return require("./vault"); });
|
|
68
|
+
// Lazy — b.session.logout composes the Clear-Site-Data header builder; keep it
|
|
69
|
+
// out of the boot require graph (no cycle, but session is a low-level primitive).
|
|
70
|
+
var clearSiteData = lazyRequire(function () { return require("./middleware/clear-site-data"); });
|
|
68
71
|
|
|
69
72
|
// Pluggable session-storage backend. Default uses cluster-storage (which
|
|
70
73
|
// in turn dispatches to the framework's main DB or external DB). An
|
|
@@ -705,6 +708,66 @@ async function destroy(token) {
|
|
|
705
708
|
return await _deleteBySidHash(_hashSid(sid));
|
|
706
709
|
}
|
|
707
710
|
|
|
711
|
+
/**
|
|
712
|
+
* @primitive b.session.logout
|
|
713
|
+
* @signature b.session.logout(res, token, opts?)
|
|
714
|
+
* @since 0.15.9
|
|
715
|
+
* @status stable
|
|
716
|
+
* @related b.session.destroy, b.middleware.clearSiteData
|
|
717
|
+
*
|
|
718
|
+
* Secure logout in one call: destroy the server-side session AND tell the
|
|
719
|
+
* browser to wipe its client-side state. It emits an RFC 9527 Clear-Site-Data
|
|
720
|
+
* response header (cookies + storage + cache + executionContexts by default)
|
|
721
|
+
* and expires the session cookie, then destroys the session row. `destroy()`
|
|
722
|
+
* alone is a store operation with no `res`, so it cannot wipe the browser's
|
|
723
|
+
* cached pages / storage / any stale tab still holding the now-revoked cookie;
|
|
724
|
+
* this composes the secure-default logout the middleware otherwise had to be
|
|
725
|
+
* mounted by hand. Returns whether a session was destroyed. Leader-only.
|
|
726
|
+
*
|
|
727
|
+
* @opts
|
|
728
|
+
* cookieName: string, // default: "sid" — the session cookie to expire
|
|
729
|
+
* types: string[], // default: the RFC 9527 Clear-Site-Data directive set
|
|
730
|
+
*
|
|
731
|
+
* @example
|
|
732
|
+
* app.post("/logout", async function (req, res) {
|
|
733
|
+
* await b.session.logout(res, req.cookies.sid);
|
|
734
|
+
* res.end("logged out");
|
|
735
|
+
* });
|
|
736
|
+
* // → emits Clear-Site-Data + expires the sid cookie + destroys the session
|
|
737
|
+
*/
|
|
738
|
+
async function logout(res, token, opts) {
|
|
739
|
+
if (!res || typeof res.setHeader !== "function") {
|
|
740
|
+
throw new SessionError("session/bad-res",
|
|
741
|
+
"b.session.logout: res must be an HTTP response with setHeader()");
|
|
742
|
+
}
|
|
743
|
+
opts = opts || {};
|
|
744
|
+
var cookieName = opts.cookieName === undefined ? "sid" : opts.cookieName;
|
|
745
|
+
if (typeof cookieName !== "string" || cookieName.length === 0) {
|
|
746
|
+
throw new SessionError("session/bad-cookie-name",
|
|
747
|
+
"b.session.logout: opts.cookieName must be a non-empty string");
|
|
748
|
+
}
|
|
749
|
+
var csd = clearSiteData();
|
|
750
|
+
var types = opts.types === undefined ? csd.DEFAULT_TYPES : opts.types;
|
|
751
|
+
// Build (and validate) the RFC 9527 header BEFORE any side effect — an
|
|
752
|
+
// unknown directive throws here, queuing nothing.
|
|
753
|
+
var clearSiteDataValue = csd.headerValue(types, "b.session.logout");
|
|
754
|
+
|
|
755
|
+
// Revoke the server-side session FIRST. If destroy() throws (a follower
|
|
756
|
+
// failing cluster.requireLeader(), or a store/DB error), no client-wipe
|
|
757
|
+
// headers have been queued — an error response can't then expire the
|
|
758
|
+
// browser cookie + Clear-Site-Data while the session row is still live,
|
|
759
|
+
// which would leave a copied token usable server-side.
|
|
760
|
+
var destroyed = await destroy(token);
|
|
761
|
+
|
|
762
|
+
// Now wipe the client-side state: RFC 9527 Clear-Site-Data (cookies /
|
|
763
|
+
// storage / cache) + expire the session cookie (belt-and-suspenders with the
|
|
764
|
+
// "cookies" directive, and effective even if the client ignores the header).
|
|
765
|
+
res.setHeader("Clear-Site-Data", clearSiteDataValue);
|
|
766
|
+
res.setHeader("Set-Cookie",
|
|
767
|
+
cookieName + "=; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=0");
|
|
768
|
+
return destroyed;
|
|
769
|
+
}
|
|
770
|
+
|
|
708
771
|
async function _deleteBySidHash(sidHash) {
|
|
709
772
|
var built = sql.delete(_sessionSqlTable(), _sessionSqlOpts())
|
|
710
773
|
.where("sidHash", sidHash)
|
|
@@ -1245,6 +1308,7 @@ module.exports = {
|
|
|
1245
1308
|
create: create,
|
|
1246
1309
|
verify: verify,
|
|
1247
1310
|
destroy: destroy,
|
|
1311
|
+
logout: logout,
|
|
1248
1312
|
destroyAllForUser: destroyAllForUser,
|
|
1249
1313
|
touch: touch,
|
|
1250
1314
|
rotate: rotate,
|
package/lib/storage.js
CHANGED
|
@@ -425,7 +425,7 @@ async function getFileStream(key, sealedKey, opts) {
|
|
|
425
425
|
* @example
|
|
426
426
|
* b.storage.init({ backend: "local", uploadDir: "./data/uploads" });
|
|
427
427
|
* var saved = await b.storage.saveRaw(Buffer.from("public-bytes"), "logo.png");
|
|
428
|
-
* // → { storedPath: "logo.png", backend: "default" }
|
|
428
|
+
* // → { storedPath: "logo.png", backend: "default", versionId: null }
|
|
429
429
|
*/
|
|
430
430
|
async function saveRaw(buffer, key, opts) {
|
|
431
431
|
_requireInit();
|
|
@@ -443,7 +443,9 @@ async function saveRaw(buffer, key, opts) {
|
|
|
443
443
|
raw: true,
|
|
444
444
|
},
|
|
445
445
|
});
|
|
446
|
-
|
|
446
|
+
// versionId is non-null only on a versioning-enabled (S3 Object-Lock)
|
|
447
|
+
// backend; capture it to target the exact version for later erasure.
|
|
448
|
+
return { storedPath: key, backend: picked.backend.name, versionId: result.versionId || null };
|
|
447
449
|
}
|
|
448
450
|
|
|
449
451
|
/**
|
|
@@ -486,14 +488,24 @@ async function getRawBuffer(key, opts) {
|
|
|
486
488
|
* Remove `key` from the routed backend. Returns `true` when the
|
|
487
489
|
* object existed and was removed, `false` when it was already
|
|
488
490
|
* absent. Emits `system.storage.delete` with `{ backend, key,
|
|
489
|
-
* existed }` so the audit chain records GDPR right-to-erasure
|
|
491
|
+
* existed, versionId }` so the audit chain records GDPR right-to-erasure
|
|
490
492
|
* flows. The sealed encryption key the caller persisted alongside
|
|
491
493
|
* the row should be discarded by the caller after a successful
|
|
492
494
|
* delete — without the bytes, the key has no recovery value.
|
|
493
495
|
*
|
|
496
|
+
* On a versioning-enabled (S3 Object-Lock) backend an unversioned
|
|
497
|
+
* delete only writes a delete-marker — the data version survives. To
|
|
498
|
+
* erase a specific version pass `versionId` (from `saveRaw`'s return or
|
|
499
|
+
* `listVersions`); a version under an active retention is refused (the
|
|
500
|
+
* call throws), and `bypassGovernanceRetention` lifts a GOVERNANCE-mode
|
|
501
|
+
* retention for callers with the permission (COMPLIANCE stays immutable).
|
|
502
|
+
* `versionId` is S3/sigv4-only and is refused on other backends.
|
|
503
|
+
*
|
|
494
504
|
* @opts
|
|
495
505
|
* classification: string, // route to a backend serving this classification
|
|
496
506
|
* backend: string, // explicit backend by name
|
|
507
|
+
* versionId: string, // erase a specific object version (S3 Object-Lock)
|
|
508
|
+
* bypassGovernanceRetention: boolean, // lift GOVERNANCE retention (not COMPLIANCE)
|
|
497
509
|
*
|
|
498
510
|
* @example
|
|
499
511
|
* b.storage.init({ backend: "local", uploadDir: "./data/uploads" });
|
|
@@ -507,12 +519,16 @@ async function deleteFile(key, opts) {
|
|
|
507
519
|
_requireInit();
|
|
508
520
|
opts = opts || {};
|
|
509
521
|
var picked = _pickBackend(opts);
|
|
510
|
-
var result = await picked.backend.delete(key
|
|
522
|
+
var result = await picked.backend.delete(key, {
|
|
523
|
+
versionId: opts.versionId,
|
|
524
|
+
bypassGovernanceRetention: opts.bypassGovernanceRetention,
|
|
525
|
+
});
|
|
511
526
|
_emit("system.storage.delete", {
|
|
512
527
|
metadata: {
|
|
513
|
-
backend:
|
|
514
|
-
key:
|
|
515
|
-
existed:
|
|
528
|
+
backend: picked.backend.name,
|
|
529
|
+
key: key,
|
|
530
|
+
existed: result,
|
|
531
|
+
versionId: opts.versionId || null,
|
|
516
532
|
},
|
|
517
533
|
});
|
|
518
534
|
return result;
|
|
@@ -556,6 +572,53 @@ async function exists(key, opts) {
|
|
|
556
572
|
}
|
|
557
573
|
}
|
|
558
574
|
|
|
575
|
+
/**
|
|
576
|
+
* @primitive b.storage.listVersions
|
|
577
|
+
* @signature b.storage.listVersions(prefix, opts?)
|
|
578
|
+
* @since 0.15.10
|
|
579
|
+
* @status stable
|
|
580
|
+
* @compliance gdpr, sox-404, soc2
|
|
581
|
+
* @related b.storage.deleteFile, b.storage.saveRaw, b.worm.create
|
|
582
|
+
*
|
|
583
|
+
* Enumerate every object VERSION and delete-marker under `prefix` on a
|
|
584
|
+
* versioning-enabled (S3 Object-Lock) backend. Plain reads only see the
|
|
585
|
+
* current version; right-to-erasure / crypto-shred on an Object-Lock
|
|
586
|
+
* bucket must target prior versions by `versionId`, which only this call
|
|
587
|
+
* surfaces. Each item is `{ key, versionId, isLatest, deleteMarker, size,
|
|
588
|
+
* lastModified, etag }`; `deleteMarker: true` rows are tombstones with no
|
|
589
|
+
* data. Pair with `deleteFile(key, { versionId })` to erase a version.
|
|
590
|
+
*
|
|
591
|
+
* Versioning is an S3/sigv4 feature — a backend without a version surface
|
|
592
|
+
* (filesystem, and the current Azure/GCS adapters) throws
|
|
593
|
+
* `VERSIONS_UNSUPPORTED` rather than silently returning the current view,
|
|
594
|
+
* so an erasure workflow can never mistake a single-version backend for a
|
|
595
|
+
* fully-enumerated one.
|
|
596
|
+
*
|
|
597
|
+
* @opts
|
|
598
|
+
* classification: string, // route to a backend serving this classification
|
|
599
|
+
* backend: string, // explicit backend by name
|
|
600
|
+
* maxResults: number, // page size
|
|
601
|
+
* keyMarker: string, // pagination cursor (from a prior page)
|
|
602
|
+
* versionIdMarker: string, // pagination cursor (from a prior page)
|
|
603
|
+
*
|
|
604
|
+
* @example
|
|
605
|
+
* var page = await b.storage.listVersions("filings/2026/");
|
|
606
|
+
* for (var v of page.items) {
|
|
607
|
+
* if (!v.isLatest) await b.storage.deleteFile(v.key, { versionId: v.versionId });
|
|
608
|
+
* }
|
|
609
|
+
*/
|
|
610
|
+
async function listVersions(prefix, opts) {
|
|
611
|
+
_requireInit();
|
|
612
|
+
opts = opts || {};
|
|
613
|
+
var picked = _pickBackend(opts);
|
|
614
|
+
if (typeof picked.backend.listVersions !== "function") {
|
|
615
|
+
throw _err("VERSIONS_UNSUPPORTED",
|
|
616
|
+
"listVersions: backend '" + picked.backend.name + "' has no version surface " +
|
|
617
|
+
"(S3/sigv4 only). A filesystem / single-version backend cannot enumerate versions.", true);
|
|
618
|
+
}
|
|
619
|
+
return picked.backend.listVersions(prefix, opts);
|
|
620
|
+
}
|
|
621
|
+
|
|
559
622
|
/**
|
|
560
623
|
* @primitive b.storage.listBackends
|
|
561
624
|
* @signature b.storage.listBackends()
|
|
@@ -1266,6 +1329,7 @@ module.exports = {
|
|
|
1266
1329
|
getRawBuffer: getRawBuffer,
|
|
1267
1330
|
deleteFile: deleteFile,
|
|
1268
1331
|
exists: exists,
|
|
1332
|
+
listVersions: listVersions,
|
|
1269
1333
|
presignedUploadUrl: presignedUploadUrl,
|
|
1270
1334
|
presignedDownloadUrl: presignedDownloadUrl,
|
|
1271
1335
|
presignedUploadPolicy: presignedUploadPolicy,
|
|
@@ -169,7 +169,7 @@ async function seal(opts) {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
// Step 3: atomic rename sealed.tmp → sealed
|
|
172
|
-
|
|
172
|
+
atomicFile.renameWithRetry(p.sealedTmp, p.sealed);
|
|
173
173
|
atomicFile.fsyncDir(opts.dataDir);
|
|
174
174
|
|
|
175
175
|
// Step 4: delete plaintext (unless keepPlaintext)
|
|
@@ -220,7 +220,7 @@ async function unseal(opts) {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
// Step 3: atomic rename plaintext.tmp → plaintext
|
|
223
|
-
|
|
223
|
+
atomicFile.renameWithRetry(p.plaintextTmp, p.plaintext);
|
|
224
224
|
atomicFile.fsyncDir(opts.dataDir);
|
|
225
225
|
|
|
226
226
|
// Step 4: delete sealed file
|
|
@@ -293,7 +293,7 @@ async function rotate(opts) {
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
// Step 3: atomic rename — swap in the new sealed file
|
|
296
|
-
|
|
296
|
+
atomicFile.renameWithRetry(p.sealedTmp, p.sealed);
|
|
297
297
|
atomicFile.fsyncDir(opts.dataDir);
|
|
298
298
|
|
|
299
299
|
return { sealedPath: p.sealed };
|
package/lib/vault/rotate.js
CHANGED
|
@@ -544,12 +544,6 @@ function _walkAndReSeal(node, oldKeys, newKeys) {
|
|
|
544
544
|
return { value: node, changed: false };
|
|
545
545
|
}
|
|
546
546
|
|
|
547
|
-
// Transaction-control statements only (BEGIN / COMMIT / ROLLBACK) - fixed
|
|
548
|
-
// keywords, no identifier / value, so they stay verbatim rather than route
|
|
549
|
-
// through b.sql (the builder has no transaction-control verb). The param is
|
|
550
|
-
// named `stmtText` so it does not shadow the module-level `sql` builder.
|
|
551
|
-
function _runStmt(db, stmtText) { db.prepare(stmtText).run(); }
|
|
552
|
-
|
|
553
547
|
function _rotateColumn(db, table, column, schema, roots, batchSize, progress) {
|
|
554
548
|
// Every statement composes through b.sql (sqlite dialect, quoteName so
|
|
555
549
|
// the concrete handle's table is quoted, not left bare for a cluster
|
|
@@ -655,8 +649,9 @@ function _rotateOverflow(db, table, oldKeys, newKeys, batchSize, progress, warni
|
|
|
655
649
|
var rows = sel.all(lastId);
|
|
656
650
|
if (rows.length === 0) break;
|
|
657
651
|
|
|
658
|
-
|
|
659
|
-
|
|
652
|
+
// One transaction per page via the shared wrapper — the rotate worker
|
|
653
|
+
// no longer hand-rolls the BEGIN/COMMIT/ROLLBACK skeleton.
|
|
654
|
+
dbSchema.runInTransaction(db, function () {
|
|
660
655
|
for (var i = 0; i < rows.length; i++) {
|
|
661
656
|
var row = rows[i];
|
|
662
657
|
var doc;
|
|
@@ -671,11 +666,7 @@ function _rotateOverflow(db, table, oldKeys, newKeys, batchSize, progress, warni
|
|
|
671
666
|
var rv = _walkAndReSeal(doc, oldKeys, newKeys);
|
|
672
667
|
if (rv.changed) upd.run(JSON.stringify(rv.value), row._id);
|
|
673
668
|
}
|
|
674
|
-
|
|
675
|
-
} catch (e) {
|
|
676
|
-
_runStmt(db, "ROLLBACK");
|
|
677
|
-
throw e;
|
|
678
|
-
}
|
|
669
|
+
});
|
|
679
670
|
processed += rows.length;
|
|
680
671
|
lastId = rows[rows.length - 1]._id;
|
|
681
672
|
_emit(progress, { phase: "rotate_overflow", table: table, rowsProcessed: processed, rowsTotal: total });
|
package/lib/watcher.js
CHANGED
|
@@ -316,6 +316,14 @@ function create(opts) {
|
|
|
316
316
|
_validateOpts(opts);
|
|
317
317
|
|
|
318
318
|
var root = nodePath.resolve(opts.root);
|
|
319
|
+
// Canonicalize to the real long path. On Windows a path with an 8.3
|
|
320
|
+
// short-name component (os.tmpdir() commonly resolves to C:\Users\RUNNER~1\…)
|
|
321
|
+
// makes the native recursive backend (ReadDirectoryChangesW) deliver
|
|
322
|
+
// long-name event paths that no longer prefix-match the watched root, which
|
|
323
|
+
// trips a libuv fs-event assertion and aborts the process on some Node builds.
|
|
324
|
+
// realpathSync.native expands short names and resolves symlinks; guarded so a
|
|
325
|
+
// non-existent root still falls through to the watcher's own not-found error.
|
|
326
|
+
try { root = nodeFs.realpathSync.native(root); } catch (_e) { /* keep resolved path */ }
|
|
319
327
|
var debounceMs = (opts.debounceMs !== undefined) ? opts.debounceMs : DEFAULT_DEBOUNCE_MS;
|
|
320
328
|
var maxPending = (opts.maxPending !== undefined) ? opts.maxPending : DEFAULT_MAX_PENDING;
|
|
321
329
|
var requestedMode = opts.mode || "fs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/core",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.10",
|
|
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.16.0"
|
|
58
58
|
},
|
|
59
59
|
"files": [
|
|
60
60
|
"index.js",
|