@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.
- package/CHANGELOG.md +4 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/.github/workflows/ci.yml +27 -0
- package/lib/vendor/blamejs/.github/workflows/codeql.yml +2 -2
- package/lib/vendor/blamejs/.github/workflows/release-container.yml +4 -4
- package/lib/vendor/blamejs/.github/workflows/scorecard.yml +1 -1
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/README.md +1 -0
- package/lib/vendor/blamejs/SECURITY.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +6 -2
- package/lib/vendor/blamejs/lib/cra-report.js +3 -3
- package/lib/vendor/blamejs/lib/middleware/age-gate.js +20 -7
- package/lib/vendor/blamejs/lib/middleware/bearer-auth.js +36 -35
- package/lib/vendor/blamejs/lib/middleware/bot-guard.js +17 -5
- package/lib/vendor/blamejs/lib/middleware/cors.js +28 -12
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +22 -14
- package/lib/vendor/blamejs/lib/middleware/daily-byte-quota.js +27 -13
- package/lib/vendor/blamejs/lib/middleware/deny-response.js +140 -0
- package/lib/vendor/blamejs/lib/middleware/dpop.js +32 -19
- package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +21 -12
- package/lib/vendor/blamejs/lib/middleware/host-allowlist.js +19 -8
- package/lib/vendor/blamejs/lib/middleware/index.js +3 -0
- package/lib/vendor/blamejs/lib/middleware/network-allowlist.js +24 -10
- package/lib/vendor/blamejs/lib/middleware/rate-limit.js +22 -5
- package/lib/vendor/blamejs/lib/middleware/require-aal.js +25 -10
- package/lib/vendor/blamejs/lib/middleware/require-auth.js +32 -16
- package/lib/vendor/blamejs/lib/middleware/require-bound-key.js +49 -18
- package/lib/vendor/blamejs/lib/middleware/require-content-type.js +19 -8
- package/lib/vendor/blamejs/lib/middleware/require-methods.js +17 -7
- package/lib/vendor/blamejs/lib/middleware/require-mtls.js +27 -14
- package/lib/vendor/blamejs/lib/network.js +4 -4
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.6.json +60 -0
- package/lib/vendor/blamejs/scripts/check-actions-currency.js +256 -0
- package/lib/vendor/blamejs/scripts/release.js +11 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +138 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/deny-response.test.js +228 -0
- 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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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 /
|
|
@@ -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();
|