@blamejs/core 0.17.17 → 0.17.18

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,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.17.x
10
10
 
11
+ - v0.17.18 (2026-07-24) — **`b.i18n` validates every inline translation tree at boot, closing a path where t() could return undefined.** b.i18n.create validated the translation trees only for the locales listed in the configured locales array. A locale present in the inline translations map but absent from locales is still reachable through an explicit t(key, vars, { locale }) override, and its tree escaped validation: a plural entry there missing the mandatory CLDR 'other' category made t() with a count return undefined instead of the key -- a contract violation that could render the literal string 'undefined' into server-rendered HTML or crash a downstream length/escape. Every inline translation tree is now validated at create, so a malformed one fails closed at boot rather than at the first request that reaches it. **Fixed:** *Every inline translation tree is validated at create, not only the configured locales* — b.i18n.create walked only opts.locales when validating translation trees up front, so a locale that appears in opts.translations but not in opts.locales was never checked even though it is reachable via an explicit locale override on t()/has(). A plural entry in such a tree that omits the mandatory 'other' category caused t(key, { count }) to select 'other', find nothing, and return undefined -- violating the documented contract that t() returns a string (the key on a miss) and never a non-string. create now validates every key of opts.translations, so a malformed tree is rejected at boot regardless of whether its locale is in opts.locales.
12
+
11
13
  - v0.17.17 (2026-07-23) — **Three verifier hardening fixes: timestamp trust anchors are required by default, OIDC id_token verification requires a configured issuer, and ed25519 DKIM uses the RFC 8463 raw-key format.** An audit of the signature-verifier surface found three verifiers that accepted or rejected the wrong thing. b.tsa.verifyToken authenticated a timestamp against a certificate embedded in the token itself and only ran the trust-anchor chain when the caller supplied one, so a self-signed token carrying the timestamping EKU was accepted as a valid timestamp; trust anchors are now required by default. b.auth.oauth's verifyIdToken skipped the CVE-2026-23552 cross-realm iss check entirely when the client was created without an issuer, so any OIDC id_token verified regardless of its issuer; an OIDC client must now be configured with an issuer to verify id_tokens. And the ed25519 DKIM verifier could not read the RFC 8463 raw 32-byte key format that every conformant sender publishes -- while the framework's own bootstrap published the non-standard SPKI form -- so ed25519, the default DKIM algorithm, was non-interoperable in both directions. **Fixed:** *ed25519 DKIM uses the RFC 8463 raw-key format on both sign and verify* — RFC 8463 §3-4 publishes an ed25519 DKIM public key as the raw 32-byte key, base64'd -- the form every conformant sender uses -- but the verifier wrapped that raw key in SubjectPublicKeyInfo PEM markers, which is not valid SPKI, so createPublicKey threw and a valid ed25519 signature returned permerror. Compounding it, b.mail.dkim.bootstrap published the key as SPKI DER (60 base64 chars) instead of the raw form (44 chars), so the framework's own default-algorithm signatures would not verify at a conformant receiver. The verifier now accepts a raw 32-byte key (wrapping it in the ed25519 SPKI header) and bootstrap publishes the raw key; RSA is unchanged. The same raw-key handling is applied on the ARC (b.mail.arc.verify) message-signature path. **Security:** *Timestamp verification requires a trust anchor by default* — b.tsa.verifyToken verified an RFC 3161 timestamp against the signer certificate embedded in the token -- which is attacker-controlled -- and ran the certificate-chain / trust-anchor check only when the caller passed trustAnchorsPem. With it omitted, a token whose self-signed leaf carried a critical, sole id-kp-timeStamping EKU was accepted as a valid timestamp: a forged proof that data existed at an arbitrary time (RFC 3161 §2.4.2 requires a trusted TSA). trustAnchorsPem is now required by default; an operator who deliberately wants trust-anchor-free verification sets allowUntrustedIssuer:true and the result carries issuerTrusted:false so the unauthenticated posture is visible (mirrors b.mdoc.verifyIssuerSigned). · *OIDC id_token verification requires a configured issuer* — b.auth.oauth.verifyIdToken wrapped the expected-issuer comparison -- the CVE-2026-23552 cross-realm / cross-issuer defense -- in a check that ran only when the client was configured with an issuer. An OIDC client (the default) created without an issuer therefore accepted an id_token with any iss, or none, as long as the signature, aud, and exp were valid: exactly the cross-realm acceptance the verifier exists to close, dangerous against a shared-signing-key multi-tenant OP. verifyIdToken now refuses to verify an OIDC id_token when no expected issuer is configured (OIDC Core §3.1.3.7); pass issuer to b.auth.oauth.create().
12
14
 
13
15
  - v0.17.16 (2026-07-23) — **A shared path-containment primitive backs both the strict resolver and static file serving.** b.safePath.resolve and b.staticServe each hand-rolled the same lexical traversal-containment check -- resolve a request path against a base and confirm it stays strictly inside. That core is now one primitive, b.safePath.confineToBase, which both compose: resolve layers its user-input strictness (reserved names, NTFS ADS markers, bidi, control chars) on top, while static serving composes only the bare containment and keeps its own separate basename gate. Static serving keeps that separation deliberately -- its containment barrier and its per-file guardFilename validation are distinct concerns -- and inherits the resolver's cross-platform-aware containment, which the runtime path module missed for a backslash traversal on a POSIX host. Static serving's basename policy is unchanged. **Added:** *b.safePath.confineToBase -- the lexical traversal-containment core* — b.safePath.confineToBase(base, rel, opts?) resolves rel against base using the target platform's path semantics and returns the confined absolute path, or null if it escapes. It is the containment barrier b.safePath.resolve layers its user-input strictness on top of, exposed for a consumer that wants ONLY traversal containment and applies its own, differently-calibrated filename validation -- so it does not reject a reserved name, NTFS ADS marker, or trailing dot the way resolve does for untrusted input. opts.platform forces windows path semantics on any host. **Changed:** *Static file serving composes the shared containment primitive* — b.staticServe's path-traversal barrier now composes b.safePath.confineToBase for its final lexical containment instead of a hand-rolled join + base-prefix check, so both the strict resolver and static serving route through one implementation. Static serving gains the cross-platform-aware containment (the runtime path module treats the other platform's separator as an ordinary filename character and missed a backslash traversal on a POSIX host). Serve behavior is unchanged: containment and the separate per-file guardFilename basename gate remain distinct steps, and static composes only the containment core rather than resolve so it does not fuse resolve's all-segment user-input strictness into the barrier (which would reject a legitimate colon-named intermediate directory that the basename gate permits). The basename policy itself is untouched.
package/lib/i18n.js CHANGED
@@ -582,15 +582,19 @@ function create(opts) {
582
582
  translations = {};
583
583
  }
584
584
  var onMissingKey = opts.onMissingKey || null;
585
- // Validate translation trees up-front so plural-shape errors surface
586
- // at boot, not at the first request that hits the broken key. Lazy
587
- // locales validate on first load.
588
- for (var li = 0; li < locales.length; li++) {
589
- var loc = locales[li];
585
+ // Validate translation trees up-front so plural-shape errors surface at
586
+ // boot, not at the first request that hits the broken key. Validate EVERY
587
+ // inline tree, not just the configured `locales`: a locale present in
588
+ // `translations` but absent from `locales` is still reachable via an explicit
589
+ // locale override, and the invariants the lookup relies on (notably
590
+ // _selectPlural assuming a mandatory `other`) must hold for it too — an
591
+ // unvalidated tree could otherwise make t() return undefined instead of the
592
+ // key. Lazy locales validate on first load.
593
+ Object.keys(translations).forEach(function (loc) {
590
594
  if (translations[loc]) {
591
595
  _validateTranslationTree(loc, translations[loc], "");
592
596
  }
593
- }
597
+ });
594
598
 
595
599
  function _ensureLocaleLoaded(locale) {
596
600
  if (loadedSet.has(locale)) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.17.17",
3
+ "version": "0.17.18",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:7533261e-9d68-4674-b24d-8a05ae89f2aa",
5
+ "serialNumber": "urn:uuid:9e77e5e1-9949-4d25-b9b0-6d7373fafe90",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-24T06:49:37.276Z",
8
+ "timestamp": "2026-07-24T12:50:30.247Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.17.17",
22
+ "bom-ref": "@blamejs/core@0.17.18",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.17.17",
25
+ "version": "0.17.18",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.17.17",
29
+ "purl": "pkg:npm/%40blamejs/core@0.17.18",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.17.17",
57
+ "ref": "@blamejs/core@0.17.18",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]