@blamejs/blamejs-shop 0.3.6 → 0.3.8

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/vendor/MANIFEST.json +2 -2
  4. package/lib/vendor/blamejs/.github/workflows/ci.yml +27 -0
  5. package/lib/vendor/blamejs/.github/workflows/codeql.yml +2 -2
  6. package/lib/vendor/blamejs/.github/workflows/release-container.yml +4 -4
  7. package/lib/vendor/blamejs/.github/workflows/scorecard.yml +1 -1
  8. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  9. package/lib/vendor/blamejs/README.md +1 -0
  10. package/lib/vendor/blamejs/SECURITY.md +2 -0
  11. package/lib/vendor/blamejs/api-snapshot.json +6 -2
  12. package/lib/vendor/blamejs/lib/cra-report.js +3 -3
  13. package/lib/vendor/blamejs/lib/middleware/age-gate.js +20 -7
  14. package/lib/vendor/blamejs/lib/middleware/bearer-auth.js +36 -35
  15. package/lib/vendor/blamejs/lib/middleware/bot-guard.js +17 -5
  16. package/lib/vendor/blamejs/lib/middleware/cors.js +28 -12
  17. package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +22 -14
  18. package/lib/vendor/blamejs/lib/middleware/daily-byte-quota.js +27 -13
  19. package/lib/vendor/blamejs/lib/middleware/deny-response.js +140 -0
  20. package/lib/vendor/blamejs/lib/middleware/dpop.js +32 -19
  21. package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +21 -12
  22. package/lib/vendor/blamejs/lib/middleware/host-allowlist.js +19 -8
  23. package/lib/vendor/blamejs/lib/middleware/index.js +3 -0
  24. package/lib/vendor/blamejs/lib/middleware/network-allowlist.js +24 -10
  25. package/lib/vendor/blamejs/lib/middleware/rate-limit.js +22 -5
  26. package/lib/vendor/blamejs/lib/middleware/require-aal.js +25 -10
  27. package/lib/vendor/blamejs/lib/middleware/require-auth.js +32 -16
  28. package/lib/vendor/blamejs/lib/middleware/require-bound-key.js +49 -18
  29. package/lib/vendor/blamejs/lib/middleware/require-content-type.js +19 -8
  30. package/lib/vendor/blamejs/lib/middleware/require-methods.js +17 -7
  31. package/lib/vendor/blamejs/lib/middleware/require-mtls.js +27 -14
  32. package/lib/vendor/blamejs/lib/network.js +4 -4
  33. package/lib/vendor/blamejs/package.json +1 -1
  34. package/lib/vendor/blamejs/release-notes/v0.14.6.json +60 -0
  35. package/lib/vendor/blamejs/scripts/check-actions-currency.js +256 -0
  36. package/lib/vendor/blamejs/scripts/release.js +11 -0
  37. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +138 -0
  38. package/lib/vendor/blamejs/test/layer-0-primitives/deny-response.test.js +228 -0
  39. package/package.json +1 -1
@@ -48,6 +48,7 @@
48
48
  var defineClass = require("../framework-error").defineClass;
49
49
  var lazyRequire = require("../lazy-require");
50
50
  var validateOpts = require("../validate-opts");
51
+ var denyResponse = require("./deny-response").denyResponse;
51
52
 
52
53
  var bCrypto = lazyRequire(function () { return require("../crypto"); });
53
54
  var audit = lazyRequire(function () { return require("../audit"); });
