@blamejs/core 0.18.56 → 0.18.57
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 +22 -0
- package/lib/constants.js +33 -0
- package/lib/mail-server-tls.js +9 -1
- package/lib/network-smtp-policy.js +18 -1
- package/lib/network-tls.js +79 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,28 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.18.x
|
|
10
10
|
|
|
11
|
+
- v0.18.57 (2026-08-28) — **A mail listener refused the key-agreement group TLS 1.3 makes mandatory, and negotiated a weaker one than it would have with no policy at all.** `b.mail.server.tls.context` applied the outbound key-agreement preference to a server context. A flat colon-separated list is an offer order to a client and an ACCEPT SET to a server, so the listener refused every group the list did not name — including `secp256r1`, which RFC 8446 §9.1 makes mandatory to implement. On an SMTP listener a refused handshake is not a downgrade to cleartext: the peer meets `530 5.7.0 Must issue a STARTTLS command first` on any retry and the message stays queued until it bounces.
|
|
12
|
+
|
|
13
|
+
The same flatness collapsed HelloRetryRequest ordering, so a peer offering `X25519:X25519MLKEM768` was served X25519 — while the same peer against a context with no policy at all was pulled up to the hybrid. Applying the list made the negotiated group worse than applying nothing.
|
|
14
|
+
|
|
15
|
+
Also: a TLS-RPT report is now sent to a receiver that publishes its `rua` address in the DNS absolute form, where it was silently never sent. **Fixed:** *A mail listener accepts every registered ECDHE group, and still prefers a post-quantum hybrid (#677)* — `b.network.tls.keyAgreementGroups` returns a flat colon-separated list. That is the right shape for a client, where node sends a key share for the first entry and takes the first mutually-supported group. OpenSSL 3.5 reads the same string on a server as the set of groups it will accept AT ALL, and only tuples separated by `/` as an order between them.
|
|
16
|
+
|
|
17
|
+
So the listener refused `secp256r1`, `secp384r1`, `secp521r1` and `X448` outright. RFC 8446 §9.1 makes `secp256r1` mandatory to implement and only recommends X25519, so a fully conforming peer with a NIST-only group policy could not complete a handshake at all — and on an SMTP listener that is lost mail rather than a downgrade, because the peer meets `530 5.7.0 Must issue a STARTTLS command first` on any cleartext retry.
|
|
18
|
+
|
|
19
|
+
It also lost post-quantum coverage, which is the reverse of the intent. A peer offering `X25519:X25519MLKEM768` was served X25519, where the same peer against a context with no list was pulled up to `X25519MLKEM768` by a HelloRetryRequest.
|
|
20
|
+
|
|
21
|
+
`b.network.tls.serverKeyAgreementGroups` is the server form, and `b.mail.server.tls.context` uses it. It emits tuples — every ML-KEM hybrid first, then every registered ECDHE curve — so the listener still prefers a hybrid and will spend a retry to reach one, while accepting any group RFC 8446 §4.2.7 registers. `SecP256r1MLKEM768` stays accepted, which the runtime's own server default refuses, so applying a policy still buys something. The finite-field `ffdhe*` groups remain absent deliberately: refusing those on a listener is a policy worth keeping, refusing registered ECDHE curves is not.
|
|
22
|
+
|
|
23
|
+
The interoperability curves widen the SHIPPED DEFAULT only. An operator who called `b.network.tls.pqc.setKeyShares` narrowed the policy deliberately — the FIPS example in that module drops the X25519-based groups — and their list is taken as written on a listener too. Adding the curves back there would have let a peer negotiate exactly what was excluded, with the restriction holding outbound and silently not holding inbound. A narrowed listener may refuse a conforming peer; that is a choice the operator made and can see, where the reverse would not be.
|
|
24
|
+
|
|
25
|
+
Every configured rank becomes its own tuple, because ordering exists only between tuples: two groups sharing one are a set the server takes in whatever order the handshake reaches them, so `["secp384r1", "X25519"]` packed together would negotiate X25519 despite secp384r1 ranking first.
|
|
26
|
+
|
|
27
|
+
An operator-supplied `ecdhCurve` is still honoured verbatim, and a malformed one still refused rather than replaced. · *A TLS-RPT report reaches a receiver that publishes its address absolutely (#676)* — `b.network.smtp.tlsRpt.submit` refused a `rua=mailto:` target written in the DNS absolute form, with `mailto: target is not a valid RFC 5322 addr-spec`.
|
|
28
|
+
|
|
29
|
+
That address is published by the receiving domain in its own `_smtp._tls` record. It is not input from the operator running the sender, and a peer spelling its hostname absolutely is using the form RFC 1035 §3.1 defines — a zone file writes it that way routinely. The effect was that a receiver asking to be told about TLS failures was never told, the report owed under RFC 8460 was not sent, and nothing signalled that it had been dropped.
|
|
30
|
+
|
|
31
|
+
One trailing dot is now folded off before the address is validated. That cannot turn a valid address into a different valid one, because a trailing dot is not legal in an addr-spec domain, so a name carrying one has exactly one reading. The result records `published` beside `to`, so an operator can see what the peer wrote and what was used. A doubled trailing dot is still refused. · *The mail TLS test file exits when it finishes* — It printed its OK line and then hung, because the handshake-measuring helper closed its two listeners but never destroyed the sockets they had already accepted — `server.close()` stops a listener accepting and leaves established connections alone. Four sockets survived each run and held the event loop open, so the file could not be used in a batch without stalling whatever invoked it.
|
|
32
|
+
|
|
11
33
|
- v0.18.56 (2026-08-27) — **A CSRF exemption turned on a header the attacker writes, and a certificate renewal reached four mail listeners but not the fifth.** `csrfProtect`'s `skipStateless` waived the token check for any request carrying an `Authorization` header. Presence is not authenticity: an attacker composing a cross-site request writes their own headers. Worse, the header says nothing about which credential authenticated the request, and `attachUser` with `tokenFrom: "both"` reads the cookie first — so a request carrying both was authenticated by exactly the ambient credential the gate protects, and skipped the gate on a header nobody read. The exemption now turns on the absence of a cookie, and it no longer waives the origin check.
|
|
12
34
|
|
|
13
35
|
The mail listeners had two cases of the same shape: a value read once at construction that the operator changes while the server runs. ManageSieve captured `opts.tlsContext`, so a certificate renewal reached its four siblings and not it — and nothing reported that, because the watcher fired and the context rebuilt exactly as expected. `mail.server.mx` captured `localDomains`, so a withdrawn domain kept drawing 250 at RCPT until a restart while every management surface agreed it was gone.
|
package/lib/constants.js
CHANGED
|
@@ -279,6 +279,38 @@ var TLS_GROUP_PREFERENCE = Object.freeze([
|
|
|
279
279
|
|
|
280
280
|
var TLS_GROUP_CURVE_STR = TLS_GROUP_PREFERENCE.join(":");
|
|
281
281
|
|
|
282
|
+
// ---- Inbound (server) accept-groups ----
|
|
283
|
+
//
|
|
284
|
+
// The list above is an OFFER ORDER, which is what a client needs. A server
|
|
285
|
+
// reads the same flat string as its ACCEPT SET: OpenSSL 3.5 treats a
|
|
286
|
+
// colon-separated list as the groups it will accept AT ALL, and only tuples
|
|
287
|
+
// separated by `/` as an order between them. Applying the outbound list to a
|
|
288
|
+
// listener therefore refused every group it did not name — including
|
|
289
|
+
// secp256r1, which RFC 8446 §9.1 makes mandatory to implement, so a fully
|
|
290
|
+
// conforming peer with a NIST-only policy could not complete a handshake. On
|
|
291
|
+
// an SMTP listener that is not a downgrade to classical: the peer meets
|
|
292
|
+
// `530 5.7.0 Must issue a STARTTLS command first` on any cleartext retry and
|
|
293
|
+
// the message stays queued until it bounces.
|
|
294
|
+
//
|
|
295
|
+
// It also lost post-quantum coverage, which is the reverse of the intent. A
|
|
296
|
+
// flat list collapses HelloRetryRequest ordering, so a peer offering
|
|
297
|
+
// `X25519:X25519MLKEM768` was served X25519, where the same peer against a
|
|
298
|
+
// context with NO list was pulled up to the hybrid by a retry.
|
|
299
|
+
//
|
|
300
|
+
// So the server form is tuples: the ACTIVE key-share preference first, then
|
|
301
|
+
// these — the ECDHE curves RFC 8446 §4.2.7 registers, as a second tuple the
|
|
302
|
+
// server will still accept but will spend a HelloRetryRequest to climb out of.
|
|
303
|
+
// The preference stays whatever the operator configured, including a
|
|
304
|
+
// `pqc.setKeyShares` that opts out of hybrids; what this adds is that a
|
|
305
|
+
// listener does not refuse a conforming peer outright.
|
|
306
|
+
//
|
|
307
|
+
// The finite-field ffdhe* groups are absent on purpose. Refusing those on a
|
|
308
|
+
// listener is a policy the framework keeps; refusing registered ECDHE curves
|
|
309
|
+
// on a listener whose whole job is accepting strangers is not.
|
|
310
|
+
var TLS_SERVER_FALLBACK_CURVES = Object.freeze([
|
|
311
|
+
"X25519", "X448", "secp256r1", "secp384r1", "secp521r1",
|
|
312
|
+
]);
|
|
313
|
+
|
|
282
314
|
// ---- RFC 8879 certificate compression ----
|
|
283
315
|
// Every compression algorithm this runtime can decompress, in the order the
|
|
284
316
|
// runtime reports them. Both halves of a TLS connection use the same list:
|
|
@@ -356,6 +388,7 @@ module.exports = {
|
|
|
356
388
|
FORMAT: FORMAT,
|
|
357
389
|
PQC_GROUPS: PQC_GROUPS,
|
|
358
390
|
TLS_GROUP_PREFERENCE: TLS_GROUP_PREFERENCE,
|
|
391
|
+
TLS_SERVER_FALLBACK_CURVES: TLS_SERVER_FALLBACK_CURVES,
|
|
359
392
|
TLS_GROUP_CURVE_STR: TLS_GROUP_CURVE_STR,
|
|
360
393
|
TLS_CERT_COMPRESSION: TLS_CERT_COMPRESSION,
|
|
361
394
|
VAULT_PREFIX: VAULT_PREFIX,
|
package/lib/mail-server-tls.js
CHANGED
|
@@ -169,6 +169,14 @@ var DEFAULT_POLL_MS = C.TIME.seconds(30);
|
|
|
169
169
|
// way to answer the question except by re-deriving the options itself — and a
|
|
170
170
|
// re-derivation agrees with the build right up until someone changes one.
|
|
171
171
|
//
|
|
172
|
+
// The SERVER form, `b.network.tls.serverKeyAgreementGroups`, not the outbound
|
|
173
|
+
// one. A flat colon list is an offer order to a client and an accept SET to a
|
|
174
|
+
// server, so the client list applied here refused every group it did not name
|
|
175
|
+
// — secp256r1 among them, which RFC 8446 §9.1 makes mandatory to implement —
|
|
176
|
+
// and collapsed the HelloRetryRequest ordering that pulls a peer up to a
|
|
177
|
+
// hybrid. The server form is tuples, which accepts every registered ECDHE
|
|
178
|
+
// curve while still preferring a hybrid.
|
|
179
|
+
//
|
|
172
180
|
// The key-agreement preference comes from `b.network.tls.keyAgreementGroups`,
|
|
173
181
|
// which is where the framework's PQC-first policy lives — the ML-KEM hybrids
|
|
174
182
|
// with a classical X25519 fallback — along with the reasoning about which key
|
|
@@ -191,7 +199,7 @@ var DEFAULT_POLL_MS = C.TIME.seconds(30);
|
|
|
191
199
|
// verification posture.
|
|
192
200
|
function _contextOptions(sourceOpts, certPem, keyPem) {
|
|
193
201
|
var base = { cert: certPem, key: keyPem };
|
|
194
|
-
var groups = networkTls().
|
|
202
|
+
var groups = networkTls().serverKeyAgreementGroups(
|
|
195
203
|
sourceOpts ? sourceOpts.ecdhCurve : undefined,
|
|
196
204
|
"b.mail.server.tls.context: opts.ecdhCurve");
|
|
197
205
|
if (groups) base.ecdhCurve = groups;
|
|
@@ -716,7 +716,21 @@ async function tlsRptSubmit(report, opts) {
|
|
|
716
716
|
} else if (/^mailto:/i.test(uri)) {
|
|
717
717
|
// Operator-side transport. Surface the prepared body so the
|
|
718
718
|
// operator can hand it to b.mail directly.
|
|
719
|
-
var
|
|
719
|
+
var publishedTarget = uri.slice("mailto:".length);
|
|
720
|
+
// The address is published by the RECEIVING domain in its own
|
|
721
|
+
// `_smtp._tls` record. It is not operator input to this sender, and a
|
|
722
|
+
// peer spelling its hostname absolutely is using the form RFC 1035 §3.1
|
|
723
|
+
// defines — a zone file writes it that way routinely.
|
|
724
|
+
//
|
|
725
|
+
// So fold one trailing dot before validating rather than validating the
|
|
726
|
+
// spelling as published. Strictness normally protects the party doing
|
|
727
|
+
// the refusing; here it costs the OTHER party the report they asked for
|
|
728
|
+
// under RFC 8460 and costs this sender nothing — the entry silently
|
|
729
|
+
// never becomes a report. That is the direction that makes the lenient
|
|
730
|
+
// reading correct. The fold cannot turn a valid address into a
|
|
731
|
+
// different valid one, because a trailing dot is not legal in an
|
|
732
|
+
// addr-spec domain, so a name carrying one has exactly one reading.
|
|
733
|
+
var mailtoTarget = publishedTarget.replace(/\.$/, "");
|
|
720
734
|
// RFC 5322 §3.4.1 addr-spec validation — refuse mailto: rua
|
|
721
735
|
// entries that aren't valid addresses. Pre-v0.8.32 the
|
|
722
736
|
// framework would forward whatever string came after
|
|
@@ -730,6 +744,9 @@ async function tlsRptSubmit(report, opts) {
|
|
|
730
744
|
entry.ok = true;
|
|
731
745
|
entry.mailto = {
|
|
732
746
|
to: mailtoTarget,
|
|
747
|
+
// What the peer actually published, kept beside what was used, so
|
|
748
|
+
// an operator reading the result can see the two are different.
|
|
749
|
+
published: publishedTarget,
|
|
733
750
|
subject: "Report Domain: " + (report["organization-name"] || "") +
|
|
734
751
|
" Submitter: " + (report["organization-name"] || "") +
|
|
735
752
|
" Report-ID: <" + (report["report-id"] || "") + ">",
|
package/lib/network-tls.js
CHANGED
|
@@ -80,6 +80,10 @@ var STATE = {
|
|
|
80
80
|
systemTrust: false,
|
|
81
81
|
baselineFingerprints: null,
|
|
82
82
|
tlsKeyShares: ["X25519MLKEM768", "SecP256r1MLKEM768", "SecP384r1MLKEM1024", "X25519"],
|
|
83
|
+
// False until pqc.setKeyShares is called. The list above is the shipped
|
|
84
|
+
// default, and a listener widens THAT for interoperability; a list the
|
|
85
|
+
// operator chose is taken as written.
|
|
86
|
+
tlsKeySharesConfigured: false,
|
|
83
87
|
};
|
|
84
88
|
|
|
85
89
|
function _normalizePem(pem) {
|
|
@@ -566,6 +570,68 @@ function keyAgreementGroups(override, where) {
|
|
|
566
570
|
return STATE.tlsKeyShares.length > 0 ? STATE.tlsKeyShares.join(":") : null;
|
|
567
571
|
}
|
|
568
572
|
|
|
573
|
+
// The same policy in the shape a LISTENER needs.
|
|
574
|
+
//
|
|
575
|
+
// keyAgreementGroups above returns a flat colon-separated list, which is an
|
|
576
|
+
// offer order on a client and an accept SET on a server — OpenSSL 3.5 reads
|
|
577
|
+
// only `/`-separated tuples as an order. Handing a listener the client form
|
|
578
|
+
// made it refuse every group the list did not name, secp256r1 included, and
|
|
579
|
+
// collapsed the retry ordering that pulls a peer up to a hybrid. So a server
|
|
580
|
+
// takes this instead, and the difference is not a detail a call site should
|
|
581
|
+
// have to remember.
|
|
582
|
+
//
|
|
583
|
+
// An override is honoured verbatim and a malformed one refused, exactly as on
|
|
584
|
+
// the client side: an operator who names their own groups gets them, and gets
|
|
585
|
+
// told when what they named cannot be parsed rather than being silently given
|
|
586
|
+
// the default.
|
|
587
|
+
// The PREFERENCE half is whatever is active — the same STATE.tlsKeyShares the
|
|
588
|
+
// client form reads, so an operator who called pqc.setKeyShares to opt out of
|
|
589
|
+
// hybrids gets listeners that do not prefer them either. Hardcoding the default
|
|
590
|
+
// here would have let a configured policy apply outbound and be silently
|
|
591
|
+
// ignored inbound, which is worse than not having the helper.
|
|
592
|
+
//
|
|
593
|
+
// The fallback tuple only ADDS acceptance; it never changes what is preferred,
|
|
594
|
+
// and anything already in the preference is not repeated into it.
|
|
595
|
+
function serverKeyAgreementGroups(override, where) {
|
|
596
|
+
if (override !== undefined && override !== null) {
|
|
597
|
+
return _groupPreferenceString(override, where || "tls.serverKeyAgreementGroups");
|
|
598
|
+
}
|
|
599
|
+
var preferred = STATE.tlsKeyShares.length > 0
|
|
600
|
+
? STATE.tlsKeyShares.slice()
|
|
601
|
+
: C.TLS_GROUP_PREFERENCE.slice();
|
|
602
|
+
if (preferred.length === 0) return null;
|
|
603
|
+
|
|
604
|
+
// EVERY configured rank is its own tuple, in order.
|
|
605
|
+
//
|
|
606
|
+
// Ordering exists only BETWEEN tuples: groups sharing one are a set the
|
|
607
|
+
// server takes in whatever order the handshake reaches them. So any two
|
|
608
|
+
// preferences packed into a single tuple stop being ranked — a hybrid beside
|
|
609
|
+
// X25519 loses to it whenever a peer offers both, and `["secp384r1",
|
|
610
|
+
// "X25519"]` silently negotiates X25519. One tuple per entry is the only
|
|
611
|
+
// shape that means what the configured list says.
|
|
612
|
+
//
|
|
613
|
+
// The acceptance-only curves come last, as a single tuple: they are there so
|
|
614
|
+
// a conforming peer is never refused, and carry no preference between
|
|
615
|
+
// themselves.
|
|
616
|
+
//
|
|
617
|
+
// They are added ONLY to the shipped default. An operator who called
|
|
618
|
+
// setKeyShares narrowed the policy deliberately — the module's own FIPS
|
|
619
|
+
// example drops the X25519-based groups — and adding those back would let a
|
|
620
|
+
// peer negotiate exactly what they excluded, with the restriction holding
|
|
621
|
+
// outbound and silently not holding inbound. A narrowed listener may refuse
|
|
622
|
+
// a conforming peer; that is the operator's choice to make, and it is
|
|
623
|
+
// visible, where the reverse would not be.
|
|
624
|
+
var named = preferred.map(function (g) { return String(g).toLowerCase(); });
|
|
625
|
+
var fallback = STATE.tlsKeySharesConfigured ? [] :
|
|
626
|
+
C.TLS_SERVER_FALLBACK_CURVES.filter(function (g) {
|
|
627
|
+
return named.indexOf(String(g).toLowerCase()) === -1;
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
var tuples = preferred.slice();
|
|
631
|
+
if (fallback.length > 0) tuples.push(fallback.join(":"));
|
|
632
|
+
return tuples.join("/");
|
|
633
|
+
}
|
|
634
|
+
|
|
569
635
|
function applyToContext(opts) {
|
|
570
636
|
opts = opts || {};
|
|
571
637
|
validateOpts(opts, ["base"], "tls.applyToContext");
|
|
@@ -699,6 +765,12 @@ function setKeyShares(list) {
|
|
|
699
765
|
}
|
|
700
766
|
for (var i = 0; i < list.length; i += 1) _validateKeyShare(list[i]);
|
|
701
767
|
STATE.tlsKeyShares = list.slice();
|
|
768
|
+
// Records that this list is a CHOICE rather than the shipped default. A
|
|
769
|
+
// listener adds interoperability curves to the default so it does not refuse
|
|
770
|
+
// a conforming peer out of the box; it must not add them to a list the
|
|
771
|
+
// operator narrowed on purpose, or a restriction would hold outbound and be
|
|
772
|
+
// silently undone inbound.
|
|
773
|
+
STATE.tlsKeySharesConfigured = true;
|
|
702
774
|
_postureGeneration += 1;
|
|
703
775
|
return getKeyShares();
|
|
704
776
|
}
|
|
@@ -707,6 +779,11 @@ function getKeyShares() { return STATE.tlsKeyShares.slice(); }
|
|
|
707
779
|
|
|
708
780
|
function resetKeyShares() {
|
|
709
781
|
STATE.tlsKeyShares = DEFAULT_PQC_KEY_SHARES.slice();
|
|
782
|
+
// Back to the shipped default in every respect, including whether a listener
|
|
783
|
+
// widens it for interoperability. Restoring the list but keeping the
|
|
784
|
+
// configured flag would leave a reset process serving a different group
|
|
785
|
+
// policy from a fresh one, which is not what a reset means.
|
|
786
|
+
STATE.tlsKeySharesConfigured = false;
|
|
710
787
|
_postureGeneration += 1;
|
|
711
788
|
return getKeyShares();
|
|
712
789
|
}
|
|
@@ -1251,6 +1328,7 @@ function _resetForTest() {
|
|
|
1251
1328
|
STATE.systemTrust = false;
|
|
1252
1329
|
STATE.baselineFingerprints = null;
|
|
1253
1330
|
STATE.tlsKeyShares = DEFAULT_PQC_KEY_SHARES.slice();
|
|
1331
|
+
STATE.tlsKeySharesConfigured = false;
|
|
1254
1332
|
}
|
|
1255
1333
|
|
|
1256
1334
|
// ---- OCSP / OCSP-stapling wrappers around node:tls ----------------
|
|
@@ -4156,6 +4234,7 @@ module.exports = {
|
|
|
4156
4234
|
detectBaselineDrift: detectBaselineDrift,
|
|
4157
4235
|
applyToContext: applyToContext,
|
|
4158
4236
|
keyAgreementGroups: keyAgreementGroups,
|
|
4237
|
+
serverKeyAgreementGroups: serverKeyAgreementGroups,
|
|
4159
4238
|
buildOptions: buildOptions,
|
|
4160
4239
|
getCaPems: getCaPems,
|
|
4161
4240
|
ocsp: ocsp,
|
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:5ef2737c-fb9a-4fb4-b7c7-2550c5030855",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
8
|
+
"timestamp": "2026-08-28T08:40:52.381Z",
|
|
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.18.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.18.57",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.18.
|
|
25
|
+
"version": "0.18.57",
|
|
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.18.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.18.57",
|
|
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.18.
|
|
57
|
+
"ref": "@blamejs/core@0.18.57",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|