@wrongstack/plugins 1.0.4 → 1.0.6
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/dist/accessibility-auditor.js +23 -5
- package/dist/context-pins.js +2 -2
- package/dist/gitignore-guard.js +1 -1
- package/dist/index.js +42 -10
- package/dist/knowledge-graph.js +2 -2
- package/dist/notify-hub.js +14 -0
- package/dist/plugin-audit-catalog.js +7 -1
- package/package.json +5 -5
|
@@ -68,10 +68,21 @@ var ATTR_TITLE = /\btitle\s*=/i;
|
|
|
68
68
|
var ATTR_PLACEHOLDER = /\bplaceholder\s*=/i;
|
|
69
69
|
var ATTR_VALUE = /\bvalue\s*=/i;
|
|
70
70
|
var ATTR_ROLE_DECORATIVE = /\brole\s*=\s*["'](?:presentation|none)["']/i;
|
|
71
|
+
var LABEL_SPAN = /<label\b[^>]*>[\s\S]*?<\/label>/gi;
|
|
72
|
+
var FIELDSET_SPAN = /<fieldset\b[^>]*>[\s\S]*?<\/fieldset>/gi;
|
|
71
73
|
var SINGLE_FILE_LABEL_NOTE = "Single-file heuristic: a label declared in a sibling component file is not visible to this scan.";
|
|
72
74
|
function escapeRegExp(value) {
|
|
73
75
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
74
76
|
}
|
|
77
|
+
function collectSpans(content, re) {
|
|
78
|
+
return [...content.matchAll(re)].map((m) => ({
|
|
79
|
+
start: m.index ?? 0,
|
|
80
|
+
end: (m.index ?? 0) + m[0].length
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
function isInsideSpan(spans, offset) {
|
|
84
|
+
return spans.some((s) => offset > s.start && offset < s.end);
|
|
85
|
+
}
|
|
75
86
|
function hasMeaningfulAlt(tag) {
|
|
76
87
|
const m = ATTR_ALT.exec(tag);
|
|
77
88
|
ATTR_ALT.lastIndex = 0;
|
|
@@ -81,7 +92,7 @@ function hasMeaningfulAlt(tag) {
|
|
|
81
92
|
if (alt.length > 0) return true;
|
|
82
93
|
return ATTR_ROLE_DECORATIVE.test(tag);
|
|
83
94
|
}
|
|
84
|
-
function hasFieldsetLegendLabel(tag, content) {
|
|
95
|
+
function hasFieldsetLegendLabel(tag, content, fieldsetSpans, inputStart) {
|
|
85
96
|
const labelledBy = tag.match(/\baria-labelledby\s*=\s*["']([^"']+)["']/i);
|
|
86
97
|
if (labelledBy?.[1]) {
|
|
87
98
|
for (const id of labelledBy[1].split(/\s+/).filter(Boolean)) {
|
|
@@ -92,8 +103,8 @@ function hasFieldsetLegendLabel(tag, content) {
|
|
|
92
103
|
const typeMatch = tag.match(/\btype\s*=\s*["']?([^"'\s>]*)["']?/i);
|
|
93
104
|
const type = typeMatch ? typeMatch[1].toLowerCase() : "text";
|
|
94
105
|
if (type === "checkbox" || type === "radio") {
|
|
95
|
-
return
|
|
96
|
-
content
|
|
106
|
+
return fieldsetSpans.some(
|
|
107
|
+
(s) => inputStart > s.start && inputStart < s.end && /<legend\b[\s\S]*?<\/legend>/i.test(content.slice(s.start, inputStart))
|
|
97
108
|
);
|
|
98
109
|
}
|
|
99
110
|
return false;
|
|
@@ -108,6 +119,12 @@ async function auditFile(filePath, projectRoot) {
|
|
|
108
119
|
const findings = [];
|
|
109
120
|
const lines = content.split(/\r?\n/);
|
|
110
121
|
const idsByValue = /* @__PURE__ */ new Map();
|
|
122
|
+
const lineStarts = [0];
|
|
123
|
+
for (let nl = content.indexOf("\n"); nl !== -1; nl = content.indexOf("\n", nl + 1)) {
|
|
124
|
+
lineStarts.push(nl + 1);
|
|
125
|
+
}
|
|
126
|
+
const labelSpans = collectSpans(content, LABEL_SPAN);
|
|
127
|
+
const fieldsetSpans = collectSpans(content, FIELDSET_SPAN);
|
|
111
128
|
function add(line, rule, severity, message, note) {
|
|
112
129
|
findings.push({
|
|
113
130
|
file: relative(projectRoot, filePath),
|
|
@@ -155,8 +172,9 @@ async function auditFile(filePath, projectRoot) {
|
|
|
155
172
|
);
|
|
156
173
|
hasLabelFor = labelForRe.test(content);
|
|
157
174
|
}
|
|
158
|
-
const
|
|
159
|
-
const
|
|
175
|
+
const inputStart = (lineStarts[i] ?? 0) + (inputMatch.index ?? 0);
|
|
176
|
+
const wrappedInLabel = isInsideSpan(labelSpans, inputStart);
|
|
177
|
+
const hasLegend = hasFieldsetLegendLabel(tag, content, fieldsetSpans, inputStart);
|
|
160
178
|
const hasPrimaryLabel = hasAriaLabel || hasTitle || hasLabelFor || wrappedInLabel || hasLegend;
|
|
161
179
|
if (!hasPrimaryLabel && hasDescribedBy) {
|
|
162
180
|
add(
|
package/dist/context-pins.js
CHANGED
|
@@ -137,7 +137,7 @@ var plugin = {
|
|
|
137
137
|
},
|
|
138
138
|
required: ["text"]
|
|
139
139
|
},
|
|
140
|
-
permission: "
|
|
140
|
+
permission: "confirm",
|
|
141
141
|
category: "Memory",
|
|
142
142
|
mutating: true,
|
|
143
143
|
async execute(input) {
|
|
@@ -175,7 +175,7 @@ var plugin = {
|
|
|
175
175
|
label: { type: "string", description: "Alternative label to remove." }
|
|
176
176
|
}
|
|
177
177
|
},
|
|
178
|
-
permission: "
|
|
178
|
+
permission: "confirm",
|
|
179
179
|
category: "Memory",
|
|
180
180
|
mutating: true,
|
|
181
181
|
async execute(input) {
|
package/dist/gitignore-guard.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -68,10 +68,21 @@ var ATTR_TITLE = /\btitle\s*=/i;
|
|
|
68
68
|
var ATTR_PLACEHOLDER = /\bplaceholder\s*=/i;
|
|
69
69
|
var ATTR_VALUE = /\bvalue\s*=/i;
|
|
70
70
|
var ATTR_ROLE_DECORATIVE = /\brole\s*=\s*["'](?:presentation|none)["']/i;
|
|
71
|
+
var LABEL_SPAN = /<label\b[^>]*>[\s\S]*?<\/label>/gi;
|
|
72
|
+
var FIELDSET_SPAN = /<fieldset\b[^>]*>[\s\S]*?<\/fieldset>/gi;
|
|
71
73
|
var SINGLE_FILE_LABEL_NOTE = "Single-file heuristic: a label declared in a sibling component file is not visible to this scan.";
|
|
72
74
|
function escapeRegExp(value) {
|
|
73
75
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
74
76
|
}
|
|
77
|
+
function collectSpans(content, re) {
|
|
78
|
+
return [...content.matchAll(re)].map((m) => ({
|
|
79
|
+
start: m.index ?? 0,
|
|
80
|
+
end: (m.index ?? 0) + m[0].length
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
function isInsideSpan(spans, offset) {
|
|
84
|
+
return spans.some((s) => offset > s.start && offset < s.end);
|
|
85
|
+
}
|
|
75
86
|
function hasMeaningfulAlt(tag) {
|
|
76
87
|
const m = ATTR_ALT.exec(tag);
|
|
77
88
|
ATTR_ALT.lastIndex = 0;
|
|
@@ -81,7 +92,7 @@ function hasMeaningfulAlt(tag) {
|
|
|
81
92
|
if (alt.length > 0) return true;
|
|
82
93
|
return ATTR_ROLE_DECORATIVE.test(tag);
|
|
83
94
|
}
|
|
84
|
-
function hasFieldsetLegendLabel(tag, content) {
|
|
95
|
+
function hasFieldsetLegendLabel(tag, content, fieldsetSpans, inputStart) {
|
|
85
96
|
const labelledBy = tag.match(/\baria-labelledby\s*=\s*["']([^"']+)["']/i);
|
|
86
97
|
if (labelledBy?.[1]) {
|
|
87
98
|
for (const id of labelledBy[1].split(/\s+/).filter(Boolean)) {
|
|
@@ -92,8 +103,8 @@ function hasFieldsetLegendLabel(tag, content) {
|
|
|
92
103
|
const typeMatch = tag.match(/\btype\s*=\s*["']?([^"'\s>]*)["']?/i);
|
|
93
104
|
const type = typeMatch ? typeMatch[1].toLowerCase() : "text";
|
|
94
105
|
if (type === "checkbox" || type === "radio") {
|
|
95
|
-
return
|
|
96
|
-
content
|
|
106
|
+
return fieldsetSpans.some(
|
|
107
|
+
(s) => inputStart > s.start && inputStart < s.end && /<legend\b[\s\S]*?<\/legend>/i.test(content.slice(s.start, inputStart))
|
|
97
108
|
);
|
|
98
109
|
}
|
|
99
110
|
return false;
|
|
@@ -108,6 +119,12 @@ async function auditFile(filePath, projectRoot) {
|
|
|
108
119
|
const findings = [];
|
|
109
120
|
const lines = content.split(/\r?\n/);
|
|
110
121
|
const idsByValue = /* @__PURE__ */ new Map();
|
|
122
|
+
const lineStarts = [0];
|
|
123
|
+
for (let nl = content.indexOf("\n"); nl !== -1; nl = content.indexOf("\n", nl + 1)) {
|
|
124
|
+
lineStarts.push(nl + 1);
|
|
125
|
+
}
|
|
126
|
+
const labelSpans = collectSpans(content, LABEL_SPAN);
|
|
127
|
+
const fieldsetSpans = collectSpans(content, FIELDSET_SPAN);
|
|
111
128
|
function add(line, rule, severity, message, note) {
|
|
112
129
|
findings.push({
|
|
113
130
|
file: relative(projectRoot, filePath),
|
|
@@ -155,8 +172,9 @@ async function auditFile(filePath, projectRoot) {
|
|
|
155
172
|
);
|
|
156
173
|
hasLabelFor = labelForRe.test(content);
|
|
157
174
|
}
|
|
158
|
-
const
|
|
159
|
-
const
|
|
175
|
+
const inputStart = (lineStarts[i] ?? 0) + (inputMatch.index ?? 0);
|
|
176
|
+
const wrappedInLabel = isInsideSpan(labelSpans, inputStart);
|
|
177
|
+
const hasLegend = hasFieldsetLegendLabel(tag, content, fieldsetSpans, inputStart);
|
|
160
178
|
const hasPrimaryLabel = hasAriaLabel || hasTitle || hasLabelFor || wrappedInLabel || hasLegend;
|
|
161
179
|
if (!hasPrimaryLabel && hasDescribedBy) {
|
|
162
180
|
add(
|
|
@@ -4475,7 +4493,7 @@ var plugin13 = {
|
|
|
4475
4493
|
},
|
|
4476
4494
|
required: ["text"]
|
|
4477
4495
|
},
|
|
4478
|
-
permission: "
|
|
4496
|
+
permission: "confirm",
|
|
4479
4497
|
category: "Memory",
|
|
4480
4498
|
mutating: true,
|
|
4481
4499
|
async execute(input) {
|
|
@@ -4513,7 +4531,7 @@ var plugin13 = {
|
|
|
4513
4531
|
label: { type: "string", description: "Alternative label to remove." }
|
|
4514
4532
|
}
|
|
4515
4533
|
},
|
|
4516
|
-
permission: "
|
|
4534
|
+
permission: "confirm",
|
|
4517
4535
|
category: "Memory",
|
|
4518
4536
|
mutating: true,
|
|
4519
4537
|
async execute(input) {
|
|
@@ -9861,7 +9879,7 @@ var plugin27 = {
|
|
|
9861
9879
|
},
|
|
9862
9880
|
required: ["path"]
|
|
9863
9881
|
},
|
|
9864
|
-
permission: "
|
|
9882
|
+
permission: "confirm",
|
|
9865
9883
|
category: "Code Quality",
|
|
9866
9884
|
mutating: true,
|
|
9867
9885
|
async execute(input) {
|
|
@@ -11049,7 +11067,7 @@ var plugin31 = {
|
|
|
11049
11067
|
},
|
|
11050
11068
|
required: ["subject", "relation", "object"]
|
|
11051
11069
|
},
|
|
11052
|
-
permission: "
|
|
11070
|
+
permission: "confirm",
|
|
11053
11071
|
category: "Memory",
|
|
11054
11072
|
mutating: true,
|
|
11055
11073
|
async execute(input) {
|
|
@@ -11150,7 +11168,7 @@ var plugin31 = {
|
|
|
11150
11168
|
},
|
|
11151
11169
|
required: ["id"]
|
|
11152
11170
|
},
|
|
11153
|
-
permission: "
|
|
11171
|
+
permission: "confirm",
|
|
11154
11172
|
category: "Memory",
|
|
11155
11173
|
mutating: true,
|
|
11156
11174
|
async execute(input) {
|
|
@@ -13626,6 +13644,20 @@ var WebhookNotificationChannel = class {
|
|
|
13626
13644
|
method: "POST",
|
|
13627
13645
|
headers: { "content-type": "application/json", ...this.#headers },
|
|
13628
13646
|
body,
|
|
13647
|
+
// The private-IP check on this URL runs ONCE, at plugin setup
|
|
13648
|
+
// (`index.ts`), against the host the operator configured. Delivery
|
|
13649
|
+
// resolves again and, with the default `redirect: 'follow'`, would
|
|
13650
|
+
// replay this POST — including `this.#headers`, which is where the
|
|
13651
|
+
// operator's `X-Webhook-Secret` / `X-Api-Key` lives — to wherever a
|
|
13652
|
+
// 307 pointed. A webhook host that is compromised, or simply
|
|
13653
|
+
// hostile, could aim that at loopback or the cloud metadata
|
|
13654
|
+
// endpoint and read the secret out of the request.
|
|
13655
|
+
//
|
|
13656
|
+
// Refusing the redirect outright is what the sibling HTTP hook
|
|
13657
|
+
// executor already does (`core/src/hooks/http-executor.ts:73`). A
|
|
13658
|
+
// webhook endpoint that answers with a redirect is misconfigured;
|
|
13659
|
+
// failing it is correct and is visible through the circuit breaker.
|
|
13660
|
+
redirect: "error",
|
|
13629
13661
|
signal: controller.signal
|
|
13630
13662
|
});
|
|
13631
13663
|
if (typeof res.arrayBuffer === "function") {
|
package/dist/knowledge-graph.js
CHANGED
|
@@ -167,7 +167,7 @@ var plugin = {
|
|
|
167
167
|
},
|
|
168
168
|
required: ["subject", "relation", "object"]
|
|
169
169
|
},
|
|
170
|
-
permission: "
|
|
170
|
+
permission: "confirm",
|
|
171
171
|
category: "Memory",
|
|
172
172
|
mutating: true,
|
|
173
173
|
async execute(input) {
|
|
@@ -268,7 +268,7 @@ var plugin = {
|
|
|
268
268
|
},
|
|
269
269
|
required: ["id"]
|
|
270
270
|
},
|
|
271
|
-
permission: "
|
|
271
|
+
permission: "confirm",
|
|
272
272
|
category: "Memory",
|
|
273
273
|
mutating: true,
|
|
274
274
|
async execute(input) {
|
package/dist/notify-hub.js
CHANGED
|
@@ -84,6 +84,20 @@ var WebhookNotificationChannel = class {
|
|
|
84
84
|
method: "POST",
|
|
85
85
|
headers: { "content-type": "application/json", ...this.#headers },
|
|
86
86
|
body,
|
|
87
|
+
// The private-IP check on this URL runs ONCE, at plugin setup
|
|
88
|
+
// (`index.ts`), against the host the operator configured. Delivery
|
|
89
|
+
// resolves again and, with the default `redirect: 'follow'`, would
|
|
90
|
+
// replay this POST — including `this.#headers`, which is where the
|
|
91
|
+
// operator's `X-Webhook-Secret` / `X-Api-Key` lives — to wherever a
|
|
92
|
+
// 307 pointed. A webhook host that is compromised, or simply
|
|
93
|
+
// hostile, could aim that at loopback or the cloud metadata
|
|
94
|
+
// endpoint and read the secret out of the request.
|
|
95
|
+
//
|
|
96
|
+
// Refusing the redirect outright is what the sibling HTTP hook
|
|
97
|
+
// executor already does (`core/src/hooks/http-executor.ts:73`). A
|
|
98
|
+
// webhook endpoint that answers with a redirect is misconfigured;
|
|
99
|
+
// failing it is correct and is visible through the circuit breaker.
|
|
100
|
+
redirect: "error",
|
|
87
101
|
signal: controller.signal
|
|
88
102
|
});
|
|
89
103
|
if (typeof res.arrayBuffer === "function") {
|
|
@@ -514,7 +514,13 @@ var HOST_PLUGIN_AUDIT_ENTRIES = [
|
|
|
514
514
|
summary: "Language Server Protocol tools and slash commands.",
|
|
515
515
|
// Active by default: with `autoStart: 'lazy'` nothing is spawned until a
|
|
516
516
|
// file of a matching language is touched, and auto-discovery only adopts
|
|
517
|
-
// servers
|
|
517
|
+
// servers from the user's own environment — never from the opened
|
|
518
|
+
// repository. That second clause used to read "already installed on the
|
|
519
|
+
// machine", which was wrong: the resolver preferred
|
|
520
|
+
// `<repo>/node_modules/.bin` over PATH on an existence check alone, so a
|
|
521
|
+
// cloned repo could supply the executable and have it spawned unprompted
|
|
522
|
+
// (WS-SEC-01). Provenance is now decided by where the binary lives; see
|
|
523
|
+
// `plug-lsp/src/utils/command-resolver.ts`. Disable the plugin with
|
|
518
524
|
// `{ name: 'lsp', enabled: false }` in config.plugins.
|
|
519
525
|
defaultState: "active",
|
|
520
526
|
canDisable: true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/plugins",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Official WrongStack collection of focused plugins for code quality, security, observability, planning, and agent coordination",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ECOSTACK TECHNOLOGY OÜ",
|
|
@@ -303,10 +303,10 @@
|
|
|
303
303
|
"vitest": "^4.1.11"
|
|
304
304
|
},
|
|
305
305
|
"dependencies": {
|
|
306
|
-
"@wrongstack/core": "1.0.
|
|
307
|
-
"@wrongstack/primitives": "1.0.
|
|
308
|
-
"@wrongstack/plugin-sdk": "1.0.
|
|
309
|
-
"@wrongstack/tools": "1.0.
|
|
306
|
+
"@wrongstack/core": "1.0.6",
|
|
307
|
+
"@wrongstack/primitives": "1.0.6",
|
|
308
|
+
"@wrongstack/plugin-sdk": "1.0.6",
|
|
309
|
+
"@wrongstack/tools": "1.0.6"
|
|
310
310
|
},
|
|
311
311
|
"scripts": {
|
|
312
312
|
"build": "node ../../scripts/build-package.mjs",
|