@blamejs/core 0.17.15 → 0.17.16

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.17.x
10
10
 
11
+ - v0.17.16 (2026-07-23) — **A shared path-containment primitive backs both the strict resolver and static file serving.** b.safePath.resolve and b.staticServe each hand-rolled the same lexical traversal-containment check -- resolve a request path against a base and confirm it stays strictly inside. That core is now one primitive, b.safePath.confineToBase, which both compose: resolve layers its user-input strictness (reserved names, NTFS ADS markers, bidi, control chars) on top, while static serving composes only the bare containment and keeps its own separate basename gate. Static serving keeps that separation deliberately -- its containment barrier and its per-file guardFilename validation are distinct concerns -- and inherits the resolver's cross-platform-aware containment, which the runtime path module missed for a backslash traversal on a POSIX host. Static serving's basename policy is unchanged. **Added:** *b.safePath.confineToBase -- the lexical traversal-containment core* — b.safePath.confineToBase(base, rel, opts?) resolves rel against base using the target platform's path semantics and returns the confined absolute path, or null if it escapes. It is the containment barrier b.safePath.resolve layers its user-input strictness on top of, exposed for a consumer that wants ONLY traversal containment and applies its own, differently-calibrated filename validation -- so it does not reject a reserved name, NTFS ADS marker, or trailing dot the way resolve does for untrusted input. opts.platform forces windows path semantics on any host. **Changed:** *Static file serving composes the shared containment primitive* — b.staticServe's path-traversal barrier now composes b.safePath.confineToBase for its final lexical containment instead of a hand-rolled join + base-prefix check, so both the strict resolver and static serving route through one implementation. Static serving gains the cross-platform-aware containment (the runtime path module treats the other platform's separator as an ordinary filename character and missed a backslash traversal on a POSIX host). Serve behavior is unchanged: containment and the separate per-file guardFilename basename gate remain distinct steps, and static composes only the containment core rather than resolve so it does not fuse resolve's all-segment user-input strictness into the barrier (which would reject a legitimate colon-named intermediate directory that the basename gate permits). The basename policy itself is untouched.
12
+
11
13
  - v0.17.15 (2026-07-23) — **The standalone multipart parser honors storage: memory and every other documented upload knob.** b.parsers.multipart(req, opts) is documented as a thin wrapper over the same engine b.middleware.bodyParser drives, but its option resolver carried a hand-maintained passthrough list that had drifted: storage ("disk" | "memory") and filenameCharsets were silently dropped, so a serverless / read-only-filesystem handler passing storage: "memory" got disk mode instead and threw when the parser tried to open a temp file. The passthrough is now derived from the parser's own defaults, so every documented knob reaches the standalone path and a future knob cannot silently vanish, and an invalid storage value throws at the call the same way the middleware does instead of falling through to disk. **Fixed:** *b.parsers.multipart honors storage: memory and filenameCharsets* — The standalone multipart parser's option resolver passed through a hand-maintained subset of the multipart knobs, omitting storage and filenameCharsets: b.parsers.multipart(req, { storage: "memory" }) silently used disk mode and then threw on a read-only / serverless filesystem, and a filename*=ISO-8859-1'' part could not be opted in via filenameCharsets. The passthrough is now derived from the parser's DEFAULTS so every documented knob reaches the standalone path (only the maxBytes/maxFiles aliases and the dispatch-only contentTypes are excluded), and an invalid storage value throws a TypeError at the call rather than falling back to disk -- matching b.middleware.bodyParser.
12
14
 
13
15
  - v0.17.14 (2026-07-23) — **The framework's file watchers are hardened against an uncatchable process abort on Windows 8.3 short-name paths.** fs.watch aborts the whole Node process -- an uncatchable libuv assertion in the directory-change backend -- when the watched path is reached through a Windows 8.3 short-name component (the shape of a path under C:\Users\SOMEUS~1\... or a short-named temp directory). The recursive file watcher already expanded the short name before watching; this closes the same gap everywhere else the framework watches a path: the feature-flag file watcher now stat-polls its config file instead of directory-watching it, and the dev-server and bundler watchers expand the short name before watching. Three structural detectors lock the invariant in so it cannot regress in a way a behavioral test cannot reach (the abort only fires on a real short-name path, which a host with 8.3 generation disabled cannot reproduce). **Changed:** *Public Suffix List refreshed to the current upstream snapshot* — The vendored Mozilla Public Suffix List is updated to the current upstream snapshot, so organizational-domain derivation reflects the latest registry delegations wherever the framework draws a registrable-domain boundary -- DMARC identifier alignment, BIMI issuer scoping, cookie-scope confinement, and same-site policy. The .data.js carrier is regenerated and re-signed across all four integrity layers. **Fixed:** *The feature-flag file watcher no longer aborts on a Windows short-name config path* — b.flags localFile provider with watch enabled directory-watched the config file via fs.watch, which aborts the process (uncatchable) when the config path resolves through a Windows 8.3 short-name component -- its surrounding try/catch cannot recover from a libuv abort(). It now stat-polls the file (StatWatcher opens no directory-change handle, so it is immune to the path form) and reloads on change with sub-second latency. · *The dev-server and bundler watchers expand a short-name path before watching* — b.dev and b.bundler watch a source directory or file via fs.watch; on Windows a path reached through an 8.3 short-name component aborts libuv. Both now resolve the real long-form path (realpathSync.native) before the watch, matching the guard the recursive b.watcher already applied. **Detectors:** *Filesystem-watch short-name safety and pid-liveness composition are enforced structurally* — Three codebase-patterns detectors close invariants a behavioral test cannot assert (the underlying abort is environment-specific to a real 8.3 short-name path): any lib fs.watch must expand the short name via realpathSync.native in the same file (or use the StatWatcher path instead); the daemon's Windows cooperative-stop sentinel must poll a synchronous existsSync rather than watch the filesystem; and a signal-0 process-liveness probe must compose the shared pid-probe classifier rather than re-roll process.kill(pid, 0) with its own EPERM/ESRCH interpretation.
package/lib/safe-path.js CHANGED
@@ -138,6 +138,53 @@ function validate(base, rel, opts) {
138
138
  catch (e) { return { ok: false, code: e.code || "safe-path/unknown", message: e.message }; }
139
139
  }
140
140
 
141
+ /**
142
+ * @primitive b.safePath.confineToBase
143
+ * @signature b.safePath.confineToBase(base, rel, opts?)
144
+ * @since 0.17.16
145
+ * @status stable
146
+ * @related b.safePath.resolve, b.staticServe.create
147
+ *
148
+ * The lexical traversal-containment core, WITHOUT the user-input
149
+ * strictness of `resolve` (no reserved-name / ADS / bidi / control-char
150
+ * refusal). Resolve `rel` against `base` using the TARGET platform's path
151
+ * semantics and confirm the result stays strictly inside `base`; return
152
+ * the confined absolute path, or `null` if it escapes.
153
+ *
154
+ * This is the barrier `resolve` layers its user-input checks on top of,
155
+ * and the one a consumer composes when it wants ONLY traversal containment
156
+ * and runs its OWN, separately-calibrated filename validation — as
157
+ * b.staticServe does, keeping its per-file basename gate (b.guardFilename)
158
+ * a distinct step rather than fusing `resolve`'s all-segment user-input
159
+ * strictness into the containment barrier.
160
+ *
161
+ * @opts
162
+ * platform: string, // "windows" forces win32 path semantics regardless of host
163
+ *
164
+ * @example
165
+ * var p = b.safePath.confineToBase("/srv/www", "docs/a.html");
166
+ * // → "/srv/www/docs/a.html" (null if rel escaped /srv/www)
167
+ */
168
+ function confineToBase(base, rel, opts) {
169
+ opts = opts || {};
170
+ if (typeof base !== "string" || base.length === 0) return null;
171
+ if (typeof rel !== "string") return null;
172
+ var platform = opts.platform || process.platform;
173
+ var isWin = platform === "win32" || platform === "windows";
174
+ // Resolve + contain using the TARGET platform's path module, NOT the
175
+ // runtime's: the runtime path module would treat the OTHER platform's
176
+ // separator as an ordinary filename character and miss a backslash
177
+ // traversal on a POSIX host (and inversely false-refuse legitimate paths).
178
+ var pathMod = isWin ? nodePath.win32 : nodePath.posix;
179
+ var baseResolved = pathMod.resolve(base);
180
+ var joined = pathMod.resolve(baseResolved, rel);
181
+ var sepChar = pathMod.sep;
182
+ if (joined !== baseResolved && joined.slice(0, baseResolved.length + 1) !== baseResolved + sepChar) {
183
+ return null;
184
+ }
185
+ return joined;
186
+ }
187
+
141
188
  function _resolveCore(base, rel, opts) {
142
189
  if (typeof base !== "string" || base.length === 0) {
143
190
  _refuse("safe-path/bad-input", "b.safePath.resolve: base must be a non-empty string");
@@ -219,11 +266,11 @@ function _resolveCore(base, rel, opts) {
219
266
  // that hole AND stops the inverse false-refusal of legitimate in-base paths.
220
267
  var pathMod = isWin ? nodePath.win32 : nodePath.posix;
221
268
  var baseResolved = pathMod.resolve(base);
222
- var joined = pathMod.resolve(baseResolved, rel);
223
- var sepChar = pathMod.sep;
224
- if (joined !== baseResolved && joined.slice(0, baseResolved.length + 1) !== baseResolved + sepChar) {
269
+ var joined = confineToBase(base, rel, { platform: platform });
270
+ if (joined === null) {
225
271
  _refuse("safe-path/escapes-base",
226
- "b.safePath.resolve: rel resolves outside base ('" + joined + "' not inside '" + baseResolved + "')");
272
+ "b.safePath.resolve: rel resolves outside base ('" +
273
+ pathMod.resolve(baseResolved, rel) + "' not inside '" + baseResolved + "')");
227
274
  }
228
275
  if (opts.realpath === true) {
229
276
  // realpath resolves symlinks on the RUNTIME filesystem, so it must use the
@@ -266,5 +313,6 @@ module.exports = {
266
313
  resolve: resolve,
267
314
  resolveOrNull: resolveOrNull,
268
315
  validate: validate,
316
+ confineToBase: confineToBase,
269
317
  SafePathError: SafePathError,
270
318
  };
package/lib/static.js CHANGED
@@ -50,6 +50,7 @@ var gateContract = require("./gate-contract");
50
50
  var lazyRequire = require("./lazy-require");
51
51
  var numericBounds = require("./numeric-bounds");
52
52
  var requestHelpers = require("./request-helpers");
53
+ var safePath = require("./safe-path");
53
54
  var validateOpts = require("./validate-opts");
54
55
  var { StaticServeError } = require("./framework-error");
55
56
 
@@ -205,11 +206,10 @@ function _assertInsideRoot(root, candidate) {
205
206
  rel.indexOf(".." + nodePath.sep) !== -1 ||
206
207
  rel.indexOf(".." + (nodePath.sep === "/" ? "\\" : "/")) !== -1 ||
207
208
  nodePath.isAbsolute(rel)) return null;
208
- var safe = nodePath.join(rootResolved, rel);
209
- // Defense-in-depth lexical containment alongside the join sanitizer.
210
- if (safe !== rootResolved &&
211
- !safe.startsWith(rootResolved + nodePath.sep)) return null;
212
- return safe;
209
+ // Final lexical containment via the shared traversal-containment primitive
210
+ // (cross-platform-aware resolve + strict base-prefix check). rel is already
211
+ // `..`-stripped and relative above, so this confines to root or returns null.
212
+ return safePath.confineToBase(rootResolved, rel);
213
213
  }
214
214
 
215
215
  // Module-level metadata cache. Entries hold:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.17.15",
3
+ "version": "0.17.16",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -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:de6a5295-f203-43a2-9beb-1d4c36015b11",
5
+ "serialNumber": "urn:uuid:361e1563-6d41-499d-b266-962cb2dc64ef",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-24T02:18:59.855Z",
8
+ "timestamp": "2026-07-24T03:55:41.218Z",
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.17.15",
22
+ "bom-ref": "@blamejs/core@0.17.16",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.17.15",
25
+ "version": "0.17.16",
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.17.15",
29
+ "purl": "pkg:npm/%40blamejs/core@0.17.16",
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.17.15",
57
+ "ref": "@blamejs/core@0.17.16",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]