@blamejs/core 0.14.16 → 0.14.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/lib/render.js CHANGED
@@ -82,17 +82,35 @@ var DEFAULT_DYNAMIC_CACHE_CONTROL = "private, no-cache, must-revalidate";
82
82
  * without losing Content-Type. Returns `undefined` — the response
83
83
  * is fully written by the time the call returns.
84
84
  *
85
+ * `opts.replacer` is forwarded to `JSON.stringify` (ECMA-262 §25.5.2,
86
+ * the second argument) so handlers can serialize values that have no
87
+ * native JSON form — `BigInt` (which otherwise throws), `Date` in a
88
+ * custom shape, `Map` / `Set`, or a redaction filter over secret-
89
+ * shaped keys — without pre-walking the body. Accepts the same
90
+ * function or property-name array `JSON.stringify` does; a non-
91
+ * function / non-array value is a config typo and throws.
92
+ *
85
93
  * @opts
86
- * status: 200, // numeric HTTP status (200/201/202/4xx/5xx)
87
- * headers: {}, // merged over defaults; later wins
94
+ * status: 200, // numeric HTTP status (200/201/202/4xx/5xx)
95
+ * headers: {}, // merged over defaults; later wins
96
+ * replacer: function|string[], // JSON.stringify replacer (BigInt/Date/redaction)
88
97
  *
89
98
  * @example
90
99
  * b.render.json(res, { ok: true, id: 42 }, { status: 201 });
91
100
  * // → response: 201, application/json, body `{"ok":true,"id":42}`
101
+ *
102
+ * b.render.json(res, { total: 9007199254740993n }, {
103
+ * replacer: function (k, v) { return typeof v === "bigint" ? v.toString() : v; },
104
+ * });
105
+ * // → body `{"total":"9007199254740993"}`
92
106
  */
