@blamejs/core 0.7.87 → 0.7.88

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 CHANGED
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.7.x
10
10
 
11
+ - **0.7.88** (2026-05-06) — `b.middleware.webAppManifest` + `b.middleware.assetlinks` — two static-content middlewares for PWA + Trusted Web Activity support. **`b.middleware.webAppManifest({ name, start_url, icons, ... })`** serves the W3C Web App Manifest at `/manifest.webmanifest` (and `/manifest.json` when `alsoAtJsonPath: true`). The framework JSON-serializes once at create() and serves with `Content-Type: application/manifest+json` per the W3C spec + `Cache-Control: public, max-age=86400` + `X-Content-Type-Options: nosniff`. The W3C-spec attribute set is allowlisted (name / short_name / description / start_url / scope / display / display_override / orientation / theme_color / background_color / icons / screenshots / shortcuts / categories / lang / dir / id / prefer_related_applications / related_applications) — typos throw at create. `name`, `start_url`, and at least one icon are required (W3C — installability minimum). HEAD + GET only. **`b.middleware.assetlinks({ statements })`** serves Digital Asset Links at `/.well-known/assetlinks.json` per Google's spec — used by Trusted Web Activity, Android App Links, Smart Lock for Passwords, WebAuthn for Android. Validates each statement carries `relation` (non-empty array) and `target` (object). Same Content-Type / Cache-Control / X-Content-Type-Options posture as the security.txt + manifest emitters.
12
+
11
13
  - **0.7.87** (2026-05-06) — Two route-guard middlewares for API hardening: `b.middleware.requireMethods` + `b.middleware.requireContentType`. **`b.middleware.requireMethods(["GET", "POST"])`** refuses any HTTP method outside the allowlist with `405 Method Not Allowed` + `Allow:` header listing the allowed methods (per RFC 9110 §15.5.6). Defends against unexpected verb routing — many CVE-class bugs trace to a route handler wired for GET that accidentally accepts arbitrary verbs (PROPFIND, OPTIONS, custom). **`b.middleware.requireContentType(["application/json"])`** refuses requests with a body (POST/PUT/PATCH by default) whose `Content-Type` isn't in the allowlist with `415 Unsupported Media Type` + `Accept:` header listing the allowed types (RFC 9110 §15.5.16). Defends against MIME-type confusion — a route that processes JSON shouldn't accept `application/x-www-form-urlencoded` even if the body parses. Both middlewares emit observability events (`middleware.requireMethods.denied` / `middleware.requireContentType.denied`) on every refusal for triage. Operators wanting to enforce content-type on idempotent verbs that DO carry bodies (rare DELETE-with-body shapes) override the default body-method list via `requireContentType(types, { methods })`.
12
14
 
