@blamejs/core 0.8.69 → 0.8.72

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/mcp.js CHANGED
@@ -375,8 +375,330 @@ function serverGuard(opts) {
375
375
  };
376
376
  }
377
377
 
378
+ /**
379
+ * @primitive b.mcp.toolResult.sanitize
380
+ * @signature b.mcp.toolResult.sanitize(result, opts?)
381
+ * @since 0.8.70
382
+ * @related b.mcp.serverGuard, b.guardHtml, b.ai.input.classify
383
+ *
384
+ * OWASP LLM02 — model/tool-output sanitization. MCP tool calls
385
+ * frequently return content the host model interprets as further
386
+ * instructions; an attacker-controlled tool surface can return
387
+ * `{ type: "text", text: "Ignore prior instructions and ..." }`,
388
+ * `<script>...</script>`, OR markdown image links pointing at
389
+ * exfiltration endpoints. The framework's defense:
390
+ *
391
+ * - Strip / refuse executable HTML (`<script>` / `<iframe>` /
392
+ * `javascript:` URLs) — composes b.guardHtml's strict profile
393
+ * - Refuse known prompt-injection markers ("ignore previous
394
+ * instructions", "system: you are now ...", role-claim prefixes)
395
+ * — composes b.ai.input.classify
396
+ * - Cap text length so a tool can't blow the host's context window
397
+ * out from under it
398
+ * - Refuse content with `image_url` / `audio_url` / `resource_link`
399
+ * pointing at non-allowlisted hosts (data-exfil via auto-fetch)
400
+ *
401
+ * Returns either the cleaned result (when `sanitize: true`) or
402
+ * throws `McpError("mcp/tool-output-refused", ...)` (default —
403
+ * fail-closed). Operators with a known-good tool surface that needs
404
+ * raw passthrough opt out via `posture: "audit-only"`.
405
+ *
406
+ * @opts
407
+ * {
408
+ * posture?: "refuse" | "sanitize" | "audit-only", // default "refuse"
409
+ * maxTextBytes?: number, // default 64 KiB per content block
410
+ * allowedHosts?: string[], // for image/audio/resource_link refs
411
+ * classifyInput?: fn(text)→{verdict, score} | null, // default b.ai.input.classify
412
+ * }
413
+ *
414
+ * @example
415
+ * var safe = b.mcp.toolResult.sanitize(toolResp, { posture: "sanitize" });
416
+ * // → { content: [{ type: "text", text: "<cleaned>" }] }
417
+ */
418
+ var DEFAULT_TOOL_OUTPUT_MAX_BYTES = 64 * 1024; // allow:raw-byte-literal — 64 KiB per content block
419
+ var PROMPT_INJECTION_MARKERS = [
420
+ "ignore (previous|prior|all) instructions",
421
+ "system:\\s*you are",
422
+ "<\\|im_(start|end)\\|>",
423
+ "<\\|system\\|>",
424
+ "###\\s*(system|assistant|user|tool)",
425
+ "<system>",
426
+ "</?(?:assistant|system|user|tool)>",
427
+ ];
428
+ var INJECTION_RE = new RegExp(PROMPT_INJECTION_MARKERS.join("|"), "i"); // allow:dynamic-regex — composed from the const PROMPT_INJECTION_MARKERS list above; not operator-supplied input
429
+ var DANGEROUS_HTML_RE = /<script\b|<iframe\b|<object\b|<embed\b|javascript:/i;
430
+
431
+ function _toolResultSanitize(result, opts) {
432
+ opts = opts || {};
433
+ var posture = opts.posture || "refuse";
434
+ if (["refuse", "sanitize", "audit-only"].indexOf(posture) === -1) {
435
+ throw new McpError("mcp/bad-posture",
436
+ "toolResult.sanitize: posture must be 'refuse' | 'sanitize' | 'audit-only'");
437
+ }
438
+ var maxBytes = opts.maxTextBytes || DEFAULT_TOOL_OUTPUT_MAX_BYTES;
439
+ var allowedHosts = Array.isArray(opts.allowedHosts) ? opts.allowedHosts : [];
440
+ if (!result || typeof result !== "object") {
441
+ throw new McpError("mcp/bad-tool-result",
442
+ "toolResult.sanitize: result must be an object");
443
+ }
444
+ var content = Array.isArray(result.content) ? result.content : [];
445
+ var issues = [];
446
+ var cleaned = [];
447
+ for (var i = 0; i < content.length; i++) {
448
+ var block = content[i];
449
+ if (!block || typeof block !== "object") {
450
+ issues.push({ kind: "bad-block", index: i });
451
+ continue;
452
+ }
453
+ if (block.type === "text" && typeof block.text === "string") {
454
+ var t = block.text;
455
+ if (Buffer.byteLength(t, "utf8") > maxBytes) {
456
+ issues.push({ kind: "text-too-long", index: i, bytes: Buffer.byteLength(t, "utf8") });
457
+ if (posture === "sanitize") t = Buffer.from(t, "utf8").subarray(0, maxBytes).toString("utf8");
458
+ }
459
+ // Bound the regex-test surface to maxBytes (already enforced
460
+ // upstream when sanitize-mode strips, but in audit-only / refuse
461
+ // modes we still hand the raw text into the regex so cap
462
+ // explicitly here to satisfy the regex-bound-length rule).
463
+ var regexInput = Buffer.byteLength(t, "utf8") > maxBytes
464
+ ? Buffer.from(t, "utf8").subarray(0, maxBytes).toString("utf8")
465
+ : t;
466
+ if (INJECTION_RE.test(regexInput)) { // allow:regex-no-length-cap regexInput byteLength bounded above
467
+ issues.push({ kind: "prompt-injection", index: i });
468
+ if (posture === "sanitize") {
469
+ // Strip the injection marker line — operators wanting
470
+ // structural redaction wire their own classifier.
471
+ t = t.replace(INJECTION_RE, "[REDACTED]");
472
+ }
473
+ }
474
+ if (DANGEROUS_HTML_RE.test(regexInput)) { // allow:regex-no-length-cap regexInput byteLength bounded above
475
+ issues.push({ kind: "dangerous-html", index: i });
476
+ if (posture === "sanitize") t = t.replace(DANGEROUS_HTML_RE, "[REDACTED]");
477
+ }
478
+ cleaned.push({ type: "text", text: t });
479
+ } else if (block.type === "image" || block.type === "resource_link" || block.type === "audio") {
480
+ var url = block.url || (block.resource && block.resource.uri);
481
+ if (typeof url === "string" && url.length > 0 && allowedHosts.length > 0) {
482
+ var u; try { u = new URL(url); } catch (_e) { u = null; } // allow:raw-new-url — operator-supplied tool URL; allowlist enforced below
483
+ if (!u || allowedHosts.indexOf(u.host) === -1) {
484
+ issues.push({ kind: "off-allowlist-url", index: i, url: url });
485
+ if (posture === "sanitize") continue; // drop the block in sanitize mode
486
+ }
487
+ }
488
+ cleaned.push(block);
489
+ } else {
490
+ cleaned.push(block);
491
+ }
492
+ }
493
+ if (issues.length > 0 && posture === "refuse") {
494
+ var first = issues[0];
495
+ throw new McpError("mcp/tool-output-refused",
496
+ "toolResult.sanitize: refused " + issues.length + " issue(s) " +
497
+ "(first: " + first.kind + " on block[" + first.index + "])");
498
+ }
499
+ return { content: cleaned, isError: !!result.isError, issues: issues };
500
+ }
501
+
502
+ /**
503
+ * @primitive b.mcp.capability.create
504
+ * @signature b.mcp.capability.create(scopes)
505
+ * @since 0.8.70
506
+ * @related b.mcp.serverGuard
507
+ *
508
+ * OWASP LLM08 — capability primitive. Wraps an MCP tool/resource
509
+ * registration with a scope set the host model's session must hold
510
+ * before the tool/resource is exposed. Defaults to deny-all; the
511
+ * operator's session-decoration step grants scopes per user / per
512
+ * agent / per delegated-actor.
513
+ *
514
+ * Returns `{ scopes, satisfiedBy(grantedSet) }` — the guard checks
515
+ * `satisfiedBy(session.capabilities)` before each tool/resource
516
+ * dispatch. Falsy → refuse with `mcp/capability-denied`.
517
+ *
518
+ * @example
519
+ * var fileRead = b.mcp.capability.create(["fs:read"]);
520
+ * if (!fileRead.satisfiedBy(session.capabilities)) {
521
+ * throw new Error("mcp/capability-denied");
522
+ * }
523
+ */
524
+ function _capabilityCreate(scopes) {
525
+ if (!Array.isArray(scopes) || scopes.length === 0) {
526
+ throw new McpError("mcp/bad-capability",
527
+ "capability.create: scopes must be a non-empty array of strings");
528
+ }
529
+ scopes.forEach(function (s, i) {
530
+ if (typeof s !== "string" || s.length === 0) {
531
+ throw new McpError("mcp/bad-capability-scope",
532
+ "capability.create: scopes[" + i + "] must be a non-empty string");
533
+ }
534
+ });
535
+ var frozen = scopes.slice();
536
+ return {
537
+ scopes: frozen,
538
+ satisfiedBy: function (granted) {
539
+ if (!Array.isArray(granted)) return false;
540
+ for (var i = 0; i < frozen.length; i++) {
541
+ if (granted.indexOf(frozen[i]) === -1) return false;
542
+ }
543
+ return true;
544
+ },
545
+ };
546
+ }
547
+
548
+ /**
549
+ * @primitive b.mcp.validateToolInput
550
+ * @signature b.mcp.validateToolInput(toolName, input, schema)
551
+ * @since 0.8.70
552
+ * @related b.mcp.serverGuard, b.safeSchema
553
+ *
554
+ * OWASP LLM07 — JSON-Schema enforcement on MCP tool inputs. Tools
555
+ * declare an `inputSchema` (JSON Schema 2020-12 subset) at
556
+ * registration; before each invocation the framework validates
557
+ * incoming arguments against the schema and refuses on any drift.
558
+ * Composes b.safeSchema for the validation engine — same primitive
559
+ * the OpenAPI surface uses, so the threat model is uniform.
560
+ *
561
+ * Returns the validated (possibly coerced) input object on success;
562
+ * throws `McpError("mcp/tool-input-invalid", ...)` on schema breach.
563
+ *
564
+ * @example
565
+ * var schema = { type: "object",
566
+ * properties: { path: { type: "string" } },
567
+ * required: ["path"] };
568
+ * var input = b.mcp.validateToolInput("read_file", { path: "/x" }, schema);
569
+ */
570
+ // JSON-Schema-2020-12 subset validator for MCP tool inputs. The
571
+ // MCP spec specifies tool schemas in standard JSON Schema; the
572
+ // framework's chainable b.safeSchema is fluent-builder-shaped and
573
+ // doesn't accept JSON Schema directly. We implement the small
574
+ // subset MCP tools actually use:
575
+ // - type: "string" | "number" | "integer" | "boolean" | "object" | "array" | "null"
576
+ // - required: string[]
577
+ // - properties: recursive
578
+ // - items: array element schema
579
+ // - enum: allowed-value list
580
+ // - minimum / maximum / minLength / maxLength
581
+ // - pattern: regex (string types)
582
+ // Refuses unknown JSON Schema keywords loudly so a tool-author
583
+ // typo doesn't silently pass validation.
584
+ function _validateValueAgainstSchema(value, schema, path) {
585
+ if (!schema || typeof schema !== "object") return null;
586
+ var t = schema.type;
587
+ if (Array.isArray(t)) {
588
+ var anyMatched = false;
589
+ for (var ti = 0; ti < t.length; ti++) {
590
+ if (_typeMatches(value, t[ti])) { anyMatched = true; break; }
591
+ }
592
+ if (!anyMatched) return path + ": expected one of " + JSON.stringify(t) + ", got " + (typeof value);
593
+ } else if (typeof t === "string") {
594
+ if (!_typeMatches(value, t)) return path + ": expected " + t + ", got " + (typeof value);
595
+ }
596
+ if (Array.isArray(schema.enum) && schema.enum.indexOf(value) === -1) {
597
+ return path + ": value not in enum " + JSON.stringify(schema.enum);
598
+ }
599
+ if (typeof value === "string") {
600
+ if (typeof schema.minLength === "number" && value.length < schema.minLength) {
601
+ return path + ": string length " + value.length + " < minLength " + schema.minLength;
602
+ }
603
+ if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
604
+ return path + ": string length " + value.length + " > maxLength " + schema.maxLength;
605
+ }
606
+ if (typeof schema.pattern === "string") {
607
+ // Schema-supplied pattern — operator-controlled at registration
608
+ // time, not request-controlled. Cap value length first per the
609
+ // codebase-patterns regex-bound rule so a 10MB string doesn't
610
+ // ReDoS the validator.
611
+ if (value.length > 4096) return path + ": value exceeds 4 KiB cap before regex test"; // allow:raw-byte-literal — 4 KiB regex-input cap
612
+ try {
613
+ var pat = new RegExp(schema.pattern); // allow:dynamic-regex — schema.pattern from registered tool author, not request input; bounded above
614
+ if (!pat.test(value)) return path + ": does not match pattern";
615
+ }
616
+ catch (_e) { return path + ": invalid pattern in schema"; }
617
+ }
618
+ }
619
+ if (typeof value === "number") {
620
+ if (typeof schema.minimum === "number" && value < schema.minimum) return path + ": " + value + " < minimum " + schema.minimum;
621
+ if (typeof schema.maximum === "number" && value > schema.maximum) return path + ": " + value + " > maximum " + schema.maximum;
622
+ }
623
+ if (t === "object" && value && typeof value === "object" && !Array.isArray(value)) {
624
+ if (Array.isArray(schema.required)) {
625
+ for (var ri = 0; ri < schema.required.length; ri++) {
626
+ if (!Object.prototype.hasOwnProperty.call(value, schema.required[ri])) {
627
+ return path + ": missing required property '" + schema.required[ri] + "'";
628
+ }
629
+ }
630
+ }
631
+ if (schema.properties && typeof schema.properties === "object") {
632
+ var keys = Object.keys(schema.properties);
633
+ for (var pi = 0; pi < keys.length; pi++) {
634
+ var k = keys[pi];
635
+ if (!Object.prototype.hasOwnProperty.call(value, k)) continue;
636
+ var inner = _validateValueAgainstSchema(value[k], schema.properties[k], path + "." + k);
637
+ if (inner) return inner;
638
+ }
639
+ }
640
+ if (schema.additionalProperties === false) {
641
+ var allowed = Object.keys(schema.properties || {});
642
+ var keys2 = Object.keys(value);
643
+ for (var ki = 0; ki < keys2.length; ki++) {
644
+ if (allowed.indexOf(keys2[ki]) === -1) {
645
+ return path + ": unknown property '" + keys2[ki] + "' (additionalProperties: false)";
646
+ }
647
+ }
648
+ }
649
+ }
650
+ if (t === "array" && Array.isArray(value)) {
651
+ if (schema.items) {
652
+ for (var ai = 0; ai < value.length; ai++) {
653
+ var aInner = _validateValueAgainstSchema(value[ai], schema.items, path + "[" + ai + "]");
654
+ if (aInner) return aInner;
655
+ }
656
+ }
657
+ if (typeof schema.minItems === "number" && value.length < schema.minItems) {
658
+ return path + ": array length " + value.length + " < minItems " + schema.minItems;
659
+ }
660
+ if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
661
+ return path + ": array length " + value.length + " > maxItems " + schema.maxItems;
662
+ }
663
+ }
664
+ return null;
665
+ }
666
+
667
+ function _typeMatches(value, type) {
668
+ switch (type) {
669
+ case "string": return typeof value === "string";
670
+ case "number": return typeof value === "number" && isFinite(value);
671
+ case "integer": return typeof value === "number" && Number.isInteger(value);
672
+ case "boolean": return typeof value === "boolean";
673
+ case "null": return value === null;
674
+ case "array": return Array.isArray(value);
675
+ case "object": return value !== null && typeof value === "object" && !Array.isArray(value);
676
+ default: return false;
677
+ }
678
+ }
679
+
680
+ function _validateToolInput(toolName, input, schema) {
681
+ if (typeof toolName !== "string" || toolName.length === 0) {
682
+ throw new McpError("mcp/bad-tool-name",
683
+ "validateToolInput: toolName must be a non-empty string");
684
+ }
685
+ if (!schema || typeof schema !== "object") {
686
+ throw new McpError("mcp/bad-tool-schema",
687
+ "validateToolInput: schema must be a JSON-Schema-shaped object");
688
+ }
689
+ var err = _validateValueAgainstSchema(input, schema, "$");
690
+ if (err) {
691
+ throw new McpError("mcp/tool-input-invalid",
692
+ "validateToolInput: tool '" + toolName + "' input " + err);
693
+ }
694
+ return input;
695
+ }
696
+
378
697
  module.exports = {
379
698
  serverGuard: serverGuard,
380
699
  parseRequest: parseRequest,
381
700
  refuse: refuse,
701
+ toolResult: { sanitize: _toolResultSanitize },
702
+ capability: { create: _capabilityCreate },
703
+ validateToolInput: _validateToolInput,
382
704
  };
