@blamejs/blamejs-shop 0.4.49 → 0.4.51

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/storefront.js +145 -0
  4. package/lib/vendor/MANIFEST.json +58 -46
  5. package/lib/vendor/blamejs/.github/workflows/ci.yml +134 -1
  6. package/lib/vendor/blamejs/.gitignore +5 -1
  7. package/lib/vendor/blamejs/CHANGELOG.md +4 -0
  8. package/lib/vendor/blamejs/README.md +1 -1
  9. package/lib/vendor/blamejs/SECURITY.md +3 -1
  10. package/lib/vendor/blamejs/api-snapshot.json +10 -2
  11. package/lib/vendor/blamejs/lib/bundler.js +2 -7
  12. package/lib/vendor/blamejs/lib/config-drift.js +17 -3
  13. package/lib/vendor/blamejs/lib/crypto-field.js +30 -0
  14. package/lib/vendor/blamejs/lib/db-declare-row-policy.js +20 -1
  15. package/lib/vendor/blamejs/lib/db-schema.js +29 -0
  16. package/lib/vendor/blamejs/lib/db.js +7 -0
  17. package/lib/vendor/blamejs/lib/guard-csv.js +13 -4
  18. package/lib/vendor/blamejs/lib/local-db-thin.js +23 -1
  19. package/lib/vendor/blamejs/lib/mail-bimi.js +16 -3
  20. package/lib/vendor/blamejs/lib/mail-scan.js +2 -5
  21. package/lib/vendor/blamejs/lib/mail.js +16 -9
  22. package/lib/vendor/blamejs/lib/mcp.js +28 -6
  23. package/lib/vendor/blamejs/lib/middleware/bot-disclose.js +7 -5
  24. package/lib/vendor/blamejs/lib/middleware/speculation-rules.js +6 -4
  25. package/lib/vendor/blamejs/lib/numeric-bounds.js +32 -0
  26. package/lib/vendor/blamejs/lib/object-store/azure-blob.js +12 -1
  27. package/lib/vendor/blamejs/lib/object-store/gcs.js +12 -1
  28. package/lib/vendor/blamejs/lib/object-store/http-put.js +11 -1
  29. package/lib/vendor/blamejs/lib/object-store/index.js +4 -0
  30. package/lib/vendor/blamejs/lib/object-store/local.js +11 -1
  31. package/lib/vendor/blamejs/lib/object-store/sigv4.js +86 -5
  32. package/lib/vendor/blamejs/lib/parsers/safe-env.js +6 -3
  33. package/lib/vendor/blamejs/lib/parsers/safe-yaml.js +6 -6
  34. package/lib/vendor/blamejs/lib/safe-buffer.js +69 -1
  35. package/lib/vendor/blamejs/lib/safe-decompress.js +3 -12
  36. package/lib/vendor/blamejs/lib/seeders.js +33 -39
  37. package/lib/vendor/blamejs/lib/storage.js +71 -7
  38. package/lib/vendor/blamejs/lib/vault/rotate.js +4 -13
  39. package/lib/vendor/blamejs/package.json +1 -1
  40. package/lib/vendor/blamejs/release-notes/v0.15.10.json +53 -0
  41. package/lib/vendor/blamejs/release-notes/v0.15.11.json +52 -0
  42. package/lib/vendor/blamejs/test/integration/object-store-worm-lock.test.js +90 -16
  43. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +150 -39
  44. package/lib/vendor/blamejs/test/layer-0-primitives/config-drift.test.js +19 -0
  45. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-aad-downgrade.test.js +96 -0
  46. package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-transaction.test.js +110 -0
  47. package/lib/vendor/blamejs/test/layer-0-primitives/declare-row-policy.test.js +43 -1
  48. package/lib/vendor/blamejs/test/layer-0-primitives/local-db-thin.test.js +28 -0
  49. package/lib/vendor/blamejs/test/layer-0-primitives/mcp.test.js +25 -0
  50. package/lib/vendor/blamejs/test/layer-0-primitives/numeric-bounds.test.js +29 -0
  51. package/lib/vendor/blamejs/test/layer-0-primitives/object-store-versioned-delete.test.js +97 -0
  52. package/lib/vendor/blamejs/test/layer-0-primitives/safe-buffer-linear-scans.test.js +94 -0
  53. package/lib/vendor/blamejs/test/layer-5-integration/bundler-output.test.js +52 -0
  54. package/package.json +1 -1
@@ -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
- return { storedPath: key, backend: picked.backend.name };
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: picked.backend.name,
514
- key: key,
515
- existed: result,
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
- _runStmt(db, "BEGIN");
659
- try {
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
- _runStmt(db, "COMMIT");
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 });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.15.9",
3
+ "version": "0.15.11",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -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 and what it exposes about the
22
- * framework: WORM protects a specific object VERSION. An unversioned
23
- * `DELETE /key` on a versioned bucket always succeeds (it writes a
24
- * delete-marker; the protected version survives untouched). The
25
- * framework's object-store delete (`backend.delete(key)` /
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
- * (a) the framework's own delete() on a retained object SUCCEEDS and
30
- * returns a clean result, masking the fact that the data version
31
- * is still there the framework delete path is not WORM-aware;
32
- * (b) the operation WORM actually blocks (delete the protected version)
33
- * can only be issued by hand-signing `DELETE /key?versionId=<v>`.
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
- * This test hand-signs that versioned delete with the framework's OWN
36
- * SigV4 signer (lib/object-store/sigv4.signRequest) so the proof is that
37
- * MinIO enforces the lock under a request the framework signed correctly
38
- * — no security bypass, real handshake, real signature.
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
  // ============================================================