13
15
  - **0.7.86** (2026-05-06) — `b.middleware.csrfProtect({ requireJsonContentType: true })` — strict-fetch mode for JSON-only API surfaces. State-changing requests (POST/PUT/PATCH/DELETE) without `Content-Type: application/json` are refused before the token check with `CSRF: state-changing requests require Content-Type: application/json.` and audit emission `csrf.denied` with `reason: "non-JSON content-type: ..."`. The browser's form-encoded POST shape is the canonical CSRF vector — a malicious page can `<form action="/transfer" method=POST>` a victim into a state-changing request without a preflight. An `application/json` body forces a CORS preflight (the browser refuses to skip it for non-simple Content-Type values), so an attacker without an operator-allowlisted CORS origin can't reach the route at all. Default `false` — operators with HTML form submissions on the same routes (mixed SPA + classic form pages) keep current behavior; pure-fetch API operators opt in.
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ /**
3
+ * assetlinks middleware — emits Digital Asset Links at
4
+ * `/.well-known/assetlinks.json` per Google's Digital Asset Links
5
+ * spec (used by Trusted Web Activity / Android App Links / Smart
6
+ * Lock for Passwords / Web Authentication for Android, etc.).
7
+ *
8
+ * var al = b.middleware.assetlinks({
9
+ * statements: [
10
+ * {
11
+ * relation: ["delegate_permission/common.handle_all_urls"],
12
+ * target: {
13
+ * namespace: "android_app",
14
+ * package_name: "com.example.app",
15
+ * sha256_cert_fingerprints: ["AB:CD:..."],
16
+ * },
17
+ * },
18
+ * ],
19
+ * });
20
+ * router.use(al);
21
+ *
22
+ * The framework JSON-serializes the statements array once at
23
+ * create() and serves with `Content-Type: application/json` per
24
+ * Google's spec. Operators with multiple linked apps include
25
+ * multiple statement entries.
26
+ */
27
+
28
+ var lazyRequire = require("../lazy-require");
29
+ var safeJson = require("../safe-json");
30
+ var validateOpts = require("../validate-opts");
31
+ var { defineClass } = require("../framework-error");
32
+
33
+ var AssetlinksError = defineClass("AssetlinksError", { alwaysPermanent: true });
34
+
35
+ var observability = lazyRequire(function () { return require("../observability"); });
36
+
37
+ function create(opts) {
38
+ validateOpts.requireObject(opts, "middleware.assetlinks", AssetlinksError);
39
+ validateOpts(opts, ["statements", "audit"], "middleware.assetlinks");
40
+
41
+ if (!Array.isArray(opts.statements) || opts.statements.length === 0) {
42
+ throw new AssetlinksError("assetlinks/no-statements",
43
+ "middleware.assetlinks: opts.statements must be a non-empty array of statement objects");
44
+ }
45
+ for (var i = 0; i < opts.statements.length; i += 1) {
46
+ var stmt = opts.statements[i];
47
+ if (!stmt || typeof stmt !== "object" || Array.isArray(stmt)) {
48
+ throw new AssetlinksError("assetlinks/bad-statement",
49
+ "middleware.assetlinks: statements[" + i + "] must be a plain object");
50
+ }
51
+ if (!Array.isArray(stmt.relation) || stmt.relation.length === 0) {
52
+ throw new AssetlinksError("assetlinks/bad-statement",
53
+ "middleware.assetlinks: statements[" + i + "].relation must be a non-empty array");
54
+ }
55
+ if (!stmt.target || typeof stmt.target !== "object") {
56
+ throw new AssetlinksError("assetlinks/bad-statement",
57
+ "middleware.assetlinks: statements[" + i + "].target must be an object");
58
+ }
59
+ }
60
+
61
+ var body = safeJson.stringify(opts.statements, { space: 2 });
62
+ var bodyBuf = Buffer.from(body, "utf8");
63
+ var auditOn = opts.audit !== false;
64
+
65
+ return function assetlinksMiddleware(req, res, next) {
66
+ var url = req.url || "";
67
+ var qIdx = url.indexOf("?");
68
+ var path = qIdx === -1 ? url : url.slice(0, qIdx);
69
+ if (path !== "/.well-known/assetlinks.json") return next();
70
+ if (req.method !== "GET" && req.method !== "HEAD") {
71
+ var bodyMsg = "Method Not Allowed";
72
+ res.writeHead(405, { // allow:raw-byte-literal — HTTP 405 status
73
+ "Allow": "GET, HEAD",
74
+ "Content-Type": "text/plain; charset=utf-8",
75
+ "Content-Length": Buffer.byteLength(bodyMsg),
76
+ });
77
+ res.end(bodyMsg);
78
+ return;
79
+ }
80
+ res.writeHead(200, { // allow:raw-byte-literal — HTTP 200 status
81
+ "Content-Type": "application/json; charset=utf-8",
82
+ "Content-Length": bodyBuf.length,
83
+ "Cache-Control": "public, max-age=86400",
84
+ "X-Content-Type-Options": "nosniff",
85
+ });
86
+ if (req.method === "HEAD") { res.end(); return; }
87
+ res.end(bodyBuf);
88
+ if (auditOn) {
89
+ try { observability().safeEvent("middleware.assetlinks.served", 1, {}); }
90
+ catch (_e) { /* obs best-effort */ }
91
+ }
92
+ };
93
+ }
94
+
95
+ module.exports = {
96
+ create: create,
97
+ };
@@ -17,6 +17,7 @@
17
17
  * 7. errorHandler — must be LAST so it catches everything that throws
