@blamejs/blamejs-shop 0.4.48 → 0.4.50
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/asset-manifest.json +1 -1
- package/lib/inventory-audits.js +5 -0
- package/lib/inventory-locations.js +66 -11
- package/lib/inventory-writeoffs.js +4 -0
- package/lib/vendor/MANIFEST.json +58 -46
- package/lib/vendor/blamejs/.github/workflows/ci.yml +134 -1
- package/lib/vendor/blamejs/.gitignore +5 -1
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/README.md +1 -1
- package/lib/vendor/blamejs/SECURITY.md +3 -1
- package/lib/vendor/blamejs/api-snapshot.json +10 -2
- package/lib/vendor/blamejs/lib/bundler.js +2 -7
- package/lib/vendor/blamejs/lib/config-drift.js +17 -3
- package/lib/vendor/blamejs/lib/crypto-field.js +30 -0
- package/lib/vendor/blamejs/lib/db-declare-row-policy.js +20 -1
- package/lib/vendor/blamejs/lib/db-schema.js +29 -0
- package/lib/vendor/blamejs/lib/db.js +7 -0
- package/lib/vendor/blamejs/lib/guard-csv.js +13 -4
- package/lib/vendor/blamejs/lib/local-db-thin.js +23 -1
- package/lib/vendor/blamejs/lib/mail-bimi.js +16 -3
- package/lib/vendor/blamejs/lib/mail-scan.js +2 -5
- package/lib/vendor/blamejs/lib/mail.js +16 -9
- package/lib/vendor/blamejs/lib/mcp.js +28 -6
- package/lib/vendor/blamejs/lib/middleware/bot-disclose.js +7 -5
- package/lib/vendor/blamejs/lib/middleware/speculation-rules.js +6 -4
- package/lib/vendor/blamejs/lib/numeric-bounds.js +32 -0
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +12 -1
- package/lib/vendor/blamejs/lib/object-store/gcs.js +12 -1
- package/lib/vendor/blamejs/lib/object-store/http-put.js +11 -1
- package/lib/vendor/blamejs/lib/object-store/index.js +4 -0
- package/lib/vendor/blamejs/lib/object-store/local.js +11 -1
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +86 -5
- package/lib/vendor/blamejs/lib/parsers/safe-env.js +6 -3
- package/lib/vendor/blamejs/lib/parsers/safe-yaml.js +6 -6
- package/lib/vendor/blamejs/lib/safe-buffer.js +69 -1
- package/lib/vendor/blamejs/lib/safe-decompress.js +3 -12
- package/lib/vendor/blamejs/lib/seeders.js +33 -39
- package/lib/vendor/blamejs/lib/storage.js +71 -7
- package/lib/vendor/blamejs/lib/vault/rotate.js +4 -13
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.10.json +53 -0
- package/lib/vendor/blamejs/release-notes/v0.15.11.json +52 -0
- package/lib/vendor/blamejs/test/integration/object-store-worm-lock.test.js +90 -16
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +150 -39
- package/lib/vendor/blamejs/test/layer-0-primitives/config-drift.test.js +19 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-aad-downgrade.test.js +96 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-transaction.test.js +110 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/declare-row-policy.test.js +43 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/local-db-thin.test.js +28 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mcp.test.js +25 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/numeric-bounds.test.js +29 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/object-store-versioned-delete.test.js +97 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-buffer-linear-scans.test.js +94 -0
- package/lib/vendor/blamejs/test/layer-5-integration/bundler-output.test.js +52 -0
- package/package.json +1 -1
|
@@ -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
|
|
|
@@ -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,
|
|
@@ -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 });
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.15.10",
|
|
4
|
+
"date": "2026-06-13",
|
|
5
|
+
"headline": "Makes S3 Object-Lock version erasure reachable through the object store, and pins the build toolchain's native binary to a reviewed hash",
|
|
6
|
+
"summary": "The object store gains the versioned-delete surface its S3 Object Lock support always needed for real erasure. An unversioned delete on a versioning-enabled (Object-Lock) bucket only writes a delete-marker — the data version survives — so the framework's own delete could report success while a record protected for compliance, or one a data subject asked to erase, stayed on disk. b.objectStore / b.storage now carry a versionId: put and saveRaw return the version they created, deleteFile(key, { versionId, bypassGovernanceRetention }) targets a specific version (refused — not silently delete-markered — when it is under an active retention), and listVersions enumerates versions and delete-markers so an erasure workflow can find them. Backends with no version surface (the filesystem backend, and the current Azure and GCS adapters) refuse a versioned delete loudly rather than silently dropping the current object. Separately, the build toolchain's native bundler binary is now verified against a reviewed SHA-256 pin so a tampered or drifted binary is caught before it bundles the framework.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "Versioned object delete + listVersions for S3 Object-Lock erasure",
|
|
13
|
+
"body": "b.storage.deleteFile and the b.objectStore sigv4 backend now accept opts.versionId to erase a specific object version, and opts.bypassGovernanceRetention to lift a GOVERNANCE-mode retention for callers with the permission (COMPLIANCE stays immutable to everyone). b.storage.saveRaw and the backend put now return the versionId they created on a versioning-enabled bucket, and a new b.storage.listVersions(prefix) / backend listVersions enumerates every version and delete-marker (key, versionId, isLatest, deleteMarker, size, lastModified, etag) so a right-to-erasure or crypto-shred workflow can target prior versions. On a backend with no version surface, listVersions throws VERSIONS_UNSUPPORTED and a versioned delete throws VERSIONID_UNSUPPORTED rather than silently acting on the current object."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "b.localDb.thin reaches SQLite resource-limit parity (limits option)",
|
|
17
|
+
"body": "b.localDb.thin now opens its node:sqlite handle with the same parse-time statement-size cap as b.db and the CLI — a SQL statement over 1 MiB is rejected at parse time, the SQLITE_LIMIT_LENGTH floor that guards prepare()/exec() of raw SQL against an attacker-influenced megaquery (SQLite's default is 1 GB). The cap is on by default; a new limits option (e.g. { sqlLength: 2 * 1024 * 1024 } or other SQLITE_LIMIT_* keys) lets an operator raise or extend it. Previously the thin opener had no limits plumbing, so a consumer on that path could not reach parity with the rest of the framework's SQLite surface."
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"heading": "Fixed",
|
|
23
|
+
"items": [
|
|
24
|
+
{
|
|
25
|
+
"title": "S3 Object-Lock version erasure is reachable through the framework delete path",
|
|
26
|
+
"body": "On a versioning-enabled (Object-Lock) bucket, an unversioned DELETE only writes a delete-marker — the protected data version survives untouched — yet the framework's delete had no versionId surface, so it issued the unversioned form and reported success while the bytes the lock protects remained. A retention or legal hold could therefore look enforced to the framework caller while the operation WORM actually blocks was unreachable. The delete path now targets the exact version: deleting a version under a COMPLIANCE retention is refused (it throws, even with bypassGovernanceRetention), a no-retention version erases cleanly, and the enforcement is proven end-to-end against MinIO through the framework's own API."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"title": "b.configDrift.verifyVendorIntegrity is now working-directory-independent",
|
|
30
|
+
"body": "The vendored-dependency integrity check resolved each manifest file path against process.cwd(), so it only worked when run from the application root. Run from anywhere else it read-failed every entry (reporting ok:false), and under a crafted working directory that happened to contain a clean vendor tree it could hash a different tree than the one actually loaded. It now resolves each file under the framework's own vendor directory by default — the tree loaded at runtime — and honors an explicit libVendorDir for verifying a deployed tree elsewhere, so the result no longer depends on where the process was started."
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"heading": "Security",
|
|
36
|
+
"items": [
|
|
37
|
+
{
|
|
38
|
+
"title": "Build toolchain native binary pinned to a reviewed hash",
|
|
39
|
+
"body": "The native bundler binary the build toolchain runs (esbuild's per-platform compiler, a development dependency that never ships in the runtime) is now verified against a SHA-256 pin captured by diffing the published package tarballs and hashing the binary. The build gate fails if the on-disk binary does not match the reviewed hash for its (version, platform); for a version that has not been reviewed it notes the gap and skips rather than trusting an unverified binary. A cross-artifact check keeps the version in agreement across package.json, the CI install step, and the hash map, so the gate can never quietly test a version that was never diffed — closing a real drift where CI had been installing an older patch than package.json declared. The reviewed diff is benign: version strings plus an installer size-bound and error-message hardening, no new install hooks, files, or network paths, and no runtime-dependency impact."
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"heading": "Detectors",
|
|
45
|
+
"items": [
|
|
46
|
+
{
|
|
47
|
+
"title": "Object-store erasure guard, esbuild-pin agreement, + structural re-anchoring of the lint detectors",
|
|
48
|
+
"body": "A new guard locks the object-store delete path to the versioned-erasure contract: b.storage.deleteFile must thread versionId to the backend, so it can never silently revert to the WORM-blind unversioned delete. A second guard enforces that the esbuild build-tool version agrees across package.json, the CI install step, and the binary-hash map, so a future bump can't update one and leave the gate testing an unreviewed version. Separately, the framework's internal codebase-pattern lint detectors were re-anchored from fixed character spans to structural code boundaries, so they keep matching the code they guard as those functions grow rather than aging out of range; reviving them surfaced a few internal validation and transaction sites that now route through shared helpers (a required positive-integer-with-range validator and an async transaction wrapper) instead of hand-rolling the check. No public API change."
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.15.11",
|
|
4
|
+
"date": "2026-06-14",
|
|
5
|
+
"headline": "Replaces a family of quadratic-time regexes that hostile input could use to stall a worker with linear scans, refuses a relocatable sealed-cell downgrade on the read side, fails closed when enabling row-level security behind a non-native driver, and scans the vendored crypto for known CVEs on every build",
|
|
6
|
+
"summary": "Several text-handling primitives stripped trailing whitespace or extracted a mail address with a regex whose backtracking is quadratic in the input length on adversarial strings — a request body, a YAML document, a CSV cell, or a From header crafted as a long run of spaces could pin a worker's CPU. Each is now a linear character scan with identical output. The HTML-content check the MCP tool surface applies gained the vbscript: and data:text/html vectors it was missing. On the data-at-rest side, an AAD-bound (or per-row-key) column now refuses a plain, unbound vault cell on read — a relocatable envelope an attacker with write access could copy in from another row defeats the cross-row binding, so the field is nulled rather than surfaced; operators mid-migration opt back in with registerTable({ allowPlainMigration: true }). declareRowPolicy now treats row-level-security as enabled only on a value that unambiguously means true, so a non-native Postgres driver that returns the string \"f\" can no longer be read as \"already on\" and silently skip the ENABLE that protects the table's rows. Finally, because the framework's crypto is vendored rather than installed, npm audit and Dependabot never see it: every build now matches the vendored versions against the OSV vulnerability database, with a complementary Semgrep pass and workflow-file static analysis alongside.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "b.safeBuffer.indexAfterOpenTag(html, tagName)",
|
|
13
|
+
"body": "A linear helper that returns the offset just past a `<tag ...>` opening tag (case-insensitive), or -1 when absent or unterminated — the insertion point a response rewriter uses to splice content after <body> or <head> without a regex. It replaces the O(n^2) html.match(/<body[^>]*>/i) shape and is stricter than it: a real tag boundary is required after the name, so <bodyfoo> is not mistaken for <body>."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"heading": "Security",
|
|
19
|
+
"items": [
|
|
20
|
+
{
|
|
21
|
+
"title": "Linear-time replacements across a family of quadratic regexes (ReDoS class)",
|
|
22
|
+
"body": "Several primitives located or stripped text with a regex whose backtracking is quadratic in V8 on adversarial input (CWE-1333): b.safeBuffer and the safe-env / safe-yaml / guard-csv parsers stripped trailing horizontal whitespace with /[ \\t]+$/; b.mail extracted the address from a `Name <addr>` header with /<([^>]+)>/; the bot-disclosure and speculation-rules response middleware found the <body> insertion point with /<body[^>]*>/i; and the BIMI certificate-chain splitter walked PEM blocks with a lazy /BEGIN[\\s\\S]*?END/ scan. A crafted field — a long run of spaces, an unterminated bracket, a body carrying many <body starts with no closing >, a chain of BEGIN markers — could drive a worker's CPU to seconds of work. Each is now a linear scan: a shared b.safeBuffer.stripTrailingHspace (backward char walk), b.safeBuffer.indexAfterOpenTag (forward indexOf walk for the tag insertion point), a forward indexOf for address extraction, and an indexOf walk for the PEM split. Output is byte-identical (the tag-find is stricter — it no longer mistakes <bodyfoo> for <body>), and 400K-character adversarial inputs that took 8–85 seconds now complete in under 2 ms."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"title": "MCP HTML-content check covers vbscript: and data:text/html",
|
|
26
|
+
"body": "The dangerous-markup check applied to text/html tool content matched <script>/<iframe>/<object>/<embed> and javascript: URLs but not the vbscript: scheme or data:text/html payloads. Both are now refused; data: URLs carrying non-HTML media (data:image/png and similar) are unaffected."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"title": "AAD-bound columns refuse a plain sealed cell on read",
|
|
30
|
+
"body": "b.cryptoField.unsealRow now refuses a plain, unbound vault: envelope found on an AAD-bound (or per-row-key) column and nulls the field instead of returning it. A plain envelope carries no per-cell binding, so a writer who could place one — copied from anywhere under the same vault root — would otherwise relocate a value across rows or columns and defeat the copy-protection the AAD binding advertises. Operators migrating pre-AAD rows up to bound ciphertext opt into a bounded acceptance window with registerTable({ allowPlainMigration: true }) and clear it when migration completes."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"title": "Row-level-security enablement fails closed on non-native drivers",
|
|
34
|
+
"body": "b.db.declareRowPolicy read pg_class.relrowsecurity to skip a redundant ENABLE ROW LEVEL SECURITY, but tested it with a bare truthiness check. A native pg driver returns a JS boolean; a proxy or ORM may return the string \"f\" for a disabled table — and \"f\" is truthy, so the check read it as already-enabled and silently skipped the ENABLE, leaving every row in the table unprotected while the migration reported success. RLS now counts as enabled only on a value that unambiguously means true (true, 1, or \"t\"/\"true\"/\"1\"/\"on\"/\"yes\"); every other shape re-issues ENABLE, a harmless no-op on an already-enabled table."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"title": "Vendored-crypto CVE scanning, complementary SAST, and workflow static analysis in CI",
|
|
38
|
+
"body": "The framework ships zero npm runtime dependencies — its crypto (the noble suite, the WebAuthn server, the PKI layer) is vendored under lib/vendor/, where npm audit, Dependabot, and Socket cannot see it. Every build now generates a CycloneDX SBOM of the vendored tree (each library carrying an npm purl) and runs it through OSV-Scanner, matching the exact pinned version against the OSV vulnerability database; a published CVE or GHSA affecting a vendored version fails the build so the copy is refreshed before merge. A Semgrep pass (registry security-audit + javascript packs at ERROR severity) runs alongside CodeQL as complementary SAST, and actionlint statically checks the workflow files. All three install the OSS tool from its upstream release, matching the existing secret-scan gate's posture."
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"heading": "Detectors",
|
|
44
|
+
"items": [
|
|
45
|
+
{
|
|
46
|
+
"title": "Quadratic trailing-whitespace and tag-find regex detectors",
|
|
47
|
+
"body": "Two codebase-pattern detectors refuse reintroduction of the quadratic shapes: the /[ \\t]+$/ trailing-whitespace strip (as .replace, .test, or via the named TRAILING_HSPACE_RE export) outside the linear helper that owns it, and the str.match(/<tag[^>]*>/) document-tag find that the response middleware must route through b.safeBuffer.indexAfterOpenTag. Each is proven to fire on the removed shape and stay silent on the linear replacement, so the ReDoS class cannot creep back into a new parser, guard, or response rewriter."
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
@@ -18,24 +18,32 @@
|
|
|
18
18
|
* has its version deleted successfully, proving the refusal is the lock
|
|
19
19
|
* and not a blanket failure.
|
|
20
20
|
*
|
|
21
|
-
* Why the versioned delete matters
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* `bucketOps.delete`) issues ONLY the unversioned form — there is no
|
|
27
|
-
* versionId surface anywhere in lib/object-store. So:
|
|
21
|
+
* Why the versioned delete matters: WORM protects a specific object
|
|
22
|
+
* VERSION. An unversioned `DELETE /key` on a versioned bucket always
|
|
23
|
+
* succeeds (it writes a delete-marker; the protected version survives
|
|
24
|
+
* untouched). To actually erase — or be refused on — a protected version
|
|
25
|
+
* the request must carry `?versionId=<v>`.
|
|
28
26
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
27
|
+
* The framework now exposes that surface: `backend.put()` returns the
|
|
28
|
+
* `versionId` it created, `backend.delete(key, { versionId })` targets a
|
|
29
|
+
* specific version (with `bypassGovernanceRetention` for GOVERNANCE mode),
|
|
30
|
+
* and `backend.listVersions(prefix)` enumerates versions + delete-markers
|
|
31
|
+
* so an erasure workflow can find them. This test proves WORM enforcement
|
|
32
|
+
* two ways:
|
|
34
33
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
34
|
+
* (a) the SHIPPED consumer path — `backend.put` / `backend.delete({
|
|
35
|
+
* versionId })` / `backend.listVersions` — reaches the lock: a
|
|
36
|
+
* versioned delete of a COMPLIANCE version THROWS (refused), even
|
|
37
|
+
* with bypassGovernanceRetention, while a no-retention version
|
|
38
|
+
* erases cleanly;
|
|
39
|
+
* (b) an independent hand-signed control with the framework's OWN SigV4
|
|
40
|
+
* signer (lib/object-store/sigv4.signRequest), establishing MinIO's
|
|
41
|
+
* ground-truth refusal under a correctly-signed request — no bypass,
|
|
42
|
+
* real handshake, real signature.
|
|
43
|
+
*
|
|
44
|
+
* An unversioned `backend.delete(key)` still writes a delete-marker (the
|
|
45
|
+
* data version survives); that remains asserted so the delete-marker vs
|
|
46
|
+
* version-erasure distinction is not silently lost.
|
|
39
47
|
*
|
|
40
48
|
* Observed MinIO behaviour (ground truth, captured live): the versioned
|
|
41
49
|
* delete of a COMPLIANCE-retained version is refused with HTTP 400
|
|
@@ -224,6 +232,72 @@ async function _runWormOnEndpoint(label, endpoint, extraConfig) {
|
|
|
224
232
|
afterRefuse.statusCode === 200 &&
|
|
225
233
|
Buffer.compare(Buffer.from(afterRefuse.body || []), payload) === 0);
|
|
226
234
|
|
|
235
|
+
// ============================================================
|
|
236
|
+
// FRAMEWORK API — the shipped versionId surface reaches the lock
|
|
237
|
+
// ============================================================
|
|
238
|
+
// Everything above is hand-signed ground truth. This block drives the
|
|
239
|
+
// SHIPPED consumer path (backend.put / backend.delete({versionId}) /
|
|
240
|
+
// backend.listVersions) to prove the framework itself now reaches WORM
|
|
241
|
+
// enforcement — no hand-signing.
|
|
242
|
+
var fwKey = "fw-worm-" + Math.floor(Math.random() * 1e6) + ".txt";
|
|
243
|
+
var fwPayload = Buffer.from("framework-immutable " + new Date().toISOString(), "utf8");
|
|
244
|
+
|
|
245
|
+
// put() now RETURNS the versionId it used to discard.
|
|
246
|
+
var fwPut = await backend.put(fwKey, fwPayload, { multipart: false });
|
|
247
|
+
check("[" + label + "] framework put() returns a versionId (was discarded before this fix)",
|
|
248
|
+
typeof fwPut.versionId === "string" && fwPut.versionId.length > 0);
|
|
249
|
+
|
|
250
|
+
await ops.setObjectRetention(bucket, fwKey, {
|
|
251
|
+
mode: "COMPLIANCE",
|
|
252
|
+
retainUntil: new Date(Date.now() + b.constants.TIME.hours(1)),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// listVersions() enumerates the version so an erasure workflow can find it.
|
|
256
|
+
var listed = await backend.listVersions(fwKey);
|
|
257
|
+
var foundFw = listed.items.filter(function (it) {
|
|
258
|
+
return it.key === fwKey && it.versionId === fwPut.versionId;
|
|
259
|
+
});
|
|
260
|
+
check("[" + label + "] framework listVersions() enumerates the protected version " +
|
|
261
|
+
"(isLatest, not a delete-marker)",
|
|
262
|
+
foundFw.length === 1 && foundFw[0].isLatest === true && foundFw[0].deleteMarker === false);
|
|
263
|
+
|
|
264
|
+
// An UNVERSIONED framework delete still writes a delete-marker — the data
|
|
265
|
+
// version survives. Asserted so the delete-marker vs version-erasure
|
|
266
|
+
// distinction is never silently lost.
|
|
267
|
+
var fwMarkerDel = await backend.delete(fwKey);
|
|
268
|
+
check("[" + label + "] framework delete(key) without versionId still succeeds (delete-marker)",
|
|
269
|
+
fwMarkerDel === true);
|
|
270
|
+
var fwSurvived = await backend.get(fwKey, { versionId: fwPut.versionId });
|
|
271
|
+
check("[" + label + "] protected version survives the unversioned framework delete",
|
|
272
|
+
Buffer.compare(Buffer.from(fwSurvived || []), fwPayload) === 0);
|
|
273
|
+
|
|
274
|
+
// delete({ versionId }) targets the protected version → WORM refuses → the
|
|
275
|
+
// framework call THROWS (no longer a silent delete-marker success).
|
|
276
|
+
var fwRefused = false;
|
|
277
|
+
try {
|
|
278
|
+
await backend.delete(fwKey, { versionId: fwPut.versionId });
|
|
279
|
+
} catch (_e) { fwRefused = true; }
|
|
280
|
+
check("[" + label + "] framework delete({versionId}) on a COMPLIANCE version is REFUSED (throws)",
|
|
281
|
+
fwRefused === true);
|
|
282
|
+
|
|
283
|
+
// bypassGovernanceRetention via the framework does NOT defeat COMPLIANCE.
|
|
284
|
+
var fwBypassRefused = false;
|
|
285
|
+
try {
|
|
286
|
+
await backend.delete(fwKey, { versionId: fwPut.versionId, bypassGovernanceRetention: true });
|
|
287
|
+
} catch (_e) { fwBypassRefused = true; }
|
|
288
|
+
check("[" + label + "] framework delete({versionId, bypassGovernanceRetention}) STILL refused — " +
|
|
289
|
+
"COMPLIANCE is immutable to everyone",
|
|
290
|
+
fwBypassRefused === true);
|
|
291
|
+
|
|
292
|
+
// Control via the framework: a no-retention version erases through
|
|
293
|
+
// delete({ versionId }) → true. Proves the refusal above is the lock, not
|
|
294
|
+
// a blanket failure of the framework versioned-delete path.
|
|
295
|
+
var fwCtlKey = "fw-control-" + Math.floor(Math.random() * 1e6) + ".txt";
|
|
296
|
+
var fwCtlPut = await backend.put(fwCtlKey, Buffer.from("fw-disposable"), { multipart: false });
|
|
297
|
+
var fwCtlErased = await backend.delete(fwCtlKey, { versionId: fwCtlPut.versionId });
|
|
298
|
+
check("[" + label + "] framework delete({versionId}) erases a no-retention version (true)",
|
|
299
|
+
fwCtlErased === true);
|
|
300
|
+
|
|
227
301
|
// ============================================================
|
|
228
302
|
// CONTROL — no retention: the version deletes fine
|
|
229
303
|
// ============================================================
|