@blamejs/core 0.8.39 → 0.8.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,7 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.8.x
10
10
 
11
+ - v0.8.40 (2026-05-07) — operator enhancements (2/2): `b.honeytoken.create({audit})` issues canary api-key / session / URL / row-id values that emit `honeytoken.tripped` audit on any positive lookup; `b.middleware.cspReport.create({onReport})` is a Reporting-API endpoint that ingests CSP / COEP / COOP violations as `csp.violation` audit rows; `b.auditTools.forensicSnapshot({out, since, passphrase, reason})` composes an audit-export slice + IR context manifest into one tamper-evident bundle for legal / regulator handover; `b.network.tls.pinsetDriftMonitor({intervalMs})` periodically compares the trust-store fingerprint set to the captured baseline and emits `network.tls.pinset.drifted` when CAs are added or removed. Adds the OpenSSF Scorecard CI workflow at `.github/workflows/scorecard.yml`. Defers items 11 (operator-supplied transform sandbox), 14 (chaos / fault-injection drills), and 15 (exploit replay corpus harness) with re-open conditions: surface when (a) operator demand surfaces OR (b) a CVE replay needs a vendored harness.
11
12
  - v0.8.39 (2026-05-07) — operator enhancements (1/2): `b.configDrift.verifyVendorIntegrity()` re-hashes every file listed in `lib/vendor/MANIFEST.json` at boot and refuses on mismatch; `b.network.allowlist.create({allow, deny})` composes on `b.ssrfGuard` to gate per-call outbound URLs against an operator CIDR/host allow set; `b.auth.atoKillSwitch.trigger({userId, reason})` is a composite ATO incident-response workflow that destroys every session for the user, applies `b.auth.lockout`, and optionally flips `b.auth.accessLock` mode in one audited call.
12
13
  - v0.8.38 (2026-05-07) — multipart parser refuses obsolete line folding (RFC 9112 §5.2 obs-fold) and CR/LF/NUL bytes in part-header values (RFC 9110 §5.5). Adds RFC 5987 / 8187 `filename*=UTF-8''…` extended-parameter support; the decoded value takes precedence over a legacy `filename=` companion.
13
14
 
package/index.js CHANGED
@@ -226,6 +226,7 @@ var appShutdown = require("./lib/app-shutdown");
226
226
  var slug = require("./lib/slug");
227
227
  var webhook = require("./lib/webhook");
228
228
  var apiKey = require("./lib/api-key");
229
+ var honeytoken = require("./lib/honeytoken");
229
230
  var credentialHash = require("./lib/credential-hash");
230
231
  var permissions = require("./lib/permissions");
231
232
  var cache = require("./lib/cache");
@@ -402,6 +403,7 @@ module.exports = {
402
403
  slug: slug,
403
404
  webhook: webhook,
404
405
  apiKey: apiKey,
406
+ honeytoken: honeytoken,
405
407
  credentialHash: credentialHash,
406
408
  permissions: permissions,
407
409
  cache: cache,
@@ -61,6 +61,7 @@ var auditSign = require("./audit-sign");
61
61
  var backupCrypto = require("./backup/crypto");
62
62
  var clusterStorage = require("./cluster-storage");
63
63
  var lazyRequire = require("./lazy-require");
64
+ var validateOpts = require("./validate-opts");
64
65
  var jsonSafe = require("./safe-json");
65
66
  var { defineClass } = require("./framework-error");
66
67
 
@@ -665,9 +666,77 @@ async function _defaultApplyPurge(args) {
665
666
  };
666
667
  }
667
668
 