18
18
  */
19
19
  var apiEncrypt = require("./api-encrypt");
20
+ var assetlinks = require("./assetlinks");
20
21
  var attachUser = require("./attach-user");
21
22
  var bearerAuth = require("./bearer-auth");
22
23
  var bodyParser = require("./body-parser");
@@ -45,6 +46,7 @@ var requireMethods = require("./require-methods");
45
46
  var securityHeaders = require("./security-headers");
46
47
  var securityTxt = require("./security-txt");
47
48
  var sse = require("./sse");
49
+ var webAppManifest = require("./web-app-manifest");
48
50
 
49
51
  module.exports = {
50
52
  requestId: requestId.create,
@@ -72,10 +74,12 @@ module.exports = {
72
74
  sse: sse.create,
73
75
  requestLog: requestLog.create,
74
76
  apiEncrypt: apiEncrypt,
77
+ assetlinks: assetlinks.create,
75
78
  dbRoleFor: dbRoleFor.create,
76
79
  dpop: dpop.create,
77
80
  hostAllowlist: hostAllowlist.create,
78
81
  networkAllowlist: networkAllowlist.create,
82
+ webAppManifest: webAppManifest.create,
79
83
 
80
84
  // Module exports for advanced use (constants, raw factory access)
81
85
  _modules: {
@@ -101,9 +105,11 @@ module.exports = {
101
105
  sse: sse,
102
106
  requestLog: requestLog,
103
107
  apiEncrypt: apiEncrypt,
108
+ assetlinks: assetlinks,
104
109
  dbRoleFor: dbRoleFor,
105
110
  dpop: dpop,
106
111
  hostAllowlist: hostAllowlist,
107
112
  networkAllowlist: networkAllowlist,
113
+ webAppManifest: webAppManifest,
108
114
  },
109
115
  };
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ /**
3
+ * web-app-manifest middleware — emits the W3C Web App Manifest at
4
+ * `/manifest.webmanifest` (and `/manifest.json` when `alsoAtJsonPath`
5
+ * is true) per the W3C Web App Manifest specification.
6
+ *
7
+ * var mf = b.middleware.webAppManifest({
8
+ * name: "Example App",
9
+ * short_name: "Example",
10
+ * start_url: "/",
11
+ * display: "standalone",
12
+ * theme_color: "#1976d2",
13
+ * background_color: "#ffffff",
14
+ * icons: [
15
+ * { src: "/icons/192.png", sizes: "192x192", type: "image/png" },
16
+ * { src: "/icons/512.png", sizes: "512x512", type: "image/png" },
17
+ * ],
18
+ * });
19
+ * router.use(mf);
20
+ *
21
+ * The manifest is JSON-serialized once at create() and served with
22
+ * `Content-Type: application/manifest+json` per the W3C spec.
23
+ *
24
+ * Per W3C — `name`, `start_url`, and at least one icon are required
25
+ * for an installable PWA. The framework throws at create() when any
26
+ * of those are missing.
27
+ */
28
+
29
+ var lazyRequire = require("../lazy-require");
30
+ var safeJson = require("../safe-json");
31
+ var validateOpts = require("../validate-opts");
32
+ var { defineClass } = require("../framework-error");
33
+
34
+ var WebAppManifestError = defineClass("WebAppManifestError", { alwaysPermanent: true });
35
+
36
+ var observability = lazyRequire(function () { return require("../observability"); });
37
+
38
+ function _isPlainArray(x) { return Array.isArray(x); }
39
+
40
+ function create(opts) {
41
+ validateOpts.requireObject(opts, "middleware.webAppManifest", WebAppManifestError);
42
+ // Allowlist subset of W3C-spec attributes operators commonly set.
43
+ // Anything outside the list throws at create — typos surface at boot.
44
+ validateOpts(opts, [
45
+ "name", "short_name", "description", "start_url", "scope",
46
+ "display", "display_override", "orientation",
47
+ "theme_color", "background_color",
48
+ "icons", "screenshots", "shortcuts",
49
+ "categories", "lang", "dir", "id",
50
+ "prefer_related_applications", "related_applications",
51
+ "alsoAtJsonPath", "audit",
52
+ ], "middleware.webAppManifest");
53
+
54
+ validateOpts.requireNonEmptyString(opts.name,
55
+ "middleware.webAppManifest: name", WebAppManifestError, "manifest/no-name");
56
+ validateOpts.requireNonEmptyString(opts.start_url,
57
+ "middleware.webAppManifest: start_url", WebAppManifestError, "manifest/no-start-url");
58
+ if (!_isPlainArray(opts.icons) || opts.icons.length === 0) {
59
+ throw new WebAppManifestError("manifest/no-icons",
60
+ "middleware.webAppManifest: icons array is required (W3C spec — at least one icon for installability)");
61
+ }
62
+
63
+ // Build the JSON body once at create.
64
+ var manifest = {};
65
+ var keys = Object.keys(opts).filter(function (k) {
66
+ return k !== "alsoAtJsonPath" && k !== "audit";
67
+ });
68
+ for (var i = 0; i < keys.length; i += 1) {
69
+ var k = keys[i];
70
+ if (opts[k] !== undefined && opts[k] !== null) manifest[k] = opts[k];
71
+ }
72
+ var body = safeJson.stringify(manifest, { space: 2 });
73
+ var bodyBuf = Buffer.from(body, "utf8");
74
+ var alsoAtJsonPath = opts.alsoAtJsonPath === true;
75
+ var auditOn = opts.audit !== false;
76
+
77
+ return function webAppManifestMiddleware(req, res, next) {
78
+ var url = req.url || "";
79
+ var qIdx = url.indexOf("?");
80
+ var path = qIdx === -1 ? url : url.slice(0, qIdx);
81
+ var matches = (path === "/manifest.webmanifest") ||
82
+ (alsoAtJsonPath && path === "/manifest.json");
83
+ if (!matches) return next();
84
+ if (req.method !== "GET" && req.method !== "HEAD") {
85
+ var bodyMsg = "Method Not Allowed";
86
+ res.writeHead(405, { // allow:raw-byte-literal — HTTP 405 status
87
+ "Allow": "GET, HEAD",
88
+ "Content-Type": "text/plain; charset=utf-8",
89
+ "Content-Length": Buffer.byteLength(bodyMsg),
90
+ });
91
+ res.end(bodyMsg);
92
+ return;
93
+ }
94
+ res.writeHead(200, { // allow:raw-byte-literal — HTTP 200 status
95
+ "Content-Type": "application/manifest+json",
96
+ "Content-Length": bodyBuf.length,
97
+ "Cache-Control": "public, max-age=86400",
98
+ "X-Content-Type-Options": "nosniff",
99
+ });
100
+ if (req.method === "HEAD") { res.end(); return; }
101
+ res.end(bodyBuf);
102
+ if (auditOn) {
103
+ try { observability().safeEvent("middleware.webAppManifest.served", 1, { path: path }); }
104
+ catch (_e) { /* obs best-effort */ }
105
+ }
106
+ };
107
+ }
108
+
109
+ module.exports = {
110
+ create: create,
111
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.7.87",
3
+ "version": "0.7.88",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:96ee5394-43c1-471e-98e9-f3b59c96f3e0",
5
+ "serialNumber": "urn:uuid:5980b589-6ae2-43c3-88ad-31cb87e43594",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-06T06:37:03.759Z",
8
+ "timestamp": "2026-05-06T06:50:00.974Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.7.87",
22
+ "bom-ref": "@blamejs/core@0.7.88",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.7.87",
25
+ "version": "0.7.88",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.7.87",
29
+ "purl": "pkg:npm/%40blamejs/core@0.7.88",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.7.87",
57
+ "ref": "@blamejs/core@0.7.88",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]