@blamejs/blamejs-shop 0.5.0 → 0.5.2

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 CHANGED
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.5.x
10
10
 
11
+ - v0.5.2 (2026-06-27) — **Vendored framework refreshed to 0.15.36.** Updates the vendored blamejs framework to 0.15.36. The upstream release is internal test-suite tooling only: it re-verifies the guard-suite class that allows a bare comma/semicolon split on token-only RFC header grammars (RRULE, RFC 9421 signature components, TLS-RPT rua, SCIM attribute paths), renames its marker to a descriptive token, and prunes five inert marker comments. No runtime code, public API, or wire format changed upstream, so there is no runtime or behavior change for this storefront and no operator action is required. **Changed:** *Vendored framework refreshed to 0.15.36* — Refreshes the vendored blamejs framework to 0.15.36, an upstream release scoped to test-suite tooling — a guard-suite class re-verified and its marker renamed, plus five inert marker comments removed. The shipped framework runtime, public API, and wire format are unchanged, so no operator action is required.
12
+
13
+ - v0.5.1 (2026-06-27) — **Vendored framework refreshed to 0.15.35 — Prometheus label-injection fix in the metrics shadow registry.** Updates the vendored blamejs framework to 0.15.35. The upstream release fixes a label-injection in the framework's metrics shadow registry: it built a metric's cardinality key by joining label pairs with commas and re-split that string when rendering the Prometheus exposition, so a comma (or `=`) inside a label VALUE forged additional label pairs in the scrape output — which downstream tenant-scoping filters, authorization selectors, recording rules, and alerting trust as a boundary — and two distinct label sets could collide into one cardinality bucket. The key now uses canonical-JSON of the label set and the render emits the structured labels instead of re-splitting, so a value's commas/equals stay inside the value and a label named `constructor` / `prototype` / `__proto__` is preserved. This storefront does not compose the metrics shadow registry, so there is no runtime change for it; the fix ships in the vendored framework regardless and no operator action is required. **Changed:** *Vendored framework refreshed to 0.15.35 (Prometheus label-injection fix)* — Refreshes the vendored blamejs framework to 0.15.35. The upstream fix stops a comma or `=` in a metric label value from forging extra Prometheus label pairs in the shadow registry's scrape output — the cardinality key is now canonical-JSON of the label set and the render emits the structured labels rather than splitting a joined string, so distinct label sets stay distinct and reserved-name labels survive. The shipped storefront does not expose the metrics shadow registry, so its runtime, public API, and wire format are unchanged; the hardened framework simply rides along.
14
+
11
15
  - v0.5.0 (2026-06-27) — **Affiliate commissions gain a tamper-evident hash chain, joining the gift-card, store-credit, and loyalty ledgers.** The affiliate-commission ledger was the last money ledger without tamper-evidence. Each commission is now linked into a per-affiliate SHA3-512 hash chain: a row names its predecessor, a uniqueness fence allows exactly one child per chain tip (so concurrent commissions for the same affiliate serialize instead of forking), and editing a commission's money fields or deleting a row from the middle breaks the linkage. Unlike the append-only gift-card / store-credit / loyalty ledgers, an affiliate commission walks a pending → paid → voided payout lifecycle in place, so the chain hashes only the immutable money facts — order, affiliate, order total, commission, currency, and time — and leaves the lifecycle columns out, meaning a legitimate payout or void never breaks the chain while an inflated amount or a removed row still does. A new `affiliates.verifyChain(affiliateId)` recomputes an affiliate's chain end to end and reports the first divergence, and accepts a trusted anchor (row count + head hash from an earlier snapshot) to also catch a truncation that deletes the most-recent commissions. Migration 0238 adds the chain columns + fence; commissions recorded before it are tolerated as an unverifiable legacy prefix, and the chain anchors fresh from the first commission recorded afterward. **Added:** *Affiliate commissions are a verifiable, fork-proof per-affiliate hash chain* — Recording a commission now links it into the affiliate's SHA3-512 hash chain — `row_hash = SHA3-512(prev_hash || canonical-json(immutable fields))` — under a `UNIQUE(affiliate_id, prev_hash)` parent fence, so a commission derived from a stale tip collides and retries instead of forking the chain, and concurrent commissions for one affiliate serialize. Because a commission's status, paid-at, payout reference, and void reason are stamped in place over its lifetime, only the immutable money facts are hashed, so a `pending → paid → voided` transition leaves the chain valid while tampering with the order total or commission amount, or deleting a commission, is detected. `affiliates.verifyChain(affiliateId)` walks the chain and reports the first break; pass `{ anchor: { count, head } }` to also rule out a tail truncation. Commissions written before this release are tolerated as an unverifiable legacy prefix and the chain anchors fresh from the next one — no backfill required. Migration 0238 adds the columns and the fence.
12
16
 
13
17
  ## v0.4.x
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.5.0",
2
+ "version": "0.5.2",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-imfe0otYErcB8rr2h6KLSGTtStirysptpXETSPY4zLv3bZoIT75Lo1dOvkOav+xL",
@@ -3,8 +3,8 @@
3
3
  "_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