93
107
  function json(res, body, opts) {
94
108
  opts = opts || {};
95
- var encoded = JSON.stringify(body);
109
+ if (opts.replacer !== undefined && opts.replacer !== null &&
110
+ typeof opts.replacer !== "function" && !Array.isArray(opts.replacer)) {
111
+ throw new TypeError("render.json: opts.replacer must be a function or an array of keys");
112
+ }
113
+ var encoded = JSON.stringify(body, opts.replacer);
96
114
  var headers = _mergedHeaders({
97
115
  "Content-Type": "application/json; charset=utf-8",
98
116
  "Content-Length": Buffer.byteLength(encoded, "utf8"),
package/lib/router.js CHANGED
@@ -112,13 +112,20 @@ function _validateRouteSpec(spec, method, pattern) {
112
112
  }
113
113
  }
114
114
 
115
- function _writeValidationError(res, where, errors) {
115
+ function _writeValidationError(req, res, where, errors) {
116
116
  if (res.writableEnded || res.headersSent) return;
117
- var body = JSON.stringify({
117
+ var payload = {
118
118
  error: "validation",
119
119
  where: where,
120
120
  issues: errors,
121
- });
121
+ };
122
+ var body = JSON.stringify(payload);
123
+ // Seal the body when an encrypted session is active; pre-session paths
124
+ // (Bearer auth, handshake reject, replay refusal) lack the encoder and
125
+ // stay plaintext. An encryption failure falls back to the plaintext body.
126
+ if (req && typeof req.apiEncryptEncode === "function") {
127
+ try { body = JSON.stringify(req.apiEncryptEncode(payload)); } catch (_e) { /* plaintext body kept */ }
128
+ }
122
129
  res.writeHead(HTTP_STATUS.BAD_REQUEST, {
123
130
  "Content-Type": "application/json; charset=utf-8",
124
131
  "Content-Length": Buffer.byteLength(body),
@@ -131,17 +138,17 @@ function _makeSchemaValidator(spec) {
131
138
  return function schemaValidator(req, res, next) {
132
139
  if (spec.params && req.params !== undefined) {
133
140
  var pp = spec.params.safeParse(req.params);
134
- if (!pp.ok) return _writeValidationError(res, "params", pp.errors);
141
+ if (!pp.ok) return _writeValidationError(req, res, "params", pp.errors);
135
142
  req.params = pp.value;
136
143
  }
137
144
  if (spec.query && req.query !== undefined) {
138
145
  var qq = spec.query.safeParse(req.query);
139
- if (!qq.ok) return _writeValidationError(res, "query", qq.errors);
146
+ if (!qq.ok) return _writeValidationError(req, res, "query", qq.errors);
140
147
  req.query = qq.value;
141
148
  }
142
149
  if (spec.body && req.body !== undefined) {
143
150
  var bb = spec.body.safeParse(req.body);
144
- if (!bb.ok) return _writeValidationError(res, "body", bb.errors);
151
+ if (!bb.ok) return _writeValidationError(req, res, "body", bb.errors);
145
152
  req.body = bb.value;
146
153
  }
147
154
  next();
@@ -321,6 +321,60 @@ function boundedChunkCollector(opts) {
321
321
  };
322
322
  }
323
323
 
324
+ /**
325
+ * @primitive b.safeBuffer.collectStream
326
+ * @signature b.safeBuffer.collectStream(stream, opts)
327
+ * @since 0.14.18
328
+ * @related b.safeBuffer.boundedChunkCollector, b.safeBuffer.toBuffer
329
+ *
330
+ * Read a Node Readable (an `http.IncomingMessage` request body, a file
331
+ * stream, an upstream response) fully into one Buffer with the byte cap
332
+ * enforced at every chunk — the streaming sibling of
333
+ * `boundedChunkCollector`. `boundedChunkCollector` is a push-based
334
+ * collector object; `collectStream` is the pump around it, so callers
335
+ * compose the stream case instead of reaching for a `(stream, opts)`
336
+ * overload that does not exist.
337
+ *
338
+ * Resolves with the concatenated Buffer when the stream ends. Rejects
339
+ * (and destroys the stream) the moment a chunk would overflow
340
+ * `maxBytes`, so a hostile sender cannot force unbounded buffering. A
341
+ * bad `maxBytes` (missing / non-finite / `Infinity`) rejects rather than
342
+ * throwing synchronously.
343
+ *
344
+ * @opts
345
+ * maxBytes: number, // REQUIRED positive finite int; total byte cap
346
+ * errorClass: Function, // caller Error subclass for the too-large reject
347
+ * sizeCode: string, // default "buffer/too-large"
348
+ * sizeMessage: string, // override the too-large message
349
+ *
350
+ * @example
351
+ * var body = await b.safeBuffer.collectStream(req, { maxBytes: 65536 });
352
+ * var json = b.safeJson.parse(body.toString("utf8"));
353
+ * // → the parsed request body, never more than 64 KiB buffered
354
+ */
355
+ function collectStream(stream, opts) {
356
+ return new Promise(function (resolve, reject) {
357
+ var collector;
358
+ try { collector = boundedChunkCollector(opts || {}); }
359
+ catch (e) { reject(e); return; }
360
+ var done = false;
361
+ function fail(e) {
362
+ if (done) return;
363
+ done = true;
364
+ try { if (stream && typeof stream.destroy === "function") stream.destroy(); }
365
+ catch (_e) { /* socket already closed */ }
366
+ reject(e);
367
+ }
368
+ stream.on("data", function (chunk) {
369
+ if (done) return;
370
+ try { collector.push(chunk); }
371
+ catch (e) { fail(e); }
372
+ });
373
+ stream.on("end", function () { if (!done) { done = true; resolve(collector.result()); } });
374
+ stream.on("error", fail);
375
+ });
376
+ }
377
+
324
378
  /**
325
379
  * @primitive b.safeBuffer.secureZero
326
380
  * @signature b.safeBuffer.secureZero(buf)
@@ -552,6 +606,7 @@ module.exports = {
552
606
  normalizeText: normalizeText,
553
607
  toBuffer: toBuffer,
554
608
  boundedChunkCollector: boundedChunkCollector,
609
+ collectStream: collectStream,
555
610
  secureZero: secureZero,
556
611
  isHex: isHex,
557
612
  hasCrlf: hasCrlf,
package/lib/sse.js CHANGED
@@ -287,7 +287,7 @@ function create(req, res, opts) {
287
287
  _writeRaw(":" + text + "\n\n");
288
288
  }
289
289
 
290
- function close() {
290
+ function close(cause) {
291
291
  if (closed) return;
292
292
  closed = true;
293
293
  if (heartbeatTimer) {
@@ -296,10 +296,12 @@ function create(req, res, opts) {
296
296
  }
297
297
  try { res.end(); } catch (_e) { /* already destroyed */ }
298
298
  if (auditOn) {
299
+ var closeMeta = { lastEventId: lastEventId };
300
+ if (cause) closeMeta.reason = cause.reason || "fault";
299
301
  audit.safeEmit({
300
302
  action: "sse.channel_closed",
301
- outcome: "success",
302
- metadata: { lastEventId: lastEventId },
303
+ outcome: cause ? "failure" : "success",
304
+ metadata: closeMeta,
303
305
  });
304
306
  }
305
307
  }
@@ -308,7 +310,7 @@ function create(req, res, opts) {
308
310
  // the heartbeat timer.
309
311
  if (typeof res.on === "function") {
310
312
  res.on("close", close);
311
- res.on("error", function (_e) { close(); });
313
+ res.on("error", function (_e) { close({ reason: "stream-error" }); });
312
314
  res.on("finish", function () { closed = true; if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; } });
313
315
  }
314
316
 
@@ -325,7 +327,7 @@ function create(req, res, opts) {
325
327
  heartbeatTimer = setInterval(function () {
326
328
  if (closed) return;
327
329
  try { _writeRaw(":keepalive\n\n"); }
328
- catch (_e) { close(); }
330
+ catch (_e) { close({ reason: "heartbeat-write-failed" }); }
329
331
  }, heartbeatMs).unref();
330
332
  }
331
333
 
package/lib/static.js CHANGED
@@ -6,7 +6,8 @@
6
6
  * quotas (cluster-shared via b.cache), Range support (RFC 7233 single-range),
7
7
  * the full conditional-request set (If-None-Match / If-Match /
8
8
  * If-Modified-Since / If-Unmodified-Since), MIME allowlist with magic-byte
9
- * verification (composes b.fileType), per-request operator hook, idle-stream
9
+ * verification (composes b.fileType), per-request operator hooks (onServe
10
+ * on the success path, onError on every refusal path), idle-stream
10
11
  * timeout, cancellation propagation, force-revoke, and compliance-retention
11
12
  * gating.
12
13
  *
@@ -16,6 +17,12 @@
16
17
  * var mw = serve.middleware; // (req, res, next)
17
18
  * await b.staticServe.integrity(absFilePath); // SRI helper (SHA-384)
18
19
  *
20
+ * // onError mirrors onServe for the refusal paths (403 / 404 / 415 /
21
+ * // 412 / 416 / 429 / 451 / 500): it receives
22
+ * // { req, res, urlPath, absPath, status, code, actor } AFTER the
23
+ * // refusal response is written. Observability-only — a throw is
24
+ * // swallowed so a broken hook can't tear down the socket.
25
+ *
19
26
  * Backwards compatible: every existing opt (root, mountPath,
20
27
  * hashedPathPattern, indexFile, defaultMaxAge, contentTypes) keeps its
21
28
  * original meaning. New opts (permissions, cache, audit, observability,
@@ -450,6 +457,7 @@ function _validateCreateOpts(opts) {
450
457
  validateOpts.auditShape(opts.audit, "staticServe.create", StaticServeError);
451
458
  validateOpts.observabilityShape(opts.observability, "staticServe.create", StaticServeError);
452
459
  validateOpts.optionalFunction(opts.onServe, "staticServe.create: onServe", StaticServeError);
460
+ validateOpts.optionalFunction(opts.onError, "staticServe.create: onError", StaticServeError);
453
461
  // contentSafety — extension-keyed gate map. Default behaviour: when
454
462
  // undefined, the framework wires b.guardAll.byExtension({ profile:
455
463
  // "strict" }) automatically so every shipped guard is ON by default.
@@ -604,7 +612,7 @@ function create(opts) {
604
612
  "root", "mountPath", "hashedPathPattern",
605
613
  "indexFile", "defaultMaxAge", "contentTypes",
606
614
  "permissions", "cache", "fileType", "retention", "revokeStore",
607
- "allowedFileTypes", "audit", "observability", "onServe",
615
+ "allowedFileTypes", "audit", "observability", "onServe", "onError",
608
616
  "acceptRanges", "auditSuccess", "auditFailures",
609
617
  "maxBytesPerActorPerWindowMs", "maxBytesAllActorsPerWindowMs",
610
618
  "bandwidthWindowMs", "maxConcurrentDownloadsPerActor", "maxIdleMs",
@@ -657,6 +665,7 @@ function create(opts) {
657
665
  contentSafety = opts.contentSafety;
658
666
  }
659
667
  var onServe = opts.onServe || null;
668
+ var onError = opts.onError || null;
660
669
  var audit = opts.audit || null;
661
670
  var auditSuccess = cfg.auditSuccess;
662
671
  var auditFailures = cfg.auditFailures;
@@ -756,6 +765,26 @@ function create(opts) {
756
765
  var actorCtx = requestHelpers.extractActorContext(req);
757
766
  var actorKey = _actorKeyFromContext(actorCtx);
758
767
 
768
+ // Request-scoped error writer. Wraps the module-level _writeError so
769
+ // every refusal path (403 / 404 / 415 / 412 / 416 / 429 / 451 / 500)
770
+ // also invokes the operator's onError hook — the success-path mirror
771
+ // of onServe. The signature matches _writeError exactly; the `code`
772
+ // argument routes through to the hook so operators can branch on the
773
+ // refusal reason. The hook is observability-only: it runs AFTER the
774
+ // response is written and a throw is swallowed so a broken sink can't
775
+ // turn a 4xx into a torn-down socket.
776
+ function writeErr(r, status, code, message, headers) {
777
+ _writeError(r, status, code, message, headers);
778
+ if (onError) {
779
+ try {
780
+ onError({
781
+ req: req, res: r, urlPath: urlPath, absPath: absPath,
782
+ status: status, code: code, actor: actorCtx,
783
+ });
784
+ } catch (_he) { /* hook best-effort */ }
785
+ }
786
+ }
787
+
759
788
  // Permission gate (403)
760
789
  var perm = await _checkPermission(req);
761
790
  if (!perm.ok) {
@@ -766,7 +795,7 @@ function create(opts) {
766
795
  outcome: "failure", reason: "permission_denied", resource: urlPath,
767
796
  }, actorCtx));
768
797
  }
769
- return _writeError(res, HTTP.FORBIDDEN, "permission_denied",
798
+ return writeErr(res, HTTP.FORBIDDEN, "permission_denied",
770
799
  "Forbidden");
771
800
  }
772
801
 
@@ -788,7 +817,7 @@ function create(opts) {
788
817
  outcome: "failure", reason: "revoked", resource: urlPath,
789
818
  }, actorCtx));
790
819
  }
791
- return _writeError(res, HTTP.NOT_FOUND, "not_found", "Not Found");
820
+ return writeErr(res, HTTP.NOT_FOUND, "not_found", "Not Found");
792
821
  }
793
822
 
794
823
  // Compliance retention (451)
@@ -800,7 +829,7 @@ function create(opts) {
800
829
  outcome: "failure", reason: "retention_blocked", resource: urlPath,
801
830
  }, actorCtx));
802
831
  }
