@blamejs/exceptd-skills 0.19.22 → 0.19.24

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.
@@ -65,14 +65,45 @@ function assertPinShape(kind, value) {
65
65
  return value;
66
66
  }
67
67
 
68
+ /**
69
+ * The only hosts this checker may ever contact. `assertPinShape` already bounds
70
+ * the catalog-sourced version to a digit-and-dot shape before it reaches a URL,
71
+ * but that guard sits a long way from the request — a later edit could add a
72
+ * caller that skips it, and static analysis reasonably flags file data reaching
73
+ * an outbound request when the barrier is that distant. Re-check the resolved
74
+ * origin at the sink so the property is enforced where it matters and is
75
+ * verifiable by reading ten lines rather than tracing the whole module.
76
+ */
77
+ const ALLOWED_ORIGINS = new Set(["https://raw.githubusercontent.com"]);
78
+
79
+ function assertAllowedUrl(rawUrl) {
80
+ let parsed;
81
+ try {
82
+ parsed = new URL(rawUrl);
83
+ } catch (_e) {
84
+ throw new Error(`refusing to fetch a malformed URL: ${JSON.stringify(rawUrl)}`);
85
+ }
86
+ if (!ALLOWED_ORIGINS.has(parsed.origin)) {
87
+ throw new Error(
88
+ `refusing to fetch ${parsed.origin} — this checker may only contact ` +
89
+ `${[...ALLOWED_ORIGINS].join(", ")}`
90
+ );
91
+ }
92
+ // Reject anything that could smuggle credentials or redirect the path.
93
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
94
+ throw new Error(`refusing to fetch a URL carrying credentials or a query/fragment: ${parsed.origin}${parsed.pathname}`);
95
+ }
96
+ return parsed.toString();
97
+ }
98
+
68
99
  async function fetchJson(url) {
69
- const res = await fetch(url, { headers: { "user-agent": UA } });
100
+ const res = await fetch(assertAllowedUrl(url), { headers: { "user-agent": UA }, redirect: "error" });
70
101
  if (!res.ok) throw new Error(`${res.status} ${url}`);
71
102
  return JSON.parse(await res.text());
72
103
  }
73
104
 
74
105
  async function fetchText(url) {
75
- const res = await fetch(url, { headers: { "user-agent": UA } });
106
+ const res = await fetch(assertAllowedUrl(url), { headers: { "user-agent": UA }, redirect: "error" });
76
107
  if (!res.ok) throw new Error(`${res.status} ${url}`);
77
108
  return res.text();
78
109
  }
@@ -199,4 +230,4 @@ if (require.main === module) main().catch((err) => {
199
230
  process.exitCode = 2;
200
231
  });
201
232
 
