@blamejs/blamejs-shop 0.3.7 → 0.3.9

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 (41) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/checkout.js +125 -1
  4. package/lib/storefront.js +1 -1
  5. package/lib/vendor/MANIFEST.json +2 -2
  6. package/lib/vendor/blamejs/.github/workflows/ci.yml +27 -0
  7. package/lib/vendor/blamejs/.github/workflows/codeql.yml +2 -2
  8. package/lib/vendor/blamejs/.github/workflows/release-container.yml +4 -4
  9. package/lib/vendor/blamejs/.github/workflows/scorecard.yml +1 -1
  10. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  11. package/lib/vendor/blamejs/README.md +1 -0
  12. package/lib/vendor/blamejs/SECURITY.md +2 -0
  13. package/lib/vendor/blamejs/api-snapshot.json +6 -2
  14. package/lib/vendor/blamejs/lib/cra-report.js +3 -3
  15. package/lib/vendor/blamejs/lib/middleware/age-gate.js +20 -7
  16. package/lib/vendor/blamejs/lib/middleware/bearer-auth.js +36 -35
  17. package/lib/vendor/blamejs/lib/middleware/bot-guard.js +17 -5
  18. package/lib/vendor/blamejs/lib/middleware/cors.js +28 -12
  19. package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +22 -14
  20. package/lib/vendor/blamejs/lib/middleware/daily-byte-quota.js +27 -13
  21. package/lib/vendor/blamejs/lib/middleware/deny-response.js +140 -0
  22. package/lib/vendor/blamejs/lib/middleware/dpop.js +32 -19
  23. package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +21 -12
  24. package/lib/vendor/blamejs/lib/middleware/host-allowlist.js +19 -8
  25. package/lib/vendor/blamejs/lib/middleware/index.js +3 -0
  26. package/lib/vendor/blamejs/lib/middleware/network-allowlist.js +24 -10
  27. package/lib/vendor/blamejs/lib/middleware/rate-limit.js +22 -5
  28. package/lib/vendor/blamejs/lib/middleware/require-aal.js +25 -10
  29. package/lib/vendor/blamejs/lib/middleware/require-auth.js +32 -16
  30. package/lib/vendor/blamejs/lib/middleware/require-bound-key.js +49 -18
  31. package/lib/vendor/blamejs/lib/middleware/require-content-type.js +19 -8
  32. package/lib/vendor/blamejs/lib/middleware/require-methods.js +17 -7
  33. package/lib/vendor/blamejs/lib/middleware/require-mtls.js +27 -14
  34. package/lib/vendor/blamejs/lib/network.js +4 -4
  35. package/lib/vendor/blamejs/package.json +1 -1
  36. package/lib/vendor/blamejs/release-notes/v0.14.6.json +60 -0
  37. package/lib/vendor/blamejs/scripts/check-actions-currency.js +256 -0
  38. package/lib/vendor/blamejs/scripts/release.js +11 -0
  39. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +138 -0
  40. package/lib/vendor/blamejs/test/layer-0-primitives/deny-response.test.js +228 -0
  41. package/package.json +1 -1