803
- return _writeError(res, HTTP.UNAVAILABLE_FOR_LEGAL_REASONS,
832
+ return writeErr(res, HTTP.UNAVAILABLE_FOR_LEGAL_REASONS,
804
833
  "retention_blocked", "Unavailable For Legal Reasons");
805
834
  }
806
835
 
@@ -820,7 +849,7 @@ function create(opts) {
820
849
  detectedMime: mimeCheck.detected || null,
821
850
  }, actorCtx));
822
851
  }
823
- return _writeError(res, HTTP.UNSUPPORTED_MEDIA_TYPE,
852
+ return writeErr(res, HTTP.UNSUPPORTED_MEDIA_TYPE,
824
853
  "mime_rejected", "Unsupported Media Type");
825
854
  }
826
855
  }
@@ -861,7 +890,7 @@ function create(opts) {
861
890
  catch (_e) {
862
891
  stats.failures += 1;
863
892
  if (gateHandle) { try { await gateHandle.close(); } catch (_ce) { /* close best-effort */ } }
864
- return _writeError(res, HTTP.INTERNAL_SERVER_ERROR,
893
+ return writeErr(res, HTTP.INTERNAL_SERVER_ERROR,
865
894
  "read_failed", "Internal Server Error");
866
895
  }
867
896
  try { await gateHandle.close(); } catch (_ce) { /* close best-effort */ }
@@ -885,7 +914,7 @@ function create(opts) {
885
914
  error: gateErr && gateErr.message,
886
915
  }, actorCtx));