@@ -84,6 +85,8 @@ function _normalizeFingerprintEntry(entry) {
84
85
  * fingerprintAllowList: string[],
85
86
  * denyList: string[],
86
87
  * onAuthenticated: function(req, res, next): void,
88
+ * onDeny: function(req, res, info): void, // own the refusal (mirrors onAuthenticated); info = { status, reason, ...metadata }
89
+ * problemDetails: boolean, // default false — emit RFC 9457 application/problem+json instead of the default JSON envelope
87
90
  * auditAction: string,
88
91
  * errorMessage: string,
89
92
  * audit: object,
@@ -100,7 +103,7 @@ function create(opts) {
100
103
  opts = opts || {};
101
104
  validateOpts(opts, [
102
105
  "fingerprintAllowList", "denyList",
103
- "onAuthenticated", "audit",
106
+ "onAuthenticated", "onDeny", "problemDetails", "audit",
104
107
  "auditAction", "errorMessage",
105
108
  ], "middleware.requireMtls");
106
109
 
@@ -109,6 +112,8 @@ function create(opts) {
109
112
  var denyList = Array.isArray(opts.denyList)
110
113
  ? opts.denyList.map(_normalizeFingerprintEntry) : [];
111
114
  var onAuthenticated = typeof opts.onAuthenticated === "function" ? opts.onAuthenticated : null;
115
+ var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
116
+ var problemMode = opts.problemDetails === true;
112
117
  var auditOn = opts.audit !== false;
113
118
  var actionBase = typeof opts.auditAction === "string" && opts.auditAction.length > 0
114
119
  ? opts.auditAction : "mtls.required";
@@ -126,16 +131,24 @@ function create(opts) {
126
131
  } catch (_e) { /* drop-silent — audit is best-effort, never blocks the request */ }
127
132
  }
128
133
 
129
- function _refuse(res, reason, metadata) {
134
+ function _refuse(req, res, reason, metadata) {
130
135
  _emit("denied", Object.assign({ reason: reason }, metadata || {}));
131
- if (typeof res.writeHead === "function") {
132
- res.writeHead(401, {
133
- "Content-Type": "application/json; charset=utf-8",
136
+ denyResponse(req, res, {
137
+ onDeny: onDeny,
138
+ problem: problemMode,
139
+ status: 401,
140
+ info: Object.assign({ status: 401, reason: reason }, metadata || {}),
141
+ problemCode: "client-certificate-required",
142
+ problemTitle: "Unauthorized",
143
+ problemDetail: errorMessage,
144
+ problemExt: { reason: reason },
145
+ headers: {
134
146
  "WWW-Authenticate": "Mutual",
135
147
  "Cache-Control": "no-store",
136
- });
137
- res.end(JSON.stringify({ error: errorMessage, reason: reason }));
138
- }
148
+ },
149
+ contentType: "application/json; charset=utf-8",
150
+ body: JSON.stringify({ error: errorMessage, reason: reason }),
151
+ });
139
152
  }
140
153
 
141
154
  return function requireMtlsMiddleware(req, res, next) {
@@ -158,10 +171,10 @@ function create(opts) {
158
171
 
159
172
  if (!authorized) {
160
173
  var authzError = (sock && sock.authorizationError) || "no-peer-cert";
161
- return _refuse(res, "tls-unauthorized", { authorizationError: String(authzError) });
174
+ return _refuse(req, res, "tls-unauthorized", { authorizationError: String(authzError) });
162
175
  }
163
176
  if (!peerCert || !peerCert.raw) {
164
- return _refuse(res, "no-peer-cert", {});
177
+ return _refuse(req, res, "no-peer-cert", {});
165
178
  }
166
179
 
167
180
  // Compute fingerprint via the framework's SHA3-512 helper. Buffer
@@ -171,17 +184,17 @@ function create(opts) {
171
184
  try {
172
185
  fp = bCrypto().hashCertFingerprint(peerCert.raw);
173
186
  } catch (e) {
174
- return _refuse(res, "fingerprint-failed", { error: (e && e.message) || String(e) });
187
+ return _refuse(req, res, "fingerprint-failed", { error: (e && e.message) || String(e) });
175
188
  }
176
189
 
177
190
  if (denyList.length > 0 && bCrypto().isCertRevoked(peerCert.raw, denyList)) {
178
- return _refuse(res, "fingerprint-on-deny-list", {
191
+ return _refuse(req, res, "fingerprint-on-deny-list", {
179
192
  fingerprint: fp.colon,
180
193
  subject: (peerCert.subject && peerCert.subject.CN) || null,
181
194
  });
182
195
  }
183
196
  if (allowList && allowList.length > 0 && !bCrypto().isCertRevoked(peerCert.raw, allowList)) {
184
- return _refuse(res, "fingerprint-not-allowed", {
197
+ return _refuse(req, res, "fingerprint-not-allowed", {
185
198
  fingerprint: fp.colon,
186
199
  subject: (peerCert.subject && peerCert.subject.CN) || null,
187
200
  });
@@ -199,7 +212,7 @@ function create(opts) {
199
212
  if (onAuthenticated) {
200
213
  try { return onAuthenticated(req, res, next); }
201
214
  catch (e) {
202
- return _refuse(res, "on-authenticated-threw", { error: (e && e.message) || String(e) });
215
+ return _refuse(req, res, "on-authenticated-threw", { error: (e && e.message) || String(e) });
203
216
  }
204
217
  }
205
218
  return next();
@@ -108,8 +108,8 @@ function _socketDefaults() {
108
108
  }
109
109
 
110
110
  /**
111
- * @primitive b.network.applyToSocket
112
- * @signature b.network.applyToSocket(socket)
111
+ * @primitive b.network.socket.applyToSocket
112
+ * @signature b.network.socket.applyToSocket(socket)
113
113
  * @since 0.7.68
114
114
  * @related b.network.bootFromEnv, b.network.snapshot
115
115
  *
@@ -124,7 +124,7 @@ function _socketDefaults() {
124
124
  * @example
125
125
  * var net = require("net");
126
126
  * var s = new net.Socket();
127
- * var ret = b.network.applyToSocket(s);
127
+ * var ret = b.network.socket.applyToSocket(s);
128
128
  * ret === s;
129
129
  * // → true
130
130
  * s.destroy();
@@ -164,7 +164,7 @@ var ntpFacade = {
164
164
  * @primitive b.network.bootFromEnv
165
165
  * @signature b.network.bootFromEnv(opts)
166
166
  * @since 0.7.68
167
- * @related b.network.snapshot, b.network.applyToSocket
167
+ * @related b.network.snapshot, b.network.socket.applyToSocket
168
168
  *
169
169
  * Read `BLAMEJS_*` environment variables once and apply the union to
170
170
  * the live network facade. Recognised keys cover NTP servers /
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.14.5",
3
+ "version": "0.14.6",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -0,0 +1,60 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.14.6",
4
+ "date": "2026-05-30",
5
+ "headline": "Access-refusal middleware can return RFC 9457 problem+json or a custom response, and several documented-but-uncallable APIs are now reachable",
6
+ "summary": "Every access-refusal middleware — the auth gates (bearer, DPoP, mTLS, AAL, bound-key), CSRF, CORS, rate-limit, bot-guard, age-gate, the host and network allowlists, and the method and content-type gates — now accepts two uniform options: `problemDetails: true` returns an RFC 9457 `application/problem+json` body, and `onDeny(req, res, info)` hands the response to the caller. With neither set the refusal is byte-for-byte what it was, so this is a drop-in change that lets a service standardize one error envelope across its API instead of working around each middleware's hardcoded body. Alongside that: `b.middleware.requireBoundKey` is now exported (it was documented and tested but never wired into the middleware surface), `b.middleware.bearerAuth` accepts `requiredScopes` (previously rejected at construction, which made its scope-enforcement path unreachable), API-key refusals send the RFC 6750 challenge code that matches the failure, two documented call paths that named a missing namespace segment are corrected, and the release flow now flags stale GitHub Actions and vendored bundles — with a ready-to-paste pin — before a dependency PR is needed.",
7
+ "sections": [
8
+ {
9
+ "heading": "Added",
10
+ "items": [
11
+ {
12
+ "title": "Uniform `onDeny` and `problemDetails` options on every access-refusal middleware",
13
+ "body": "Each request-lifecycle middleware that refuses a request now takes `problemDetails: true` to emit an RFC 9457 `application/problem+json` body (composing `b.problemDetails`) and `onDeny(req, res, info)` to take over the response entirely; `info` carries the status, a machine reason, and the middleware-specific fields. The deny-path response headers (`Allow`, `WWW-Authenticate`, `Retry-After`, `Accept`) survive every mode. When neither option is set the response is unchanged. Covers `requireAuth`, `requireAal`, `requireMethods`, `requireContentType`, `requireMtls`, `requireBoundKey`, `bearerAuth`, `dpop`, `csrfProtect`, `fetchMetadata`, `botGuard`, `ageGate`, `hostAllowlist`, `networkAllowlist`, `cors`, `rateLimit`, and `dailyByteQuota` (whose existing `onExceeded` keeps working as an alias of `onDeny`)."
14
+ }
15
+ ]
16
+ },
17
+ {
18
+ "heading": "Fixed",
19
+ "items": [
20
+ {
21
+ "title": "`b.middleware.requireBoundKey` is now callable",
22
+ "body": "The Bearer-API-key middleware was documented (with examples and tests) but never exported on `b.middleware`, so `b.middleware.requireBoundKey(...)` threw `undefined is not a function`. It is now wired into the middleware surface."
23
+ },
24
+ {
25
+ "title": "`b.middleware.bearerAuth` accepts `requiredScopes`",
26
+ "body": "The RFC 6750 scope-enforcement path read `opts.requiredScopes`, but the option was rejected at construction with `unknown option`, making the 403 `insufficient_scope` behavior unreachable. `requiredScopes` is now an accepted option."
27
+ },
28
+ {
29
+ "title": "RFC 6750 challenge codes on API-key refusals",
30
+ "body": "`b.middleware.requireBoundKey` now sends the `WWW-Authenticate` error code that matches the failure: `insufficient_scope` on a 403 missing-scope, `invalid_token` on an unknown or revoked token, and no error code on a 401 that presented no credentials (RFC 6750 §3). It previously sent `invalid_request` for every refusal."
31
+ },
32
+ {
33
+ "title": "Corrected two documented call paths",
34
+ "body": "The compliance and network references named a path that dropped a namespace segment: the conformity-assessment scaffold is at `b.cra.report.conformityAssessment` (not `b.cra.conformityAssessment`), and the per-socket tuning helper is at `b.network.socket.applyToSocket` (not `b.network.applyToSocket`). The documented signatures now match the callable paths."
35
+ },
36
+ {
37
+ "title": "GitHub Actions pins refreshed",
38
+ "body": "`github/codeql-action` 4.35.5 to 4.36.0, and `docker/login-action`, `docker/setup-buildx-action`, and `docker/setup-qemu-action` to their latest releases."
39
+ }
40
+ ]
41
+ },
42
+ {
43
+ "heading": "Detectors",
44
+ "items": [
45
+ {
46
+ "title": "`@primitive` reachability gate",
47
+ "body": "A new check resolves every documented `b.X.Y` primitive against the actual public surface and fails the build when a documented path is not callable (factory-instance shorthands excluded). This is the gate that would have caught the `requireBoundKey` and call-path issues above."
48
+ },
49
+ {
50
+ "title": "Deny-path composition gate",
51
+ "body": "A new check requires every access-refusal middleware to route its refusal through the shared deny-response writer, so a future middleware cannot reintroduce a hardcoded body that locks callers out of `onDeny` / `problemDetails`."
52
+ },
53
+ {
54
+ "title": "Actions and vendor currency in the release flow",
55
+ "body": "The release flow now fails the cut when a SHA-pinned GitHub Action or a vendored bundle is behind its latest upstream release. The actions report prints a ready-to-paste `owner/repo@<sha> # vX.Y.Z` pin and every file and line that uses it, so the bump is copy-paste rather than an after-the-fact dependency PR. Transient registry or API errors stay advisory so a flaky network response does not block an unrelated release."
56
+ }
57
+ ]
58
+ }
59
+ ]
60
+ }
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+ /**
3
+ * GitHub-Actions-currency gate — the sibling of
4
+ * `scripts/check-vendor-currency.js` for the CI/CD supply chain.
5
+ *
6
+ * Walks every `.github/workflows/*.yml`, reads each SHA-pinned
7
+ * `uses: owner/repo[/subpath]@<sha> # vX.Y.Z` reference, and asserts
8
+ * the pinned version (from the trailing comment the pinact discipline
9
+ * requires) matches the latest upstream release. A stale action
10
+ * becomes a release blocker HERE — caught in the pre-merge gate suite
11
+ * — instead of being surfaced after-the-fact by a Dependabot PR.
12
+ *
13
+ * Run locally:
14
+ * node scripts/check-actions-currency.js
15
+ * node scripts/check-actions-currency.js --json // structured output
16
+ * node scripts/check-actions-currency.js --warn // exit 0, print only
17
+ *
18
+ * Run in CI: the workflow passes GITHUB_TOKEN in the environment so the
19
+ * GitHub API gives the authenticated 5000/hour budget instead of the
20
+ * 60/hour unauthenticated per-IP limit. `stale` fails the gate;
21
+ * transient `api-error` results are advisory unless
22
+ * BLAMEJS_ACTIONS_CURRENCY_STRICT=1 converts them into hard fails too.
23
+ *
24
+ * Actions deliberately pinned to an older major (a new major the repo
25
+ * has not adopted) carry a SPECIAL_MAP entry pinning the expected
26
+ * major so the gate doesn't fight an intentional hold.
27
+ */
28
+
29
+ var fs = require("fs");
30
+ var path = require("path");
31
+ var https = require("https");
32
+
33
+ var WORKFLOWS_DIR = path.join(__dirname, "..", ".github", "workflows");
34
+
35
+ var WARN_ONLY = process.argv.indexOf("--warn") !== -1;
36
+ var JSON_OUT = process.argv.indexOf("--json") !== -1;
37
+ var TIMEOUT_MS = 10000;
38
+
39
+ // Per-action overrides. Keyed by "owner/repo".
40
+ // { type: "hold-major", major: N, reason: "..." } — only flag stale
41
+ // WITHIN the pinned major; a newer major is an intentional hold.
42
+ // { type: "skip", reason: "..." } — never flag.
43
+ var SPECIAL_MAP = {
44
+ // (none — every pinned action tracks upstream latest)
45
+ };
46
+
47
+ function _githubGet(apiPath) {
48
+ return new Promise(function (resolve, reject) {
49
+ var headers = {
50
+ "User-Agent": "blamejs-actions-currency/1",
51
+ "Accept": "application/vnd.github+json",
52
+ };
53
+ // Authenticated requests get the 5000/hour budget. Both env names
54
+ // are accepted (GITHUB_TOKEN in Actions, GH_TOKEN for local gh).
55
+ var token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
56
+ if (token) headers.Authorization = "Bearer " + token;
57
+ var req = https.get("https://api.github.com" + apiPath, { timeout: TIMEOUT_MS, headers: headers }, function (res) {
58
+ var chunks = [];
59
+ res.on("data", function (c) { chunks.push(c); });
60
+ res.on("end", function () {
61
+ if (res.statusCode !== 200) {
62
+ return reject(new Error("github " + apiPath + " status " + res.statusCode));
63
+ }
64
+ try { resolve(JSON.parse(Buffer.concat(chunks).toString("utf8"))); }
65
+ catch (e) { reject(e); }
66
+ });
67
+ });
68
+ req.on("timeout", function () { req.destroy(new Error("github " + apiPath + " timed out after " + TIMEOUT_MS + "ms")); });
69
+ req.on("error", reject);
70
+ });
71
+ }
72
+
73
+ async function _resolveSha(ownerRepo, ref) {
74
+ // Resolve a tag to the COMMIT sha it points at — exactly what the
75
+ // pinact discipline pins (`owner/repo@<commit-sha> # tag`). The
76
+ // commits endpoint dereferences annotated tags to their commit.
77
+ var c = await _githubGet("/repos/" + ownerRepo + "/commits/" + encodeURIComponent(ref));
78
+ if (!c || typeof c.sha !== "string") throw new Error("could not resolve sha for " + ownerRepo + "@" + ref);
79
+ return c.sha;
80
+ }
81
+
82
+ async function _latestVersion(ownerRepo) {
83
+ // Prefer the published "latest" release tag; fall back to the
84
+ // highest semver tag for actions that ship tags without GitHub
85
+ // Releases (e.g. ludeeus/action-shellcheck). Returns { tag, sha }
86
+ // so the report can hand back a ready-to-paste pin line.
87
+ var tag = null;
88
+ try {
89
+ var rel = await _githubGet("/repos/" + ownerRepo + "/releases/latest");
90
+ // Only trust the release tag when it is semver-shaped. Some repos
91
+ // (github/codeql-action) publish a non-semver bundle tag as their
92
+ // "latest release" (codeql-bundle-vX.Y.Z) while the ACTION is
93
+ // versioned on separate vN.N.N tags — fall through to tags then.
94
+ if (rel && typeof rel.tag_name === "string" && _semverParse(rel.tag_name)) tag = rel.tag_name;
95
+ } catch (_e) { /* fall through to tags */ }
96
+ if (!tag) {
97
+ var tags = await _githubGet("/repos/" + ownerRepo + "/tags?per_page=100");
98
+ if (!Array.isArray(tags) || tags.length === 0) {
99
+ throw new Error("no releases or tags for " + ownerRepo);
100
+ }
101
+ var best = null;
102
+ for (var i = 0; i < tags.length; i++) {
103
+ var p = _semverParse(tags[i].name);
104
+ if (p && (!best || _semverCompare(p, best.parsed) > 0)) {
105
+ best = { name: tags[i].name, parsed: p };
106
+ }
107
+ }
108
+ if (!best) throw new Error("no semver-shaped tag for " + ownerRepo);
109
+ tag = best.name;
110
+ }
111
+ return { tag: tag, sha: await _resolveSha(ownerRepo, tag) };
112
+ }
113
+
114
+ function _semverParse(v) {
115
+ // Accept partial tags (v4 / v4.1) by treating missing segments as 0.
116
+ var m = String(v).match(/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
117
+ if (!m) return null;
118
+ return [parseInt(m[1], 10), parseInt(m[2] || "0", 10), parseInt(m[3] || "0", 10)];
119
+ }
120
+
121
+ function _semverCompare(a, b) {
122
+ if (!a || !b) return 0;
123
+ for (var i = 0; i < 3; i++) {
124
+ if (a[i] > b[i]) return 1;
125
+ if (a[i] < b[i]) return -1;
126
+ }
127
+ return 0;
128
+ }
129
+
130
+ // Collect distinct SHA-pinned actions across every workflow file.
131
+ // Returns { "owner/repo": { version, refs: [{ file, line, subpath }] } }.
132
+ function _collectPinnedActions() {
133
+ var out = {};
134
+ var files = fs.readdirSync(WORKFLOWS_DIR).filter(function (f) {
135
+ return f.endsWith(".yml") || f.endsWith(".yaml");
136
+ });
137
+ // `uses: owner/repo[/subpath]@<40-hex-sha> # vX.Y[.Z]`
138
+ var re = /uses:\s*([A-Za-z0-9._-]+\/[A-Za-z0-9._-]+)(\/[^@\s]+)?@([0-9a-f]{40})\s*#\s*v?(\d+(?:\.\d+){0,2})/;
139
+ for (var f = 0; f < files.length; f++) {
140
+ var rel = ".github/workflows/" + files[f];
141
+ var lines = fs.readFileSync(path.join(WORKFLOWS_DIR, files[f]), "utf8").split("\n");
142
+ for (var L = 0; L < lines.length; L++) {
143
+ var m = lines[L].match(re);
144
+ if (!m) continue;
145
+ var ownerRepo = m[1];
146
+ var subpath = m[2] || "";
147
+ var version = m[4];
148
+ if (!out[ownerRepo]) out[ownerRepo] = { version: version, refs: [] };
149
+ out[ownerRepo].refs.push({ file: rel, line: L + 1, subpath: subpath });
150
+ // If the same repo is pinned at two different versions across
151
+ // files, record the lowest so a partial bump still flags.
152
+ if (_semverCompare(_semverParse(version), _semverParse(out[ownerRepo].version)) < 0) {
153
+ out[ownerRepo].version = version;
154
+ }
155
+ }
156
+ }
157
+ return out;
158
+ }
159
+
160
+ async function _checkOne(ownerRepo, entry) {
161
+ var special = SPECIAL_MAP[ownerRepo];
162
+ if (special && special.type === "skip") {
163
+ return { action: ownerRepo, status: "skipped", reason: special.reason, pinned: entry.version };
164
+ }
165
+ var pinned = _semverParse(entry.version);
166
+ try {
167
+ var info = await _latestVersion(ownerRepo);
168
+ var latest = _semverParse(info.tag);
169
+ var cmp = _semverCompare(pinned, latest);
170
+ var status = cmp >= 0 ? "current" : "stale";
171
+ if (special && special.type === "hold-major" && latest && latest[0] > special.major) {
172
+ // A newer major exists but the repo intentionally holds an
173
+ // older major — only flag stale WITHIN the held major.
174
+ status = "current";
175
+ }
176
+ return {
177
+ action: ownerRepo,
178
+ pinned: entry.version,
179
+ latest: info.tag,
180
+ latestSha: info.sha,
181
+ status: status,
182
+ refs: entry.refs,
183
+ };
184
+ } catch (e) {
185
+ return {
186
+ action: ownerRepo,
187
+ pinned: entry.version,
188
+ status: "api-error",
189
+ error: (e && e.message) || String(e),
190
+ refs: entry.refs,
191
+ };
192
+ }
193
+ }
194
+
195
+ async function main() {
196
+ var pinned = _collectPinnedActions();
197
+ var actions = Object.keys(pinned).sort();
198
+ var results = [];
199
+ // Sequential + polite — the action count is small and serial GETs
200
+ // keep us well inside the API budget on shared CI IPs.
201
+ for (var i = 0; i < actions.length; i++) {
202
+ results.push(await _checkOne(actions[i], pinned[actions[i]]));
203
+ }
204
+
205
+ if (JSON_OUT) {
206
+ process.stdout.write(JSON.stringify({ results: results }, null, 2) + "\n");
207
+ } else {
208
+ process.stdout.write("[actions-currency] " + actions.length + " SHA-pinned action(s) inspected:\n");
209
+ for (var j = 0; j < results.length; j++) {
210
+ var r = results[j];
211
+ var label = r.status === "current" ? "OK"
212
+ : r.status === "stale" ? "STALE"
213
+ : r.status === "api-error" ? "ERR"
214
+ : r.status === "skipped" ? "skip"
215
+ : r.status;
216
+ var line = " [" + label + "] " + r.action + " " + r.pinned;
217
+ if (r.latest) line += " -> " + r.latest;
218
+ if (r.reason) line += " (" + r.reason + ")";
219
+ if (r.error) line += " (api: " + r.error + ")";
220
+ process.stdout.write(line + "\n");
221
+ // For stale entries, print a ready-to-paste pin line + every
222
+ // file:line that needs the bump, so updating is copy-paste.
223
+ if (r.status === "stale" && r.latestSha) {
224
+ process.stdout.write(" pin: " + r.action + "@" + r.latestSha + " # " + r.latest + "\n");
225
+ for (var rf = 0; rf < (r.refs || []).length; rf++) {
226
+ process.stdout.write(" used: " + r.refs[rf].file + ":" + r.refs[rf].line + "\n");
227
+ }
228
+ }
229
+ }
230
+ }
231
+
232
+ var stale = results.filter(function (r) { return r.status === "stale"; });
233
+ var errored = results.filter(function (r) { return r.status === "api-error"; });
234
+
235
+ if (WARN_ONLY) {
236
+ if (stale.length || errored.length) {
237
+ process.stdout.write("[actions-currency] --warn: " + stale.length + " stale, " +
238
+ errored.length + " errored — exit 0 anyway\n");
239
+ }
240
+ process.exit(0);
241
+ }
242
+
243
+ var strictErrors = process.env.BLAMEJS_ACTIONS_CURRENCY_STRICT === "1";
244
+ if (stale.length > 0 || (strictErrors && errored.length > 0)) {
245
+ process.stdout.write("[actions-currency] FAIL — " + stale.length + " stale, " +
246
+ errored.length + " api-error(s). Bump the pinned SHA + version comment to the latest release.\n");
247
+ process.exit(1);
248
+ }
249
+ process.stdout.write("[actions-currency] OK — every pinned action matches the latest upstream release\n");
250
+ process.exit(0);
251
+ }
252
+
253
+ main().catch(function (e) {
254
+ process.stderr.write("[actions-currency] script crashed: " + (e && e.stack || e) + "\n");
255
+ process.exit(2);
256
+ });
@@ -258,6 +258,17 @@ function cmdPrepare(opts) {
258
258
  _run("node", ["scripts/validate-source-comment-blocks.js"]);
259
259
  _ok("eslint + codebase-patterns + source-comment-blocks clean");
260
260
 
261
+ _section("supply-chain currency");
262
+ // A stale SHA-pinned GitHub Action or vendored bundle becomes a
263
+ // release blocker HERE — with a ready-to-paste pin line in the
264
+ // actions report — instead of an after-the-fact Dependabot PR.
265
+ // Each script treats only an actually-newer upstream version as a
266
+ // failure; transient registry / API errors stay advisory (exit 0)
267
+ // so a flaky network response doesn't block the cut.
268
+ _run("node", ["scripts/check-actions-currency.js"]);
269
+ _run("node", ["scripts/check-vendor-currency.js"]);
270
+ _ok("github actions + vendored bundles current");
271
+
261
272
  console.log("\nnext: node scripts/release.js smoke");
262
273
  }
263
274
 
@@ -295,6 +295,7 @@ var VALID_ALLOW_CLASSES = {
295
295
  "bare-json-parse": 1,
296
296
  "bare-split-on-quoted-header": 1,
297
297
  "console-direct": 1,
298
+ "deny-path-hardcoded-response": 1,
298
299
  "duplicate-regex": 1,
299
300
  "dynamic-regex": 1,
300
301
  "dynamic-require": 1,
@@ -313,6 +314,7 @@ var VALID_ALLOW_CLASSES = {
313
314
  "no-number-money-arithmetic": 1,
314
315
  "numeric-opt-Infinity": 1,
315
316
  "numeric-opt-no-bounds-check": 1,
317
+ "primitive-unreachable": 1,
316
318
  "process-exit": 1,
317
319
  "raw-byte-literal": 1,
318
320
  "raw-hash-compare": 1,
@@ -2356,6 +2358,16 @@ async function testNoDuplicateCodeBlocks() {
2356
2358
  files: ["lib/guard-filename.js:verifyExtractionPath", "lib/hal.js:resource", "lib/vault-aad.js:_canonicalize"],
2357
2359
  reason: "v0.13.13 — coincidental token shingle of the generic split-then-walk-segments idiom (`x.split(sep); for (...) { var seg = ...; if (...) throw/continue }`). guard-filename verifyExtractionPath walks path components refusing per-segment Windows-extraction hazards (reserved names / NTFS-ADS / trailing-dot); hal.js:resource builds a HAL resource by walking link/embedded keys; vault-aad.js:_canonicalize canonicalizes AAD key-value segments. Three unrelated domains (path safety / hypermedia link assembly / crypto AAD canonicalization) — no shared behaviour to extract; the only commonality is the universal split-and-loop control-flow shape.",
2358
2360
  },
2361
+ {
2362
+ mode: "family-subset",
2363
+ files: [
2364
+ "lib/middleware/host-allowlist.js:create",
2365
+ "lib/middleware/require-auth.js:create",
2366
+ "lib/middleware/require-content-type.js:create",
2367
+ "lib/middleware/require-methods.js:create",
2368
+ ],
2369
+ reason: "v0.14.6 — the shared shingle is the uniform deny-path denyResponse() call-site shape. The deny-response WRITER is already extracted to lib/middleware/deny-response.js; what repeats here is each create()'s ctx object literal ({ onDeny, problem, status, info, problemCode, problemTitle, problemDetail, contentType, body }). Each middleware passes a DIFFERENT status (405 / 415 / 421 / 401), problemCode, title, default body and headers — the literal IS the per-middleware configuration the consumer-facing onDeny / problemDetails convention drives. Consolidating further would mean a config table strictly less readable than the inline ctx, and the per-middleware values are exactly the divergence the dup detector can't see.",
2370
+ },
2359
2371
  {
2360
2372
  mode: "family-subset",
2361
2373
  files: [
@@ -10474,7 +10486,133 @@ function testSafeGuardHasMustComposeDetector() {
10474
10486
  unpaired);
10475
10487
  }
10476
10488
 
10489
+ // ---- Pattern: every `@primitive b.X.Y` doc block resolves to a real
10490
+ // callable on the public surface (advertised-vs-actual
10491
+ // reachability). A wiki page that documents an
10492
+ // uncallable path is an operator-facing lie: an operator
10493
+ // following the docs gets `undefined is not a function`.
10494
+ //
10495
+ // Two shapes this catches:
10496
+ // - a wiring gap (the primitive's `create` exists but was never
10497
+ // wired into index.js, so `b.middleware.requireBoundKey` was
10498
+ // undefined);
10499
+ // - a doc-path drop (`b.cra.conformityAssessment` where the real
10500
+ // path is `b.cra.report.conformityAssessment`).
10501
+ //
10502
+ // Factory namespaces (the parent exposes `create`) document instance
10503
+ // methods on the create()-return value with namespace shorthand
10504
+ // (`b.auth.oauth.parseCallback` is reached via
10505
+ // `b.auth.oauth.create(...).parseCallback`). Those are NOT gaps, so a
10506
+ // parent that exposes `create` is skipped.
10507
+ function testPrimitiveReachability() {
10508
+ // class: primitive-unreachable
10509
+ var bSurface;
10510
+ try { bSurface = require("../../index.js"); }
10511
+ catch (_e) { check("primitive-reachability — index.js require", false); return; }
10512
+
10513
+ function resolve(dotted) {
10514
+ var parts = dotted.split(".");
10515
+ var cur = bSurface;
10516
+ for (var i = 1; i < parts.length; i += 1) {
10517
+ if (cur == null) return undefined;
10518
+ cur = cur[parts[i]];
10519
+ }
10520
+ return cur;
10521
+ }
10522
+
10523
+ // @primitive paths that legitimately don't resolve as a flat member
10524
+ // (documented elsewhere / intentional). Keyed by dotted name.
10525
+ var REACHABILITY_ALLOWLIST = {
10526
+ // (none — every surfaced gap is fixed in-tree)
10527
+ };
10528
+
10529
+ var libFiles = _libFiles();
10530
+ var unreachable = [];
10531
+ for (var i = 0; i < libFiles.length; i += 1) {
10532
+ var rel = _relPath(libFiles[i]);
10533
+ var src = fs.readFileSync(libFiles[i], "utf8");
10534
+ var re = /@primitive\s+(b\.[A-Za-z0-9_.]+)/g;
10535
+ var m;
10536
+ while ((m = re.exec(src)) !== null) {
10537
+ var name = m[1];
10538
+ if (REACHABILITY_ALLOWLIST[name]) continue;
10539
+ if (typeof resolve(name) !== "undefined") continue;
10540
+ var parts = name.split(".");
10541
+ var parentName = parts.slice(0, -1).join(".");
10542
+ var parent = resolve(parentName);
10543
+ // Flag only flat namespaces (object parent without a `create`
10544
+ // factory). Factory-instance shorthands are skipped.
10545
+ if (parent && typeof parent === "object" && typeof parent.create !== "function") {
10546
+ unreachable.push({
10547
+ file: rel,
10548
+ line: 1,
10549
+ content: "@primitive " + name + " documents a b.* path that does not resolve (parent `" +
10550
+ parentName + "` is a flat namespace without this member). Wire it into the " +
10551
+ "namespace, or correct the @primitive/@signature path.",
10552
+ });
10553
+ }
10554
+ }
10555
+ }
10556
+ _report("every @primitive b.X.Y doc block resolves to a callable on the public surface " +
10557
+ "(advertised-vs-actual reachability; factory-instance shorthands excluded)",
10558
+ unreachable);
10559
+ }
10560
+
10561
+ // ---- Pattern: every access-refusal middleware routes its deny
10562
+ // response through lib/middleware/deny-response.js so a
10563
+ // consumer can override the body / Content-Type (emit
10564
+ // RFC 9457 application/problem+json) via the uniform
10565
+ // onDeny / problemDetails opts. A new deny-path
10566
+ // middleware that hardcodes
10567
+ // `res.writeHead(<4xx/5xx>, { "Content-Type": ... })`
10568
+ // locks consumers out of the response shape — the
10569
+ // integration-friction class that blocked downstream
10570
+ // adoption (the rate-limit text/plain 429). ----
10571
+ function testDenyPathComposesDenyResponse() {
10572
+ // class: deny-path-hardcoded-response
10573
+ var MW_ROOT = path.resolve(LIB_ROOT, "middleware");
10574
+ // NOT access-refusals: content-servers that 4xx when the
10575
+ // .well-known resource isn't configured, and the CSP report-ingest
10576
+ // machine endpoint. Their 4xx is not a user-facing access denial.
10577
+ var NOT_DENY_PATH = {
10578
+ "lib/middleware/assetlinks.js": ".well-known content-server; 4xx = not-configured, not access-refusal",
10579
+ "lib/middleware/security-txt.js": ".well-known content-server; 4xx = not-configured, not access-refusal",
10580
+ "lib/middleware/web-app-manifest.js": "manifest content-server; 4xx = not-configured, not access-refusal",
10581
+ "lib/middleware/csp-report.js": "CSP report-ingest machine endpoint; 4xx = malformed-report rejection on a browser-posting sink",
10582
+ "lib/middleware/deny-response.js": "the shared deny-response helper itself",
10583
+ };
10584
+ var denyStatusRe = new RegExp("writeHead\\(\\s*(?:4\\d\\d|5\\d\\d|requestHelpers\\.HTTP_STATUS\\." +
10585
+ "(?:FORBIDDEN|UNAUTHORIZED|NOT_FOUND|BAD_REQUEST|METHOD_NOT_ALLOWED|UNSUPPORTED_MEDIA_TYPE|" +
10586
+ "TOO_MANY_REQUESTS|UNAVAILABLE_FOR_LEGAL_REASONS|MISDIRECTED_REQUEST))");
10587
+ var composesRe = /require\(\s*["']\.\/deny-response["']\s*\)/;
10588
+ var files = fs.readdirSync(MW_ROOT).filter(function (f) { return f.endsWith(".js"); });
10589
+ var violations = [];
10590
+ for (var i = 0; i < files.length; i += 1) {
10591
+ var rel = "lib/middleware/" + files[i];
10592
+ if (NOT_DENY_PATH[rel]) continue;
10593
+ var src = fs.readFileSync(path.join(MW_ROOT, files[i]), "utf8");
10594
+ if (!denyStatusRe.test(src)) continue; // doesn't write a deny status
10595
+ if (composesRe.test(src)) continue; // routes through the helper
10596
+ var lines = src.split("\n");
10597
+ var ln = 1;
10598
+ for (var L = 0; L < lines.length; L += 1) {
10599
+ if (denyStatusRe.test(lines[L])) { ln = L + 1; break; }
10600
+ }
10601
+ violations.push({
10602
+ file: rel,
10603
+ line: ln,
10604
+ content: "access-refusal middleware writes a deny-status response without composing deny-response.js — " +
10605
+ "route it through denyResponse() so consumers get onDeny / problemDetails (RFC 9457). If this " +
10606
+ "is NOT an access-refusal (content-server / machine endpoint), add it to NOT_DENY_PATH with a reason.",
10607
+ });
10608
+ }
10609
+ _report("every access-refusal middleware composes deny-response.js (onDeny / problemDetails override path)",
10610
+ violations);
10611
+ }
10612
+
10477
10613
  async function run() {
10614
+ testPrimitiveReachability();
10615
+ testDenyPathComposesDenyResponse();
10478
10616
  testNoOrphanAllowClass();
10479
10617
  testNoRawByteLiterals();
10480
10618
  testNoRawTimeLiterals();