@blamejs/blamejs-shop 0.2.5 → 0.2.7

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.13.16",
3
+ "version": "0.13.18",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.13.17",
4
+ "date": "2026-05-27",
5
+ "headline": "Template engine can render from a string with no views directory — for serverless / read-only filesystems",
6
+ "summary": "b.template.create previously required a viewsDir that exists on disk, and rendering always read the template (and its layout/partials) from that directory — unusable on a read-only or ephemeral serverless filesystem where the templates aren't on disk. The engine now accepts a source string directly: viewsDir is optional, and the returned engine exposes renderString(source, data?, opts?) and compileString(source, opts?) that compile and render from a string with no disk read. {% extends %} and {{> partial}} in a string source resolve through an operator-supplied opts.resolve(name) -> string callback (without it, an extends throws a clear error and a missing partial inlines empty, matching the file path). The same HTML-escaping, expression grammar, and extends/partial-depth caps apply. The file-backed render / compile / precompileAll still work exactly as before when a viewsDir is configured, and now refuse with a clear error when one isn't.",
7
+ "sections": [
8
+ {
9
+ "heading": "Added",
10
+ "items": [
11
+ {
12
+ "title": "`engine.renderString` / `engine.compileString` — render templates from a string, no viewsDir",
13
+ "body": "`b.template.create({})` (no `viewsDir`) returns a string-only engine; `renderString(source, data?, { resolve })` and `compileString(source, { resolve })` compile and render from a source string with zero filesystem access — the read-only / serverless path. `{% extends %}` and `{{> partial}}` resolve through `opts.resolve(name) -> string`. The HTML escaping, grammar, and depth caps are identical to the file path. When a `viewsDir` IS configured, `render`/`compile`/`precompileAll` behave exactly as before; without one they refuse with `viewsDir not configured`. `renderString(source, { resolve })` may omit the data argument — an opts object carrying a function `resolve` is recognized as opts, not data."
14
+ }
15
+ ]
16
+ },
17
+ {
18
+ "heading": "Security",
19
+ "items": [
20
+ {
21
+ "title": "Vendored `@simplewebauthn/server` refreshed 13.3.0 → 13.3.1",
22
+ "body": "The vendored WebAuthn server bundle (`b.auth.passkey`'s registration/authentication verification) is refreshed to the latest upstream patch, with the MANIFEST version, CPE, and SHA-256 integrity hashes updated and the bundle re-verified."
23
+ }
24
+ ]
25
+ }
26
+ ]
27
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.13.18",
4
+ "date": "2026-05-27",
5
+ "headline": "`bodyParser` multipart can buffer uploads in memory — no tmp directory for serverless / read-only filesystems",
6
+ "summary": "The multipart/form-data sub-parser previously streamed every file part to a tmp directory on disk (os.tmpdir() by default), which fails on a read-only or ephemeral serverless filesystem. A new multipart.storage option selects where file parts land: \"disk\" (default, unchanged — req.files[].path points at a tmp file cleaned up on response end) or \"memory\" (req.files[].buffer holds the assembled bytes, with no filesystem access at all). Both modes enforce the same per-file (fileSize), per-field, and total-request (totalSize) caps, so memory mode adds no new memory-exhaustion surface. The file object shape is stable across both modes — disk sets path with buffer null, memory sets buffer with path null — so a handler branches on whichever is non-null. An invalid storage value is rejected when the middleware is constructed.",
7
+ "sections": [
8
+ {
9
+ "heading": "Added",
10
+ "items": [
11
+ {
12
+ "title": "`bodyParser` multipart `storage: \"memory\"` — buffer uploads in RAM instead of a tmp directory",
13
+ "body": "`b.middleware.bodyParser({ multipart: { storage: \"memory\" } })` buffers each uploaded file part in memory and exposes it as `req.files[].buffer` (a Buffer), with no `os.tmpdir()` write and no tmp-file cleanup — the read-only / serverless path. The default `storage: \"disk\"` is unchanged: file parts stream to a tmp file, `req.files[].path` points at it, and it is removed when the response finishes. Both modes apply the existing `fileSize` / per-field `maxBytes` / `totalSize` caps and SHA3-512 hash each part during streaming, so memory mode is bounded by the same limits and adds no new DoS surface. The `req.files[]` shape is stable across modes (disk: `path` set, `buffer` null; memory: `buffer` set, `path` null). A `storage` value other than `\"disk\"` or `\"memory\"` throws a `TypeError` at construction."
14
+ }
15
+ ]
16
+ }
17
+ ]
18
+ }
@@ -1999,6 +1999,74 @@ function testTemplateBasicRender() {
1999
1999
  } finally { fs.rmSync(dir, { recursive: true, force: true }); }
2000
2000
  }
2001
2001
 
2002
+ function testTemplateRenderString() {
2003
+ // Serverless / read-only-FS path: render from a source string with no
2004
+ // viewsDir + no disk read. HTML escaping still applies.
2005
+ var eng = b.template.create({});
2006
+ check("renderString: no viewsDir engine created", eng.viewsDir === null);
2007
+ var out = eng.renderString("<h1>{{ greeting }}, {{ name }}!</h1>", { greeting: "Hi", name: "Alice" });
2008
+ check("renderString: substitutes + escapes",
2009
+ out === "<h1>Hi, Alice!</h1>");
2010
+ var hostile = eng.renderString("<p>{{ x }}</p>", { x: "<script>alert(1)</script>" });
2011
+ check("renderString: user values escaped",
2012
+ hostile.indexOf("<script>") === -1 && hostile.indexOf("&lt;script&gt;") !== -1);
2013
+
2014
+ // extends + partial resolved via opts.resolve (no disk).
2015
+ var views = {
2016
+ base: "<html>{% block body %}default{% endblock %}</html>",
2017
+ greet: "<p>hi {{ n }}</p>",
2018
+ };
2019
+ var composed = eng.renderString(
2020
+ "{% extends \"base\" %}{% block body %}{{> greet}}{% endblock %}",
2021
+ { n: "Bo" },
2022
+ { resolve: function (name) { return views[name]; } });
2023
+ check("renderString: extends + partial via opts.resolve",
2024
+ composed === "<html><p>hi Bo</p></html>");
2025
+
2026
+ // extends with no resolver → clean throw (not a crash).
2027
+ var threwExtends = null;
2028
+ try { eng.renderString("{% extends \"base\" %}{% block body %}x{% endblock %}", {}); }
2029
+ catch (e) { threwExtends = e; }
2030
+ check("renderString: extends without opts.resolve refuses",
2031
+ threwExtends && /extends/.test(threwExtends.message));
2032
+
2033
+ // A missing partial inlines empty (same as the file path).
2034
+ check("renderString: missing partial inlines empty",
2035
+ eng.renderString("a{{> nope}}b", {}, { resolve: function () { return undefined; } }) === "ab");
2036
+
2037
+ // Data omitted: renderString(source, { resolve }) treats the opts as
2038
+ // opts (not data) when it carries a function `resolve` + no 3rd arg.
2039
+ var noData = eng.renderString(
2040
+ "{% extends \"base\" %}{% block body %}static{% endblock %}",
2041
+ { resolve: function (name) { return views[name]; } });
2042
+ check("renderString: opts-as-2nd-arg (data omitted) honored",
2043
+ noData === "<html>static</html>");
2044
+
2045
+ // compileString returns a reusable AST.
2046
+ var ast = eng.compileString("<b>{{ v }}</b>");
2047
+ check("compileString: returns an AST", ast && ast.type === "Template");
2048
+
2049
+ // String templates are byte-capped against hostile input (untrusted
2050
+ // source); the default cap refuses an oversize source, and a hostile
2051
+ // tag stream can't drive a ReDoS through the block resolver.
2052
+ var threwCap = null;
2053
+ try { eng.compileString("x".repeat(300000)); } catch (e) { threwCap = e; }
2054
+ check("compileString: oversize source refused (maxBytes)",
2055
+ threwCap && /maxBytes/.test(threwCap.message));
2056
+ check("compileString: opts.maxBytes raises the cap",
2057
+ eng.compileString("y".repeat(300000), { maxBytes: 1000000 }) !== null);
2058
+ var t0 = Date.now();
2059
+ try { eng.compileString("{%block\tA%}".repeat(40000)); } catch (e2) { void e2; }
2060
+ check("compileString: pathological block-open stream bounded (no ReDoS)",
2061
+ (Date.now() - t0) < 1000);
2062
+
2063
+ // The file-backed methods refuse on a no-viewsDir engine.
2064
+ var threwFile = null;
2065
+ try { eng.render("anything", {}); } catch (e) { threwFile = e; }
2066
+ check("render() without viewsDir refuses",
2067
+ threwFile && /viewsDir not configured/.test(threwFile.message));
2068
+ }
2069
+
2002
2070
  function testTemplateRawExpression() {
2003
2071
  var dir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-tpl-"));
2004
2072
  try {
@@ -2232,10 +2300,15 @@ function testTemplateMissingViewsDir() {
2232
2300
  catch (e) { threw = e; }
2233
2301
  check("create() rejects missing viewsDir", threw && /viewsDir does not exist/.test(threw.message));
2234
2302
 
2303
+ // viewsDir is now optional — create({}) returns a string-only engine
2304
+ // (serverless path); the file-backed render() refuses instead.
2235
2305
  threw = null;
2236
- try { b.template.create({}); }
2237
- catch (e) { threw = e; }
2238
- check("create() requires viewsDir", threw && /viewsDir.*required/.test(threw.message));
2306
+ var eng = null;
2307
+ try { eng = b.template.create({}); } catch (e) { threw = e; }
2308
+ check("create() without viewsDir succeeds (string-only engine)", threw === null && eng !== null);
2309
+ threw = null;
2310
+ try { eng.render("x", {}); } catch (e) { threw = e; }
2311
+ check("render() without viewsDir refuses", threw && /viewsDir not configured/.test(threw.message));
2239
2312
  }
2240
2313
 
2241
2314
  function testTemplateSurface() {
@@ -6526,6 +6599,72 @@ async function testBodyParserMultipartFile() {
6526
6599
  }
6527
6600
  }
6528
6601
 
6602
+ async function testBodyParserMultipartMemoryStorage() {
6603
+ // storage: "memory" — file parts buffered in RAM, exposed as
6604
+ // req.files[].buffer with no filesystem touch (serverless / read-only fs).
6605
+ var bp = b.middleware.bodyParser({ multipart: { storage: "memory" } });
6606
+ var boundary = "----blamejs-test-boundary-mem";
6607
+ var fileBytes = Buffer.from("hello in-memory file body");
6608
+ var body = _buildMultipartBody(boundary, [
6609
+ { name: "title", value: "Mem File" },
6610
+ { name: "upload", value: fileBytes, filename: "mem.txt", contentType: "text/plain" },
6611
+ ]);
6612
+ var req = _mockBodyReq("POST",
6613
+ { "content-type": "multipart/form-data; boundary=" + boundary,
6614
+ "content-length": String(body.length) }, body);
6615
+ var res = _mockBodyRes();
6616
+ await _runBodyParser(bp, req, res);
6617
+ check("multipart memory: text field parsed", req.body.title === "Mem File");
6618
+ check("multipart memory: file captured", req.files.length === 1);
6619
+ check("multipart memory: buffer is a Buffer", Buffer.isBuffer(req.files[0].buffer));
6620
+ check("multipart memory: buffer content matches", req.files[0].buffer.toString("utf8") === "hello in-memory file body");
6621
+ check("multipart memory: path is null (no disk)", req.files[0].path === null);
6622
+ check("multipart memory: size matches", req.files[0].size === fileBytes.length);
6623
+ check("multipart memory: hash present", typeof req.files[0].hash === "string" && req.files[0].hash.length === 128);
6624
+ // res.finish cleanup is a no-op for memory files (no path) — must not throw.
6625
+ var threwOnFinish = false;
6626
+ try { res.emit("finish"); } catch (_e) { threwOnFinish = true; }
6627
+ check("multipart memory: finish cleanup no-op (no throw)", !threwOnFinish);
6628
+
6629
+ // Per-file size cap still enforced in memory mode.
6630
+ var bpCap = b.middleware.bodyParser({ multipart: { storage: "memory", fileSize: 8 } });
6631
+ var capBody = _buildMultipartBody(boundary, [
6632
+ { name: "upload", value: Buffer.from("way over the eight byte cap"),
6633
+ filename: "big.txt", contentType: "text/plain" },
6634
+ ]);
6635
+ var capReq = _mockBodyReq("POST",
6636
+ { "content-type": "multipart/form-data; boundary=" + boundary,
6637
+ "content-length": String(capBody.length) }, capBody);
6638
+ var capRes = _mockBodyRes();
6639
+ await _runBodyParser(bpCap, capReq, capRes);
6640
+ check("multipart memory: oversize file refused (413)", capRes.statusCode === 413);
6641
+
6642
+ // Bad storage value throws at construction (entry-point tier).
6643
+ var threwBadStorage = false;
6644
+ try { b.middleware.bodyParser({ multipart: { storage: "s3" } }); }
6645
+ catch (e) { threwBadStorage = /storage must be/.test(e.message); }
6646
+ check("multipart memory: invalid storage throws at config", threwBadStorage);
6647
+
6648
+ // Disk mode unchanged: still exposes .path, buffer null.
6649
+ var tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-bp-mem-"));
6650
+ try {
6651
+ var bpDisk = b.middleware.bodyParser({ multipart: { storage: "disk", tmpDir: tmpDir } });
6652
+ var dBody = _buildMultipartBody(boundary, [
6653
+ { name: "upload", value: Buffer.from("on disk"), filename: "d.txt", contentType: "text/plain" },
6654
+ ]);
6655
+ var dReq = _mockBodyReq("POST",
6656
+ { "content-type": "multipart/form-data; boundary=" + boundary,
6657
+ "content-length": String(dBody.length) }, dBody);
6658
+ var dRes = _mockBodyRes();
6659
+ await _runBodyParser(bpDisk, dReq, dRes);
6660
+ check("multipart memory: disk mode keeps .path", typeof dReq.files[0].path === "string" && fs.existsSync(dReq.files[0].path));
6661
+ check("multipart memory: disk mode buffer null", dReq.files[0].buffer === null);
6662
+ dRes.emit("finish");
6663
+ } finally {
6664
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_e) {}
6665
+ }
6666
+ }
6667
+
6529
6668
  async function testBodyParserMultipartFilenameTraversal() {
6530
6669
  var tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-bp-"));
6531
6670
  try {
@@ -17597,6 +17736,7 @@ async function run() {
17597
17736
  testTemplateSurface();
17598
17737
  testTemplateEscapeHtml();
17599
17738
  testTemplateBasicRender();
17739
+ testTemplateRenderString();
17600
17740
  testTemplateRawExpression();
17601
17741
  testTemplateIfElse();
17602
17742
  testTemplateForLoop();
@@ -17789,6 +17929,7 @@ async function run() {
17789
17929
  await testBodyParserKeepRawBody();
17790
17930
  await testBodyParserMultipartFields();
17791
17931
  await testBodyParserMultipartFile();
17932
+ await testBodyParserMultipartMemoryStorage();
17792
17933
  await testBodyParserMultipartFilenameTraversal();
17793
17934
  await testBodyParserMultipartFileSizeLimit();
17794
17935
  await testBodyParserMultipartMimeAllowlist();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
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": {