887
916
  }
888
- return _writeError(res, HTTP.INTERNAL_SERVER_ERROR,
917
+ return writeErr(res, HTTP.INTERNAL_SERVER_ERROR,
889
918
  "content_safety_threw", "Internal Server Error");
890
919
  }
891
920
  if (!gateDecision.ok || gateDecision.action === "refuse") {
@@ -898,7 +927,7 @@ function create(opts) {
898
927
  issues: gateContract.summarizeIssues(gateDecision.issues),
899
928
  }, actorCtx));
900
929
  }
901
- return _writeError(res, HTTP.UNSUPPORTED_MEDIA_TYPE,
930
+ return writeErr(res, HTTP.UNSUPPORTED_MEDIA_TYPE,
902
931
  "content_safety_refused", "Unsupported Media Type");
903
932
  }
904
933
  if (gateDecision.action === "sanitize" && gateDecision.sanitized) {
@@ -929,7 +958,7 @@ function create(opts) {
929
958
  if (ifMatch && ifMatch !== "*" && ifMatch !== meta.etag) {
930
959
  stats.failures += 1;
931
960
  _emitObs("staticServe.precondition_failed", 1, { route: urlPath, header: "if-match" });
932
- return _writeError(res, HTTP.PRECONDITION_FAILED || 412,
961
+ return writeErr(res, HTTP.PRECONDITION_FAILED || 412,
933
962
  "precondition_failed", "Precondition Failed");
934
963
  }
935
964
 
@@ -956,7 +985,7 @@ function create(opts) {
956
985
  if (isFinite(ius) && Math.floor(meta.mtimeMs / C.TIME.seconds(1)) > Math.floor(ius / C.TIME.seconds(1))) {
957
986
  stats.failures += 1;
958
987
  _emitObs("staticServe.precondition_failed", 1, { route: urlPath, header: "if-unmodified-since" });
959
- return _writeError(res, HTTP.PRECONDITION_FAILED,
988
+ return writeErr(res, HTTP.PRECONDITION_FAILED,
960
989
  "precondition_failed", "Precondition Failed");
961
990
  }
962
991
  }
@@ -970,19 +999,19 @@ function create(opts) {
970
999
  if (range && (range.malformed || range.multi)) {
971
1000
  stats.failures += 1;
972
1001
  _emitObs("staticServe.range_invalid", 1, { route: urlPath });
973
- return _writeError(res, HTTP.RANGE_NOT_SATISFIABLE, "range_not_satisfiable",
1002
+ return writeErr(res, HTTP.RANGE_NOT_SATISFIABLE, "range_not_satisfiable",
974
1003
  "Range Not Satisfiable", { "Content-Range": "bytes */" + meta.size });
975
1004
  }
976
1005
  if (range && range.unsatisfiable) {
977
1006
  stats.failures += 1;
978
1007
  _emitObs("staticServe.range_invalid", 1, { route: urlPath });
979
- return _writeError(res, HTTP.RANGE_NOT_SATISFIABLE, "range_not_satisfiable",
1008
+ return writeErr(res, HTTP.RANGE_NOT_SATISFIABLE, "range_not_satisfiable",
980
1009
  "Range Not Satisfiable", { "Content-Range": "bytes */" + meta.size });
981
1010
  }
982
1011
  if (range && cfg.maxRangeBytes !== Infinity && range.length > cfg.maxRangeBytes) {
983
1012
  stats.failures += 1;
984
1013
  _emitObs("staticServe.range_too_large", 1, { route: urlPath });
985
- return _writeError(res, HTTP.RANGE_NOT_SATISFIABLE, "range_too_large",
1014
+ return writeErr(res, HTTP.RANGE_NOT_SATISFIABLE, "range_too_large",
986
1015
  "Range Not Satisfiable", { "Content-Range": "bytes */" + meta.size });
987
1016
  }
988
1017
  if (range) {
@@ -1005,7 +1034,7 @@ function create(opts) {
1005
1034
  current: concCheck.current, cap: concCheck.cap,
1006
1035
  }, actorCtx));
1007
1036
  }
1008
- return _writeError(res, HTTP.TOO_MANY_REQUESTS,
1037
+ return writeErr(res, HTTP.TOO_MANY_REQUESTS,
1009
1038
  "concurrency_cap", "Too Many Requests",
1010
1039
  { "Retry-After": "5" });
1011
1040
  }
@@ -1021,7 +1050,7 @@ function create(opts) {
1021
1050
  scope: bwCheck.scope, used: bwCheck.used, cap: bwCheck.cap,
1022
1051
  }, actorCtx));
1023
1052
  }
1024
- return _writeError(res, HTTP.TOO_MANY_REQUESTS,
1053
+ return writeErr(res, HTTP.TOO_MANY_REQUESTS,
1025
1054
  "bandwidth_quota", "Too Many Requests",
1026
1055
  { "Retry-After": String(bwCheck.retryAfter) });
1027
1056
  }
@@ -1077,7 +1106,7 @@ function create(opts) {
1077
1106
  error: e && e.message,
1078
1107
  }, actorCtx));
1079
1108
  }