202
- module.exports = { VERSION_SHAPES, assertPinShape };
233
+ module.exports = { VERSION_SHAPES, assertPinShape, ALLOWED_ORIGINS, assertAllowedUrl };
@@ -224,12 +224,30 @@ function _unresolvedThreads(prNum) {
224
224
  // scanning unavailable, API error, or the GET-param form not supported) so a
225
225
  // transient failure fails OPEN rather than blocking a release — the pre-flight
226
226
  // checklist is the human backstop.
227
- function _openCodeqlAlerts(prNum) {
228
- var rv = _capture("gh", ["api", "repos/:owner/:repo/code-scanning/alerts",
229
- "-X", "GET",
230
- "-f", "ref=refs/pull/" + prNum + "/merge",
231
- "-f", "state=open",
232
- "-f", "tool_name=CodeQL"]);
227
+ // Two refs, and BOTH are load-bearing — each alone has a blind spot that has
228
+ // already bitten:
229
+ //
230
+ // refs/pull/<N>/merge answers "did this PR introduce an alert". Scoping to
231
+ // it ALONE let two medium alerts sitting on refs/heads/main since
232
+ // 2026-08-08 return zero, riding through eight consecutive releases while
233
+ // the phase printed "zero open CodeQL alerts" — GitHub only annotates a PR
234
+ // with alerts that PR introduces.
235
+ // (no ref) answers against the DEFAULT BRANCH, catching exactly
236
+ // those pre-existing findings — but on its own it misses an alert this PR
237
+ // introduces that is not on main yet, which is the case the PR-scoped query
238
+ // was there for.
239
+ //
240
+ // So the gate blocks on the union. Replacing one query with the other just
241
+ // trades which blind spot you have.
242
+ //
243
+ // Filtered to tool_name=CodeQL deliberately: OpenSSF Scorecard writes to the
244
+ // same code-scanning surface, and its accepted-out-of-scope policy alerts would
245
+ // otherwise make this list permanently non-empty and therefore useless.
246
+ function _codeqlAlertsForRef(ref) {
247
+ var args = ["api", "repos/:owner/:repo/code-scanning/alerts", "-X", "GET",
248
+ "-f", "state=open", "-f", "tool_name=CodeQL", "-f", "per_page=100"];
249
+ if (ref) args.push("-f", "ref=" + ref);
250
+ var rv = _capture("gh", args);
233
251
  if (rv.status !== 0) return null;
234
252
  try {
235
253
  var arr = JSON.parse(rv.stdout || "[]");
@@ -237,6 +255,23 @@ function _openCodeqlAlerts(prNum) {
237
255
  } catch (_e) { return null; }
238
256
  }
239
257
 
258
+ function _openCodeqlAlerts(prNum) {
259
+ var onDefault = _codeqlAlertsForRef(null);
260
+ var onPr = _codeqlAlertsForRef("refs/pull/" + prNum + "/merge");
261
+ // Either query failing means the question is unanswered. Never let a failed
262
+ // lookup read as "no alerts" — that is the same absent-input-passes shape
263
+ // this whole gate exists to close.
264
+ if (onDefault === null || onPr === null) return null;
265
+
266
+ var byNumber = new Map();
267
+ onDefault.forEach(function (a) { byNumber.set(a.number, { alert: a, scope: "default branch" }); });
268
+ onPr.forEach(function (a) {
269
+ if (byNumber.has(a.number)) byNumber.get(a.number).scope = "default branch + this PR";
270
+ else byNumber.set(a.number, { alert: a, scope: "introduced by this PR" });
271
+ });
272
+ return [...byNumber.values()];
273
+ }
274
+
240
275
  // ---- Subcommands ---------------------------------------------------------
241
276
 
242
277
  // The derived-artifact regeneration, in the one order that is correct. Shared
@@ -499,12 +534,16 @@ function cmdWatch() {
499
534
  "verify manually per the pre-flight checklist before merge.");
500
535
  } else if (codeqlAlerts.length > 0) {
501
536
  console.log("\nopen CodeQL alerts (" + codeqlAlerts.length + "):");
502
- codeqlAlerts.forEach(function (a) {
537
+ codeqlAlerts.forEach(function (e) {
538
+ var a = e.alert;
503
539
  var loc = (a.most_recent_instance && a.most_recent_instance.location) || {};
504
540
  var sev = (a.rule && (a.rule.security_severity_level || a.rule.severity)) || "?";
505
541
  console.log(" ⚠ " + (a.rule && a.rule.id) + " [" + sev + "] " +
506
- (loc.path ? loc.path + ":" + loc.start_line : "") + " " + (a.html_url || ""));
542
+ (loc.path ? loc.path + ":" + loc.start_line : "") + " " +
543
+ "(" + e.scope + ") " + (a.html_url || ""));
507
544
  });
545
+ console.log("\nThe union of the default branch and this PR's merge ref: an alert already on main " +
546
+ "blocks the release as surely as one this PR introduces.");
508
547
  console.log("\nFix real findings in code (push, let CodeQL re-scan) OR dismiss by-design FPs with a " +
509
548
  "written reason, then re-run: node scripts/release.js watch");
510
549
  process.exit(3); // allow:process-exit-after-stdout-write — maintainer-run release orchestrator; the guidance line above is human-read on a TTY, not a piped result channel