@blamejs/core 0.7.105 → 0.7.107

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.
@@ -0,0 +1,470 @@
1
+ "use strict";
2
+ /**
3
+ * WCAG 2.2 audit-only scanner for HTML.
4
+ *
5
+ * Scans operator-supplied HTML for accessibility violations against
6
+ * the WCAG 2.2 Recommendation (https://www.w3.org/TR/WCAG22/) without
7
+ * modifying the document. Returns a structured report of findings the
8
+ * operator wires into a CI gate, audit log, or development warning.
9
+ *
10
+ * var audit = b.guardHtml.wcag.audit(htmlString, {
11
+ * level: "AA", // | "A" | "AAA"
12
+ * ignore: ["1.4.3"], // SCs the operator's deployment opts out of
13
+ * });
14
+ *
15
+ * Design constraints:
16
+ * - PURE static analysis — no rendering, no JS execution.
17
+ * - The scanner is conservative: it flags clear violations (missing
18
+ * alt text, empty heading text) and high-confidence patterns
19
+ * (out-of-order headings, form fields without label / aria-label /
20
+ * aria-labelledby). It does NOT attempt color-contrast checks,
21
+ * focus-indicator checks, or text-spacing checks (require rendered
22
+ * styles or runtime CSS).
23
+ */
24
+
25
+ var validateOpts = require("./validate-opts");
26
+ var lazyRequire = require("./lazy-require");
27
+ var { defineClass } = require("./framework-error");
28
+
29
+ var GuardHtmlWcagError = defineClass("GuardHtmlWcagError", { alwaysPermanent: true });
30
+
31
+ var observability = lazyRequire(function () { return require("./observability"); });
32
+ var aria = require("./guard-html-wcag-aria");
33
+ var tables = require("./guard-html-wcag-tables");
34
+ var forms = require("./guard-html-wcag-forms");
35
+ var tagwalk = require("./guard-html-wcag-tagwalk");
36
+
37
+ var SC_REGISTRY = Object.freeze({
38
+ "1.1.1": { level: "A", name: "Non-text Content" },
39
+ "1.3.1": { level: "A", name: "Info and Relationships" },
40
+ "2.4.1": { level: "A", name: "Bypass Blocks" },
41
+ "2.4.2": { level: "A", name: "Page Titled" },
42
+ "2.4.4": { level: "A", name: "Link Purpose (In Context)" },
43
+ "3.1.1": { level: "A", name: "Language of Page" },
44
+ "3.3.2": { level: "A", name: "Labels or Instructions" },
45
+ "4.1.1": { level: "A", name: "Parsing (deprecated in 2.2; retained for back-compat)" },
46
+ "4.1.2": { level: "A", name: "Name, Role, Value" },
47
+ "2.4.11": { level: "AA", name: "Focus Not Obscured (Minimum)" },
48
+ "2.4.13": { level: "AAA", name: "Focus Appearance" },
49
+ "2.5.7": { level: "AA", name: "Dragging Movements" },
50
+ "2.5.8": { level: "AA", name: "Target Size (Minimum)" },
51
+ "3.2.6": { level: "A", name: "Consistent Help" },
52
+ "3.3.7": { level: "A", name: "Redundant Entry" },
53
+ "3.3.8": { level: "AA", name: "Accessible Authentication (Minimum)" },
54
+ "3.3.9": { level: "AAA", name: "Accessible Authentication (Enhanced)" },
55
+ });
56
+
57
+ var VALID_LEVELS = Object.freeze(["A", "AA", "AAA"]);
58
+
59
+ function _meetsLevel(scLevel, requestedLevel) {
60
+ if (requestedLevel === "AAA") return true;
61
+ if (requestedLevel === "AA") return scLevel === "A" || scLevel === "AA";
62
+ return scLevel === "A";
63
+ }
64
+
65
+ function _newReport(scopeUrl) {
66
+ return {
67
+ findings: [],
68
+ summary: { error: 0, warning: 0, info: 0 },
69
+ totalFindings: 0,
70
+ scopeUrl: scopeUrl || null,
71
+ scannedAt: Date.now(),
72
+ };
73
+ }
74
+
75
+ function _addFinding(report, finding) {
76
+ report.findings.push(finding);
77
+ report.totalFindings += 1;
78
+ report.summary[finding.severity] = (report.summary[finding.severity] || 0) + 1;
79
+ }
80
+
81
+ var _TAG_RE = tagwalk.TAG_RE;
82
+ var _parseAttrs = tagwalk.parseAttrs;
83
+ var _lineColAt = tagwalk.lineColAt;
84
+
85
+ function _innerText(html, tagOpenEnd, tagName) {
86
+ var lower = tagName.toLowerCase();
87
+ var closeRe = new RegExp("</\\s*" + lower + "\\s*>", "i"); // allow:dynamic-regex — `lower` is a tag name from the framework's static SC registry, not operator input; `\\s*` and tag name are RegExp-safe (no special chars)
88
+ closeRe.lastIndex = tagOpenEnd;
89
+ var m = closeRe.exec(html);
90
+ if (!m) return "";
91
+ var raw = html.slice(tagOpenEnd, m.index);
92
+ return raw.replace(/<[^>]+>/g, "").replace(/&[a-z]+;|&#\d+;/gi, "").trim();
93
+ }
94
+
95
+ // ---- Per-element checks ----
96
+
97
+ function _checkImgAlt(html, attrs, tagName, offset, report, opts) {
98
+ if (tagName !== "img") return;
99
+ if (opts.ignore.indexOf("1.1.1") !== -1) return;
100
+ if ("alt" in attrs) return;
101
+ var pos = _lineColAt(html, offset);
102
+ _addFinding(report, {
103
+ sc: "1.1.1",
104
+ level: "A",
105
+ severity: "error",
106
+ element: "img",
107
+ line: pos.line,
108
+ column: pos.column,
109
+ message: "img element missing alt attribute (use alt=\"\" for purely decorative images)",
110
+ remediation: "Add alt=\"<descriptive text>\" or alt=\"\" if purely decorative",
111
+ });
112
+ }
113
+
114
+ function _checkInputLabel(html, attrs, tagName, offset, report, opts, ctx) {
115
+ if (tagName !== "input") return;
116
+ if (opts.ignore.indexOf("3.3.2") !== -1) return;
117
+ var inputType = (attrs.type || "text").toLowerCase();
118
+ if (["hidden", "submit", "button", "image", "reset"].indexOf(inputType) !== -1) return;
119
+ var hasLabel = "aria-label" in attrs ||
120
+ "aria-labelledby" in attrs ||
121
+ "title" in attrs ||
122
+ (attrs.id && ctx.labelledIds[attrs.id]);
123
+ if (!hasLabel) {
124
+ var pos = _lineColAt(html, offset);
125
+ _addFinding(report, {
126
+ sc: "3.3.2",
127
+ level: "A",
128
+ severity: "error",
129
+ element: "input",
130
+ line: pos.line,
131
+ column: pos.column,
132
+ message: "Form input has no associated label (no aria-label, aria-labelledby, title, or matching <label for=...>)",
133
+ remediation: "Add <label for=\"<id>\">...</label> or aria-label=\"<text>\" attribute",
134
+ });
135
+ }
136
+ if (!("name" in attrs) && opts.ignore.indexOf("4.1.2") === -1) {
137
+ var pos2 = _lineColAt(html, offset);
138
+ _addFinding(report, {
139
+ sc: "4.1.2",
140
+ level: "A",
141
+ severity: "warning",
142
+ element: "input",
143
+ line: pos2.line,
144
+ column: pos2.column,
145
+ message: "input element has no name attribute (required for form submission + assistive-tech identification)",
146
+ remediation: "Add name=\"<field-name>\" attribute",
147
+ });
148
+ }
149
+ }
150
+
151
+ function _checkAnchorScheduled(html, attrs, tagName, offset, report, opts) {
152
+ if (tagName !== "a") return null;
153
+ if (opts.ignore.indexOf("2.4.4") !== -1) return null;
154
+ if (!("href" in attrs)) return null;
155
+ var hasAccessibleName = "aria-label" in attrs ||
156
+ "aria-labelledby" in attrs ||
157
+ "title" in attrs;
158
+ return { offset: offset, attrs: attrs, hasAccessibleName: hasAccessibleName };
159
+ // void the unused vars so the linter is happy
160
+ }
161
+
162
+ function _checkButtonText(html, tagOpenEnd, attrs, offset, report, opts) {
163
+ if (opts.ignore.indexOf("4.1.2") !== -1) return;
164
+ var inner = _innerText(html, tagOpenEnd, "button");
165
+ if (inner.length > 0) return;
166
+ if ("aria-label" in attrs || "aria-labelledby" in attrs ||
167
+ "title" in attrs) return;
168
+ var pos = _lineColAt(html, offset);
169
+ _addFinding(report, {
170
+ sc: "4.1.2",
171
+ level: "A",
172
+ severity: "error",
173
+ element: "button",
174
+ line: pos.line,
175
+ column: pos.column,
176
+ message: "button has no visible text and no aria-label / title (assistive tech reads it as \"button\" with no purpose)",
177
+ remediation: "Add visible text content, aria-label=\"<purpose>\", or aria-labelledby=\"<idref>\"",
178
+ });
179
+ }
180
+
181
+ function _checkHeadingOrder(html, attrs, tagName, offset, report, opts, ctx) {
182
+ if (!/^h[1-6]$/.test(tagName)) return;
183
+ if (opts.ignore.indexOf("1.3.1") !== -1) return;
184
+ var level = parseInt(tagName.charAt(1), 10); // allow:raw-byte-literal — base-10 parse radix
185
+ if (ctx.headingLevels.length === 0) {
186
+ if (level !== 1) {
187
+ var pos = _lineColAt(html, offset);
188
+ _addFinding(report, {
189
+ sc: "1.3.1",
190
+ level: "A",
191
+ severity: "warning",
192
+ element: tagName,
193
+ line: pos.line,
194
+ column: pos.column,
195
+ message: "First heading on the page is " + tagName + " (expected h1; missing/late h1 hurts navigation for screen readers)",
196
+ remediation: "Promote the first heading to h1, or insert an h1 above it",
197
+ });
198
+ }
199
+ } else {
200
+ var lastLevel = ctx.headingLevels[ctx.headingLevels.length - 1];
201
+ if (level > lastLevel + 1) {
202
+ var pos2 = _lineColAt(html, offset);
203
+ _addFinding(report, {
204
+ sc: "1.3.1",
205
+ level: "A",
206
+ severity: "warning",
207
+ element: tagName,
208
+ line: pos2.line,
209
+ column: pos2.column,
210
+ message: "Heading skips levels (" + tagName + " follows h" + lastLevel +
211
+ "; intermediate level skipped)",
212
+ remediation: "Insert intermediate heading or demote " + tagName + " to h" + (lastLevel + 1),
213
+ });
214
+ }
215
+ }
216
+ var inner = _innerText(html, ctx.lastTagEndOffset, tagName);
217
+ if (inner.length === 0) {
218
+ var pos3 = _lineColAt(html, offset);
219
+ _addFinding(report, {
220
+ sc: "1.3.1",
221
+ level: "A",
222
+ severity: "error",
223
+ element: tagName,
224
+ line: pos3.line,
225
+ column: pos3.column,
226
+ message: "Empty heading element (" + tagName + " has no text content)",
227
+ remediation: "Add the heading text or remove the empty heading element",
228
+ });
229
+ }
230
+ ctx.headingLevels.push(level);
231
+ }
232
+
233
+ // ---- Page-level checks ----
234
+
235
+ function _checkHtmlLang(html, report, opts) {
236
+ if (opts.ignore.indexOf("3.1.1") !== -1) return;
237
+ var m = /<html\b([^>]*)>/i.exec(html);
238
+ if (!m) return;
239
+ var attrs = _parseAttrs(m[1]);
240
+ if (!attrs.lang || !attrs.lang.trim()) {
241
+ var pos = _lineColAt(html, m.index);
242
+ _addFinding(report, {
243
+ sc: "3.1.1",
244
+ level: "A",
245
+ severity: "error",
246
+ element: "html",
247
+ line: pos.line,
248
+ column: pos.column,
249
+ message: "html element missing lang attribute (assistive tech can't pick the right voice / pronunciation)",
250
+ remediation: "Add lang=\"<BCP47-tag>\" e.g. lang=\"en\" or lang=\"en-US\"",
251
+ });
252
+ }
253
+ }
254
+
255
+ function _checkPageTitle(html, report, opts) {
256
+ if (opts.ignore.indexOf("2.4.2") !== -1) return;
257
+ if (!/<head\b/i.test(html)) return;
258
+ var m = /<title\b[^>]*>([^]*?)<\/title>/i.exec(html);
259
+ if (!m) {
260
+ var pos = _lineColAt(html, html.search(/<head\b/i));
261
+ _addFinding(report, {
262
+ sc: "2.4.2",
263
+ level: "A",
264
+ severity: "error",
265
+ element: "title",
266
+ line: pos.line,
267
+ column: pos.column,
268
+ message: "Page has no <title> element",
269
+ remediation: "Add <title>Descriptive page title</title> inside <head>",
270
+ });
271
+ return;
272
+ }
273
+ var title = m[1].replace(/<[^>]+>/g, "").trim();
274
+ if (title.length === 0) {
275
+ var pos2 = _lineColAt(html, m.index);
276
+ _addFinding(report, {
277
+ sc: "2.4.2",
278
+ level: "A",
279
+ severity: "error",
280
+ element: "title",
281
+ line: pos2.line,
282
+ column: pos2.column,
283
+ message: "<title> element is empty",
284
+ remediation: "Add descriptive text inside <title>",
285
+ });
286
+ return;
287
+ }
288
+ if (title.length < 4 || /^untitled/i.test(title)) {
289
+ var pos3 = _lineColAt(html, m.index);
290
+ _addFinding(report, {
291
+ sc: "2.4.2",
292
+ level: "A",
293
+ severity: "warning",
294
+ element: "title",
295
+ line: pos3.line,
296
+ column: pos3.column,
297
+ message: "<title> is too short or generic (\"" + title + "\")",
298
+ remediation: "Use a descriptive page title that distinguishes the page from siblings",
299
+ });
300
+ }
301
+ }
302
+
303
+ function _checkSkipLink(html, report, opts) {
304
+ if (opts.ignore.indexOf("2.4.1") !== -1) return;
305
+ if (!/<body\b/i.test(html)) return;
306
+ if (/<a[^>]+href=["']#[a-zA-Z][^"']*["'][^>]*>\s*(skip|jump)\b/i.test(html)) return;
307
+ var bodyMatch = /<body\b[^>]*>/i.exec(html);
308
+ var pos = _lineColAt(html, bodyMatch ? bodyMatch.index : 0);
309
+ _addFinding(report, {
310
+ sc: "2.4.1",
311
+ level: "A",
312
+ severity: "info",
313
+ element: "body",
314
+ line: pos.line,
315
+ column: pos.column,
316
+ message: "No \"skip to content\" link detected at the start of the page",
317
+ remediation: "Add <a href=\"#main\" class=\"sr-only\">Skip to content</a> as the first focusable element in <body>",
318
+ });
319
+ }
320
+
321
+ function _checkAnchors(html, scheduled, report) {
322
+ for (var i = 0; i < scheduled.length; i++) {
323
+ var s = scheduled[i];
324
+ var tagOpen = html.indexOf(">", s.offset);
325
+ if (tagOpen === -1) continue;
326
+ var inner = _innerText(html, tagOpen + 1, "a");
327
+ var visibleText = inner.length > 0;
328
+ if (visibleText || s.hasAccessibleName) continue;
329
+ var pos = _lineColAt(html, s.offset);
330
+ _addFinding(report, {
331
+ sc: "2.4.4",
332
+ level: "A",
333
+ severity: "error",
334
+ element: "a",
335
+ line: pos.line,
336
+ column: pos.column,
337
+ message: "Link has no accessible name (no visible text, aria-label, aria-labelledby, or title)",
338
+ remediation: "Add link text, aria-label=\"<purpose>\", or aria-labelledby=\"<idref>\"",
339
+ });
340
+ }
341
+ }
342
+
343
+ // ---- Main audit ----
344
+
345
+ function audit(html, opts) {
346
+ opts = opts || {};
347
+ validateOpts(opts, [
348
+ "level", "ignore", "checkAll", "scopeUrl",
349
+ "skipAria", "allowedRoles", "skipTables",
350
+ "skipForms", "allowedAutocomplete",
351
+ ], "guardHtml.wcag.audit");
352
+ if (typeof html !== "string") {
353
+ throw new GuardHtmlWcagError("guard-html-wcag/bad-input",
354
+ "audit: html must be a string");
355
+ }
356
+ var level = opts.level || "AA";
357
+ if (VALID_LEVELS.indexOf(level) === -1) {
358
+ throw new GuardHtmlWcagError("guard-html-wcag/bad-level",
359
+ "audit: level must be one of " + VALID_LEVELS.join(", "));
360
+ }
361
+ var ignore = Array.isArray(opts.ignore) ? opts.ignore : [];
362
+ for (var i = 0; i < ignore.length; i++) {
363
+ if (typeof ignore[i] !== "string") {
364
+ throw new GuardHtmlWcagError("guard-html-wcag/bad-ignore",
365
+ "audit: ignore[" + i + "] must be a string SC like \"1.4.3\"");
366
+ }
367
+ }
368
+ var report = _newReport(opts.scopeUrl);
369
+ var scanOpts = { level: level, ignore: ignore };
370
+
371
+ // Page-level checks
372
+ _checkHtmlLang(html, report, scanOpts);
373
+ _checkPageTitle(html, report, scanOpts);
374
+ _checkSkipLink(html, report, scanOpts);
375
+
376
+ // Per-element walker
377
+ var ctx = {
378
+ headingLevels: [],
379
+ labelledIds: Object.create(null),
380
+ lastTagEndOffset: 0,
381
+ scheduledAnchors: [],
382
+ };
383
+ var labelRe = /<label\b[^>]*\bfor\s*=\s*["']([^"']+)["'][^>]*>/gi;
384
+ var lm;
385
+ while ((lm = labelRe.exec(html))) {
386
+ ctx.labelledIds[lm[1]] = true;
387
+ }
388
+
389
+ function _scLevelEligible(sc) {
390
+ var sce = SC_REGISTRY[sc];
391
+ if (!sce) return true;
392
+ return _meetsLevel(sce.level, level);
393
+ }
394
+
395
+ _TAG_RE.lastIndex = 0;
396
+ var m;
397
+ while ((m = _TAG_RE.exec(html))) {
398
+ // Skip closing tags (m[0] starts with "</"); only run per-element
399
+ // checks on opening tags so we don't double-fire on each pair.
400
+ if (m[0].charAt(1) === "/") continue;
401
+ var tagName = m[1].toLowerCase();
402
+ var attrs = _parseAttrs(m[2]);
403
+ var offset = m.index;
404
+ var endOffset = m.index + m[0].length;
405
+ ctx.lastTagEndOffset = endOffset;
406
+
407
+ if (_scLevelEligible("1.1.1")) _checkImgAlt(html, attrs, tagName, offset, report, scanOpts);
408
+ if (_scLevelEligible("3.3.2") || _scLevelEligible("4.1.2")) _checkInputLabel(html, attrs, tagName, offset, report, scanOpts, ctx);
409
+ if (_scLevelEligible("2.4.4")) {
410
+ var sched = _checkAnchorScheduled(html, attrs, tagName, offset, report, scanOpts);
411
+ if (sched) ctx.scheduledAnchors.push(sched);
412
+ }
413
+ if (_scLevelEligible("4.1.2") && tagName === "button") {
414
+ _checkButtonText(html, endOffset, attrs, offset, report, scanOpts);
415
+ }
416
+ if (_scLevelEligible("1.3.1")) _checkHeadingOrder(html, attrs, tagName, offset, report, scanOpts, ctx);
417
+ }
418
+
419
+ _checkAnchors(html, ctx.scheduledAnchors, report);
420
+
421
+ // ARIA validation pass — fold its findings into the report.
422
+ if (opts.skipAria !== true) {
423
+ var ariaFindings = aria.audit(html, {
424
+ allowedRoles: opts.allowedRoles,
425
+ scopeUrl: opts.scopeUrl,
426
+ });
427
+ for (var ai = 0; ai < ariaFindings.length; ai++) {
428
+ _addFinding(report, ariaFindings[ai]);
429
+ }
430
+ }
431
+ // Table-semantics pass — caption / scope / layout-table heuristics.
432
+ if (opts.skipTables !== true) {
433
+ var tableFindings = tables.audit(html, { scopeUrl: opts.scopeUrl });
434
+ for (var ti = 0; ti < tableFindings.length; ti++) {
435
+ _addFinding(report, tableFindings[ti]);
436
+ }
437
+ }
438
+ // Forms pass — fieldset/legend / autocomplete / textarea labelling.
439
+ if (opts.skipForms !== true) {
440
+ var formFindings = forms.audit(html, {
441
+ allowedAutocomplete: opts.allowedAutocomplete,
442
+ scopeUrl: opts.scopeUrl,
443
+ });
444
+ for (var fi = 0; fi < formFindings.length; fi++) {
445
+ _addFinding(report, formFindings[fi]);
446
+ }
447
+ }
448
+
449
+ // Heuristic score: 1 - weighted-violations / heuristic-max
450
+ var weighted = report.summary.error * 3 + report.summary.warning * 1.5 + // allow:raw-byte-literal — severity weights for heuristic score
451
+ report.summary.info * 0.5; // allow:raw-byte-literal — severity weights for heuristic score
452
+ var maxFor = Math.max(50, weighted * 2); // allow:raw-byte-literal — heuristic-score floor
453
+ report.score = Math.max(0, 1 - weighted / maxFor);
454
+
455
+ try { observability().safeEvent("guard-html.wcag.audited", 1, {
456
+ level: level, errors: String(report.summary.error),
457
+ }); } catch (_e) { /* drop-silent */ }
458
+
459
+ return report;
460
+ }
461
+
462
+ module.exports = {
463
+ audit: audit,
464
+ aria: aria,
465
+ tables: tables,
466
+ forms: forms,
467
+ SC_REGISTRY: SC_REGISTRY,
468
+ VALID_LEVELS: VALID_LEVELS,
469
+ GuardHtmlWcagError: GuardHtmlWcagError,
470
+ };
package/lib/guard-html.js CHANGED
@@ -118,6 +118,7 @@
118
118
  */
119
119
 
120
120
  var codepointClass = require("./codepoint-class");
121
+ var guardHtmlWcag = require("./guard-html-wcag");
121
122
  var lazyRequire = require("./lazy-require");
122
123
  var gateContract = require("./gate-contract");
123
124
  var C = require("./constants");
@@ -972,5 +973,8 @@ module.exports = {
972
973
  DANGEROUS_SCHEMES: DANGEROUS_SCHEMES,
973
974
  CLOBBER_GLOBALS: CLOBBER_GLOBALS,
974
975
  CLOBBER_PRONE_TAGS: CLOBBER_PRONE_TAGS,
976
+ // WCAG 2.2 audit-only mode (b.guardHtml.wcag.audit) — accessibility
977
+ // scanner that emits violations without modifying HTML.
978
+ wcag: guardHtmlWcag,
975
979
  GuardHtmlError: GuardHtmlError,
976
980
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.7.105",
3
+ "version": "0.7.107",
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:503d52be-ebde-43d9-99cf-866e8585557a",
5
+ "serialNumber": "urn:uuid:31469bcd-ab0f-4150-bfe2-c7d79a72a1c7",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-06T11:23:52.585Z",
8
+ "timestamp": "2026-05-06T12:31:16.570Z",
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.7.105",
22
+ "bom-ref": "@blamejs/core@0.7.107",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.7.105",
25
+ "version": "0.7.107",
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.7.105",
29
+ "purl": "pkg:npm/%40blamejs/core@0.7.107",
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.7.105",
57
+ "ref": "@blamejs/core@0.7.107",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]