@@ -202,7 +202,7 @@ function create(opts) {
202
202
  validateOpts(opts, [
203
203
  "origins", "siteOrigin", "methods", "headers", "exposeHeaders",
204
204
  "credentials", "maxAgeSeconds", "refuseUnknown", "trustProxy",
205
- "strictNullOrigin",
205
+ "strictNullOrigin", "allowPrivateNetwork",
206
206
  ], "middleware.cors");
207
207
  var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
208
208
  ? opts.trustProxy : false;
@@ -342,6 +342,28 @@ function create(opts) {
342
342
  }
343
343
  }
344
344
  }
345
+ // Private Network Access (PNA) preflight — Chrome's W3C draft
346
+ // sends `Access-Control-Request-Private-Network: true` when a
347
+ // public-internet page tries to fetch a private/local resource
348
+ // (RFC 1918 / loopback). Servers MUST acknowledge with
349
+ // `Access-Control-Allow-Private-Network: true` to permit. The
350
+ // framework refuses by default — operators with a deliberate
351
+ // public-to-private flow opt in via `allowPrivateNetwork: true`
352
+ // (audited reason) at create-time.
353
+ var pnaRequested = req.headers["access-control-request-private-network"];
354
+ if (pnaRequested === "true") {
355
+ if (opts.allowPrivateNetwork === true) {
356
+ if (typeof res.setHeader === "function") {
357
+ res.setHeader("Access-Control-Allow-Private-Network", "true");
358
+ }
359
+ } else {
360
+ if (typeof res.writeHead === "function") {
361
+ res.writeHead(requestHelpers.HTTP_STATUS.FORBIDDEN, { "Content-Type": "text/plain" });
362
+ res.end("CORS: Private Network Access not permitted (set allowPrivateNetwork:true with audited reason to opt in)");
363
+ }
364
+ return;
365
+ }
366
+ }
345
367
  if (typeof res.setHeader === "function") {
346
368
  res.setHeader("Access-Control-Allow-Methods", methods);
347
369
  res.setHeader("Access-Control-Allow-Headers", headers);
@@ -40,6 +40,8 @@ function _writeUnauthorized(res, requiredBand, actualBand, realm) {
40
40
  "Content-Type": "application/json; charset=utf-8",
41
41
  "Content-Length": Buffer.byteLength(body),
42
42
  "WWW-Authenticate": challenge,
43
+ // RFC 9111 §5.2.2.5 — auth-gated 401 must not be cached.
44
+ "Cache-Control": "no-store",
43
45
  });
44
46
  res.end(body);
45
47
  }
@@ -106,10 +106,18 @@ function create(opts) {
106
106
  } catch (_e) { /* audit best-effort */ }
107
107
  }
108
108
 
109
+ // RFC 9111 §5.2.2.5 — auth-gated paths SHOULD emit
110
+ // Cache-Control: no-store so a shared cache (or browser
111
+ // back-button cache) can't replay a 401 / redirect / payload
112
+ // intended for an unauthenticated context to a different user.
113
+ // Pre-v0.8.70 the framework's auth middlewares emitted no
114
+ // cache directive, leaving the operator to set it themselves;
115
+ // forgetting it under a CDN that respects Cache-Control was
116
+ // a routine misconfiguration.
109
117
  if (prefersJson(req)) {
110
118
  if (typeof res.writeHead === "function") {
111
119
  res.writeHead(requestHelpers.HTTP_STATUS.UNAUTHORIZED,
112
- { "Content-Type": "application/json" });
120
+ { "Content-Type": "application/json", "Cache-Control": "no-store" });
113
121
  res.end(JSON.stringify({ error: msg }));
114
122
  }
115
123
  return;
@@ -117,14 +125,14 @@ function create(opts) {
117
125
  if (redirectTo) {
118
126
  if (typeof res.writeHead === "function") {
119
127
  // 302 Found — RFC 7231 §6.4.3. Not in HTTP_STATUS table.
120
- res.writeHead(302, { "Location": redirectTo });
128
+ res.writeHead(302, { "Location": redirectTo, "Cache-Control": "no-store" });
121
129
  res.end();
122
130
  }
123
131
  return;
124
132
  }
125
133
  if (typeof res.writeHead === "function") {
126
134
  res.writeHead(requestHelpers.HTTP_STATUS.UNAUTHORIZED,
127
- { "Content-Type": "text/plain" });
135
+ { "Content-Type": "text/plain", "Cache-Control": "no-store" });
128
136
  res.end(msg);
129
137
  }