@@ -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();
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ /**
3
+ * Deny-path response convention — every access-refusal middleware
4
+ * routes its refusal through lib/middleware/deny-response.js, giving a
5
+ * consumer one uniform way to shape it:
6
+ *
7
+ * - default — the middleware's existing body + Content-Type
8
+ * (no behavior change)
9
+ * - problemDetails:true — RFC 9457 application/problem+json
10
+ * - onDeny(req,res,info) — the consumer fully owns the response; a
11
+ * throwing or no-op hook falls through to the
12
+ * default rather than hanging the request
13
+ *
14
+ * The deny-path response headers (Allow / WWW-Authenticate /
15
+ * Retry-After / Accept) survive every mode.
16
+ *
17
+ * The shared b.testing mocks don't capture setHeader + writeHead body
18
+ * + writableEnded together (problem mode writes via setHeader; default
19
+ * mode via writeHead), so — like require-auth-cache-control.test.js —
20
+ * this file rolls one complete response mock.
21
+ */
22
+
23
+ var helpers = require("../helpers");
24
+ var b = helpers.b;
25
+ var check = helpers.check;
26
+
27
+ function _mkRes() {
28
+ var hdrs = {};
29
+ return {
30
+ statusCode: 0,
31
+ headersSent: false,
32
+ writableEnded: false,
33
+ body: null,
34
+ setHeader: function (k, v) { hdrs[k.toLowerCase()] = v; },
35
+ getHeader: function (k) { return hdrs[k.toLowerCase()]; },
36
+ writeHead: function (sc, h) {
37
+ this.statusCode = sc;
38
+ this.headersSent = true;
39
+ if (h && typeof h === "object") {
40
+ for (var k in h) if (Object.prototype.hasOwnProperty.call(h, k)) {
41
+ hdrs[k.toLowerCase()] = h[k];
42
+ }
43
+ }
44
+ return this;
45
+ },
46
+ end: function (x) {
47
+ if (x !== undefined && x !== null) this.body = x;
48
+ this.writableEnded = true;
49
+ },
50
+ _hdrs: hdrs,
51
+ };
52
+ }
53
+
54
+ function _mkReq(opts) {
55
+ opts = opts || {};
56
+ return {
57
+ method: opts.method || "GET",
58
+ url: opts.url || "/x",
59
+ pathname: opts.pathname || "/x",
60
+ headers: opts.headers || {},
61
+ socket: { remoteAddress: "127.0.0.1", encrypted: false },
62
+ connection: {},
63
+ };
64
+ }
65
+
66
+ function _json(s) { try { return JSON.parse(s); } catch (_e) { return {}; } }
67
+
68
+ async function _drive(mw, req) {
69
+ var res = _mkRes();
70
+ await mw(req, res, function () { res._nextCalled = true; });
71
+ return res;
72
+ }
73
+
74
+ async function run() {
75
+ var denyResponse = require("../../lib/middleware/deny-response").denyResponse;
76
+
77
+ // ---- Helper unit: the three resolution modes ----
78
+ (function () {
79
+ // default mode
80
+ var res = _mkRes();
81
+ denyResponse(_mkReq(), res, {
82
+ status: 403, contentType: "text/plain", body: "no",
83
+ headers: { "Allow": "GET" },
84
+ info: { status: 403, reason: "x" },
85
+ problemTitle: "Forbidden",
86
+ });
87
+ check("denyResponse default: status + content-type + body + extra header",
88
+ res.statusCode === 403 && res._hdrs["content-type"] === "text/plain" &&
89
+ res.body === "no" && res._hdrs["allow"] === "GET");
90
+
91
+ // problem mode
92
+ var res2 = _mkRes();
93
+ denyResponse(_mkReq(), res2, {
94
+ problem: true, status: 403, contentType: "text/plain", body: "no",
95
+ headers: { "Allow": "GET" }, problemCode: "x-refused", problemTitle: "Forbidden",
96
+ problemDetail: "nope", info: { status: 403, reason: "x" },
97
+ });
98
+ var p = _json(res2.body);
99
+ check("denyResponse problem: application/problem+json + RFC 9457 fields + extra header survives",
100
+ res2.statusCode === 403 && res2._hdrs["content-type"] === "application/problem+json" &&
101
+ p.status === 403 && p.title === "Forbidden" && p.detail === "nope" &&
102
+ typeof p.type === "string" && res2._hdrs["allow"] === "GET");
103
+
104
+ // onDeny owns
105
+ var seen = null;
106
+ var res3 = _mkRes();
107
+ denyResponse(_mkReq(), res3, {
108
+ onDeny: function (req, rs, info) { seen = info; rs.writeHead(418, {}); rs.end("teapot"); },
109
+ status: 403, contentType: "text/plain", body: "no", info: { status: 403, reason: "x" },
110
+ });
111
+ check("denyResponse onDeny owns response + receives info",
112
+ res3.statusCode === 418 && res3.body === "teapot" && seen && seen.reason === "x");
113
+
114
+ // onDeny throws -> falls through to default (audited via onThrow)
115
+ var threw = false;
116
+ var res4 = _mkRes();
117
+ denyResponse(_mkReq(), res4, {
118
+ onDeny: function () { throw new Error("boom"); },
119
+ onThrow: function () { threw = true; },
120
+ status: 403, contentType: "text/plain", body: "default-after-throw",
121
+ info: { status: 403, reason: "x" },
122
+ });
123
+ check("denyResponse onDeny throws -> default written + onThrow audited",
124
+ res4.statusCode === 403 && res4.body === "default-after-throw" && threw === true);
125
+
126
+ // onDeny no-op (doesn't write) -> falls through to default
127
+ var res5 = _mkRes();
128
+ denyResponse(_mkReq(), res5, {
129
+ onDeny: function () { /* returns without writing */ },
130
+ status: 403, contentType: "text/plain", body: "default-after-noop",
131
+ info: { status: 403, reason: "x" },
132
+ });
133
+ check("denyResponse onDeny no-op -> default written (no hang)",
134
+ res5.statusCode === 403 && res5.body === "default-after-noop");
135
+ })();
136
+
137
+ // ---- require-methods (405 / text/plain) ----
138
+ (function () {
139
+ var def = _mkRes();
140
+ b.middleware.requireMethods(["GET"])(_mkReq({ method: "DELETE" }), def, function () {});
141
+ check("requireMethods default 405 text/plain + Allow",
142
+ def.statusCode === 405 && def._hdrs["content-type"] === "text/plain; charset=utf-8" &&
143
+ def._hdrs["allow"] === "GET" && def.body === "Method Not Allowed");
144
+
145
+ var pj = _mkRes();
146
+ b.middleware.requireMethods(["GET"], { problemDetails: true })(_mkReq({ method: "DELETE" }), pj, function () {});
147
+ check("requireMethods problemDetails 405 problem+json + Allow survives",
148
+ pj.statusCode === 405 && pj._hdrs["content-type"] === "application/problem+json" &&
149
+ pj._hdrs["allow"] === "GET" && _json(pj.body).status === 405);
150
+ })();
151
+
152
+ // ---- rate-limit (429) — X-RateLimit-* + Retry-After survive problem mode ----
153
+ (function () {
154
+ var mw = b.middleware.rateLimit({ backend: "memory", algorithm: "fixed-window", limit: 1, windowMs: 60000, problemDetails: true });
155
+ var req = _mkReq();
156
+ mw(req, _mkRes(), function () {});
157
+ var res = _mkRes();
158
+ mw(req, res, function () {});
159
+ check("rateLimit problemDetails 429 problem+json + X-RateLimit-Limit survives",
160
+ res.statusCode === 429 && res._hdrs["content-type"] === "application/problem+json" &&
161
+ res._hdrs["x-ratelimit-limit"] === "1" && _json(res.body).status === 429);
162
+ })();
163
+
164
+ // ---- age-gate (451 / JSON envelope default) ----
165
+ (function () {
166
+ var opts = { getAge: function () { return 5; }, requireAge: 13, consentRequired: 13 };
167
+ var def = _mkRes();
168
+ b.middleware.ageGate(opts)(_mkReq(), def, function () {});
169
+ check("ageGate default 451 JSON envelope",
170
+ def.statusCode === 451 && def._hdrs["content-type"] === "application/json; charset=utf-8" &&
171
+ _json(def.body).parentalConsent === false);
172
+
173
+ var pj = _mkRes();
174
+ b.middleware.ageGate(Object.assign({ problemDetails: true }, opts))(_mkReq(), pj, function () {});
175
+ check("ageGate problemDetails 451 problem+json + requireAge extension",
176
+ pj.statusCode === 451 && pj._hdrs["content-type"] === "application/problem+json" &&
177
+ _json(pj.body).requireAge === 13);
178
+ })();
179
+
180
+ // ---- cors (403) ----
181
+ (function () {
182
+ var mw = b.middleware.cors({ origins: ["https://ok.example"], problemDetails: true });
183
+ var res = _mkRes();
184
+ mw(_mkReq({ headers: { origin: "https://evil.example" } }), res, function () {});
185
+ check("cors problemDetails 403 problem+json on disallowed origin",
186
+ res.statusCode === 403 && res._hdrs["content-type"] === "application/problem+json");
187
+ })();
188
+
189
+ // ---- require-bound-key — now reachable on b.middleware + RFC 6750 challenge ----
190
+ (function () {
191
+ check("requireBoundKey reachable on b.middleware (was unwired)",
192
+ typeof b.middleware.requireBoundKey === "function");
193
+ })();
194
+
195
+ await (async function () {
196
+ var mw = b.middleware.requireBoundKey({
197
+ resolver: async function () { return { id: "k1", scopes: ["other"], boundFields: {} }; },
198
+ requiredScopes: ["needed"],
199
+ });
200
+ var scope = await _drive(mw, _mkReq({ headers: { authorization: "Bearer abc" } }));
201
+ check("requireBoundKey 403 missing-scope -> WWW-Authenticate insufficient_scope (RFC 6750)",
202
+ scope.statusCode === 403 && /insufficient_scope/.test(scope._hdrs["www-authenticate"] || ""));
203
+ var noTok = await _drive(mw, _mkReq());
204
+ check("requireBoundKey 401 no-token omits error code (RFC 6750 §3)",
205
+ noTok.statusCode === 401 && noTok._hdrs["www-authenticate"] === 'Bearer realm="api"');
206
+ })();
207
+
208
+ // ---- bearer-auth — requiredScopes now accepted (was rejected by validateOpts) ----
209
+ await (async function () {
210
+ var mw = b.middleware.bearerAuth({
211
+ verify: async function () { return { id: 1, scopes: ["read"] }; },
212
+ requiredScopes: ["admin"],
213
+ });
214
+ var res = await _drive(mw, _mkReq({ headers: { authorization: "Bearer good" } }));
215
+ check("bearerAuth requiredScopes reachable -> 403 insufficient_scope",
216
+ res.statusCode === 403 && /insufficient_scope/.test(res._hdrs["www-authenticate"] || "") &&
217
+ _json(res.body).required[0] === "admin");
218
+ })();
219
+ }
220
+
221
+ module.exports = { run: run };
222
+
223
+ if (require.main === module) {
224
+ run().then(
225
+ function () { console.log("OK — " + helpers.getChecks() + " checks passed"); },
226
+ function (e) { console.error("FAIL:", e.stack || e); process.exit(1); }
227
+ );
228
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {