@blamejs/blamejs-shop 0.1.29 → 0.1.31

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 (34) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +4 -1
  3. package/SECURITY.md +9 -0
  4. package/lib/admin.js +317 -1
  5. package/lib/asset-manifest.json +1 -1
  6. package/lib/product-qa.js +88 -0
  7. package/lib/storefront.js +640 -1
  8. package/lib/vendor/MANIFEST.json +2 -2
  9. package/lib/vendor/blamejs/CHANGELOG.md +10 -0
  10. package/lib/vendor/blamejs/README.md +4 -0
  11. package/lib/vendor/blamejs/api-snapshot.json +116 -2
  12. package/lib/vendor/blamejs/index.js +8 -0
  13. package/lib/vendor/blamejs/lib/acme.js +2 -2
  14. package/lib/vendor/blamejs/lib/auth/dpop.js +14 -44
  15. package/lib/vendor/blamejs/lib/base32.js +154 -0
  16. package/lib/vendor/blamejs/lib/dbsc.js +5 -18
  17. package/lib/vendor/blamejs/lib/json-schema.js +740 -0
  18. package/lib/vendor/blamejs/lib/jwk.js +127 -0
  19. package/lib/vendor/blamejs/lib/middleware/bot-guard.js +43 -6
  20. package/lib/vendor/blamejs/lib/totp.js +10 -31
  21. package/lib/vendor/blamejs/lib/uri-template.js +286 -0
  22. package/lib/vendor/blamejs/package.json +1 -1
  23. package/lib/vendor/blamejs/release-notes/v0.12.64.json +18 -0
  24. package/lib/vendor/blamejs/release-notes/v0.12.65.json +27 -0
  25. package/lib/vendor/blamejs/release-notes/v0.12.66.json +18 -0
  26. package/lib/vendor/blamejs/release-notes/v0.12.68.json +27 -0
  27. package/lib/vendor/blamejs/release-notes/v0.12.69.json +27 -0
  28. package/lib/vendor/blamejs/test/layer-0-primitives/base32.test.js +79 -0
  29. package/lib/vendor/blamejs/test/layer-0-primitives/bot-guard.test.js +102 -0
  30. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +4 -1
  31. package/lib/vendor/blamejs/test/layer-0-primitives/json-schema.test.js +134 -0
  32. package/lib/vendor/blamejs/test/layer-0-primitives/jwk.test.js +72 -0
  33. package/lib/vendor/blamejs/test/layer-0-primitives/uri-template.test.js +99 -0
  34. package/package.json +1 -1
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.12.68",
4
+ "date": "2026-05-26",
5
+ "headline": "`b.jwk` — RFC 7638 JWK thumbprint",
6
+ "summary": "Compute the RFC 7638 thumbprint of a JSON Web Key — the canonical base64url(SHA-256(canonical-JSON)) identifier used to name a key (DPoP jkt bindings, ACME account-key thumbprints, DBSC session pins, kid derivation). b.jwk.thumbprint(jwk) returns the digest; b.jwk.canonicalize(jwk) returns the exact JSON that is hashed — only the key-type's required members, member names in lexicographic order, no whitespace, so the same key always yields the same thumbprint regardless of how its JWK was serialized. The standard key types are supported (EC, RSA, oct, OKP per RFC 8037) plus AKP, the IANA key type Node uses for ML-DSA / SLH-DSA post-quantum public keys; SHA-256 is the default, with hash: \"sha384\" | \"sha512\" for RFC 9278 thumbprint-with-hash. Verified against the RFC 7638 §3.1 worked example. b.auth.dpop, b.acme, and b.dbsc now compute their thumbprints through this primitive.",
7
+ "sections": [
8
+ {
9
+ "heading": "Added",
10
+ "items": [
11
+ {
12
+ "title": "`b.jwk.thumbprint` / `b.jwk.canonicalize`",
13
+ "body": "RFC 7638 JWK thumbprint. `thumbprint(jwk, opts)` returns `base64url(hash(canonical-JSON))` — only the key-type's required members feed the hash, so optional fields (`kid`, `use`, `alg`, …) never change the result. `canonicalize(jwk)` returns the canonical JSON string itself. Supports EC / RSA / oct / OKP and the AKP post-quantum key type; SHA-256 default, `hash` selects SHA-384 / SHA-512. Throws `JwkError` on an invalid key or unknown hash."
14
+ }
15
+ ]
16
+ },
17
+ {
18
+ "heading": "Changed",
19
+ "items": [
20
+ {
21
+ "title": "DPoP, ACME, and DBSC compose `b.jwk`",
22
+ "body": "`b.auth.dpop` (the `jkt` proof-key thumbprint), `b.acme` (the RFC 8555 account-key authorization), and `b.dbsc` (the session-pin thumbprint) now compute RFC 7638 thumbprints through `b.jwk` instead of carrying their own implementations. Behavior is unchanged — DPoP still refuses symmetric key types, and each surface keeps its own error codes."
23
+ }
24
+ ]
25
+ }
26
+ ]
27
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.12.69",
4
+ "date": "2026-05-26",
5
+ "headline": "`b.middleware.botGuard` no longer blocks browsers that omit Sec-Fetch-Mode",
6
+ "summary": "b.middleware.botGuard treated a missing Sec-Fetch-Mode header as a bot signal and returned 403 Forbidden, which refused legitimate browsers on any origin where the browser does not emit Fetch Metadata: every plain-HTTP non-localhost origin (Umbrel apps, LAN and *.local reverse-proxy deployments) and Safari before 16.4 even over HTTPS. Browsers only send Sec-Fetch-* in a secure context, so its absence is normal there — not a bot. Sec-Fetch-Mode is now advisory only: it never blocks, and it sets req.suspectedBot in mode:\"tag\" only on a secure-context HTML GET where a modern browser would have sent it. Drive-by bots are still blocked by the missing-Accept-Language and User-Agent heuristics. No configuration change is needed; if you had widened skipPaths or disabled bot-guard to work around this, you can revert that.",
7
+ "sections": [
8
+ {
9
+ "heading": "Fixed",
10
+ "items": [
11
+ {
12
+ "title": "`b.middleware.botGuard` no longer 403s browsers over plain HTTP or older Safari",
13
+ "body": "A missing `Sec-Fetch-Mode` was a blocking heuristic, but browsers omit Fetch Metadata outside a secure context (every plain-HTTP non-localhost origin — Umbrel, LAN, `*.local` proxies) and Safari < 16.4 omits it even over HTTPS. Those legitimate browsers were refused with `403 Forbidden`. `Sec-Fetch-Mode` is now advisory: it never blocks, and only sets `req.suspectedBot` in `mode: \"tag\"` on a secure-context HTML GET. The `Accept-Language` and User-Agent heuristics (which catch the same bots) are unchanged."
14
+ }
15
+ ]
16
+ },
17
+ {
18
+ "heading": "Detectors",
19
+ "items": [
20
+ {
21
+ "title": "reserved-hostname trailing-dot detector recognizes regex strips",
22
+ "body": "The codebase-patterns gate that requires stripping the RFC 1034 trailing root-zone dot before a reserved-hostname comparison now also recognizes end-anchored regex strips (`.replace(/\\.$/, …)`), not only the `charAt` / `while`-loop forms."
23
+ }
24
+ ]
25
+ }
26
+ ]
27
+ }
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ /**
3
+ * Layer 0 — b.base32 (RFC 4648).
4
+ * Oracle: the RFC 4648 §10 test vectors for both the standard Base32
5
+ * alphabet and the extended-hex alphabet.
6
+ */
7
+
8
+ var b = require("../../index");
9
+ var helpers = require("../helpers");
10
+ var check = helpers.check;
11
+ function code(fn) { try { fn(); return "NO-THROW"; } catch (e) { return e.code; } }
12
+
13
+ // RFC 4648 §10 — standard Base32.
14
+ var STD = [["", ""], ["f", "MY======"], ["fo", "MZXQ===="], ["foo", "MZXW6==="],
15
+ ["foob", "MZXW6YQ="], ["fooba", "MZXW6YTB"], ["foobar", "MZXW6YTBOI======"]];
16
+ // RFC 4648 §10 — Base32 extended hex.
17
+ var HEX = [["", ""], ["f", "CO======"], ["fo", "CPNG===="], ["foo", "CPNMU==="],
18
+ ["foob", "CPNMUOG="], ["fooba", "CPNMUOJ1"], ["foobar", "CPNMUOJ1E8======"]];
19
+
20
+ function testSurface() {
21
+ check("b.base32.encode is a function", typeof b.base32.encode === "function");
22
+ check("b.base32.decode is a function", typeof b.base32.decode === "function");
23
+ check("b.base32.Base32Error is a class", typeof b.base32.Base32Error === "function");
24
+ }
25
+
26
+ function testVectors() {
27
+ var ep = 0, dp = 0;
28
+ STD.forEach(function (t) {
29
+ if (b.base32.encode(Buffer.from(t[0])) === t[1]) ep++; else check("std encode " + JSON.stringify(t[0]), false);
30
+ if (b.base32.decode(t[1]).toString() === t[0]) dp++; else check("std decode " + t[1], false);
31
+ });
32
+ check("RFC 4648 standard: all encode vectors", ep === STD.length);
33
+ check("RFC 4648 standard: all decode vectors", dp === STD.length);
34
+ var hep = 0, hdp = 0;
35
+ HEX.forEach(function (t) {
36
+ if (b.base32.encode(Buffer.from(t[0]), { variant: "rfc4648-hex" }) === t[1]) hep++; else check("hex encode " + JSON.stringify(t[0]), false);
37
+ if (b.base32.decode(t[1], { variant: "rfc4648-hex" }).toString() === t[0]) hdp++; else check("hex decode " + t[1], false);
38
+ });
39
+ check("RFC 4648 hex: all encode vectors", hep === HEX.length);
40
+ check("RFC 4648 hex: all decode vectors", hdp === HEX.length);
41
+ }
42
+
43
+ function testOptions() {
44
+ check("padding:false omits =", b.base32.encode(Buffer.from("f"), { padding: false }) === "MY");
45
+ check("round-trip random 20 bytes", (function () {
46
+ var buf = require("crypto").randomBytes(20);
47
+ return b.base32.decode(b.base32.encode(buf)).equals(buf);
48
+ })());
49
+ // loose decode: lower-case, spaces, dashes, missing padding.
50
+ check("loose decodes lower-case + separators", b.base32.decode("mzxw 6ytb-oi", { loose: true }).toString() === "foobar");
51
+ check("strict rejects lower-case", code(function () { b.base32.decode("mzxw6ytb"); }) === "base32/bad-char");
52
+ check("strict rejects bad char", code(function () { b.base32.decode("MZXW60TB"); }) === "base32/bad-char");
53
+ check("bad variant throws", code(function () { b.base32.encode(Buffer.from("x"), { variant: "nope" }); }) === "base32/bad-variant");
54
+ check("non-buffer encode throws", code(function () { b.base32.encode("not a buffer"); }) === "base32/bad-input");
55
+ check("non-string decode throws", code(function () { b.base32.decode(123); }) === "base32/bad-input");
56
+ // Embedded / non-trailing padding is malformed and must be rejected
57
+ // (not silently truncated at the first "=").
58
+ check("rejects embedded padding then data", code(function () { b.base32.decode("MZXW=6YTB"); }) === "base32/bad-char");
59
+ check("rejects padding then data (loose)", code(function () { b.base32.decode("mz=xw", { loose: true }); }) === "base32/bad-char");
60
+ check("accepts valid trailing padding", b.base32.decode("MZXW6YQ=").toString() === "foob");
61
+ }
62
+
63
+ function testTotpComposition() {
64
+ // The TOTP secret produced by generateSecret must decode through b.base32.
65
+ var secret = b.auth.totp.generateSecret();
66
+ check("totp secret decodes via b.base32 (loose)", b.base32.decode(secret, { loose: true }).length > 0);
67
+ // The TOTP secret is unpadded standard Base32 → re-encoding the decoded
68
+ // bytes (unpadded) reproduces the secret exactly.
69
+ check("totp secret round-trips through b.base32", b.base32.encode(b.base32.decode(secret, { loose: true }), { padding: false }) === secret);
70
+ }
71
+
72
+ async function run() {
73
+ testSurface();
74
+ testVectors();
75
+ testOptions();
76
+ testTotpComposition();
77
+ }
78
+ module.exports = { run: run };
79
+ if (require.main === module) { run().then(function () { console.log("[base32] OK — " + helpers.getChecks() + " checks passed"); }, function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }); }
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ /**
3
+ * Layer 0 — b.middleware.botGuard.
4
+ * Focus: the Sec-Fetch-Mode heuristic must never refuse a real browser.
5
+ * Browsers omit Fetch Metadata (Sec-Fetch-*) on plain-HTTP non-localhost
6
+ * origins (Umbrel, LAN / *.local reverse proxies) AND in Safari < 16.4
7
+ * even over HTTPS — so a missing Sec-Fetch-Mode is advisory-only (tags in
8
+ * mode:"tag", never blocks). Drive-by bots are still blocked by the
9
+ * missing-Accept-Language and User-Agent heuristics.
10
+ */
11
+
12
+ var b = require("../../index");
13
+ var helpers = require("../helpers");
14
+ var check = helpers.check;
15
+
16
+ var BROWSER_UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36";
17
+
18
+ function _run(opts, reqInit) {
19
+ var mw = b.middleware.botGuard(opts || {});
20
+ var req = b.testing.mockReq(reqInit);
21
+ var res = b.testing.mockRes();
22
+ var nexted = false;
23
+ mw(req, res, function () { nexted = true; });
24
+ var cap = res._captured();
25
+ return { nexted: nexted, blocked: cap.status === 403, status: cap.status, body: cap.body, suspectedBot: req.suspectedBot };
26
+ }
27
+
28
+ function testSurface() {
29
+ check("b.middleware.botGuard is a function", typeof b.middleware.botGuard === "function");
30
+ check("returns a (req,res,next) middleware", b.middleware.botGuard({}).length === 3);
31
+ }
32
+
33
+ function testSecFetchNeverBlocks() {
34
+ // The reported defect: a real browser on a plain-HTTP non-localhost
35
+ // origin (Umbrel app / LAN proxy) sends Accept-Language but no Sec-Fetch-*.
36
+ var umbrel = _run({ mode: "block" }, { method: "GET", url: "/", headers: { "accept-language": "en-US", "user-agent": BROWSER_UA, host: "umbrel-dev.local:3080" } });
37
+ check("plain-HTTP browser (Umbrel) is NOT blocked", umbrel.nexted && !umbrel.blocked);
38
+
39
+ // Safari < 16.4 omits Sec-Fetch-* even over HTTPS — must not 403 either.
40
+ var safari = _run({ mode: "block" }, { method: "GET", url: "/", headers: { "accept-language": "en-US", "user-agent": "Mozilla/5.0 (Macintosh) Version/15.6 Safari/605", host: "app.example.com" }, socket: { encrypted: true } });
41
+ check("Safari-over-HTTPS (no Sec-Fetch) is NOT blocked", safari.nexted && !safari.blocked);
42
+
43
+ // localhost over plain HTTP, no Sec-Fetch — also fine.
44
+ var local = _run({ mode: "block" }, { method: "GET", url: "/", headers: { "accept-language": "en", "user-agent": BROWSER_UA, host: "localhost:3000" } });
45
+ check("localhost browser is NOT blocked", local.nexted && !local.blocked);
46
+
47
+ // A secure-context browser that DID send Sec-Fetch-Mode passes (sanity).
48
+ var modern = _run({ mode: "block" }, { method: "GET", url: "/", headers: { "accept-language": "en", "user-agent": BROWSER_UA, "sec-fetch-mode": "navigate", host: "app.example.com" }, socket: { encrypted: true } });
49
+ check("modern HTTPS browser passes", modern.nexted && !modern.blocked);
50
+ }
51
+
52
+ function testBotsStillBlocked() {
53
+ // Missing Accept-Language remains a hard block.
54
+ var noLang = _run({ mode: "block" }, { method: "GET", url: "/", headers: { "user-agent": BROWSER_UA, host: "app.example.com" }, socket: { encrypted: true } });
55
+ check("missing Accept-Language is blocked", noLang.blocked && noLang.status === 403 && noLang.body === "Forbidden");
56
+
57
+ // Known automation UA remains a hard block.
58
+ var curl = _run({ mode: "block" }, { method: "GET", url: "/", headers: { "accept-language": "en", "user-agent": "curl/8.4.0", host: "app.example.com" } });
59
+ check("curl UA is blocked", curl.blocked && curl.status === 403);
60
+
61
+ var py = _run({ mode: "block" }, { method: "GET", url: "/", headers: { "accept-language": "en", "user-agent": "python-requests/2.31.0", host: "x" } });
62
+ check("python-requests UA is blocked", py.blocked);
63
+ }
64
+
65
+ function testTagModeAdvisory() {
66
+ // mode:"tag" — secure context, no Sec-Fetch-Mode → advisory tag, never blocks.
67
+ var tagged = _run({ mode: "tag" }, { method: "GET", url: "/", headers: { "accept-language": "en", "user-agent": BROWSER_UA, host: "app.example.com" }, socket: { encrypted: true } });
68
+ check("tag mode: secure-context Sec-Fetch miss tags but continues", tagged.nexted && !tagged.blocked && tagged.suspectedBot === "missing-sec-fetch-mode");
69
+
70
+ // mode:"tag" — plain-HTTP non-localhost → NOT tagged for Sec-Fetch (insecure context).
71
+ var untagged = _run({ mode: "tag" }, { method: "GET", url: "/", headers: { "accept-language": "en", "user-agent": BROWSER_UA, host: "umbrel.local" } });
72
+ check("tag mode: plain-HTTP origin is NOT tagged for Sec-Fetch", untagged.nexted && !untagged.suspectedBot);
73
+ }
74
+
75
+ function testOverridesAndSkips() {
76
+ // allowedAgents override beats the deny-list.
77
+ var allowed = _run({ mode: "block", allowedAgents: [/^curl\//i] }, { method: "GET", url: "/", headers: { "accept-language": "en", "user-agent": "curl/8.4.0", host: "x" } });
78
+ check("allowedAgents override lets curl through", allowed.nexted && !allowed.blocked);
79
+
80
+ // API routes skip the browser-fingerprint checks (onlyForHtml default).
81
+ var api = _run({ mode: "block" }, { method: "GET", url: "/api/data", pathname: "/api/data", headers: { "user-agent": BROWSER_UA, host: "x" } });
82
+ check("API route skips fingerprint checks", api.nexted && !api.blocked);
83
+
84
+ // skipPaths bypass.
85
+ var skipped = _run({ mode: "block", skipPaths: ["/healthz"] }, { method: "GET", url: "/healthz", pathname: "/healthz", headers: { "user-agent": "curl/8" } });
86
+ check("skipPaths bypasses bot-guard", skipped.nexted && !skipped.blocked);
87
+
88
+ // RegExp patterns are required (string patterns refused at create()).
89
+ var threw = null;
90
+ try { b.middleware.botGuard({ blockedAgents: ["badbot"] }); } catch (e) { threw = e.code; }
91
+ check("string blockedAgents pattern is refused", threw === "bot-guard/bad-pattern");
92
+ }
93
+
94
+ async function run() {
95
+ testSurface();
96
+ testSecFetchNeverBlocks();
97
+ testBotsStillBlocked();
98
+ testTagModeAdvisory();
99
+ testOverridesAndSkips();
100
+ }
101
+ module.exports = { run: run };
102
+ if (require.main === module) { run().then(function () { console.log("[bot-guard] OK — " + helpers.getChecks() + " checks passed"); }, function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }); }
@@ -7963,7 +7963,10 @@ function testHostnameCompareTrailingDotNormalize() {
7963
7963
  catch (_e) { continue; }
7964
7964
  if (!reservedHostLiteralRe.test(content)) continue;
7965
7965
  var hasStrip = /\.charAt\([^)]*length\s*-\s*1\)\s*===\s*"\."/.test(content) ||
7966
- /while[\s\S]{0,80}length\s*>\s*0[\s\S]{0,80}charAt[\s\S]{0,80}===\s*"\."/.test(content);
7966
+ /while[\s\S]{0,80}length\s*>\s*0[\s\S]{0,80}charAt[\s\S]{0,80}===\s*"\."/.test(content) ||
7967
+ // end-anchored regex strip of one-or-more trailing dots:
7968
+ // .replace(/\.$/, ...) / .replace(/\.+$/, ...) / .replace(/\.*$/, ...)
7969
+ /\.replace\(\s*\/\\\.[+*]?\$\//.test(content);
7967
7970
  if (hasStrip) continue;
7968
7971
  var m = content.match(reservedHostLiteralRe);
7969
7972
  var lineNum = content.slice(0, m.index).split("\n").length;
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ /**
3
+ * Layer 0 — b.jsonSchema (JSON Schema 2020-12).
4
+ * Oracle: the official json-schema-org/JSON-Schema-Test-Suite draft2020-12
5
+ * (1292 of 1295 cases pass during development; the 3 skipped require the
6
+ * bundled dialect metaschema or $vocabulary selection — both opt-in). This
7
+ * file embeds a representative slice across the vocabulary plus the surface
8
+ * + reference-resolution + annotation cases that exercise the tricky paths.
9
+ */
10
+
11
+ var b = require("../../index");
12
+ var helpers = require("../helpers");
13
+ var check = helpers.check;
14
+ function code(fn) { try { fn(); return "NO-THROW"; } catch (e) { return e.code; } }
15
+
16
+ function testSurface() {
17
+ check("b.jsonSchema.validate is a function", typeof b.jsonSchema.validate === "function");
18
+ check("b.jsonSchema.compile is a function", typeof b.jsonSchema.compile === "function");
19
+ check("b.jsonSchema.isValid is a function", typeof b.jsonSchema.isValid === "function");
20
+ check("b.jsonSchema.DIALECT is 2020-12", b.jsonSchema.DIALECT === "https://json-schema.org/draft/2020-12/schema");
21
+ check("b.jsonSchema.JsonSchemaError is a class", typeof b.jsonSchema.JsonSchemaError === "function");
22
+ check("compile rejects non-schema", code(function () { b.jsonSchema.compile(42); }) === "json-schema/bad-schema");
23
+ var v = b.jsonSchema.compile({ type: "integer" });
24
+ check("compiled validator has validate + isValid", typeof v.validate === "function" && typeof v.isValid === "function");
25
+ }
26
+
27
+ function testAssertions() {
28
+ check("type integer accepts int", b.jsonSchema.isValid({ type: "integer" }, 3));
29
+ check("type integer rejects float", !b.jsonSchema.isValid({ type: "integer" }, 3.5));
30
+ check("type rejects wrong type", !b.jsonSchema.isValid({ type: "string" }, 1));
31
+ check("enum", b.jsonSchema.isValid({ enum: ["a", "b"] }, "b") && !b.jsonSchema.isValid({ enum: ["a"] }, "z"));
32
+ check("const deep-equal", b.jsonSchema.isValid({ const: { a: [1, 2] } }, { a: [1, 2] }) && !b.jsonSchema.isValid({ const: { a: [1] } }, { a: [2] }));
33
+ check("multipleOf", b.jsonSchema.isValid({ multipleOf: 3 }, 9) && !b.jsonSchema.isValid({ multipleOf: 3 }, 10));
34
+ check("maximum/exclusiveMaximum", b.jsonSchema.isValid({ maximum: 5 }, 5) && !b.jsonSchema.isValid({ exclusiveMaximum: 5 }, 5));
35
+ check("minLength counts code points", !b.jsonSchema.isValid({ minLength: 2 }, "😀") && b.jsonSchema.isValid({ maxLength: 1 }, "😀"));
36
+ check("pattern", b.jsonSchema.isValid({ pattern: "^a+$" }, "aaa") && !b.jsonSchema.isValid({ pattern: "^a+$" }, "b"));
37
+ }
38
+
39
+ function testArrays() {
40
+ check("prefixItems + items", b.jsonSchema.isValid({ prefixItems: [{ type: "number" }], items: { type: "string" } }, [1, "a", "b"]));
41
+ check("items rejects bad tail", !b.jsonSchema.isValid({ prefixItems: [{ type: "number" }], items: { type: "string" } }, [1, 2]));
42
+ check("uniqueItems", b.jsonSchema.isValid({ uniqueItems: true }, [1, 2, 3]) && !b.jsonSchema.isValid({ uniqueItems: true }, [1, 1]));
43
+ check("contains + minContains", b.jsonSchema.isValid({ contains: { const: 2 }, minContains: 2 }, [2, 2, 3]) && !b.jsonSchema.isValid({ contains: { const: 2 }, minContains: 2 }, [2, 3]));
44
+ check("maxItems/minItems", !b.jsonSchema.isValid({ maxItems: 1 }, [1, 2]) && !b.jsonSchema.isValid({ minItems: 2 }, [1]));
45
+ }
46
+
47
+ function testObjects() {
48
+ var s = { type: "object", properties: { n: { type: "integer" } }, required: ["n"], additionalProperties: false };
49
+ check("properties + required pass", b.jsonSchema.isValid(s, { n: 1 }));
50
+ check("required missing fails", !b.jsonSchema.isValid(s, {}));
51
+ check("additionalProperties:false rejects extra", !b.jsonSchema.isValid(s, { n: 1, x: 2 }));
52
+ check("patternProperties", b.jsonSchema.isValid({ patternProperties: { "^x": { type: "number" } } }, { x1: 1 }) && !b.jsonSchema.isValid({ patternProperties: { "^x": { type: "number" } } }, { x1: "a" }));
53
+ check("propertyNames", !b.jsonSchema.isValid({ propertyNames: { pattern: "^a" } }, { b: 1 }));
54
+ check("dependentRequired", !b.jsonSchema.isValid({ dependentRequired: { a: ["b"] } }, { a: 1 }));
55
+ check("dependentSchemas", !b.jsonSchema.isValid({ dependentSchemas: { a: { required: ["b"] } } }, { a: 1 }));
56
+ }
57
+
58
+ function testApplicators() {
59
+ check("allOf", b.jsonSchema.isValid({ allOf: [{ type: "number" }, { minimum: 0 }] }, 5) && !b.jsonSchema.isValid({ allOf: [{ type: "number" }, { minimum: 0 }] }, -1));
60
+ check("anyOf", b.jsonSchema.isValid({ anyOf: [{ type: "string" }, { type: "number" }] }, 1) && !b.jsonSchema.isValid({ anyOf: [{ type: "string" }] }, 1));
61
+ check("oneOf exactly one", b.jsonSchema.isValid({ oneOf: [{ multipleOf: 2 }, { multipleOf: 3 }] }, 4) && !b.jsonSchema.isValid({ oneOf: [{ multipleOf: 2 }, { multipleOf: 3 }] }, 6));
62
+ check("not", b.jsonSchema.isValid({ not: { type: "string" } }, 1) && !b.jsonSchema.isValid({ not: { type: "string" } }, "x"));
63
+ check("if/then/else", b.jsonSchema.isValid({ if: { type: "number" }, then: { minimum: 0 }, else: { type: "string" } }, 5) && b.jsonSchema.isValid({ if: { type: "number" }, then: { minimum: 0 }, else: { type: "string" } }, "x") && !b.jsonSchema.isValid({ if: { type: "number" }, then: { minimum: 0 } }, -1));
64
+ check("boolean schema true/false", b.jsonSchema.isValid(true, 42) && !b.jsonSchema.isValid(false, 42));
65
+ }
66
+
67
+ function testUnevaluated() {
68
+ // unevaluatedProperties sees annotations from $ref inside allOf.
69
+ var s = {
70
+ $defs: { one: { properties: { a: true } } },
71
+ allOf: [{ $ref: "#/$defs/one" }, { properties: { b: true } }],
72
+ unevaluatedProperties: false,
73
+ };
74
+ check("unevaluatedProperties + ref-in-allOf accepts evaluated", b.jsonSchema.isValid(s, { a: 1, b: 2 }));
75
+ check("unevaluatedProperties + ref-in-allOf rejects unevaluated", !b.jsonSchema.isValid(s, { a: 1, c: 3 }));
76
+ check("unevaluatedItems", b.jsonSchema.isValid({ prefixItems: [{ type: "number" }], unevaluatedItems: false }, [1]) && !b.jsonSchema.isValid({ prefixItems: [{ type: "number" }], unevaluatedItems: false }, [1, 2]));
77
+ }
78
+
79
+ function testRefs() {
80
+ // $ref to $defs + $anchor.
81
+ check("$ref to $defs", b.jsonSchema.isValid({ $defs: { pos: { minimum: 0 } }, $ref: "#/$defs/pos" }, 5));
82
+ check("$anchor ref", b.jsonSchema.isValid({ $defs: { p: { $anchor: "pos", minimum: 0 } }, $ref: "#pos" }, 5));
83
+ // External schema via opts.schemas (no network).
84
+ var ext = { "https://example.com/int": { type: "integer" } };
85
+ check("external $ref via opts.schemas", b.jsonSchema.isValid({ $ref: "https://example.com/int" }, 3, { schemas: ext }));
86
+ check("external $ref rejects", !b.jsonSchema.isValid({ $ref: "https://example.com/int" }, "x", { schemas: ext }));
87
+ // $dynamicRef / $dynamicAnchor (the recursive bookend pattern).
88
+ var dyn = {
89
+ $id: "https://example.com/tree",
90
+ $dynamicAnchor: "node",
91
+ type: "object",
92
+ properties: { data: true, children: { type: "array", items: { $dynamicRef: "#node" } } },
93
+ };
94
+ check("$dynamicRef recursion validates", b.jsonSchema.isValid(dyn, { data: 1, children: [{ data: 2, children: [] }] }));
95
+ }
96
+
97
+ function testErrorsShape() {
98
+ var r = b.jsonSchema.validate({ type: "object", properties: { n: { type: "integer" } } }, { n: "bad" });
99
+ check("validate returns {valid, errors}", r.valid === false && Array.isArray(r.errors) && r.errors.length >= 1);
100
+ check("error names instancePath + keyword", r.errors[0].instancePath === "/n" && r.errors[0].keyword === "type");
101
+ }
102
+
103
+ function testFormat() {
104
+ // format is an annotation by default (does not assert).
105
+ check("format annotation by default", b.jsonSchema.isValid({ type: "string", format: "email" }, "not-an-email"));
106
+ // assertFormat:true turns it into an assertion.
107
+ check("assertFormat rejects bad email", !b.jsonSchema.isValid({ type: "string", format: "email" }, "nope", { assertFormat: true }));
108
+ check("assertFormat accepts good date-time", b.jsonSchema.isValid({ type: "string", format: "date-time" }, "2020-01-01T00:00:00Z", { assertFormat: true }));
109
+ // time requires an offset and valid ranges (RFC 3339 full-time).
110
+ check("time rejects missing offset", !b.jsonSchema.isValid({ format: "time" }, "12:00:00", { assertFormat: true }));
111
+ check("time rejects out-of-range", !b.jsonSchema.isValid({ format: "time" }, "25:61:61Z", { assertFormat: true }));
112
+ check("time accepts offset form", b.jsonSchema.isValid({ format: "time" }, "12:00:00+05:30", { assertFormat: true }));
113
+ // date enforces real field ranges.
114
+ check("date rejects month 13", !b.jsonSchema.isValid({ format: "date" }, "2020-13-01", { assertFormat: true }));
115
+ check("date accepts valid", b.jsonSchema.isValid({ format: "date" }, "2020-02-29", { assertFormat: true }));
116
+ // uri rejects raw spaces and relative refs.
117
+ check("uri rejects raw space", !b.jsonSchema.isValid({ format: "uri" }, "http://e xample.com", { assertFormat: true }));
118
+ check("uri rejects relative", !b.jsonSchema.isValid({ format: "uri" }, "/relative/path", { assertFormat: true }));
119
+ check("uri accepts absolute", b.jsonSchema.isValid({ format: "uri" }, "https://example.com/x", { assertFormat: true }));
120
+ }
121
+
122
+ async function run() {
123
+ testSurface();
124
+ testAssertions();
125
+ testArrays();
126
+ testObjects();
127
+ testApplicators();
128
+ testUnevaluated();
129
+ testRefs();
130
+ testErrorsShape();
131
+ testFormat();
132
+ }
133
+ module.exports = { run: run };
134
+ if (require.main === module) { run().then(function () { console.log("[json-schema] OK — " + helpers.getChecks() + " checks passed"); }, function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }); }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /**
3
+ * Layer 0 — b.jwk (RFC 7638 thumbprint).
4
+ * Oracle: the RFC 7638 §3.1 worked example (an RSA key whose SHA-256
5
+ * thumbprint is "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"), plus
6
+ * canonicalization + per-kty + composition (DPoP / DBSC delegate here).
7
+ */
8
+
9
+ var b = require("../../index");
10
+ var helpers = require("../helpers");
11
+ var check = helpers.check;
12
+ function code(fn) { try { fn(); return "NO-THROW"; } catch (e) { return e.code; } }
13
+
14
+ // RFC 7638 §3.1 example key (with extra members that must be ignored).
15
+ var RFC_RSA = {
16
+ kty: "RSA",
17
+ n: "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
18
+ e: "AQAB", alg: "RS256", kid: "2011-04-29", use: "sig",
19
+ };
20
+ var RFC_THUMB = "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs";
21
+
22
+ function testSurface() {
23
+ check("b.jwk.thumbprint is a function", typeof b.jwk.thumbprint === "function");
24
+ check("b.jwk.canonicalize is a function", typeof b.jwk.canonicalize === "function");
25
+ check("b.jwk.JwkError is a class", typeof b.jwk.JwkError === "function");
26
+ }
27
+
28
+ function testRfc7638() {
29
+ check("RFC 7638 §3.1 thumbprint matches", b.jwk.thumbprint(RFC_RSA) === RFC_THUMB);
30
+ check("optional members are ignored", b.jwk.thumbprint({ kty: "RSA", n: RFC_RSA.n, e: "AQAB" }) === RFC_THUMB);
31
+ check("canonical JSON is lexicographic + minimal", b.jwk.canonicalize(RFC_RSA) === '{"e":"AQAB","kty":"RSA","n":"' + RFC_RSA.n + '"}');
32
+ }
33
+
34
+ function testKtys() {
35
+ // EC required members are crv, kty, x, y (lexicographic).
36
+ check("EC canonical", b.jwk.canonicalize({ kty: "EC", crv: "P-256", x: "X", y: "Y", d: "secret" }) === '{"crv":"P-256","kty":"EC","x":"X","y":"Y"}');
37
+ check("OKP canonical (RFC 8037)", b.jwk.canonicalize({ kty: "OKP", crv: "Ed25519", x: "X" }) === '{"crv":"Ed25519","kty":"OKP","x":"X"}');
38
+ check("oct canonical", b.jwk.canonicalize({ kty: "oct", k: "GawgguFyGrWKav7AX4VKUg" }) === '{"k":"GawgguFyGrWKav7AX4VKUg","kty":"oct"}');
39
+ check("AKP canonical (PQC)", b.jwk.canonicalize({ kty: "AKP", alg: "ML-DSA-87", pub: "UFVC" }) === '{"alg":"ML-DSA-87","kty":"AKP","pub":"UFVC"}');
40
+ check("different keys → different thumbprints", b.jwk.thumbprint({ kty: "oct", k: "AAAA" }) !== b.jwk.thumbprint({ kty: "oct", k: "BBBB" }));
41
+ }
42
+
43
+ function testHashOption() {
44
+ check("sha384 differs from sha256", b.jwk.thumbprint(RFC_RSA, { hash: "sha384" }) !== RFC_THUMB);
45
+ check("sha512 is a string", typeof b.jwk.thumbprint(RFC_RSA, { hash: "sha512" }) === "string");
46
+ check("bad hash throws", code(function () { b.jwk.thumbprint(RFC_RSA, { hash: "md5" }); }) === "jwk/bad-hash");
47
+ }
48
+
49
+ function testErrors() {
50
+ check("missing kty throws", code(function () { b.jwk.thumbprint({ n: "x", e: "y" }); }) === "jwk/bad-jwk");
51
+ check("unsupported kty throws", code(function () { b.jwk.thumbprint({ kty: "XYZ" }); }) === "jwk/unsupported-kty");
52
+ check("missing required member throws", code(function () { b.jwk.thumbprint({ kty: "EC", crv: "P-256", x: "X" }); }) === "jwk/bad-jwk");
53
+ check("non-object throws", code(function () { b.jwk.thumbprint("nope"); }) === "jwk/bad-jwk");
54
+ }
55
+
56
+ function testComposition() {
57
+ // DPoP and DBSC compute their thumbprints through b.jwk.
58
+ var ec = { kty: "EC", crv: "P-256", x: "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", y: "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0" };
59
+ check("dpop.thumbprint composes b.jwk", b.auth.dpop.thumbprint(ec) === b.jwk.thumbprint(ec));
60
+ check("dpop refuses symmetric kty", code(function () { b.auth.dpop.thumbprint({ kty: "oct", k: "x" }); }) === "auth-dpop/refused-kty");
61
+ }
62
+
63
+ async function run() {
64
+ testSurface();
65
+ testRfc7638();
66
+ testKtys();
67
+ testHashOption();
68
+ testErrors();
69
+ testComposition();
70
+ }
71
+ module.exports = { run: run };
72
+ if (require.main === module) { run().then(function () { console.log("[jwk] OK — " + helpers.getChecks() + " checks passed"); }, function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }); }
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ /**
3
+ * Layer 0 — b.uriTemplate (RFC 6570).
4
+ * Oracle: the official uri-templates/uritemplate-test suite (spec-examples,
5
+ * extended-tests, negative-tests) — all 135 cases pass during development.
6
+ * This file embeds a representative slice across all four levels plus the
7
+ * surface, compile-reuse, and malformed-template paths.
8
+ */
9
+
10
+ var b = require("../../index");
11
+ var helpers = require("../helpers");
12
+ var check = helpers.check;
13
+ function code(fn) { try { fn(); return "NO-THROW"; } catch (e) { return e.code; } }
14
+
15
+ var VARS = {
16
+ count: ["one", "two", "three"], dom: ["example", "com"],
17
+ dub: "me/too", hello: "Hello World!", half: "50%", "var": "value",
18
+ who: "fred", base: "http://example.com/home/", path: "/foo/bar",
19
+ list: ["red", "green", "blue"], keys: { semi: ";", dot: ".", comma: "," },
20
+ v: "6", x: "1024", y: "768", empty: "", undef: undefined,
21
+ };
22
+
23
+ function testSurface() {
24
+ check("b.uriTemplate.expand is a function", typeof b.uriTemplate.expand === "function");
25
+ check("b.uriTemplate.compile is a function", typeof b.uriTemplate.compile === "function");
26
+ check("b.uriTemplate.UriTemplateError is a class", typeof b.uriTemplate.UriTemplateError === "function");
27
+ }
28
+
29
+ function eq(tmpl, want) { check(tmpl + " → " + want, b.uriTemplate.expand(tmpl, VARS) === want); }
30
+
31
+ function testLevels() {
32
+ // Level 1 — simple string expansion.
33
+ eq("{var}", "value");
34
+ eq("{hello}", "Hello%20World%21");
35
+ // Level 2 — reserved + fragment.
36
+ eq("{+var}", "value");
37
+ eq("{+path}/here", "/foo/bar/here");
38
+ eq("{#path}", "#/foo/bar");
39
+ eq("{+half}", "50%25");
40
+ // Level 3 — multiple vars + path/label/params/query operators.
41
+ eq("{x,y}", "1024,768");
42
+ eq("{+x,hello,y}", "1024,Hello%20World!,768");
43
+ eq("{/var}", "/value");
44
+ eq("{.who}", ".fred");
45
+ eq("{;x,y}", ";x=1024;y=768");
46
+ eq("{?x,y}", "?x=1024&y=768");
47
+ eq("{&x}", "&x=1024");
48
+ // empty value under named operators.
49
+ eq("{;empty}", ";empty");
50
+ eq("{?empty}", "?empty=");
51
+ }
52
+
53
+ function testLevel4() {
54
+ // Prefix modifier.
55
+ eq("{var:3}", "val");
56
+ eq("{var:30}", "value");
57
+ // Explode — list.
58
+ eq("{list}", "red,green,blue");
59
+ eq("{list*}", "red,green,blue");
60
+ eq("{/list*}", "/red/green/blue");
61
+ eq("{?list*}", "?list=red&list=green&list=blue");
62
+ // Explode — associative array.
63
+ eq("{keys}", "semi,%3B,dot,.,comma,%2C");
64
+ eq("{;keys*}", ";semi=%3B;dot=.;comma=%2C");
65
+ eq("{?keys*}", "?semi=%3B&dot=.&comma=%2C");
66
+ // Undefined variables are omitted.
67
+ eq("{undef}", "");
68
+ eq("x{?undef}", "x");
69
+ // Undefined / null members of a list or map are ignored (RFC 6570 §3.2.1).
70
+ check("undefined list member skipped", b.uriTemplate.expand("{?l*}", { l: ["a", undefined, "b", null, "c"] }) === "?l=a&l=b&l=c");
71
+ check("undefined list member skipped (joined)", b.uriTemplate.expand("{l}", { l: ["a", undefined, "b"] }) === "a,b");
72
+ check("all-undefined list omitted", b.uriTemplate.expand("x{?l*}", { l: [undefined, null] }) === "x");
73
+ check("undefined map value skipped", b.uriTemplate.expand("{?m*}", { m: { a: 1, b: undefined, c: 3 } }) === "?a=1&c=3");
74
+ check("undefined map value skipped (joined)", b.uriTemplate.expand("{m}", { m: { a: "1", b: undefined, c: "3" } }) === "a,1,c,3");
75
+ }
76
+
77
+ function testCompileReuse() {
78
+ var t = b.uriTemplate.compile("/users/{id}{?fields*}");
79
+ check("compiled expand A", t.expand({ id: 7, fields: ["name", "email"] }) === "/users/7?fields=name&fields=email");
80
+ check("compiled expand B (reused)", t.expand({ id: 9 }) === "/users/9");
81
+ }
82
+
83
+ function testMalformed() {
84
+ check("unclosed expression throws", code(function () { b.uriTemplate.expand("{var", {}); }) === "uri-template/unclosed");
85
+ check("reserved operator throws", code(function () { b.uriTemplate.expand("{=var}", {}); }) === "uri-template/reserved-operator");
86
+ check("non-numeric prefix throws", code(function () { b.uriTemplate.expand("{var:x}", {}); }) === "uri-template/bad-prefix");
87
+ check("prefix on list throws", code(function () { b.uriTemplate.expand("{list:3}", VARS); }) === "uri-template/prefix-on-list");
88
+ check("unmatched brace throws", code(function () { b.uriTemplate.expand("/id*}", {}); }) === "uri-template/unmatched-brace");
89
+ }
90
+
91
+ async function run() {
92
+ testSurface();
93
+ testLevels();
94
+ testLevel4();
95
+ testCompileReuse();
96
+ testMalformed();
97
+ }
98
+ module.exports = { run: run };
99
+ if (require.main === module) { run().then(function () { console.log("[uri-template] OK — " + helpers.getChecks() + " checks passed"); }, function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
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": {