@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.
- package/CHANGELOG.md +4 -0
- package/lib/admin.js +143 -1
- package/lib/asset-manifest.json +3 -3
- package/lib/storefront.js +130 -4
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/README.md +1 -1
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/lib/middleware/body-parser.js +95 -49
- package/lib/vendor/blamejs/lib/template.js +159 -23
- package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +12 -12
- package/lib/vendor/blamejs/lib/vendor/simplewebauthn-server.cjs +31 -28
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.13.17.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.13.18.json +18 -0
- package/lib/vendor/blamejs/test/00-primitives.js +144 -3
- package/package.json +1 -1
|
@@ -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("<script>") !== -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
|
-
|
|
2237
|
-
catch (e) { threw = e; }
|
|
2238
|
-
check("create()
|
|
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