1080
- return _writeError(res, HTTP.INTERNAL_SERVER_ERROR, "onServe_threw",
1109
+ return writeErr(res, HTTP.INTERNAL_SERVER_ERROR, "onServe_threw",
1081
1110
  "Internal Server Error");
1082
1111
  }
1083
1112
  }
@@ -29,6 +29,7 @@
29
29
  */
30
30
 
31
31
  var { defineClass } = require("./framework-error");
32
+ var codepointClass = require("./codepoint-class");
32
33
 
33
34
  var UriTemplateError = defineClass("UriTemplateError", { alwaysPermanent: true });
34
35
 
@@ -59,7 +60,8 @@ function _pctEncode(str, allowReserved) {
59
60
  var ch = str.charAt(i);
60
61
  // Preserve existing percent-encoded triplets when the reserved set is
61
62
  // allowed (operators "+" and "#").
62
- if (allowReserved && ch === "%" && /^[0-9A-Fa-f]{2}$/.test(str.substr(i + 1, 2))) {
63
+ // allow:regex-no-length-cap the substr is a fixed 2-char window
64
+ if (allowReserved && ch === "%" && codepointClass.HEX_PAIR_RE.test(str.substr(i + 1, 2))) {
63
65
  out += str.substr(i, 3); i += 2; continue;
64
66
  }
65
67
  if (UNRESERVED.test(ch) || (allowReserved && RESERVED.test(ch))) { out += ch; continue; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.14.16",
3
+ "version": "0.14.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:44556dbf-5411-4644-ab81-5f0571d5e036",
5
+ "serialNumber": "urn:uuid:b96f0679-8967-43f9-b412-aa598ea52508",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-06-01T02:09:53.597Z",
8
+ "timestamp": "2026-06-02T15:25:35.604Z",
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.14.16",
22
+ "bom-ref": "@blamejs/core@0.14.18",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.14.16",
25
+ "version": "0.14.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.14.16",
29
+ "purl": "pkg:npm/%40blamejs/core@0.14.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.14.16",
57
+ "ref": "@blamejs/core@0.14.18",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]