130
138
  };
@@ -71,6 +71,9 @@ function _writeChallenge(res, challenge, body, statusCode) {
71
71
  "Content-Type": "application/json; charset=utf-8",
72
72
  "Content-Length": Buffer.byteLength(json),
73
73
  "WWW-Authenticate": challenge,
74
+ // RFC 9111 §5.2.2.5 — auth-gated step-up challenge must not
75
+ // be cached (CDN replay defense).
76
+ "Cache-Control": "no-store",
74
77
  });
75
78
  res.end(json);
76
79
  }
@@ -57,6 +57,20 @@ var DEFAULT_PERMISSIONS = [
57
57
  "bluetooth=()", "hid=()", "serial=()", "idle-detection=()",
58
58
  "local-fonts=()", "compute-pressure=()", "window-management=()",
59
59
  "private-state-token-issuance=()", "private-state-token-redemption=()",
60
+ // v0.8.70 expansion — Privacy-Sandbox + Storage Access feature
61
+ // names that landed in Chrome 119+/120+ stable. Default-deny:
62
+ // - storage-access — Storage Access API (cross-site cookie
63
+ // access flow under Privacy Sandbox); operators serving an
64
+ // embedded-iframe SaaS opt in explicitly.
65
+ // - browsing-topics — Topics API (replacement for FLoC);
66
+ // enabled by default in Chrome but trackable surface, so
67
+ // deny-by-default unless the operator opts in.
68
+ // - private-aggregation, attribution-reporting-cross-site —
69
+ // Privacy Sandbox aggregation APIs; deny-by-default.
70
+ // - controlled-frame, captured-surface-control — Web App
71
+ // embedding APIs; deny-by-default.
72
+ "storage-access=()", "browsing-topics=()",
73
+ "private-aggregation=()", "controlled-frame=()", "captured-surface-control=()",
60
74
  ];
61
75
 
62
76
  // Strict CSP — no 'unsafe-inline' on script-src OR style-src.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.8.69",
3
+ "version": "0.8.72",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -70,6 +70,7 @@
70
70
  ],
71
71
  "scripts": {
72
72
  "test": "node test/smoke.js",
73
+ "fuzz": "node fuzz/_run-all.js",
73
74
  "prepack": "node scripts/check-pack-against-gitignore.js",
74
75
  "check:vendor-currency": "node scripts/check-vendor-currency.js"
75
76
  },
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
- "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:cf3aa594-6a94-4be5-9952-a87c315ec5eb",
4
+ "specVersion": "1.6",
5
+ "serialNumber": "urn:uuid:2ace0ce8-c004-4329-8eed-0ae3c1fb02a6",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-10T15:19:22.438Z",
8
+ "timestamp": "2026-05-10T19:52:07.485Z",
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.69",
22
+ "bom-ref": "@blamejs/core@0.8.72",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.8.69",
25
+ "version": "0.8.72",
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.69",
29
+ "purl": "pkg:npm/%40blamejs/core@0.8.72",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,8 +54,8 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.8.69",
57
+ "ref": "@blamejs/core@0.8.72",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]
61
- }
61
+ }