669
+ // forensicSnapshot — post-compromise composer that bundles an audit
670
+ // archive slice, current break-glass grants, the active incident
671
+ // report (if any), and process-runtime metadata into a single signed
672
+ // bundle. The operator passes this to legal / regulators / the IR
673
+ // team as one tamper-evident artifact.
674
+ //
675
+ // var snap = await b.auditTools.forensicSnapshot({
676
+ // out: "/forensics/2026-05-07-incident-42",
677
+ // since: Date.now() - C.TIME.days(7),
678
+ // passphrase: process.env.AUDIT_BUNDLE_PASSPHRASE,
679
+ // incidentId: "inc-2026-05-07-42",
680
+ // reason: "ATO investigation: 14 failed MFA from new geo, user u_42",
681
+ // actor: { id: "alice@ops.example.com", role: "incident-commander" },
682
+ // });
683
+ async function forensicSnapshot(opts) {
684
+ opts = opts || {};
685
+ _requirePassphrase(opts.passphrase);
686
+ _requireOutDir(opts.out, "forensicSnapshot");
687
+ var sinceMs = _toMs(opts.since);
688
+ if (sinceMs == null) {
689
+ throw new AuditToolsError("audit-tools/no-since",
690
+ "forensicSnapshot: opts.since is required");
691
+ }
692
+ validateOpts.requireNonEmptyString(opts.reason, "reason", AuditToolsError, "audit-tools/no-reason");
693
+ var sliceResult = await exportSlice({
694
+ out: opts.out,
695
+ since: sinceMs,
696
+ until: Date.now(),
697
+ passphrase: opts.passphrase,
698
+ readRows: opts.readRows,
699
+ readCoveringCheckpoint: opts.readCoveringCheckpoint,
700
+ });
701
+ // Compose snapshot manifest with operator-supplied IR context.
702
+ var manifest = {
703
+ snapshotKind: "forensic",
704
+ incidentId: opts.incidentId || null,
705
+ reason: opts.reason,
706
+ actor: opts.actor || null,
707
+ composedAt: new Date().toISOString(),
708
+ auditSliceFile: sliceResult && sliceResult.path,
709
+ auditSliceCount: sliceResult && sliceResult.rowCount,
710
+ runtime: {
711
+ nodeVersion: process.version,
712
+ platform: process.platform,
713
+ arch: process.arch,
714
+ pid: process.pid,
715
+ uptimeSec: Math.round(process.uptime()),
716
+ },
717
+ };
718
+ var manifestPath = require("node:path").join(opts.out, "forensic-snapshot.json");
719
+ require("node:fs").writeFileSync(manifestPath, _canonicalize(manifest), "utf8");
720
+ try {
721
+ require("./audit").safeEmit({
722
+ action: "audit.forensic_snapshot.composed",
723
+ outcome: "success",
724
+ metadata: {
725
+ out: opts.out,
726
+ incidentId: manifest.incidentId,
727
+ reason: opts.reason,
728
+ actor: opts.actor || null,
729
+ rowCount: manifest.auditSliceCount || 0,
730
+ },
731
+ });
732
+ } catch (_e) { /* audit best-effort */ }
733
+ return Object.assign({}, manifest, { manifestPath: manifestPath });
734
+ }
735
+
668
736
  module.exports = {
669
737
  archive: archive,
670
738
  exportSlice: exportSlice,
739
+ forensicSnapshot: forensicSnapshot,
671
740
  verifyBundle: verifyBundle,
672
741
  purge: purge,
673
742
  BUNDLE_FORMAT: BUNDLE_FORMAT,
package/lib/audit.js CHANGED
@@ -248,6 +248,8 @@ var FRAMEWORK_NAMESPACES = [
248
248
  "tcpa10dlc", // b.tcpa10dlc (tcpa10dlc.consent_recorded / consent_revoked)
249
249
  "iabmspa", // b.iabMspa (iabmspa.processing_refused)
250
250
  "vendor", // b.configDrift.verifyVendorIntegrity (vendor.integrity.verified / tampered)
251
+ "honeytoken", // b.honeytoken (honeytoken.issued / tripped)
252
+ "csp", // b.middleware.cspReport (csp.violation)
251
253
  ];
252
254
  var registeredNamespaces = new Set(FRAMEWORK_NAMESPACES);
253
255
 
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ /**
3
+ * b.honeytoken — canary credential framework. Generates decoy values
4
+ * (fake api-key shapes, fake admin URLs, fake DB row references) that
5
+ * are NEVER handed to a real client; their presence in a request,
6
+ * log, or DB lookup means an attacker found something they shouldn't
7
+ * have. The framework registers each token at issuance and refuses
8
+ * silently in production but always emits a `honeytoken.tripped`
9
+ * audit row on any positive lookup.
10
+ *
11
+ * var honey = b.honeytoken.create({ audit: b.audit });
12
+ *
13
+ * var token = honey.issue({
14
+ * kind: "apiKey",
15
+ * metadata: { plantedAt: "GET /admin/keys/404", linkedTo: "u_42" },
16
+ * });
17
+ * // → { value: "bk_canary_8f3a7b2e0c…", id: "ht_<hex>" }
18
+ *
19
+ * if (honey.lookup(req.headers["x-api-key"])) {
20
+ * // attacker is using the canary; tripped event already audited
21
+ * return res.status(403).end();
22
+ * }
23
+ *
24
+ * Canary value shapes (`kind`):
25
+ * - "apiKey" → `bk_canary_<32 hex>` (matches b.apiKey shape)
26
+ * - "session" → `bks_canary_<48 hex>` (matches b.session shape)
27
+ * - "url" → `/admin/canary-<32 hex>` (planted as a clickable link)
28
+ * - "rowId" → `ht_canary_<32 hex>` (planted as a fake foreign key)
29
+ *
30
+ * Audit shape:
31
+ * - `honeytoken.issued` — outcome=success; metadata: { id, kind }
32
+ * - `honeytoken.tripped` — outcome=failure; metadata: { id, kind,
33
+ * metadata, observedAt, observedActor }
34
+ */
35
+
36
+ var crypto = require("./crypto");
37
+ var lazyRequire = require("./lazy-require");
38
+ var validateOpts = require("./validate-opts");
39
+ var { defineClass } = require("./framework-error");
40
+
41
+ var audit = lazyRequire(function () { return require("./audit"); });
42
+
43
+ var HoneytokenError = defineClass("HoneytokenError", { alwaysPermanent: true });
44
+
45
+ var KINDS = Object.freeze({
46
+ apiKey: function () { return "bk_canary_" + crypto.generateToken(16); }, // allow:raw-byte-literal — 16-byte (128-bit) canary entropy
47
+ session: function () { return "bks_canary_" + crypto.generateToken(24); }, // allow:raw-byte-literal — 24-byte (192-bit) canary entropy
48
+ url: function () { return "/admin/canary-" + crypto.generateToken(16); }, // allow:raw-byte-literal — 16-byte canary entropy
49
+ rowId: function () { return "ht_canary_" + crypto.generateToken(16); }, // allow:raw-byte-literal — 16-byte canary entropy
50
+ });
51
+
52
+ function create(opts) {
53
+ opts = opts || {};
54
+ validateOpts(opts, ["audit"], "honeytoken.create");
55
+
56
+ var registry = new Map(); // value → { id, kind, metadata, issuedAt }
57
+
58
+ function issue(spec) {
59
+ spec = spec || {};
60
+ validateOpts(spec, ["kind", "metadata"], "honeytoken.issue");
61
+ var kind = spec.kind;
62
+ if (typeof KINDS[kind] !== "function") {
63
+ throw new HoneytokenError(
64
+ "honeytoken/unknown-kind",
65
+ "honeytoken.issue: unknown kind '" + kind + "' " +
66
+ "(supported: " + Object.keys(KINDS).join(", ") + ")");
67
+ }
68
+ var value = KINDS[kind]();
69
+ var id = "ht_" + crypto.generateToken(8); // allow:raw-byte-literal — 8-byte registry id
70
+ var record = Object.freeze({
71
+ id: id,
72
+ kind: kind,
73
+ metadata: spec.metadata || null,
74
+ issuedAt: Date.now(),
75
+ });
76
+ registry.set(value, record);
77
+ try {
78
+ audit().safeEmit({
79
+ action: "honeytoken.issued",
80
+ outcome: "success",
81
+ metadata: { id: id, kind: kind },
82
+ });
83
+ } catch (_e) { /* audit best-effort */ }
84
+ return { id: id, value: value };
85
+ }
86
+
87
+ function lookup(value, observedActor) {
88
+ if (typeof value !== "string" || value.length === 0) return null;
89
+ var record = registry.get(value);
90
+ if (!record) return null;
91
+ try {
92
+ audit().safeEmit({
93
+ action: "honeytoken.tripped",
94
+ outcome: "failure",
95
+ metadata: {
96
+ id: record.id,
97
+ kind: record.kind,
98
+ metadata: record.metadata,
99
+ observedAt: Date.now(),
100
+ observedActor: observedActor || null,
101
+ },
102
+ });
103
+ } catch (_e) { /* audit best-effort */ }
104
+ return record;
105
+ }
106
+
107
+ function revoke(id) {
108
+ var found = false;
109
+ registry.forEach(function (record, value) {
110
+ if (record.id === id) {
111
+ registry.delete(value);
112
+ found = true;
113
+ }
114
+ });
115
+ return found;
116
+ }
117
+
118
+ function size() { return registry.size; }
119
+
120
+ return {
121
+ issue: issue,
122
+ lookup: lookup,
123
+ revoke: revoke,
124
+ size: size,
125
+ };
126
+ }
127
+
128
+ module.exports = {
129
+ create: create,
130
+ KINDS: Object.freeze(Object.keys(KINDS)),
131
+ HoneytokenError: HoneytokenError,
132
+ };
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ /**
3
+ * b.middleware.cspReport — Reporting-API endpoint for CSP / COEP /
4
+ * COOP / Permissions-Policy violations.
5
+ *
6
+ * The framework's default CSP appends `report-to default;` (see
7
+ * lib/middleware/security-headers.js); operators wire the matching
8
+ * `Reporting-Endpoints: default="https://app.example.com/csp-report"`
9
+ * header — and mount this middleware at the configured path. Browsers
10
+ * POST batches of violations as `application/reports+json`.
11
+ *
12
+ * var cspReport = b.middleware.cspReport.create({
13
+ * audit: b.audit,
14
+ * onReport: function (report) { metrics.count("csp.violation", 1, { directive: report.body.effectiveDirective }); },
15
+ * maxBytes: C.BYTES.kib(64),
16
+ * });
17
+ * router.post("/csp-report", cspReport);
18
+ *
19
+ * Audit shape: `csp.violation` (failure) per report; metadata carries
20
+ * the report.body fields (blockedURL, documentURL, effectiveDirective,
21
+ * sample, statusCode). Sample is truncated to 200 chars.
22
+ *
23
+ * Validation:
24
+ * - Refuses non-POST methods with 405
25
+ * - Refuses bodies > maxBytes (default 64 KiB) with 413
26
+ * - Refuses non-JSON bodies with 400
27
+ * - Accepts `application/reports+json` AND legacy `application/csp-report`
28
+ */
29
+
30
+ var C = require("../constants");
31
+ var lazyRequire = require("../lazy-require");
32
+ var safeBuffer = require("../safe-buffer");
33
+ var safeJson = require("../safe-json");
34
+ var validateOpts = require("../validate-opts");
35
+
36
+ var audit = lazyRequire(function () { return require("../audit"); });
37
+
38
+ var DEFAULT_MAX_BYTES = C.BYTES.kib(64);
39
+ var SAMPLE_TRUNCATE = 200;
40
+
41
+ function _truncate(value) {
42
+ if (typeof value !== "string") return value;
43
+ if (value.length <= SAMPLE_TRUNCATE) return value;
44
+ return value.slice(0, SAMPLE_TRUNCATE) + "…";
45
+ }
46
+
47
+ function _normalizeOne(reportLike) {
48
+ // Reporting API shape: { type, age, url, user_agent, body: {...} }
49
+ // Legacy CSP shape: { "csp-report": { ... } }
50
+ if (!reportLike || typeof reportLike !== "object") return null;
51
+ if (reportLike["csp-report"] && typeof reportLike["csp-report"] === "object") {
52
+ var legacy = reportLike["csp-report"];
53
+ return {
54
+ type: "csp-violation",
55
+ url: legacy["document-uri"] || null,
56
+ body: {
57
+ documentURL: legacy["document-uri"] || null,
58
+ blockedURL: legacy["blocked-uri"] || null,
59
+ effectiveDirective: legacy["effective-directive"] || legacy["violated-directive"] || null,
60
+ statusCode: legacy["status-code"] || null,
61
+ sample: _truncate(legacy["script-sample"] || ""),
62
+ sourceFile: legacy["source-file"] || null,
63
+ lineNumber: legacy["line-number"] || null,
64
+ },
65
+ };
66
+ }
67
+ if (reportLike.type && reportLike.body && typeof reportLike.body === "object") {
68
+ return {
69
+ type: reportLike.type,
70
+ url: reportLike.url || null,
71
+ body: {
72
+ documentURL: reportLike.body.documentURL || null,
73
+ blockedURL: reportLike.body.blockedURL || null,
74
+ effectiveDirective: reportLike.body.effectiveDirective || null,
75
+ statusCode: reportLike.body.statusCode || null,
76
+ sample: _truncate(reportLike.body.sample || ""),
77
+ sourceFile: reportLike.body.sourceFile || null,
78
+ lineNumber: reportLike.body.lineNumber || null,
79
+ },
80
+ };
81
+ }
82
+ return null;
83
+ }
84
+
85
+ function create(opts) {
86
+ opts = opts || {};
87
+ validateOpts(opts, ["audit", "onReport", "maxBytes"], "middleware.cspReport");
88
+ var maxBytes = (typeof opts.maxBytes === "number" && isFinite(opts.maxBytes) && opts.maxBytes > 0)
89
+ ? opts.maxBytes : DEFAULT_MAX_BYTES;
90
+ var onReport = (typeof opts.onReport === "function") ? opts.onReport : null;
91
+
92
+ return async function cspReport(req, res, _next) {
93
+ if (req.method !== "POST") {
94
+ res.writeHead(405, { "Allow": "POST" }); // allow:raw-byte-literal — HTTP 405 status
95
+ res.end();
96
+ return;
97
+ }
98
+ var body;
99
+ try {
100
+ body = await safeBuffer.boundedChunkCollector(req, { maxBytes: maxBytes });
101
+ } catch (_e) {
102
+ res.writeHead(413); // allow:raw-byte-literal — HTTP 413 status
103
+ res.end();
104
+ return;
105
+ }
106
+ var parsed;
107
+ try { parsed = safeJson.parse(body.toString("utf8")); }
108
+ catch (_e) {
109
+ res.writeHead(400); // allow:raw-byte-literal — HTTP 400 status
110
+ res.end();
111
+ return;
112
+ }
113
+ var reports = Array.isArray(parsed) ? parsed : [parsed];
114
+ for (var i = 0; i < reports.length; i++) {
115
+ var normalized = _normalizeOne(reports[i]);
116
+ if (!normalized) continue;
117
+ try {
118
+ audit().safeEmit({
119
+ action: "csp.violation",
120
+ outcome: "failure",
121
+ metadata: Object.assign({ type: normalized.type, url: normalized.url }, normalized.body),
122
+ });
123
+ } catch (_e) { /* audit best-effort */ }
124
+ if (onReport) {
125
+ try { onReport(normalized); } catch (_e) { /* hook best-effort */ }
126
+ }
127
+ }
128
+ res.writeHead(204); // allow:raw-byte-literal — HTTP 204 status
129
+ res.end();
130
+ };
131
+ }
132
+
133
+ module.exports = { create: create };
@@ -32,6 +32,7 @@ var cookies = require("./cookies");
32
32
  var cors = require("./cors");
33
33
  var dailyByteQuota = require("./daily-byte-quota");
34
34
  var cspNonce = require("./csp-nonce");
35
+ var cspReport = require("./csp-report");
35
36
  var csrfProtect = require("./csrf-protect");
36
37
  var dbRoleFor = require("./db-role-for");
37
38
  var dpop = require("./dpop");
@@ -88,6 +89,7 @@ module.exports = {
88
89
  compression: compression.create,
89
90
  cookies: cookies.create,
90
91
  cspNonce: cspNonce.create,
92
+ cspReport: cspReport.create,
91
93
  securityTxt: securityTxt.create,
92
94
  sse: sse.create,
93
95
  requestLog: requestLog.create,
@@ -329,6 +329,67 @@ function captureBaselineFingerprints() {
329
329
  STATE.baselineFingerprints = STATE.cas.map(function (e) { return e.meta.fingerprint256; });
330
330
  }
331
331
 
332
+ // pinsetDriftMonitor — periodic check that emits audit + observability
333
+ // events when the trust-store fingerprint set drifts from the captured
334
+ // baseline. Different intent from expiryMonitor: this fires when a
335
+ // CA is added or removed (by operator config-flip OR by a tampered
336
+ // MANIFEST / vendor refresh), not when an existing one approaches
337
+ // validity expiry.
338
+ //
339
+ // b.network.tls.captureBaselineFingerprints(); // at boot
340
+ // var mon = b.network.tls.pinsetDriftMonitor({
341
+ // intervalMs: C.TIME.minutes(15),
342
+ // onDrift: function (drift) { /* operator hook */ },
343
+ // });
344
+ //
345
+ // Audit emissions:
346
+ // network.tls.pinset.drift_check — every check, ok / warn
347
+ // network.tls.pinset.drifted — when added.length || removed.length
348
+ function pinsetDriftMonitor(opts) {
349
+ opts = opts || {};
350
+ var intervalMs = opts.intervalMs;
351
+ var auditOn = opts.audit !== false;
352
+ if (typeof intervalMs !== "number" || !isFinite(intervalMs) || intervalMs <= 0) {
353
+ throw new TlsTrustError("tls/bad-interval",
354
+ "tls.pinsetDriftMonitor: intervalMs must be a positive finite number");
355
+ }
356
+ function _tick() {
357
+ var drift;
358
+ try { drift = detectBaselineDrift(); }
359
+ catch (_e) { return; }
360
+ if (drift === null) return; // baseline not captured; nothing to compare
361
+ if (auditOn) {
362
+ try {
363
+ audit().safeEmit({
364
+ action: "network.tls.pinset.drift_check",
365
+ outcome: drift.drifted ? "warn" : "ok",
366
+ metadata: { added: drift.added.length, removed: drift.removed.length },
367
+ });
368
+ } catch (_e) { /* drop-silent */ }
369
+ }
370
+ if (drift.drifted) {
371
+ try { observability().safeEvent("network.tls.pinset.drifted", 1, {}); }
372
+ catch (_e) { /* drop-silent */ }
373
+ if (auditOn) {
374
+ try {
375
+ audit().safeEmit({
376
+ action: "network.tls.pinset.drifted",
377
+ outcome: "failure",
378
+ metadata: { added: drift.added, removed: drift.removed },
379
+ });
380
+ } catch (_e) { /* drop-silent */ }
381
+ }
382
+ if (typeof opts.onDrift === "function") {
383
+ try { opts.onDrift(drift); } catch (_e) { /* operator hook */ }
384
+ }
385
+ }
386
+ }
387
+ var handle = safeAsync.repeating(_tick, intervalMs, { name: "tls-pinset-drift-monitor" });
388
+ return {
389
+ stop: function () { if (handle) { handle.stop(); handle = null; } },
390
+ };
391
+ }
392
+
332
393
  function detectBaselineDrift() {
333
394
  if (!STATE.baselineFingerprints) return null;
334
395
  var current = STATE.cas.map(function (e) { return e.meta.fingerprint256; });
@@ -1683,6 +1744,7 @@ module.exports = {
1683
1744
  purgeExpired: purgeExpired,
1684
1745
  expiringSoon: expiringSoon,
1685
1746
  expiryMonitor: expiryMonitor,
1747
+ pinsetDriftMonitor: pinsetDriftMonitor,
1686
1748
  useSystemTrust: useSystemTrust,
1687
1749
  isSystemTrustEnabled: isSystemTrustEnabled,
1688
1750
  getTrustStore: getTrustStore,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.8.39",
3
+ "version": "0.8.40",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -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:319bfdfd-c9b3-4f86-a42e-c3441466295e",
5
+ "serialNumber": "urn:uuid:7c951f5a-156e-49be-91c0-8a5a420faf2c",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-07T16:17:54.954Z",
8
+ "timestamp": "2026-05-07T16:35:12.500Z",
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.8.39",
22
+ "bom-ref": "@blamejs/core@0.8.40",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.8.39",
25
+ "version": "0.8.40",
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.8.39",
29
+ "purl": "pkg:npm/%40blamejs/core@0.8.40",
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.8.39",
57
+ "ref": "@blamejs/core@0.8.40",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]