4
4
  "packages": {
5
5
  "blamejs": {
6
- "version": "0.15.34",
7
- "tag": "v0.15.34",
6
+ "version": "0.15.36",
7
+ "tag": "v0.15.36",
8
8
  "license": "Apache-2.0",
9
9
  "author": "blamejs contributors",
10
10
  "source": "https://github.com/blamejs/blamejs",
@@ -857,6 +857,8 @@
857
857
  "release-notes/v0.15.32.json": "lib/vendor/blamejs/release-notes/v0.15.32.json",
858
858
  "release-notes/v0.15.33.json": "lib/vendor/blamejs/release-notes/v0.15.33.json",
859
859
  "release-notes/v0.15.34.json": "lib/vendor/blamejs/release-notes/v0.15.34.json",
860
+ "release-notes/v0.15.35.json": "lib/vendor/blamejs/release-notes/v0.15.35.json",
861
+ "release-notes/v0.15.36.json": "lib/vendor/blamejs/release-notes/v0.15.36.json",
860
862
  "release-notes/v0.15.4.json": "lib/vendor/blamejs/release-notes/v0.15.4.json",
861
863
  "release-notes/v0.15.5.json": "lib/vendor/blamejs/release-notes/v0.15.5.json",
862
864
  "release-notes/v0.15.6.json": "lib/vendor/blamejs/release-notes/v0.15.6.json",
@@ -1534,7 +1536,7 @@
1534
1536
  "test/smoke.js": "lib/vendor/blamejs/test/smoke.js"
1535
1537
  },
1536
1538
  "bundler": "shallow git clone of release tag from github.com/blamejs/blamejs",
1537
- "bundledAt": "2026-06-26",
1539
+ "bundledAt": "2026-06-27",
1538
1540
  "hashes": {
1539
1541
  ".clusterfuzzlite/Dockerfile": "sha256:6587c80f02f3d622b782380ff1aaa7753b948023ff1d4af8ac27709d1876c218",
1540
1542
  ".clusterfuzzlite/build.sh": "sha256:b9f5a627272c65f6569fa20dcbb32c383ca3a1183ce2562d481408a2fbaf9881",
@@ -1565,7 +1567,7 @@
1565
1567
  ".npmrc": "sha256:66f104e7d07c496d2d0409988225e8c0e4ceb8d247dbcac3be75b2128d20ce66",
1566
1568
  ".pinact.yaml": "sha256:0213ffda55961dc49b64c0a5dfa3c0567419633b1499d57eaf7c8d842d7da6c7",
1567
1569
  "ARCHITECTURE.md": "sha256:9b1c8d2b1b7a41838eb348b0a008e4b4369718fd72bfe2974b37155f7536d35b",
1568
- "CHANGELOG.md": "sha256:98f034fdf893233e36e89e1f8eaa1548f1de448d67f5afef4e07b401bccb4aa2",
1570
+ "CHANGELOG.md": "sha256:7b4cf6d91c65bf01fda536a83f26198e722bf97cef6c88f3cfd58436ebc2e43a",
1569
1571
  "CODE_OF_CONDUCT.md": "sha256:148a281960fff7c2fe6554dab66da572c72245ddeb00b0d14811558397bff386",
1570
1572
  "CONTRIBUTING.md": "sha256:bb4dbdbc8598da31dbce653a8ed322e08ff46560173f2eb67a4d684653948332",
1571
1573
  "GOVERNANCE.md": "sha256:906df6afb1f552b27b9acb50f7f96c47b917a2f1021cd4e987dbf4ee0e0a821b",
@@ -1575,7 +1577,7 @@
1575
1577
  "NOTICE": "sha256:f487fa47a11aca0f89e2615cdd3c713e9842abf7a30d8d328eeeae1c864aa774",
1576
1578
  "README.md": "sha256:3ddcc197b003da0b02db8bdd1aef1e943c94f7eab613c633d6a45bb11d0a80e9",
1577
1579
  "SECURITY.md": "sha256:77d8c2bcc04b425a08ef30e51204cebddbd48954b028a259c7a8412dbfeab40a",
1578
- "api-snapshot.json": "sha256:610a4caba49d95d48cd4a129b4071e1756903eaea0de8eb904e6d76e1a887501",
1580
+ "api-snapshot.json": "sha256:204692f40f115ba8cc211fd485feb2a11bfae877b9c013aba085c0729cf267bc",
1579
1581
  "assets/BlameJS_Logo.png": "sha256:3c65699753c771b48ef9ac7f45bb40815ec19a23afcdd0cd30ef4601bbbe293e",
1580
1582
  "assets/BlameJS_Logo.svg": "sha256:dda44f3fb1343d5de9db6b1fcdb75fc649c57e7a99a8e8239fcf852e3841e1a8",
1581
1583
  "bench/README.md": "sha256:74202f2507fd840bfc1ac6c681975d9273cf36cca6e0f72655f138337304033c",
@@ -1872,7 +1874,7 @@
1872
1874
  "lib/cache-redis.js": "sha256:661ba39417085682cee1e5524685c9cb1e1277c6f1cef9ca4ae808e457d5499e",
1873
1875
  "lib/cache-status.js": "sha256:f82bf22698c9b4fa10b7dc581fd39b83709267f105195908eee29612a14df15d",
1874
1876
  "lib/cache.js": "sha256:e38929b261a6c9ea2dd822f39f2669c2ee5bbc6729517ced49471227d1d5ddb9",
1875
- "lib/calendar.js": "sha256:08896ec0e8e07e7da3a94c18046cba9ea082e5a5af16ab459604862f70fd11ef",
1877
+ "lib/calendar.js": "sha256:040bcf1385ffea3eb5779f1192f7f01889e498a3773de4c81d3cd37fc5b98307",
1876
1878
  "lib/canonical-json.js": "sha256:3546e2a4b7692c4f8c77602e8804a8aaebff29b2183329588578c1d6f0df3eee",
1877
1879
  "lib/cbor.js": "sha256:9b6a6799b10646e50705f9e0839fbac2f3b8df6e41943170c15e308215936feb",
1878
1880
  "lib/cdn-cache-control.js": "sha256:adc1e5bdc59492de9e2f688dbee279a0ab09166adba609c4b5355028c4d3e0d1",
@@ -2028,10 +2030,10 @@
2028
2030
  "lib/handlers.js": "sha256:860d07682bc6199c5d06f2d91f4ba9ab26ff5e2d9a97c72af6a700505d0e44e2",
2029
2031
  "lib/honeytoken.js": "sha256:288a69a6c22107d41185c88ec86ca1bf424b3c10d349d90d5fc5037749db0568",
2030
2032
  "lib/html-balance.js": "sha256:da49a83a2b17e9e5feb3f061e59bb595ceb37d8dfb6a7c1da7ab230705b7259f",
2031
- "lib/http-client-cache.js": "sha256:a4d41376c761d731427844e1aba76822ffb597e1d892b6df93d5ef807489ede3",
2033
+ "lib/http-client-cache.js": "sha256:52a0ef4ce9bfa99886f0e26c7fd4649b6cb6294c0f7e46279f23c76437a8fc64",
2032
2034
  "lib/http-client-cookie-jar.js": "sha256:50aee1674d2c2ac73fc153fffd6be6d2628326cf85ac15e7d19b9bcd1e727826",
2033
2035
  "lib/http-client.js": "sha256:99f1a4faf97e858794d4422a8cc9a813154d251413ffce328635a7e93e85d62b",
2034
- "lib/http-message-signature.js": "sha256:45fbc4462af19f6237ba678aed38341c8702301220c79d61a05d08114aafcf63",
2036
+ "lib/http-message-signature.js": "sha256:3d5faf7637ceea9dfa1d281848d11e790100e113f5ef3366fc4c733d5f29b4b9",
2035
2037
  "lib/http2-teardown.js": "sha256:61d291c34e321e18b64d60a4c0253e638550fff7dc32568b980d3aa13bb178e2",
2036
2038
  "lib/i18n-messageformat.js": "sha256:2c5012ac3fbc6f56191765bda090889ab58206d130cf05c81963d965744d63d6",
2037
2039
  "lib/i18n.js": "sha256:e2663046590798f2dde5cb73f14973ca509070841dff7c9697546e5dc0ec195a",
@@ -2107,7 +2109,7 @@
2107
2109
  "lib/mcp-tool-registry.js": "sha256:df3480e6a0d29a9734bdb19d322f83bff5ebfdd756620f947cf593878dea8776",
2108
2110
  "lib/mcp.js": "sha256:c65af74fdb3cf30436699aba5051c4d6e8cb972887cbaaecc46250d557599953",
2109
2111
  "lib/mdoc.js": "sha256:bc31d38d6fdfac8db8f040dfa4d33ab40405f56340bb4ec978f3ba98e93f6433",
2110
- "lib/metrics.js": "sha256:edc2a3f08a48ab67c9e805cb75839cc9d326ac334478b0f30db0423102ee5713",
2112
+ "lib/metrics.js": "sha256:55d445b738b2cfc8ceb1bfa7a1e69b306497e8065ea86d61e4f4087873acce1b",
2111
2113
  "lib/middleware/age-gate.js": "sha256:dc8c61a4d41f4a781220c9c97eb475fb2867f5ee444eb2756badcbc5b8411795",
2112
2114
  "lib/middleware/ai-act-disclosure.js": "sha256:6fc62940d85813019b31c586fc86853570329a939565a6947f81052d50977c42",
2113
2115
  "lib/middleware/api-encrypt.js": "sha256:f90210bfc4fe04b50f6d72e14676a140c5759e255ed359567066a1e80ab5fb63",
@@ -2115,7 +2117,7 @@
2115
2117
  "lib/middleware/asyncapi-serve.js": "sha256:a0f0b62b264a5cd8a24e3cfddae722a84b5e2206729f77403906a1d1cfcd7c7b",
2116
2118
  "lib/middleware/attach-user.js": "sha256:776424918bf302378da99a85ff20c91a42f8c9f528ab435cd7a06b95ec523d6d",
2117
2119
  "lib/middleware/bearer-auth.js": "sha256:d0cb9d65a724a0badf9a5164397d12bf6a3afa9a2ae5e75922799b832741ee59",
2118
- "lib/middleware/body-parser.js": "sha256:488ec4572f7208d05a664b285ad3b6c888f623127d6bb50bbc1659a67a259f73",
2120
+ "lib/middleware/body-parser.js": "sha256:a2a0af8ac8cecd9e6c842680029d6b409122d0639e33cc7204089e9c0daf6b73",
2119
2121
  "lib/middleware/bot-disclose.js": "sha256:2b98da494ee51ba7c8efc0d5fccbf11ba335649e71726d83847116c340720bf0",
2120
2122
  "lib/middleware/bot-guard.js": "sha256:ba724a897f646fcab7c384eb6e5c076e77e83b8e1c8d82a5aab79e8df616af8c",
2121
2123
  "lib/middleware/clear-site-data.js": "sha256:9b489c42aa6d4ee9f4b1e6e410b211f5d28134fc1d3933502f7504d64bc68c26",
@@ -2154,7 +2156,7 @@
2154
2156
  "lib/middleware/require-methods.js": "sha256:3b1b7ee5844969e813a46f26e78ed116ccfa28d2c6a9b821bb5d8c998ae95ea9",
2155
2157
  "lib/middleware/require-mtls.js": "sha256:f823a6d26af0969f28bced4ab166533dc6a5547df2f9120392d77acdbff3ba97",
2156
2158
  "lib/middleware/require-step-up.js": "sha256:dd8967633dc9133e54b1c4965247c77be087d6d34c395746d319b3af8e81e2eb",
2157
- "lib/middleware/scim-server.js": "sha256:2f38d77c7e949df9e219e4b351a6eb047dc85a7337aca932ac4b598e953c471d",
2159
+ "lib/middleware/scim-server.js": "sha256:edb2b85326310247c19e2b9005df6b9af92d689d611759a824649f67b8283b15",
2158
2160
  "lib/middleware/security-headers.js": "sha256:87506e572454033b33955d58d9f0cbfdc14edc63c1cbe7e01f0dc4f07753c64d",
2159
2161
  "lib/middleware/security-txt.js": "sha256:3140d6537aa09a7052d83088840358949dd36393697fa2215f8f915d31aaa112",
2160
2162
  "lib/middleware/span-http-server.js": "sha256:8f22bf4feb5f05a201da20b89425f0e904ed6368c91b2fcd1be9a0f5cb067696",
@@ -2179,7 +2181,7 @@
2179
2181
  "lib/network-heartbeat.js": "sha256:f14ec7fa4667cbb04477d91ae59d6253b6c9ccc43071bc558e737cc672f43975",
2180
2182
  "lib/network-nts.js": "sha256:90ce47584ad58c171e8029eb1d5df8ba4ba130c4038d1fe260109685c34c8f75",
2181
2183
  "lib/network-proxy.js": "sha256:9342c534863f905000d2432b67c26dd7a5ea02be25aaf9ebe05976079e61d4c9",
2182
- "lib/network-smtp-policy.js": "sha256:95927f23bb7cafaf716dee869bb89d1a3d84bc307167365d08c12e9db9c50370",
2184
+ "lib/network-smtp-policy.js": "sha256:4d10a339d5c471cdff217c5514038eb2e30f4ecb97a38cb4405165ee0f9dcf7e",
2183
2185
  "lib/network-tls.js": "sha256:554b64c77389e1c961da8d061c63c806be994960466fb26f2b2268dbec0a369b",
2184
2186
  "lib/network-tsig.js": "sha256:41dc85a6e816cce1c58c36891dfc6bafc612b68978936107a09a5a553563e0d7",
2185
2187
  "lib/network.js": "sha256:40cb01627bf45741cac22ef79f5985f69323e47732d54cf3ce28c06882830c25",
@@ -2346,7 +2348,7 @@
2346
2348
  "oss-fuzz/projects/blamejs/README.md": "sha256:ae13b7bb79ed8d69b1b3276e5562807a0349fb6e6b7d11cf1f683aad1eafdb4b",
2347
2349
  "oss-fuzz/projects/blamejs/build.sh": "sha256:0ced1cf21782c97be7f8d74faf5e27a308b60b2f858836fb5ca3b8c4e939a8f7",
2348
2350
  "oss-fuzz/projects/blamejs/project.yaml": "sha256:59f2cb83aa622325a175b77416fe155be15b70a9c798bd1a78bba05763b1b03d",
2349
- "package.json": "sha256:ca031cc44ce6ade614e436bd1f29d01cd2d495ad41c55a819c02c85a5f3a2f96",
2351
+ "package.json": "sha256:585a646ad04f1afaa7faf4d273a16f5d8c6d530071d20db7b83f045ddaeaff73",
2350
2352
  "release-notes/v0.0.x.json": "sha256:7a49819f30068ee119000cad7010194882bb8bfaa12acbdab4dfc066efb7982f",
2351
2353
  "release-notes/v0.1.x.json": "sha256:6742a8c17f947c5cb76f69dead7eea86b942d80621d914b774ba5488e09937e5",
2352
2354
  "release-notes/v0.10.x.json": "sha256:fe498045daf88337bd3d987e5964aa42c99a50e1685b6f09e51f698b8687726f",
@@ -2383,6 +2385,8 @@
2383
2385
  "release-notes/v0.15.32.json": "sha256:af99a8a782dcc30aff3a135c27179b064d63181fc4c302c31da632c1615b68ce",
2384
2386
  "release-notes/v0.15.33.json": "sha256:295ea9939b74498c7f0be6afdc8bc511f97cb2383a436c3040c58b0600598b19",
2385
2387
  "release-notes/v0.15.34.json": "sha256:2a1b36d897d8e9f2cece051c098030f08d9c6ee6bef136800bd19453ad65f188",
2388
+ "release-notes/v0.15.35.json": "sha256:e317b2252734aace7a0f734e3ab40c3a0c4ac208d678c0e9834be34eb424d15b",
2389
+ "release-notes/v0.15.36.json": "sha256:392c15724efc02fad5825aaf93b3f5dcef90a0cb9909d61f86e777f3061045ec",
2386
2390
  "release-notes/v0.15.4.json": "sha256:6ac7fa0ef1728c27e71b2050d1b07a810f9b4b1440ccddbf28ad56e2f54d8585",
2387
2391
  "release-notes/v0.15.5.json": "sha256:cca1d0edd5d6fc41b512d19d98be224b990dcab41478622c11962f0fcb1bb09a",
2388
2392
  "release-notes/v0.15.6.json": "sha256:0e3b9e5e43b70b61dd258c3003d1b8729cd3c26c62a34dedcca81bbec5d31077",
@@ -2629,7 +2633,7 @@
2629
2633
  "test/layer-0-primitives/cluster-storage.test.js": "sha256:238b3b3db0eba3e6312a863710533178f566347b90e161e564481aa826707647",
2630
2634
  "test/layer-0-primitives/cluster-vault-rotation.test.js": "sha256:3514e9e71d6c39e805248f58ad2f41528d091e196c0f3766a032675677161b2d",
2631
2635
  "test/layer-0-primitives/cms-codec.test.js": "sha256:7e46078ed82be5b69d22c48f22dba37ea5015371c2a8cf5f94fb1a792fb7bb78",
2632
- "test/layer-0-primitives/codebase-patterns.test.js": "sha256:69d9da97b002a40c9edb39a6848d7369c2b43e2e09b935bdf9e9e8228765f40a",
2636
+ "test/layer-0-primitives/codebase-patterns.test.js": "sha256:4180fd47eef2c81248ec83433e31f4975eefb0441bb3f3366a25a5a98ce5d0b9",
2633
2637
  "test/layer-0-primitives/codepoint-class.test.js": "sha256:19d1b69efa7e0e9f7ef2392ca264167767a5ec5890ee339b2cfd8a7818035d27",
2634
2638
  "test/layer-0-primitives/compliance-ai-act.test.js": "sha256:5ee4ad05d12233cb3c5457ef10a727833710bbc1ce1318838f9f9ef5d2cb8d4b",
2635
2639
  "test/layer-0-primitives/compliance-cascade.test.js": "sha256:ee02cf14541a837a9d7977c6ea6bf7f9210bed293925d93c976e31f270aebec4",
@@ -2862,7 +2866,7 @@
2862
2866
  "test/layer-0-primitives/mcp-tool-registry.test.js": "sha256:4d9a9a367489d93afcc8e0bca3442e0aefc683e40ddd29e6e2cddbe3a3b8732f",
2863
2867
  "test/layer-0-primitives/mcp.test.js": "sha256:896a9b55fd9d21a1ec2b0d0753b5796eb045911ad28992ba17338285406ac2f9",
2864
2868
  "test/layer-0-primitives/mdoc.test.js": "sha256:2fe1086192385e71f858554d676b299b86594d34226e1d774ffefc97b5fa03a4",
2865
- "test/layer-0-primitives/metrics-shadow-registry.test.js": "sha256:8ea834fd6b549c18976b535660a53aafccb1c6f0b7762086e8dbbc7ab8d832b6",
2869
+ "test/layer-0-primitives/metrics-shadow-registry.test.js": "sha256:a91a9efdb4aa28caed846b969d7c1e5ebfc57067afad85800daa430942d02725",
2866
2870
  "test/layer-0-primitives/metrics-snapshot.test.js": "sha256:aa405be22acb6680b062d1de1aa7ce00029e414218a0e4e521e10a14b2254851",
2867
2871
  "test/layer-0-primitives/middleware-compose-pipeline.test.js": "sha256:a779468bde043b6de4add5d4405a33973163890ee667b833e3330373ee64f77d",
2868
2872
  "test/layer-0-primitives/mime-parse.test.js": "sha256:73ccfea7d08b0c3b5fb899215806d4f430c02bf63e21835024a4cdcfc4254b31",
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.15.x
10
10
 
11
+ - v0.15.36 (2026-06-27) — **Internal test-suite hardening only — the published library's runtime behavior and public API are unchanged (source-comment marker text aside).** The codebase-patterns guard class that allows a bare comma/semicolon split on token-only RFC header grammars was re-verified from scratch and renamed to a descriptive token, with its old name recorded as retired. Each live marked site (RRULE, RFC 9421 component identifiers, TLS-RPT rua, SCIM attribute paths) was re-read and confirmed to split a grammar with no quoted-string members. Five marker comments that suppressed nothing were removed or turned into plain explanatory comments. No runtime code, public API, or wire format changed. **Detectors:** *Bare token-only header-split suppression class re-verified, renamed, and pruned of inert markers* — Each marked bare `.split(",")` / `.split(";")` on an RFC header value was re-read and confirmed to operate on a token-only grammar (no quoted-string members, so a quote-aware splitter is unnecessary): RFC 5545 RRULE, RFC 9421 signature component identifiers, RFC 8460 TLS-RPT rua, and RFC 7644 SCIM attribute paths. The guard class was renamed to a descriptive token and its old name added to the retired-token set. Five marker comments that the detector never actually evaluated (two sat on a date-normalizing `.replace`, three in header parsers the guard intentionally does not scan) were removed or converted to plain comments. This is test-suite tooling plus source-comment text; no shipped framework behavior changed.
12
+
13
+ - v0.15.35 (2026-06-26) — **`b.metrics` shadow registry no longer lets a comma in a label value forge extra Prometheus label pairs — a label-injection that downstream tenant-scoping or authorization selectors could be tricked by.** The shadow registry serialized a metric's label set into a `name=value,name=value` string key and re-split it on `,` to build the Prometheus exposition. A label VALUE containing a comma therefore split into multiple label pairs, so a single operator-set label could forge additional label pairs in the scrape output (for example turning one `route` label into a `route` plus an attacker-named label), which downstream tenant-scoping filters, authorization selectors, recording rules, and alerting trust as a boundary; two distinct label sets could also collide into one cardinality bucket. The label key now uses canonical-JSON of the string-coerced label set (the same approach the main registry already used) and the render path emits the structured label set kept alongside that key rather than splitting or re-parsing, so a `,` or `=` inside a label value stays inside the value and a label NAMED `constructor`, `prototype`, or `__proto__` (all valid Prometheus label names) is preserved instead of being dropped. Note: the cardinality keys exposed by `shadowRegistry().snapshot()` are now canonical-JSON strings (e.g. `{"route":"/api"}`) rather than `route=/api`. **Changed:** *shadowRegistry().snapshot() cardinality keys are canonical-JSON* — As a consequence of the label-injection fix, the per-series cardinality keys in a shadow-registry snapshot are now canonical-JSON strings (e.g. `{"tenant":"a"}`) instead of the previous `tenant=a` form. Code that reads those keys directly from snapshot() should parse them as JSON; the Prometheus render output is unchanged for conforming label values. **Security:** *Label values can no longer forge extra Prometheus label pairs in the shadow registry* — b.metrics shadowRegistry built a metric's cardinality key by joining `name=value` pairs with commas, then split that string on commas when rendering the Prometheus exposition. A comma (and `=`) in a label value therefore broke one value into several forged label pairs in the scrape output, and two different label sets could collide into the same cardinality bucket. The key is now canonical-JSON of the string-coerced label set and the render emits the structured label set kept alongside that key rather than splitting or re-parsing, so a label value is emitted as a single quoted value with its commas/equals intact, distinct label sets get distinct keys, and a label NAMED `constructor`, `prototype`, or `__proto__` (each a valid Prometheus label name) is preserved rather than dropped. Deterministic regression tests assert a comma-bearing label value produces exactly one label pair and that the reserved-name labels survive render.
14
+
11
15
  - v0.15.34 (2026-06-26) — **`b.mail.crypto.pgp.verify` now accepts every valid RSA OpenPGP signature — one whose value happened to have a high zero byte (about 1 in 256) was being rejected.** An OpenPGP RSA signature is an integer modulo the key's modulus, and ~1 in 256 signatures have a value that begins with a zero byte. The MPI encoding strips those leading zero bytes (RFC 9580 §3.2), but b.mail.crypto.pgp.verify passed the stripped value straight to the RSA verification, which requires a signature exactly the modulus byte length — so a perfectly valid signature was intermittently reported invalid. The signature is now left-padded back to the modulus width before verification, exactly as the Ed25519 path already pads its components. Verification of every valid RSA signature is now reliable, including signatures produced by other OpenPGP implementations. Also includes internal test-tooling: two more guard-suite suppression classes were re-verified and their tokens retired. **Fixed:** *RSA OpenPGP signatures with a high zero byte now verify reliably* — b.mail.crypto.pgp.verify read the RSA signature MPI (whose leading zero bytes the OpenPGP wire format strips) and handed it to the RSA verification without restoring the stripped bytes. Node's RSA verify requires the signature to be exactly the modulus byte length, so a signature whose value had one or more high zero bytes — about 1 in 256 of all signatures, for any key — was rejected as invalid even though it was correct. The signature is now left-padded to the modulus width before verification (the same correction the Ed25519 verification path already applied to its R/S components). This affects signatures the framework produces and signatures from other OpenPGP implementations alike. A deterministic regression test searches for a short-MPI signature and asserts it verifies. **Detectors:** *Two more suppression-marker classes re-verified and their tokens retired* — The raw-hash-compare and seal-without-aad guard classes were re-read and confirmed (a data-residency region tag compared with === — not a secret; and two intentional non-AEAD-bound seals — a non-regulated plain-mode column whose AAD is enforced by the posture seal-envelope floor where it matters, and a throwaway vault-readiness probe), then renamed to descriptive tokens with their old names added to the retired-token set. Test-suite tooling only.
12
16
 
13
17
  - v0.15.33 (2026-06-26) — **Internal test-suite hardening only — the published library's runtime behavior and public API are unchanged (source-comment marker text aside).** Three more suppression classes in the codebase-patterns guard suite were re-verified from scratch and renamed to descriptive tokens, with their old names recorded as retired and their in-source marker comments updated to match. No runtime code, public API, or wire format changed. **Detectors:** *Three more suppression-marker classes re-verified and their tokens retired* — Continuing the re-verification pass: each marked site for the math-random-noncrypto, raw-new-url, and dynamic-require guard classes was re-read and confirmed (non-security jitter/sampling; URL parsing for shape/origin inspection or behind the safe wrapper; operator-supplied module loads), then the class was renamed to a descriptive token and its old name added to the retired-token set. This is test-suite tooling; no shipped framework behavior changed.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 1,
3
- "frameworkVersion": "0.15.34",
4
- "createdAt": "2026-06-26T23:09:44.793Z",
3
+ "frameworkVersion": "0.15.36",
4
+ "createdAt": "2026-06-27T07:23:28.597Z",
5
5
  "exports": {
6
6
  "a2a": {
7
7
  "type": "object",
@@ -1119,7 +1119,7 @@ function _firstParamValue(prop, paramName) {
1119
1119
 
1120
1120
  function _icalRruleToJscal(rrule) {
1121
1121
  var out = { "@type": "RecurrenceRule", frequency: "daily" };
1122
- var parts = String(rrule).split(";"); // allow:bare-split-on-quoted-header — RFC 5545 RRULE grammar has no quoted-string members; values are token-only
1122
+ var parts = String(rrule).split(";"); // allow:bare-split-on-quoted-header-token-grammar — RFC 5545 RRULE grammar has no quoted-string members; values are token-only
1123
1123
  for (var i = 0; i < parts.length; i += 1) {
1124
1124
  var kv = parts[i].split("=");
1125
1125
  if (kv.length !== 2) continue;
@@ -1129,11 +1129,11 @@ function _icalRruleToJscal(rrule) {
1129
1129
  else if (key === "INTERVAL") out.interval = parseInt(val, 10);
1130
1130
  else if (key === "COUNT") out.count = parseInt(val, 10);
1131
1131
  else if (key === "UNTIL") out.until = _icalDateTimeToUtc(val);
1132
- else if (key === "BYDAY") out.byDay = val.split(",").map(function (d) { // allow:bare-split-on-quoted-header — RFC 5545 BYDAY values are token-only
1132
+ else if (key === "BYDAY") out.byDay = val.split(",").map(function (d) { // allow:bare-split-on-quoted-header-token-grammar — RFC 5545 BYDAY values are token-only
1133
1133
  return { "@type": "NDay", day: d.slice(-2).toLowerCase() };
1134
1134
  });
1135
- else if (key === "BYMONTH") out.byMonth = val.split(","); // allow:bare-split-on-quoted-header — RFC 5545 BYMONTH values are integer-only
1136
- else if (key === "BYMONTHDAY") out.byMonthDay = val.split(",").map(function (n) { return parseInt(n, 10); }); // allow:bare-split-on-quoted-header — RFC 5545 BYMONTHDAY values are integer-only
1135
+ else if (key === "BYMONTH") out.byMonth = val.split(","); // allow:bare-split-on-quoted-header-token-grammar — RFC 5545 BYMONTH values are integer-only
1136
+ else if (key === "BYMONTHDAY") out.byMonthDay = val.split(",").map(function (n) { return parseInt(n, 10); }); // allow:bare-split-on-quoted-header-token-grammar — RFC 5545 BYMONTHDAY values are integer-only
1137
1137
  }
1138
1138
  return out;
1139
1139
  }
@@ -1168,13 +1168,13 @@ function _utcDateTimeToIcal(s) {
1168
1168
  // JSCalendar UTCDateTime "2026-05-22T10:00:00.123Z" →
1169
1169
  // "20260522T100000Z" (RFC 5545 §3.3.5 form 2 has NO fractional
1170
1170
  // seconds; strict ICS consumers reject `T100000.123Z`).
1171
- return String(s).replace(/\.\d+/, "").replace(/[-:]/g, ""); // allow:bare-split-on-quoted-header — not a header split
1171
+ return String(s).replace(/\.\d+/, "").replace(/[-:]/g, "");
1172
1172
  }
1173
1173
 
1174
1174
  function _localDateTimeToIcal(s) {
1175
1175
  // JSCalendar LocalDateTime "2026-05-22T09:00:00.123" →
1176
1176
  // "20260522T090000" (same fractional-second strip as the UTC form).
1177
- return String(s).replace(/\.\d+/, "").replace(/[-:]/g, ""); // allow:bare-split-on-quoted-header — not a header split
1177
+ return String(s).replace(/\.\d+/, "").replace(/[-:]/g, "");
1178
1178
  }
1179
1179
 
1180
1180
  function _isUtcDateTime(s) {
@@ -212,7 +212,7 @@ function _buildCacheKey(method, url, varyHeaderValues) {
212
212
  // uncacheable.
213
213
  function _extractVaryValues(varyHeader, requestHeaders) {
214
214
  if (typeof varyHeader !== "string" || varyHeader.length === 0) return [];
215
- var names = varyHeader.split(",").map(function (s) { // allow:bare-split-on-quoted-header — RFC 9110 §12.5.5 Vary is a comma-list of field-names (token grammar); no quoted-string
215
+ var names = varyHeader.split(",").map(function (s) { // RFC 9110 §12.5.5 Vary is a comma-list of field-names (token grammar); no quoted-string, so a bare split is correct
216
216
  return s.trim().toLowerCase();
217
217
  }).filter(function (s) { return s.length > 0; });
218
218
  if (names.indexOf("*") !== -1) return null; // sentinel: "uncacheable"
@@ -262,7 +262,7 @@ function _evaluateStorage(method, statusCode, responseHeaders, sharedCache) {
262
262
 
263
263
  // Vary: * is uncacheable per RFC 9110 §12.5.5.
264
264
  if (typeof varyHeader === "string" && varyHeader.indexOf("*") !== -1) {
265
- var trimmed = varyHeader.split(",").map(function (s) { return s.trim(); }); // allow:bare-split-on-quoted-header — RFC 9110 §12.5.5 Vary field-names; token grammar only
265
+ var trimmed = varyHeader.split(",").map(function (s) { return s.trim(); }); // RFC 9110 §12.5.5 Vary field-names; token grammar only, so a bare split is correct
266
266
  if (trimmed.indexOf("*") !== -1) {
267
267
  return { cacheable: false, reason: "vary-star", freshnessMs: -1, directives: directives, varyHeader: varyHeader };
268
268
  }
@@ -319,7 +319,7 @@ function sign(msg, opts) {
319
319
  // header isn't already supplied. Operators wanting to use the
320
320
  // RFC 9530 "sha-512" identifier (SHA-512 instead of SHA3-512) supply
321
321
  // the header themselves; the framework emits SHA3-512.
322
- var coveredLower = opts.covered.map(function (c) { return c.split(";")[0].toLowerCase(); }); // allow:bare-split-on-quoted-header — opts.covered is operator-supplied component-id list (e.g. "content-digest;sf"); component identifiers are RFC 9421 §2.1 derived-field names with token-only grammar; no quoted-string
322
+ var coveredLower = opts.covered.map(function (c) { return c.split(";")[0].toLowerCase(); }); // allow:bare-split-on-quoted-header-token-grammar — opts.covered is operator-supplied component-id list (e.g. "content-digest;sf"); component identifiers are RFC 9421 §2.1 derived-field names with token-only grammar; no quoted-string
323
323
  if (coveredLower.indexOf("content-digest") !== -1 &&
324
324
  _resolveHeader(m.headers, "content-digest") === null) {
325
325
  if (m.body == null) {
@@ -451,7 +451,7 @@ function _parseSignature(headerValue, label) {
451
451
  if (headerValue.indexOf(prefix) !== 0) {
452
452
  // Multiple signature labels can appear; comma-separated. Find the
453
453
  // matching label.
454
- var parts = headerValue.split(","); // allow:bare-split-on-quoted-header — RFC 9421 §2.4 Signature header values are `label=:b64:` form; base64 alphabet excludes `,` and the label tokens are RFC 8941 §3.3.4 sf-token (no DQUOTE in practice)
454
+ var parts = headerValue.split(","); // allow:bare-split-on-quoted-header-token-grammar — RFC 9421 §2.4 Signature header values are `label=:b64:` form; base64 alphabet excludes `,` and the label tokens are RFC 8941 §3.3.4 sf-token (no DQUOTE in practice)
455
455
  for (var i = 0; i < parts.length; i++) {
456
456
  var p = parts[i].trim();
457
457
  if (p.indexOf(prefix) === 0) {
@@ -519,7 +519,7 @@ function verify(msg, opts) {
519
519
  // If content-digest is covered, recompute and compare. RFC 9421 §B.2.5
520
520
  // mandates that verifiers re-run the digest over the body — a stale
521
521
  // header from a proxy would otherwise verify trivially.
522
- var coveredLower = parsedInput.covered.map(function (c) { return c.split(";")[0].toLowerCase(); }); // allow:bare-split-on-quoted-header — same as sign() above: covered items are RFC 9421 §2.1 component-ids, token grammar
522
+ var coveredLower = parsedInput.covered.map(function (c) { return c.split(";")[0].toLowerCase(); }); // allow:bare-split-on-quoted-header-token-grammar — same as sign() above: covered items are RFC 9421 §2.1 component-ids, token grammar
523
523
  if (coveredLower.indexOf("content-digest") !== -1) {
524
524
  if (m.body == null) {
525
525
  return { valid: false, reason: "content-digest-no-body" };
@@ -1352,6 +1352,14 @@ function shadowRegistry(opts) {
1352
1352
  var counters = Object.create(null);
1353
1353
  var gauges = Object.create(null);
1354
1354
  var info = Object.create(null);
1355
+ // Maps a canonical-JSON cardinality key → the string-coerced, null-prototype
1356
+ // label object that produced it, so the render path reads the structured
1357
+ // labels directly instead of re-parsing the key. Re-parsing would both lose a
1358
+ // label NAMED `constructor` / `prototype` / `__proto__` (all valid Prometheus
1359
+ // label names) to a prototype-pollution-hardened JSON parse, and reintroduce a
1360
+ // serialize-then-split round-trip. Populated only when a value is actually
1361
+ // stored, so it stays bounded by the cardinality cap.
1362
+ var labelSets = Object.create(null);
1355
1363
  var lastCardinalityAuditMs = 0;
1356
1364
 
1357
1365
  function _cardinalityHit(metric) {
@@ -1375,19 +1383,27 @@ function shadowRegistry(opts) {
1375
1383
  }
1376
1384
  }
1377
1385
 
1378
- function _labelKey(labels) {
1379
- if (!labels || typeof labels !== "object") return "";
1380
- var keys = Object.keys(labels).sort(); // allow:bare-canonicalize-walk label-set canonicalization for cardinality keying
1381
- var parts = [];
1382
- for (var i = 0; i < keys.length; i += 1) {
1383
- parts.push(keys[i] + "=" + String(labels[keys[i]]));
1384
- }
1385
- return parts.join(",");
1386
+ function _coerceLabels(labels) {
1387
+ // { key, set }: the canonical-JSON cardinality key (collision-proof and
1388
+ // injection-proof, matching the main registry's _labelsKey) plus the
1389
+ // string-coerced, null-prototype label object it came from. The render path
1390
+ // reads `set` directly rather than re-parsing `key`, so a `,` or `=` in a
1391
+ // label VALUE can never forge extra label pairs and a label NAMED
1392
+ // `constructor` / `prototype` / `__proto__` survives verbatim. The
1393
+ // null-prototype object makes a `__proto__` label a normal own property
1394
+ // rather than a prototype assignment.
1395
+ if (!labels || typeof labels !== "object") return { key: "", set: null };
1396
+ var keys = Object.keys(labels);
1397
+ if (keys.length === 0) return { key: "", set: null };
1398
+ var coerced = Object.create(null);
1399
+ for (var i = 0; i < keys.length; i += 1) coerced[keys[i]] = String(labels[keys[i]]);
1400
+ return { key: canonicalJson.stringify(coerced), set: coerced };
1386
1401
  }
1387
1402
 
1388
1403
  function inc(name, labels) {
1389
1404
  if (!counterSet[name]) return;
1390
- var lk = _labelKey(labels);
1405
+ var ck = _coerceLabels(labels);
1406
+ var lk = ck.key;
1391
1407
  if (!counters[name]) counters[name] = Object.create(null);
1392
1408
  var current = counters[name][lk];
1393
1409
  if (current === undefined) {
@@ -1396,6 +1412,7 @@ function shadowRegistry(opts) {
1396
1412
  return;
1397
1413
  }
1398
1414
  counters[name][lk] = 1;
1415
+ if (ck.set) labelSets[lk] = ck.set;
1399
1416
  } else {
1400
1417
  counters[name][lk] = current + 1;
1401
1418
  }
@@ -1407,13 +1424,15 @@ function shadowRegistry(opts) {
1407
1424
  throw new MetricsError("metrics-shadow/bad-gauge-value",
1408
1425
  "shadowRegistry.set: '" + name + "' value must be a finite number");
1409
1426
  }
1410
- var lk = _labelKey(labels);
1427
+ var ck = _coerceLabels(labels);
1428
+ var lk = ck.key;
1411
1429
  if (!gauges[name]) gauges[name] = Object.create(null);
1412
1430
  if (gauges[name][lk] === undefined && Object.keys(gauges[name]).length >= cap) {
1413
1431
  _cardinalityHit(name);
1414
1432
  return;
1415
1433
  }
1416
1434
  gauges[name][lk] = value;
1435
+ if (ck.set) labelSets[lk] = ck.set;
1417
1436
  }
1418
1437
 
1419
1438
  function setInfo(name, value) {
@@ -1449,22 +1468,21 @@ function shadowRegistry(opts) {
1449
1468
  for (var li = 0; li < lks.length; li += 1) {
1450
1469
  var lk = lks[li];
1451
1470
  if (lk === "") { out.push(metric + " " + labelMap[lk]); continue; }
1452
- // The label-key string was assembled by `_labelKey` from a
1453
- // single shadow-registry call's `labels` object — values
1454
- // are framework-internal (operator code that supplied them
1455
- // is bounded by guards upstream); split on `,` is safe.
1456
- // Not a header-value parse (which would need a quoted-
1457
- // string aware split per RFC 9110).
1458
- var lpairs = lk.split(","); // allow:bare-split-on-quoted-header — framework-internal label-key (assembled by _labelKey), not an HTTP header parse
1471
+ // Read the structured label set kept alongside the canonical key (see
1472
+ // _coerceLabels) rather than re-parsing the key. No serialize-then-
1473
+ // split round-trip, so a `,` or `=` in a label value stays inside the
1474
+ // value and can never forge extra label pairs; and a label NAMED
1475
+ // `constructor` / `prototype` / `__proto__` survives instead of being
1476
+ // stripped by a prototype-pollution-hardened parse.
1477
+ var labelObj = labelSets[lk];
1478
+ if (!labelObj || typeof labelObj !== "object") { out.push(metric + " " + labelMap[lk]); continue; }
1479
+ var lnames = Object.keys(labelObj).sort(); // allow:bare-canonicalize-walk — deterministic label ordering in the exposition line
1459
1480
  var formatted = [];
1460
- for (var pi = 0; pi < lpairs.length; pi += 1) {
1461
- var eqIdx = lpairs[pi].indexOf("=");
1462
- if (eqIdx === -1) continue;
1463
- var lname = lpairs[pi].slice(0, eqIdx);
1464
- var lvalue = lpairs[pi].slice(eqIdx + 1);
1481
+ for (var pi = 0; pi < lnames.length; pi += 1) {
1482
+ var lname = lnames[pi];
1465
1483
  if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(lname)) continue; // allow:regex-no-length-cap — Prometheus label-name shape
1466
1484
  // Prometheus exposition: escape `\`, `"`, `\n` in label values.
1467
- lvalue = String(lvalue).replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n"); // allow:regex-no-length-cap — fixed-char-set escape // allow:duplicate-regex — Prometheus value escape shape
1485
+ var lvalue = String(labelObj[lname]).replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n"); // allow:regex-no-length-cap — fixed-char-set escape // allow:duplicate-regex — Prometheus value escape shape
1468
1486
  formatted.push(lname + '="' + lvalue + '"');
1469
1487
  }
1470
1488
  out.push(metric + "{" + formatted.join(",") + "} " + labelMap[lk]);
@@ -1516,9 +1534,10 @@ function shadowRegistry(opts) {
1516
1534
  }
1517
1535
 
1518
1536
  function reset() {
1519
- counters = Object.create(null);
1520
- gauges = Object.create(null);
1521
- info = Object.create(null);
1537
+ counters = Object.create(null);
1538
+ gauges = Object.create(null);
1539
+ info = Object.create(null);
1540
+ labelSets = Object.create(null);
1522
1541
  lastCardinalityAuditMs = 0;
1523
1542
  }
1524
1543
 
@@ -351,7 +351,7 @@ function _detectSmuggling(req) {
351
351
  // (RFC 9112 §6.1). Anything else is a smuggling vector or
352
352
  // server-side decode error.
353
353
  if (typeof te === "string" && te.length > 0) {
354
- var tokens = te.toLowerCase().split(",").map(function (t) { return t.trim(); }); // allow:bare-split-on-quoted-header — RFC 9112 §6.1 Transfer-Encoding values (chunked / gzip / deflate / identity) are token-only; no quoted-string in the grammar
354
+ var tokens = te.toLowerCase().split(",").map(function (t) { return t.trim(); }); // RFC 9112 §6.1 Transfer-Encoding values (chunked / gzip / deflate / identity) are token-only; no quoted-string in the grammar, so a bare split is correct
355
355
  var last = tokens[tokens.length - 1];
356
356
  if (last !== "chunked") {
357
357
  return {
@@ -237,8 +237,8 @@ async function _dispatch(req, res, basePath, bearer, opts, maxPageSize, bulkCfg)
237
237
  count: pageSize,
238
238
  sortBy: query.sortBy || null,
239
239
  sortOrder: query.sortOrder || null,
240
- attributes: query.attributes ? query.attributes.split(",") : null, // allow:bare-split-on-quoted-header — RFC 7644 §3.9 attributes/excludedAttributes are SCIM attribute paths (URN-ish identifiers); grammar excludes DQUOTE
241
- excludedAttributes: query.excludedAttributes ? query.excludedAttributes.split(",") : null, // allow:bare-split-on-quoted-header — same SCIM attribute-name grammar
240
+ attributes: query.attributes ? query.attributes.split(",") : null, // allow:bare-split-on-quoted-header-token-grammar — RFC 7644 §3.9 attributes/excludedAttributes are SCIM attribute paths (URN-ish identifiers); grammar excludes DQUOTE
241
+ excludedAttributes: query.excludedAttributes ? query.excludedAttributes.split(",") : null, // allow:bare-split-on-quoted-header-token-grammar — same SCIM attribute-name grammar
242
242
  }, ctx);
243
243
  _writeJson(res, H.OK, {
244
244
  schemas: [SCIM_MESSAGE_LIST],
@@ -613,7 +613,7 @@ async function tlsRptFetchPolicy(domain, opts) {
613
613
  var rua = [];
614
614
  for (var p = 0; p < pairs.length; p += 1) {
615
615
  if (pairs[p][0] === "rua") {
616
- var uris = pairs[p][1].split(","); // allow:bare-split-on-quoted-header — allow:raw-time-literal — TLS-RPT rua grammar (RFC 8460 §3): rua = tlsrpt-uri *("," tlsrpt-uri); URIs percent-encode reserved chars, no quoted-string
616
+ var uris = pairs[p][1].split(","); // allow:bare-split-on-quoted-header-token-grammar — allow:raw-time-literal — TLS-RPT rua grammar (RFC 8460 §3): rua = tlsrpt-uri *("," tlsrpt-uri); URIs percent-encode reserved chars, no quoted-string
617
617
  for (var u = 0; u < uris.length; u += 1) {
618
618
  var uri = uris[u].trim();
619
619
  if (uri.length > 0) rua.push(uri);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.15.34",
3
+ "version": "0.15.36",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.15.35",
4
+ "date": "2026-06-26",
5
+ "headline": "`b.metrics` shadow registry no longer lets a comma in a label value forge extra Prometheus label pairs — a label-injection that downstream tenant-scoping or authorization selectors could be tricked by",
6
+ "summary": "The shadow registry serialized a metric's label set into a `name=value,name=value` string key and re-split it on `,` to build the Prometheus exposition. A label VALUE containing a comma therefore split into multiple label pairs, so a single operator-set label could forge additional label pairs in the scrape output (for example turning one `route` label into a `route` plus an attacker-named label), which downstream tenant-scoping filters, authorization selectors, recording rules, and alerting trust as a boundary; two distinct label sets could also collide into one cardinality bucket. The label key now uses canonical-JSON of the string-coerced label set (the same approach the main registry already used) and the render path emits the structured label set kept alongside that key rather than splitting or re-parsing, so a `,` or `=` inside a label value stays inside the value and a label NAMED `constructor`, `prototype`, or `__proto__` (all valid Prometheus label names) is preserved instead of being dropped. Note: the cardinality keys exposed by `shadowRegistry().snapshot()` are now canonical-JSON strings (e.g. `{\"route\":\"/api\"}`) rather than `route=/api`.",
7
+ "sections": [
8
+ {
9
+ "heading": "Security",
10
+ "items": [
11
+ {
12
+ "title": "Label values can no longer forge extra Prometheus label pairs in the shadow registry",
13
+ "body": "b.metrics shadowRegistry built a metric's cardinality key by joining `name=value` pairs with commas, then split that string on commas when rendering the Prometheus exposition. A comma (and `=`) in a label value therefore broke one value into several forged label pairs in the scrape output, and two different label sets could collide into the same cardinality bucket. The key is now canonical-JSON of the string-coerced label set and the render emits the structured label set kept alongside that key rather than splitting or re-parsing, so a label value is emitted as a single quoted value with its commas/equals intact, distinct label sets get distinct keys, and a label NAMED `constructor`, `prototype`, or `__proto__` (each a valid Prometheus label name) is preserved rather than dropped. Deterministic regression tests assert a comma-bearing label value produces exactly one label pair and that the reserved-name labels survive render."
14
+ }
15
+ ]
16
+ },
17
+ {
18
+ "heading": "Changed",
19
+ "items": [
20
+ {
21
+ "title": "shadowRegistry().snapshot() cardinality keys are canonical-JSON",
22
+ "body": "As a consequence of the label-injection fix, the per-series cardinality keys in a shadow-registry snapshot are now canonical-JSON strings (e.g. `{\"tenant\":\"a\"}`) instead of the previous `tenant=a` form. Code that reads those keys directly from snapshot() should parse them as JSON; the Prometheus render output is unchanged for conforming label values."
23
+ }
24
+ ]
25
+ }
26
+ ]
27
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.15.36",
4
+ "date": "2026-06-27",
5
+ "headline": "Internal test-suite hardening only — the published library's runtime behavior and public API are unchanged (source-comment marker text aside)",
6
+ "summary": "The codebase-patterns guard class that allows a bare comma/semicolon split on token-only RFC header grammars was re-verified from scratch and renamed to a descriptive token, with its old name recorded as retired. Each live marked site (RRULE, RFC 9421 component identifiers, TLS-RPT rua, SCIM attribute paths) was re-read and confirmed to split a grammar with no quoted-string members. Five marker comments that suppressed nothing were removed or turned into plain explanatory comments. No runtime code, public API, or wire format changed.",
7
+ "sections": [
8
+ {
9
+ "heading": "Detectors",
10
+ "items": [
11
+ {
12
+ "title": "Bare token-only header-split suppression class re-verified, renamed, and pruned of inert markers",
13
+ "body": "Each marked bare `.split(\",\")` / `.split(\";\")` on an RFC header value was re-read and confirmed to operate on a token-only grammar (no quoted-string members, so a quote-aware splitter is unnecessary): RFC 5545 RRULE, RFC 9421 signature component identifiers, RFC 8460 TLS-RPT rua, and RFC 7644 SCIM attribute paths. The guard class was renamed to a descriptive token and its old name added to the retired-token set. Five marker comments that the detector never actually evaluated (two sat on a date-normalizing `.replace`, three in header parsers the guard intentionally does not scan) were removed or converted to plain comments. This is test-suite tooling plus source-comment text; no shipped framework behavior changed."
14
+ }
15
+ ]
16
+ }
17
+ ]
18
+ }
@@ -319,7 +319,7 @@ var VALID_ALLOW_CLASSES = {
319
319
  "backup-adapter-storage-without-posture-check": 1,
320
320
  "bare-canonicalize-walk": 1,
321
321
  "bare-error-throw": 1,
322
- "bare-split-on-quoted-header": 1,
322
+ "bare-split-on-quoted-header-token-grammar": 1,
323
323
  "console-direct": 1,
324
324
  "deny-path-hardcoded-response": 1,
325
325
  "duplicate-regex": 1,
@@ -389,6 +389,7 @@ var RETIRED_ALLOW_TOKENS = {
389
389
  "dynamic-require": "renamed to 'dynamic-require-operator-module' (2026-06-26 re-verify pass) — every site loads an operator-supplied path (migration / seed / extensibility entry, centralized in module-loader.js) or is diagnostic message text; re-verify before reusing",
390
390
  "raw-hash-compare": "renamed to 'raw-hash-compare-nonsecret-tag' (2026-06-26 re-verify pass) — the one site compares a data-residency region tag with ===, not a secret hash; re-verify before reusing",
391
391
  "seal-without-aad": "renamed to 'seal-without-aad-by-design' (2026-06-26 re-verify pass) — both sites intentionally seal without AAD (a non-regulated plain-mode table whose AAD is enforced by the posture sealEnvelopeFloor where required, and a throwaway vault-readiness probe sentinel); re-verify before reusing",
392
+ "bare-split-on-quoted-header": "renamed to 'bare-split-on-quoted-header-token-grammar' (2026-06-26 re-verify pass) — every live marker splits an RFC token-only header grammar with no quoted-string members (RRULE / RFC 9421 component-ids / TLS-RPT rua / SCIM attribute paths), so a bare comma/semicolon split is correct; re-verify the grammar before reusing. The re-verify also found 5 markers were inert (detector pre-filter skips their file, or the line is a .replace not a split) and cleared them",
392
393
  };
393
394
 
394
395
  function testNoRetiredAllowTokenReRegistered() {
@@ -10320,7 +10321,7 @@ function testNoBoolStringCoerceShape() {
10320
10321
  //
10321
10322
  // Surfaced 2026-05-11 — `b.cdnCacheControl.parse` did this.
10322
10323
  function testNoBareCommaSplitOnQuotedHeader() {
10323
- // class: bare-split-on-quoted-header
10324
+ // class: bare-split-on-quoted-header-token-grammar
10324
10325
  var files = _libFiles();
10325
10326
  var bad = [];
10326
10327
  for (var fi = 0; fi < files.length; fi++) {
@@ -10342,7 +10343,7 @@ function testNoBareCommaSplitOnQuotedHeader() {
10342
10343
  });
10343
10344
  }
10344
10345
  }
10345
- bad = _filterMarkers(bad, "bare-split-on-quoted-header");
10346
+ bad = _filterMarkers(bad, "bare-split-on-quoted-header-token-grammar");
10346
10347
  _report("RFC structured-fields parser must use quote-aware top-level splitter, not bare `.split(\",\") / .split(\";\")` (RFC 8941 §3.3.3 quoted-string values can contain delimiter chars — cdn-cache-control.parse bug class)",
10347
10348
  bad);
10348
10349
  }
@@ -28,7 +28,7 @@ function testInc() {
28
28
  shadow.inc("hits", { route: "/api" });
29
29
  var snap = shadow.snapshot();
30
30
  check("counter incremented", snap.counters.hits[""] === 2);
31
- check("labeled counter tracked", snap.counters.hits["route=/api"] === 1);
31
+ check("labeled counter tracked", snap.counters.hits['{"route":"/api"}'] === 1);
32
32
  // Non-mirrored counter silently ignored.
33
33
  shadow.inc("not_mirrored");
34
34
  check("non-mirrored ignored", snap.counters.not_mirrored === undefined);
@@ -43,7 +43,7 @@ function testSet() {
43
43
  shadow.set("queue_depth", 7, { tenant: "a" });
44
44
  var snap = shadow.snapshot();
45
45
  check("gauge set", snap.gauges.queue_depth[""] === 42);
46
- check("labeled gauge", snap.gauges.queue_depth["tenant=a"] === 7);
46
+ check("labeled gauge", snap.gauges.queue_depth['{"tenant":"a"}'] === 7);
47
47
  }
48
48
 
49
49
  function testCardinalityCap() {
@@ -83,6 +83,60 @@ function testPrometheusPreservesLabels() {
83
83
  out.indexOf("# TYPE tenant_b_hits_total counter") !== -1);
84
84
  }
85
85
 
86
+ function testPrometheusLabelValueInjection() {
87
+ // A `,` or `=` in a label VALUE must stay inside the value — it must NOT
88
+ // forge extra label pairs in the exposition output (label injection that
89
+ // downstream tenant-scoping / authz selectors / recording rules would trust
90
+ // as a boundary). The shadow registry previously serialized the label set to
91
+ // a `name=value,...` key and re-split it on `,` for rendering, so a comma in
92
+ // a value split into multiple forged pairs.
93
+ var shadow = b.metrics.snapshot.shadowRegistry({
94
+ namespace: "tenant_x",
95
+ counters: ["hits_total"],
96
+ });
97
+ shadow.inc("hits_total", { route: 'a,evil="bad' });
98
+ var out = shadow.render({ format: "prometheus" });
99
+ // The full value (comma + escaped quote) stays inside route's quotes.
100
+ check("label value with a comma stays inside the value (no forged pair)",
101
+ out.indexOf('route="a,evil=\\"bad"') !== -1);
102
+ // The series line carries exactly ONE label pair (no injected second label).
103
+ var line = out.split("\n").filter(function (l) { return l.indexOf("tenant_x_hits_total{") === 0; })[0] || "";
104
+ var braces = line.slice(line.indexOf("{") + 1, line.lastIndexOf("}"));
105
+ var labelNames = (braces.match(/(^|,)[a-zA-Z_][a-zA-Z0-9_]*="/g) || []).length; // allow:regex-no-length-cap — counts top-level label-name= boundaries in a bounded exposition line
106
+ check("exactly one label pair (no injected label)", labelNames === 1);
107
+ }
108
+
109
+ function testPrometheusReservedLabelNames() {
110
+ // A label whose NAME is `constructor`, `prototype`, or `__proto__` is a valid
111
+ // Prometheus label name ([a-zA-Z_][a-zA-Z0-9_]*) and must survive render. The
112
+ // shadow registry must NOT route the label key through a prototype-pollution-
113
+ // hardened JSON parse (which strips those names), nor lose the dimension.
114
+ var shadow = b.metrics.snapshot.shadowRegistry({
115
+ namespace: "tenant_y",
116
+ counters: ["hits_total"],
117
+ });
118
+ shadow.inc("hits_total", { constructor: "a" });
119
+ shadow.inc("hits_total", { prototype: "b" });
120
+ // `{ __proto__: "c" }` as a literal sets the prototype (a no-op for a string);
121
+ // the real vector is a labels object that came from parsed external data, so
122
+ // its `__proto__` is an own enumerable property.
123
+ shadow.inc("hits_total", JSON.parse('{"__proto__":"c"}'));
124
+ var out = shadow.render({ format: "prometheus" });
125
+ check("label named constructor survives render",
126
+ out.indexOf('tenant_y_hits_total{constructor="a"} 1') !== -1);
127
+ check("label named prototype survives render",
128
+ out.indexOf('tenant_y_hits_total{prototype="b"} 1') !== -1);
129
+ check("label named __proto__ survives render",
130
+ out.indexOf('tenant_y_hits_total{__proto__="c"} 1') !== -1);
131
+ // No dimension collapsed into a bare unlabeled series: every value line for
132
+ // this metric carries a label brace.
133
+ var valueLines = out.split("\n").filter(function (l) {
134
+ return l.indexOf("tenant_y_hits_total") === 0 && l.indexOf("# TYPE") !== 0;
135
+ });
136
+ var bare = valueLines.filter(function (l) { return l.indexOf("{") === -1; });
137
+ check("no unlabeled bare series leaked", bare.length === 0);
138
+ }
139
+
86
140
  function testRefusalsAtConfigTime() {
87
141
  var threw;
88
142
  threw = false; try { b.metrics.snapshot.shadowRegistry({}); } catch (_e) { threw = true; }
@@ -105,6 +159,8 @@ function run() {
105
159
  testSet();
106
160
  testCardinalityCap();
107
161
  testPrometheusPreservesLabels();
162
+ testPrometheusLabelValueInjection();
163
+ testPrometheusReservedLabelNames();
108
164
  testRefusalsAtConfigTime();
109
165
  }
110
166
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {