@blamejs/core 0.16.19 → 0.16.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/lib/auth/jwt.js +34 -0
- package/lib/auth/saml.js +19 -1
- package/lib/guard-jwt.js +18 -2
- package/lib/http-message-signature.js +34 -0
- package/lib/i18n-messageformat.js +14 -3
- package/lib/i18n.js +25 -15
- package/lib/mail-scan.js +23 -16
- package/lib/session.js +12 -4
- package/lib/webhook.js +12 -2
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.16.x
|
|
10
10
|
|
|
11
|
+
- v0.16.21 (2026-07-12) — **Bind the declared JWS/HTTP-signature algorithm to the key's real type across b.auth.jwt and b.crypto.httpSig (closing an algorithm-confusion allowlist bypass), refuse a JWT whose payload is not a JSON object in b.guardJwt, and enforce the SAML InResponseTo replay check on the holder-of-key confirmation.** Covering untested adversarial branches across the token and assertion verifiers surfaced a set of fail-open auth defects, fixed at the root. b.auth.jwt.verify gated its algorithm allowlist on the self-declared JWS alg header while node:crypto verified against the key's intrinsic type, so a token signed by an ML-DSA key but declaring a SLH-DSA alg passed a SLH-only allowlist and verified against the ML-DSA key -- a full algorithm-allowlist bypass (CWE-347). The declared alg is now bound to the key's real type at both sign and verify. The same class existed unenforced in b.crypto.httpSig (a peer could sign with a classical ed25519 key while declaring the PQC alg ml-dsa-65, emitting an authenticated but false PQC-signed label); it now binds the alg to the key's type at sign and verify too. b.guardJwt skipped its entire claim-sanity block (required-claims + exp/nbf/iat) when the payload did not decode to a JSON object, so a token carrying no readable claims set was admitted at strict; a non-object payload is now refused symmetrically with the header. And b.auth.saml validated InResponseTo on the Bearer subject-confirmation but not the holder-of-key one, silently dropping an operator's replay binding on every HoK assertion; the holder-of-key path now applies the identical check. **Security:** *b.auth.jwt binds the declared algorithm to the key's real type (algorithm-confusion bypass)* — verify() constrained the algorithm allowlist against the self-declared JWS `alg` header -- a value the token issuer controls -- while node:crypto.verify(null, ...) selects the actual algorithm from the KeyObject's intrinsic type. The two could diverge: a token signed by an ML-DSA-87 key but declaring alg="SLH-DSA-SHAKE-256f" passed an SLH-only allowlist and was verified against the ML-DSA public key, and sign() could emit a token whose header alg misstated its own signing algorithm -- the exact mislabeled artifact the verify bypass consumes (CWE-347 algorithm confusion). Both sign() and verify() now assert the declared alg matches the resolved key's asymmetricKeyType, so the allowlist genuinely constrains which algorithm authenticated the token; a matched alg/key round-trip is unaffected. · *b.crypto.httpSig binds the declared algorithm to the key's real type* — HTTP Message Signature sign() and verify() validated the declared `alg` against the supported set but never bound it to the key's type -- so a peer could sign with a classical ed25519 key while declaring the post-quantum alg ml-dsa-65 (an authenticated but false PQC-signed label a verifier trusting the label would honor), the same algorithm-confusion class. sign() now refuses to emit a token whose alg misstates the private key's type, and verify() refuses -- before the crypto check -- a resolved key whose type differs from the declared alg. Legitimate ed25519 and ml-dsa-65 round-trips are unaffected. · *b.guardJwt refuses a JWT whose payload is not a JSON object* — The guard skipped its entire payload claim-sanity block -- required-claims and exp/nbf/iat -- whenever the payload segment did not decode to a JSON object (undecodable base64url, non-JSON bytes, a JSON primitive, or a JSON array), so a token carrying no readable claims set was admitted at the strict profile (validate returned ok and gate returned serve), defeating the guard's advertised missing-claim refusals. A non-object payload is now refused with a high-severity payload-decode finding, symmetric with the header-decode path, at every profile. · *b.auth.saml enforces the InResponseTo replay check on holder-of-key confirmations* — verifyResponse validated the SubjectConfirmationData InResponseTo against the expected AuthnRequest ID on the Bearer confirmation path but not the holder-of-key one, so an operator that opted into InResponseTo binding (solicited-response / replay correlation) silently lost it on every holder-of-key assertion -- a HoK assertion with a mismatched or absent InResponseTo was accepted, and the returned value was hardcoded to null. The holder-of-key path now applies the identical constant-time InResponseTo check the Bearer path uses and echoes the validated value, so the replay binding holds on both confirmation methods.
|
|
12
|
+
|
|
13
|
+
- v0.16.20 (2026-07-12) — **Make the ClamAV scan-verdict classifier fail closed on a coalesced infected+clean reply, refuse a session under a strict device-binding threshold when the anomaly score can't be computed, guard the MessageFormat select renderer against prototype-chain keys, and reject a non-canonical webhook timestamp.** Covering untested adversarial branches across the mail scanner, the session device-binding verifier, the i18n message renderer, and the webhook verifier surfaced a set of fail-open and correctness defects, fixed at the root. The mail scanner's ClamAV INSTREAM reply classifier tested the benign OK token before the malign FOUND token, so a reply carrying both (a coalesced or stale-then-fresh reply on a reused connection) resolved to clean and delivered an infected message; the classifier now tests FOUND, then ERROR, then OK, so a malign signal always dominates and only an OK with no FOUND/ERROR is clean. Session verification under a strict maxAnomalyScore device-binding policy admitted a relocated device whenever no decisive anomaly score could be produced (no scorer supplied, or a scorer that threw or returned a non-finite value) -- it now fails closed, matching the unreadable-binding discipline. The ICU-style MessageFormat select renderer looked up an end-user-supplied select value with a bare object index, so a value naming an Object.prototype member (__proto__ / constructor / toString) returned an inherited member and either rendered garbage or threw a render-time DoS; every case lookup is now own-property only. And the webhook verifier accepted any string that Number() maps to the signed timestamp (scientific, trailing .0, hex, leading zero/plus), making the authenticated t= field malleable; it now requires the canonical decimal. **Fixed:** *b.webhook verifier rejects a non-canonical authenticated timestamp* — The verifier parsed the authenticated t= field with Number() and validated only the post-coercion integer, then re-composed the signed string from that integer -- so any string Number() maps to the same value (scientific 1.7e9, a trailing .0, hex 0x..., a leading zero/plus/whitespace) re-canonicalized into the identical signed string and verified, making the authenticated timestamp field malleable. The verifier now also requires the raw bytes to equal the canonical decimal the signer emits, the same strict digits-only parse the Stripe-compatible verifier already applied; the only legitimate producer (b.webhook.signer) emits the canonical form, so there is no interop cost. · *b.i18n.dir honors a custom RTL language configured in any case* — dir() folds the requested locale's primary subtag to lower case before the RTL-membership test, but a custom rtlLanguages list was stored verbatim, so an operator-supplied entry like "AR" or "CKB" never matched the lower-cased lookup and the language rendered left-to-right. The configured list is now folded to lower case the same way the lookup is, so a custom RTL entry matches regardless of case. · *b.i18n.t resolves a leaf shadowed by a namespace in a more-specific locale* — The fallback-chain lookup returned the first locale where the dotted key resolved to any non-undefined value, including a nested namespace object -- so a namespace at that path in the requested locale halted the chain and shadowed a real translation leaf defined in a fallback locale, leaking the raw key into the UI (and has() reported false). The lookup now treats only a renderable leaf (a string or a plural-shaped object) as a hit and keeps walking the chain past a namespace object, so a leaf in a fallback locale resolves. **Security:** *b.mail.scan fails closed on a coalesced infected+clean ClamAV reply* — The ClamAV INSTREAM reply classifier tested the benign `stream: OK` token before the malign `... FOUND` token, so a reply that carried both -- a coalesced or stale-then-fresh reply on a reused connection, or an intermediary that concatenates two responses -- resolved to a clean verdict and delivered an infected message (a scan bypass). The classifier is a security verdict, so it now tests FOUND (infected) first, then ERROR (do-not-deliver), then OK (clean); an unrecognized shape is still an error. A reply containing a FOUND signal can no longer be downgraded to clean by an adjacent OK token. · *b.session.verify fails closed under a strict anomaly threshold when the score can't be computed* — Under the strict maxAnomalyScore device-binding policy, verify() only assigned a fingerprint anomaly score when the supplied scorer returned a finite number. With no scorer (the option is documented as pairing with one but is omittable), or a scorer that threw or returned a non-finite value, the score stayed null and the refusal guard (score present AND above threshold) was false, so verify RETURNED a session bound to one device when presented from another. An uncomputable anomaly score on genuine fingerprint drift now fails closed (verify returns null), matching the fail-closed discipline the unreadable-binding branch already applied; the legitimate path (a finite score at or below the threshold admits, with the score surfaced) is unchanged. · *b.i18n MessageFormat select renderer guards against prototype-chain keys* — The ICU-style MessageFormat select renderer resolved a case with a bare object index on an end-user-supplied select value, so a value naming an Object.prototype member -- __proto__ / constructor / toString / hasOwnProperty / valueOf -- returned a truthy INHERITED member, bypassing the `other` fallback: an inherited value with no length rendered as empty (silent output corruption), and an inherited function threw when rendered as a non-array (a request-level DoS). Every select/plural case lookup is now own-property only, so an attacker-supplied select value that names a prototype member falls through to the `other` case. **Detectors:** *MessageFormat case lookups must be own-property* — A structural check asserts that the MessageFormat renderer resolves a select/plural case through the own-property helper rather than a bare object index, so an end-user-supplied select value can never reach the prototype chain and re-open the corruption/DoS class.
|
|
14
|
+
|
|
11
15
|
- v0.16.19 (2026-07-12) — **Close a family of entity- and whitespace-hidden dangerous-URL-scheme and CSS-injection bypasses across b.guardHtml / b.guardSvg / b.guardMarkdown behind one shared normalizer, bound a DNS decompression-pointer name bomb, refuse an ambiguous audit-anchor canonicalization, and stop an empty-string sealed field from crashing an AAD-table write.** Covering untested adversarial branches across the content guards, a DNS wire parser, the audit-log anchor, and the sealed-field codec surfaced a family of fail-open defects, fixed at the root. The most serious span the content-guard URL-scheme and CSS-injection denylists: a browser removes tab/lf/cr from anywhere in a URL and trims a leading/trailing control-or-space run before resolving the scheme, and character-reference-decodes a style attribute before the CSS parser sees it, so an entity-encoded tab, an entity-encoded leading space, an HTML named entity, or an entity-encoded CSS token could all navigate/execute while reading past a denylist that matched the raw bytes. b.guardHtml missed the whitespace normalization entirely and matched CSS against raw bytes; b.guardMarkdown decoded only numeric entities; b.guardSvg had a partial fix. All three now route their scheme and CSS-token checks through one pair of shared codepoint-class normalizers, so no guard can strip a different set of encodings than another. A DNS compression-pointer chain could decompress a single name past the RFC 1035 255-octet / label caps (a decompression-amplification DoS); the audit-log anchor's newline-delimited signed bytes let one signature validate for several different tip/previous-tip splits (defeating the linkage tamper-evidence); an SVG animation element permitted under a permissive profile lost its open tag; and an empty-string sealed column crashed an AAD-table insert. **Fixed:** *b.guardSvg preserves a profile-permitted safe animation element* — Under a permissive profile that allows animation, a safe animation element (for example an <animate> whose attributeName targets a visual property) had its open tag dropped while its end tag was still emitted, leaving an orphan close tag and silently stripping an element the profile permits. The sanitize path now affirmatively re-permits the safe-target case; an unsafe-target animation (attributeName=href and similar) is still dropped and neutralized. · *b.cryptoField.sealRow seals an empty-string field as a tamper-evident envelope* — sealRow dispatched every non-null value -- including an empty string -- to one of three envelope-seal branches that disagreed on empty plaintext: the plain and per-row-key branches handled it, but the AAD branch is fail-closed and threw, so a write to an AAD-sealed table whose sealed column held an empty string crashed the insert. An empty string now encodes to a non-empty typed marker before sealing, so it becomes a real authenticated envelope (never a bare plaintext empty string) that round-trips to an empty string on read. On the read side, a bare empty string found in an AAD-bound or per-row-key sealed column -- which after this change can only be an envelope-downgrade by a DB-write attacker -- fails closed (the cell is nulled and audited) instead of being accepted as a valid empty value, so the sealed-column tamper-evidence guarantee holds for empty values too. **Security:** *b.guardHtml / b.guardSvg / b.guardMarkdown refuse entity- and whitespace-hidden dangerous URL schemes* — The URL-scheme denylist on every URL-bearing attribute (href / xlink:href / markdown link, image, autolink, reference-definition) is resolved after normalizing the value the way a browser does: character-reference decoding (numeric AND the HTML5 named-entity ASCII subset browsers honor, e.g. 	 / :), removing tab/lf/cr from anywhere, and trimming a leading/trailing C0-control-or-space run. Previously b.guardHtml stripped neither tab/lf/cr nor an entity-encoded leading space, and b.guardMarkdown decoded only numeric entities, so payloads such as java	script:, java	script:, and  javascript: resolved to an empty scheme and were served/rendered as safe while a browser executed them as javascript:. The normalization now lives in two shared codepoint-class primitives (b.codepointClass.decodeMarkupEntities and b.codepointClass.stripUrlSchemeWhitespace) that all three guards compose, so no guard can fold away a different set of encodings than another. · *b.guardHtml / b.guardSvg detect entity-encoded and whitespace-hidden CSS injection in style attributes* — A style attribute is HTML/XML character-reference-decoded before the CSS parser sees it, so width:expression(, background:url(javascript:...), and behavior:url(...) reach CSS as expression( / url(javascript:...) / behavior: with no literal danger token in the raw bytes. The CSS-danger check matched the raw attribute value, so these bypassed the denylist and were served verbatim (a stored CSS-injection XSS). Both guards now match the entity-decoded value AND fold the URL-scheme whitespace a browser strips inside url(...) (tab / lf / cr), so an entity-hidden tab in a CSS URL scheme (url(java	script:) reaching CSS as url(java<TAB>script:), which navigates as javascript:) can no longer defeat the contiguous javascript: pattern either. A plain (unencoded) dangerous style is still caught and a benign style is untouched. · *b.safeDns bounds a decompressed name across the whole compression-pointer chain* — readName tracked its per-name octet and label budgets per stack frame, so each compression-pointer jump restarted a fresh RFC 1035 255-octet / label budget and a pointer was charged as only its 2 on-wire bytes. A crafted pointer chain therefore decompressed a single name well past the advertised 255-octet / label caps (a decompression-amplification DoS on untrusted DNS responses). The octet and label accountants now thread through the whole pointer chain, so the composite decompressed name is bounded by the same caps that bound a single in-line name; independent names are still measured independently and no name that decompresses within the caps is newly rejected. · *b.auditSign refuses an anchor whose fields carry the record delimiter* — The chain-anchor signed bytes are a newline-delimited layout of format / counter / tipHash / prevTipHash / createdAt. A literal newline inside any operator-influenced string field (format / tipHash / prevTipHash) let content migrate across a field boundary without changing the signed bytes, so one signature was valid for several different { tipHash, prevTipHash } splits -- an attacker could rewrite a stored anchor's apparent tip/linkage while keeping verify() happy, defeating the prevTipHash linkage tamper-evidence. anchor() now refuses a delimiter-bearing field at sign time and verifyAnchor refuses one at verify time, so an ambiguous anchor can neither be minted nor accepted; every legitimate (delimiter-free) anchor is byte-identical and unaffected. **Detectors:** *Guard scheme extractors and CSS-danger checks must compose the shared normalizers* — Two structural checks assert that a content guard which resolves a URL scheme for a denylist routes the decoded value through b.codepointClass.stripUrlSchemeWhitespace, and that a guard's CSS-danger check matches the entity-decoded style value via b.codepointClass.decodeMarkupEntities -- so a future guard cannot silently fold away a narrower set of encodings than the browser and re-open the bypass class.
|
|
12
16
|
|
|
13
17
|
- v0.16.18 (2026-07-12) — **Refuse a stale FIDO metadata BLOB on the operator-fetch path, harden the GraphQL DoS-shape guard against escaped strings and malformed queries, parse POSIX leading-space octal in tar headers, and fix the FIDO certified-level, self-update 304, and swap-maxBytes paths — surfaced by covering previously-untested error and adversarial branches.** Covering untested error and adversarial branches across four security-relevant primitives surfaced seven defects, fixed at the root. The most serious: b.auth.fidoMds3.fetch() reimplemented the metadata-BLOB validation inline and omitted the stale-BLOB refusal the internal path enforced — so a signed-but-expired BLOB (nextUpdate in the past) was accepted and cached, letting an attacker who serves an ancient correctly-signed BLOB freeze an operator's revoked/compromised-authenticator list at a time of their choosing. Both paths now route through one _verifyBlobWithRoots source of truth, so no check can be present on one and missing on the other. The GraphQL query-shape guard (the pre-schema depth/alias DoS walker) miscounted on a string literal containing an escaped quote and on a brace-unbalanced malformed query, weakening the depth/alias limits it exists to enforce. The FIDO certified-level resolver lexically compared status reports and let an undated later report (a decertification or downgrade) lose to an earlier dated one, so a now-decertified authenticator could still read as certified. A tar reader misparsed POSIX leading-space-padded octal header fields to 0 (a size misparse desynced the block walker and rejected archives other tars extract). self-update's documented 304 If-None-Match fast-path was dead (it threw on every unchanged conditional poll), and selfUpdate.swap read an opts.maxBytes it never declared, so it could refuse a large binary that selfUpdate.verify accepted. **Fixed:** *b.auth.fidoMds3 certified-level honors an undated later decertification or downgrade* — _certifiedLevel compared status reports lexically and coerced a missing effectiveDate to an empty string, which sorts before any real date — so a later report with no effectiveDate (a NOT_FIDO_CERTIFIED decertification, or a downgrade/upgrade) always lost to an earlier dated grant, and certifiedLevel froze at the stale, higher historical value. A step-up policy reading certifiedLevel >= N could therefore accept a now-decertified authenticator. Reports without a comparable date now fall back to array order (append order = chronological), so a later decertification/downgrade wins. · *b.archive.read.tar parses POSIX leading-space-padded octal header fields* — A tar numeric header field (mode / uid / gid / size / mtime / checksum) that is left-padded with ASCII spaces — POSIX-legal, and emitted by star / BSD / Java / Perl tars — was misparsed to 0 because the octal reader treated a leading space as a field terminator. A misparsed size desynced the 512-byte block walker (it read the file body as the next header) and rejected an archive other tars extract cleanly. The reader now skips leading-space padding before reading octal digits. · *b.selfUpdate.poll returns a no-update result on a 304 Not Modified* — The documented If-None-Match / ETag fast-path was unreachable: poll delegated HTTP status handling to the HTTP client, which rejects every non-2xx (304 included) as an error before poll could inspect the status. A conditional poll that correctly received a 304 threw selfupdate/poll-failed instead of returning { available: false, statusCode: 304 }. The 304 (and the non-2xx) branch is now reached, so ETag-conditional polling works as documented. · *b.selfUpdate.swap accepts the maxBytes it re-reads under* — swap re-reads the newly-installed bytes to re-hash them (closing the verify→swap window) under an opts.maxBytes cap, but its option schema never declared maxBytes — so a caller who passed it was refused with selfupdate/bad-opts and swap always used the fixed 1 GiB default, which could refuse a large binary that selfUpdate.verify (which does declare maxBytes) accepted. swap now declares maxBytes (validated like verify's), so the two caps match; rollback, which re-reads nothing, still does not accept it. **Security:** *b.auth.fidoMds3.fetch refuses a stale (expired) metadata BLOB* — The operator-facing fetch path (which trusts caCertificate roots) reimplemented the metadata-BLOB parse and verify inline and omitted the stale-BLOB refusal that the default-roots path enforced, so a BLOB whose nextUpdate is already in the past was accepted and cached even though FIDO MDS3 §3.1.7 says it must not be trusted. An attacker serving an ancient, correctly-signed BLOB could pin an operator to a revoked/compromised-authenticator list frozen at that time. The JWS + x5c-chain verify, payload-shape checks, and stale-BLOB refusal now live in one _verifyBlobWithRoots helper that both the operator-fetch and default-roots paths route through, so the checks can never drift apart again. · *b.guardGraphql hardens the pre-schema DoS-shape walker against escaped strings and malformed queries* — The query-shape walker that enforces depth and alias limits before the query reaches a schema miscounted in two ways: it treated an escaped quote inside a valid GraphQL string literal as the string's terminator (desyncing its in-string state), and it popped its depth-indexed alias counter on every closing brace even for a brace-unbalanced malformed query (underflowing the counter). Both weakened the depth/alias caps the guard exists to enforce against a DoS-shaped query. The walker now tracks string escapes correctly and guards the alias-counter stack against imbalance.
|
package/lib/auth/jwt.js
CHANGED
|
@@ -126,6 +126,27 @@ function _resolveAlgorithm(alg) {
|
|
|
126
126
|
return ALGORITHM_TO_NODE[alg];
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// Bind the declared `alg` to the key that will actually produce/verify the
|
|
130
|
+
// signature. node:crypto.sign/verify with a null digest select the algorithm
|
|
131
|
+
// from the KeyObject's intrinsic type — NOT from the JWS `alg` header — so the
|
|
132
|
+
// header value is only an unauthenticated label unless we cross-check it here.
|
|
133
|
+
// Without this, verify()'s algorithm allowlist gates on that label while the
|
|
134
|
+
// crypto runs against whatever the key is: an ML-DSA-signed token can declare
|
|
135
|
+
// alg="SLH-DSA-SHAKE-256f" and pass an SLH-only allowlist (CWE-347 algorithm
|
|
136
|
+
// confusion / allowlist bypass), and sign() can emit a token whose header
|
|
137
|
+
// misstates its own signature algorithm. `alg` MUST already be a registry key
|
|
138
|
+
// (callers validate via _resolveAlgorithm or the verify allowlist gate).
|
|
139
|
+
function _assertAlgMatchesKey(alg, key, kind) {
|
|
140
|
+
var expectedNodeType = ALGORITHM_TO_NODE[alg];
|
|
141
|
+
var actual = key && typeof key.asymmetricKeyType === "string" ? key.asymmetricKeyType : null;
|
|
142
|
+
if (actual !== expectedNodeType) {
|
|
143
|
+
throw new AuthError("auth-jwt/alg-key-mismatch",
|
|
144
|
+
"declared alg '" + alg + "' requires a '" + expectedNodeType + "' key but the " +
|
|
145
|
+
kind + " key is '" + (actual || "unknown") + "' — the JWS alg header must be bound to " +
|
|
146
|
+
"the key's actual algorithm (CWE-347 algorithm confusion)");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
129
150
|
// ---- sign ----
|
|
130
151
|
|
|
131
152
|
async function sign(claims, opts) {
|
|
@@ -136,6 +157,10 @@ async function sign(claims, opts) {
|
|
|
136
157
|
var alg = opts.algorithm || DEFAULT_ALGORITHM;
|
|
137
158
|
_resolveAlgorithm(alg);
|
|
138
159
|
var key = _toKeyObject(opts.privateKey, "private");
|
|
160
|
+
// Refuse to emit a token whose header alg misstates the signing key's real
|
|
161
|
+
// algorithm — that mislabeled token is exactly what the verify-side
|
|
162
|
+
// allowlist bypass consumes. Config-time throw (operator catches the typo).
|
|
163
|
+
_assertAlgMatchesKey(alg, key, "private");
|
|
139
164
|
|
|
140
165
|
var nowMs = opts.now || Date.now();
|
|
141
166
|
var nowSec = Math.floor(nowMs / C.TIME.seconds(1));
|
|
@@ -306,6 +331,15 @@ async function verify(token, opts) {
|
|
|
306
331
|
"token alg='" + decoded.header.alg + "' is not in the allowed list [" + allowed.join(", ") + "]");
|
|
307
332
|
}
|
|
308
333
|
|
|
334
|
+
// The allowlist gate above only constrains the SELF-DECLARED header.alg — a
|
|
335
|
+
// label the issuer controls. node:crypto.verify(null, ...) selects the real
|
|
336
|
+
// algorithm from the KeyObject, so a token can pass an SLH-only allowlist
|
|
337
|
+
// while being verified with an ML-DSA key (or vice versa). Bind the declared
|
|
338
|
+
// alg to the resolved key's actual algorithm before verifying, so the
|
|
339
|
+
// allowlist genuinely constrains which algorithm authenticated the token
|
|
340
|
+
// (CWE-347). decoded.header.alg is a registry key here (it passed the gate).
|
|
341
|
+
_assertAlgMatchesKey(decoded.header.alg, key, "public");
|
|
342
|
+
|
|
309
343
|
var verified = false;
|
|
310
344
|
try {
|
|
311
345
|
verified = nodeCrypto.verify(null, Buffer.from(decoded.signingInput, "ascii"), key, decoded.signature);
|
package/lib/auth/saml.js
CHANGED
|
@@ -704,6 +704,24 @@ function create(opts) {
|
|
|
704
704
|
}
|
|
705
705
|
var recipHok = _attr(scdHok, "Recipient");
|
|
706
706
|
if (!recipHok || recipHok !== opts.assertionConsumerServiceUrl) continue; // §3.1→§4.1.4.2 — Recipient is mandatory; absent fails the endpoint binding
|
|
707
|
+
// §3.1→§4.1.4.2 — a solicited response's SubjectConfirmationData
|
|
708
|
+
// InResponseTo MUST equal the stored AuthnRequest ID. The Bearer path
|
|
709
|
+
// below enforces this when the operator supplies expectedInResponseTo;
|
|
710
|
+
// holder-of-key is a sibling reader of the SAME SubjectConfirmationData
|
|
711
|
+
// and applies the identical replay check. Omitting it let an operator
|
|
712
|
+
// that opted into InResponseTo binding silently lose it on every HoK
|
|
713
|
+
// confirmation (possession-proof is a separate control, not a licence
|
|
714
|
+
// to skip the solicited-response correlation the operator requested).
|
|
715
|
+
var irtHok = _attr(scdHok, "InResponseTo");
|
|
716
|
+
if (vopts.expectedInResponseTo) {
|
|
717
|
+
if (irtHok === null || irtHok === undefined ||
|
|
718
|
+
!timingSafeEqual(irtHok, vopts.expectedInResponseTo)) {
|
|
719
|
+
throw new AuthError("auth-saml/bad-in-response-to",
|
|
720
|
+
"SubjectConfirmation InResponseTo does not match expected " +
|
|
721
|
+
"AuthnRequest ID (replay defense)");
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
matchedInResponseTo = irtHok;
|
|
707
725
|
hokOk = true;
|
|
708
726
|
break;
|
|
709
727
|
}
|
|
@@ -836,7 +854,7 @@ function create(opts) {
|
|
|
836
854
|
sessionIndex: sessionIndex,
|
|
837
855
|
attributes: attributes,
|
|
838
856
|
audience: audience,
|
|
839
|
-
inResponseTo: bearerOk ? matchedInResponseTo : null,
|
|
857
|
+
inResponseTo: (bearerOk || hokOk) ? matchedInResponseTo : null,
|
|
840
858
|
issuer: issuer,
|
|
841
859
|
};
|
|
842
860
|
}
|
package/lib/guard-jwt.js
CHANGED
|
@@ -312,9 +312,25 @@ function _detectIssues(input, opts) {
|
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
// Payload claim sanity
|
|
315
|
+
// Payload claim sanity. A JWT claims set MUST be a JSON object (RFC 7519
|
|
316
|
+
// §7.2); a payload that does not decode to one — undecodable base64url,
|
|
317
|
+
// non-JSON bytes, a JSON primitive, or a JSON array — is refused
|
|
318
|
+
// symmetrically with the header-decode path above. Skipping it silently
|
|
319
|
+
// (the earlier behaviour) let the required-claims and exp/nbf/iat sanity
|
|
320
|
+
// checks never run, so a token carrying no readable claims set passed at
|
|
321
|
+
// strict with the guard's advertised required-claims enforcement bypassed.
|
|
316
322
|
var payload = _b64urlDecodeJson(payloadSeg);
|
|
317
|
-
if (payload
|
|
323
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
324
|
+
issues.push({
|
|
325
|
+
kind: "payload-decode", severity: "high",
|
|
326
|
+
ruleId: "jwt.payload-decode",
|
|
327
|
+
snippet: "JWT payload is not a decodable JSON object (a JWT claims " +
|
|
328
|
+
"set must be a JSON object per RFC 7519 §7.2) or contains " +
|
|
329
|
+
"prototype-pollution keys",
|
|
330
|
+
});
|
|
331
|
+
return issues;
|
|
332
|
+
}
|
|
333
|
+
{
|
|
318
334
|
var nowSec = Math.floor(Date.now() / 1000); // seconds-per-millisecond conversion
|
|
319
335
|
|
|
320
336
|
// exp in the past.
|
|
@@ -379,6 +379,24 @@ function sign(msg, opts) {
|
|
|
379
379
|
}
|
|
380
380
|
validateOpts.requireNonEmptyString(opts.privateKey,
|
|
381
381
|
"httpSig.sign: privateKey (PEM)", HttpSigError, "BAD_OPT");
|
|
382
|
+
// Bind the declared alg to the signing key's real type. SUPPORTED_ALGS names
|
|
383
|
+
// ARE node's asymmetricKeyType values ("ed25519" / "ml-dsa-65"), so signing
|
|
384
|
+
// an ed25519 key under alg="ml-dsa-65" would emit an authenticated `alg`
|
|
385
|
+
// label that misstates the real algorithm -- a false PQC-signed claim that a
|
|
386
|
+
// verifier trusting the label would honor. Refuse the mislabeled artifact at
|
|
387
|
+
// sign time (matching every other signature verifier in the framework and the
|
|
388
|
+
// verify-side check below; alg-confusion family, CWE-347).
|
|
389
|
+
// Parse only to read the type; an unparseable key is left for the sign step
|
|
390
|
+
// below to reject (SIGN_FAILED), preserving that contract. A parseable key
|
|
391
|
+
// whose type differs from the declared alg is the mislabel we refuse here.
|
|
392
|
+
var signKeyType = null;
|
|
393
|
+
try { signKeyType = nodeCrypto.createPrivateKey(opts.privateKey).asymmetricKeyType; }
|
|
394
|
+
catch (_e) { signKeyType = null; }
|
|
395
|
+
if (signKeyType !== null && signKeyType !== opts.alg) {
|
|
396
|
+
throw _err("BAD_OPT",
|
|
397
|
+
"httpSig.sign: alg '" + opts.alg + "' does not match the private key's type '" +
|
|
398
|
+
signKeyType + "' (the alg label must be bound to the key's real algorithm)");
|
|
399
|
+
}
|
|
382
400
|
if (!Array.isArray(opts.covered) || opts.covered.length === 0) {
|
|
383
401
|
throw _err("BAD_OPT", "httpSig.sign: covered must be a non-empty array");
|
|
384
402
|
}
|
|
@@ -614,6 +632,22 @@ function verify(msg, opts) {
|
|
|
614
632
|
if (typeof publicKeyPem !== "string" || publicKeyPem.length === 0) {
|
|
615
633
|
return { valid: false, reason: "unknown-keyid", keyid: p.keyid };
|
|
616
634
|
}
|
|
635
|
+
// Bind the (authenticated) declared alg to the resolved key's real type
|
|
636
|
+
// BEFORE the crypto check. nodeCrypto.verify(null, ...) selects the algorithm
|
|
637
|
+
// from the key, not from p.alg, so a key whose type differs from p.alg means
|
|
638
|
+
// the alg label misstates the real signature algorithm (e.g. a classical
|
|
639
|
+
// ed25519 key under a declared PQC alg="ml-dsa-65"). Refuse rather than verify
|
|
640
|
+
// under a mislabeled alg (alg-confusion family, CWE-347) -- SUPPORTED_ALGS
|
|
641
|
+
// names ARE the asymmetricKeyType values.
|
|
642
|
+
// Parse only to read the type; an unparseable key is left for the crypto
|
|
643
|
+
// step below to reject (verify-threw), preserving that contract. A parseable
|
|
644
|
+
// key whose type differs from the declared alg is the mislabel we refuse here.
|
|
645
|
+
var verifyKeyType = null;
|
|
646
|
+
try { verifyKeyType = nodeCrypto.createPublicKey(publicKeyPem).asymmetricKeyType; }
|
|
647
|
+
catch (_e) { verifyKeyType = null; }
|
|
648
|
+
if (verifyKeyType !== null && verifyKeyType !== p.alg) {
|
|
649
|
+
return { valid: false, reason: "alg-key-mismatch", alg: p.alg, keyType: verifyKeyType };
|
|
650
|
+
}
|
|
617
651
|
|
|
618
652
|
// If content-digest is covered, recompute and compare. RFC 9421 §B.2.5
|
|
619
653
|
// mandates that verifiers re-run the digest over the body — a stale
|
|
@@ -362,6 +362,17 @@ function _renderSequence(nodes, vars, locale, hashContext, depth) {
|
|
|
362
362
|
return out;
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
// Own-property case lookup. A select value is operator/end-user supplied, so
|
|
366
|
+
// String(sv) can be `__proto__` / `constructor` / `toString` / `hasOwnProperty`
|
|
367
|
+
// etc. A bare `node.cases[key]` would then return a truthy INHERITED
|
|
368
|
+
// Object.prototype member, bypassing the `other` fallback and either rendering
|
|
369
|
+
// garbage (a proto value with no `.length`) or throwing on `_renderSequence` of
|
|
370
|
+
// a non-array (a request DoS). Every case-map lookup goes through this so no key
|
|
371
|
+
// can reach the prototype chain.
|
|
372
|
+
function _ownCase(cases, key) {
|
|
373
|
+
return Object.prototype.hasOwnProperty.call(cases, key) ? cases[key] : undefined;
|
|
374
|
+
}
|
|
375
|
+
|
|
365
376
|
function _renderNode(node, vars, locale, hashContext, depth) {
|
|
366
377
|
if (node.type === "literal") return node.value;
|
|
367
378
|
if (node.type === "hash") {
|
|
@@ -381,18 +392,18 @@ function _renderNode(node, vars, locale, hashContext, depth) {
|
|
|
381
392
|
}
|
|
382
393
|
var adjusted = n - (node.offset || 0);
|
|
383
394
|
var exact = "=" + n;
|
|
384
|
-
var caseBody = node.cases
|
|
395
|
+
var caseBody = _ownCase(node.cases, exact);
|
|
385
396
|
if (!caseBody) {
|
|
386
397
|
var pr = _pluralRules(locale, node.type === "ordinal" ? "ordinal" : "cardinal");
|
|
387
398
|
var category = pr.select(adjusted);
|
|
388
|
-
caseBody = node.cases
|
|
399
|
+
caseBody = _ownCase(node.cases, category) || _ownCase(node.cases, "other");
|
|
389
400
|
}
|
|
390
401
|
return _renderSequence(caseBody, vars, locale, adjusted, depth + 1);
|
|
391
402
|
}
|
|
392
403
|
if (node.type === "select") {
|
|
393
404
|
var sv = vars[node.name];
|
|
394
405
|
var key = (sv === undefined || sv === null) ? "other" : String(sv);
|
|
395
|
-
var body = node.cases
|
|
406
|
+
var body = _ownCase(node.cases, key) || _ownCase(node.cases, "other");
|
|
396
407
|
return _renderSequence(body, vars, locale, hashContext, depth + 1);
|
|
397
408
|
}
|
|
398
409
|
return "";
|
package/lib/i18n.js
CHANGED
|
@@ -250,12 +250,19 @@ function _validateRtlList(value) {
|
|
|
250
250
|
if (!Array.isArray(value)) {
|
|
251
251
|
throw _err("BAD_OPT", "i18n.create: rtlLanguages must be an array of language subtags");
|
|
252
252
|
}
|
|
253
|
+
// dir() folds the requested locale's primary subtag to lower case before
|
|
254
|
+
// the membership test, so fold the configured list the same way. Language
|
|
255
|
+
// subtags are conventionally lower case but BCP 47 is case-insensitive;
|
|
256
|
+
// normalizing only the lookup side would silently drop RTL for an
|
|
257
|
+
// operator-supplied entry like "AR" or "CKB".
|
|
258
|
+
var normalized = [];
|
|
253
259
|
for (var i = 0; i < value.length; i++) {
|
|
254
260
|
if (typeof value[i] !== "string" || value[i].length === 0) {
|
|
255
261
|
throw _err("BAD_OPT", "i18n.create: rtlLanguages[" + i + "] must be a non-empty string");
|
|
256
262
|
}
|
|
263
|
+
normalized.push(value[i].toLowerCase());
|
|
257
264
|
}
|
|
258
|
-
return new Set(
|
|
265
|
+
return new Set(normalized);
|
|
259
266
|
}
|
|
260
267
|
|
|
261
268
|
// ---- Translation tree validation ----
|
|
@@ -658,7 +665,14 @@ function create(opts) {
|
|
|
658
665
|
_ensureLocaleLoaded(loc);
|
|
659
666
|
if (!translations[loc]) continue;
|
|
660
667
|
var v = _resolveKey(translations[loc], key);
|
|
661
|
-
if (v
|
|
668
|
+
if (v === undefined) continue;
|
|
669
|
+
// Only a renderable leaf is a hit: a string, or a plural-shaped
|
|
670
|
+
// object. A nested namespace object at this path is not a
|
|
671
|
+
// translation value — skip it and keep walking the fallback chain so
|
|
672
|
+
// a leaf defined in a less-specific parent or fallback locale still
|
|
673
|
+
// resolves, instead of the namespace shadowing it and the raw key
|
|
674
|
+
// leaking into the UI.
|
|
675
|
+
if (typeof v === "string" || _isPluralShape(v)) {
|
|
662
676
|
return { value: v, foundIn: loc };
|
|
663
677
|
}
|
|
664
678
|
}
|
|
@@ -717,14 +731,12 @@ function create(opts) {
|
|
|
717
731
|
var raw;
|
|
718
732
|
if (typeof found.value === "string") {
|
|
719
733
|
raw = found.value;
|
|
720
|
-
} else
|
|
734
|
+
} else {
|
|
735
|
+
// Guaranteed plural-shaped: _lookupRaw only reports a hit for a
|
|
736
|
+
// renderable leaf (string or plural-shaped object), so a namespace
|
|
737
|
+
// object never reaches here — it is skipped during the chain walk.
|
|
721
738
|
var count = (vars && typeof vars.count === "number") ? vars.count : 0;
|
|
722
739
|
raw = _selectPlural(found.value, count, found.foundIn, callerOpts.ordinal === true);
|
|
723
|
-
} else {
|
|
724
|
-
// Operator stored a nested tree at this key but called t() against
|
|
725
|
-
// the namespace. Return the key-path as a missing-key signal.
|
|
726
|
-
_emitObs("i18n.missing", { locale: locale, key: key });
|
|
727
|
-
return key;
|
|
728
740
|
}
|
|
729
741
|
|
|
730
742
|
// ICU MessageFormat path — when the operator opts in via
|
|
@@ -767,14 +779,12 @@ function create(opts) {
|
|
|
767
779
|
callerOpts = callerOpts || {};
|
|
768
780
|
if (typeof key !== "string" || key.length === 0) return false;
|
|
769
781
|
var locale = _resolveLocale(callerOpts.locale);
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
//
|
|
773
|
-
//
|
|
782
|
+
// _lookupRaw only reports a hit for a renderable leaf (a string or a
|
|
783
|
+
// plural-shaped object); a nested namespace object is skipped during
|
|
784
|
+
// the chain walk. So has() correctly reports false for a namespace key
|
|
785
|
+
// yet true when a fallback locale supplies the leaf — callers can gate
|
|
774
786
|
// "show this UI block only if translated" on leaf entries.
|
|
775
|
-
|
|
776
|
-
if (_isPluralShape(found.value)) return true;
|
|
777
|
-
return false;
|
|
787
|
+
return _lookupRaw(key, locale) !== null;
|
|
778
788
|
}
|
|
779
789
|
|
|
780
790
|
function formatNumber(value, formatOpts, callerOpts) {
|
package/lib/mail-scan.js
CHANGED
|
@@ -236,7 +236,7 @@ function create(opts) {
|
|
|
236
236
|
return _failTo(auditImpl, e, ms);
|
|
237
237
|
});
|
|
238
238
|
}
|
|
239
|
-
return _scanClamavInstream(messageBytes).then(function (rv) {
|
|
239
|
+
return _scanClamavInstream(messageBytes, scanOpts).then(function (rv) {
|
|
240
240
|
rv.durationMs = Date.now() - t0;
|
|
241
241
|
_emitScanResult(auditImpl, rv);
|
|
242
242
|
return rv;
|
|
@@ -339,9 +339,11 @@ function create(opts) {
|
|
|
339
339
|
});
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
function _scanClamavInstream(messageBytes) {
|
|
342
|
+
function _scanClamavInstream(messageBytes, scanOpts) {
|
|
343
343
|
return new Promise(function (resolve, reject) {
|
|
344
|
-
var sock =
|
|
344
|
+
var sock = (scanOpts && scanOpts._socket && typeof scanOpts._socket.write === "function")
|
|
345
|
+
? scanOpts._socket
|
|
346
|
+
: net.createConnection({ host: opts.host, port: opts.port });
|
|
345
347
|
var collector = safeBuffer.boundedChunkCollector({
|
|
346
348
|
maxBytes: caps.maxResponseBytes,
|
|
347
349
|
errorClass: MailScanError,
|
|
@@ -383,21 +385,26 @@ function create(opts) {
|
|
|
383
385
|
var reply = collector.result().toString("utf8").replace(/[\r\n\0]+$/g, ""); // allow:regex-no-length-cap — trailing-trim anchored
|
|
384
386
|
// ClamAV INSTREAM reply: "<id>: <verdict>" where verdict is
|
|
385
387
|
// "stream: OK", "stream: <Sig.Name> FOUND", or "INSTREAM size
|
|
386
|
-
// limit exceeded. ERROR".
|
|
387
|
-
|
|
388
|
+
// limit exceeded. ERROR". This is a security verdict classifier,
|
|
389
|
+
// so it is fail-closed: the malign FOUND signal is tested BEFORE
|
|
390
|
+
// the benign OK signal. A reply that carries both a FOUND token
|
|
391
|
+
// and an OK token (a coalesced / stale-then-fresh reply on a
|
|
392
|
+
// reused connection, or an intermediary that concatenates two
|
|
393
|
+
// responses) must resolve to "infected", never downgrade to
|
|
394
|
+
// "clean". An unrecognized shape is "error" (do-not-deliver), so
|
|
395
|
+
// the only path to "clean" is an OK with no FOUND / no ERROR.
|
|
396
|
+
var m = reply.match(/stream:\s+(.+?)\s+FOUND\b/); // allow:regex-no-length-cap — anchored to fixed FOUND token
|
|
397
|
+
if (m) {
|
|
398
|
+
resolve({ verdict: "infected", threats: [m[1]] });
|
|
399
|
+
} else if (/ERROR/i.test(reply)) { // allow:regex-no-length-cap — anchored to fixed ERROR token
|
|
400
|
+
resolve({ verdict: "error", threats: [] });
|
|
401
|
+
} else if (/stream:\s+OK\b/.test(reply)) { // allow:regex-no-length-cap — anchored to fixed token
|
|
388
402
|
resolve({ verdict: "clean", threats: [] });
|
|
389
403
|
} else {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
resolve({ verdict: "error", threats: [] });
|
|
395
|
-
} else {
|
|
396
|
-
// Unrecognized reply shape — treat as error so the caller
|
|
397
|
-
// gets a definite "do not deliver" signal instead of a
|
|
398
|
-
// silent clean verdict.
|
|
399
|
-
resolve({ verdict: "error", threats: [] });
|
|
400
|
-
}
|
|
404
|
+
// Unrecognized reply shape — treat as error so the caller
|
|
405
|
+
// gets a definite "do not deliver" signal instead of a
|
|
406
|
+
// silent clean verdict.
|
|
407
|
+
resolve({ verdict: "error", threats: [] });
|
|
401
408
|
}
|
|
402
409
|
});
|
|
403
410
|
|
package/lib/session.js
CHANGED
|
@@ -681,10 +681,18 @@ async function verify(token, verifyOpts) {
|
|
|
681
681
|
if (verifyOpts.requireFingerprintMatch === true) {
|
|
682
682
|
return null;
|
|
683
683
|
}
|
|
684
|
-
if (typeof verifyOpts.maxAnomalyScore === "number"
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
684
|
+
if (typeof verifyOpts.maxAnomalyScore === "number") {
|
|
685
|
+
// Genuine drift under a declared strict anomaly threshold. Refuse when
|
|
686
|
+
// the computed score exceeds the threshold — AND when no decisive score
|
|
687
|
+
// could be produced (no scorer supplied, or the scorer threw / returned
|
|
688
|
+
// a non-finite value, leaving fingerprintAnomalyScore null). An
|
|
689
|
+
// uncomputable anomaly score on real drift must FAIL CLOSED, not be
|
|
690
|
+
// treated as "below threshold" and silently admit a relocated device.
|
|
691
|
+
// Same fail-closed discipline the unreadable-binding branch above applies.
|
|
692
|
+
if (fingerprintAnomalyScore === null ||
|
|
693
|
+
fingerprintAnomalyScore > verifyOpts.maxAnomalyScore) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
688
696
|
}
|
|
689
697
|
}
|
|
690
698
|
}
|
package/lib/webhook.js
CHANGED
|
@@ -669,8 +669,18 @@ function verifier(opts) {
|
|
|
669
669
|
throw _failure("MISSING_TIMESTAMP", "webhook: t= field missing from signature header", "missing-timestamp", ctxReq);
|
|
670
670
|
}
|
|
671
671
|
var ts = Number(parsed.t);
|
|
672
|
-
|
|
673
|
-
|
|
672
|
+
// The authenticated timestamp is re-composed into the signed string from
|
|
673
|
+
// `ts` (the Number) below, so it must be the exact canonical decimal the
|
|
674
|
+
// signer emits (`String(Math.floor(now / 1000))`). Validating only the
|
|
675
|
+
// post-coercion value would accept every string Number() maps to the same
|
|
676
|
+
// integer — scientific ("1.7e9"), a trailing ".0", hex ("0x..."), a
|
|
677
|
+
// leading "0"/"+"/whitespace — each of which re-canonicalizes into the
|
|
678
|
+
// signed string and re-verifies, making the authenticated t= field
|
|
679
|
+
// malleable. Require the raw bytes to equal String(ts) so the sole
|
|
680
|
+
// canonical form the signer produces is the sole form accepted (matching
|
|
681
|
+
// the strict digits-only parse the Stripe verifier already applies).
|
|
682
|
+
if (!numericBounds.isNonNegativeFiniteInt(ts) || String(ts) !== parsed.t) {
|
|
683
|
+
throw _failure("BAD_TIMESTAMP", "webhook: t= field is not a canonical non-negative integer, got " + JSON.stringify(parsed.t), "bad-timestamp", ctxReq);
|
|
674
684
|
}
|
|
675
685
|
if (parsed.id === null || parsed.id.length === 0) {
|
|
676
686
|
throw _failure("MISSING_ID", "webhook: id= field missing from signature header", "missing-id", ctxReq);
|
package/package.json
CHANGED
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:
|
|
5
|
+
"serialNumber": "urn:uuid:96b6222d-f898-4aa7-beb8-1b0c830965bd",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-12T14:22:26.336Z",
|
|
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.16.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.16.21",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.16.
|
|
25
|
+
"version": "0.16.21",
|
|
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.16.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.16.21",
|
|
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.16.
|
|
57
|
+
"ref": "@blamejs/core@0.16.21",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|